Compare commits

...

191 Commits

Author SHA1 Message Date
advplyr 38f05a857f Version bump v2.20.0 2025-03-17 17:11:01 -05:00
mikiher 40504da4d7 Improve book library page query performance for author sort order (#4080)
* Add migration to create authorNames* columns, in libraryItems including update triggers and indices

* Add authorNames columns and indices to LibraryItem model

* Add database triggers for updating author names in libraryItems (for new databases)

* Populate authorNames during book scanning

* Update book sorting to use new authorNames columns

* Add an index on podcastEpisodes.publishedAt

* Fix group_concat order by and update to sqlite 3.44.2

---------

Co-authored-by: advplyr <advplyr@protonmail.com>
2025-03-17 17:09:49 -05:00
advplyr bba09626a7 Merge pull request #4115 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-03-17 17:07:30 -05:00
thehijacker 6c968bfca4 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1090 of 1090 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2025-03-17 14:24:50 +01:00
J. Lavoie 8fa733e144 Translated using Weblate (French)
Currently translated at 99.3% (1083 of 1090 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2025-03-17 09:04:44 +01:00
peter cerny e76fbda9e0 Translated using Weblate (Slovak)
Currently translated at 8.3% (91 of 1090 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sk/
2025-03-17 00:02:33 +01:00
peter cerny 9fedab738f Translated using Weblate (Slovak)
Currently translated at 7.7% (84 of 1090 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sk/
2025-03-17 00:02:31 +01:00
peter cerny 5d8a88dc08 Translated using Weblate (Slovak)
Currently translated at 7.5% (82 of 1090 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sk/
2025-03-17 00:02:31 +01:00
SunSpring 23d20f4a5c Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1090 of 1090 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-03-17 00:02:30 +01:00
biuklija 3dc2022239 Translated using Weblate (Croatian)
Currently translated at 100.0% (1090 of 1090 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-03-17 00:02:29 +01:00
advplyr b2001eca23 Added translation using Weblate (Slovak) 2025-03-17 00:02:28 +01:00
Jan-Eric Myhrgren 0f7867a12a Translated using Weblate (Swedish)
Currently translated at 94.5% (1031 of 1090 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-17 00:02:27 +01:00
Максим Горпиніч 43706aac6d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1090 of 1090 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-03-17 00:02:26 +01:00
Jan-Eric Myhrgren 5c7865f945 Translated using Weblate (Swedish)
Currently translated at 94.5% (1031 of 1090 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-17 00:02:24 +01:00
advplyr 7f8de7915c Update remove playlist translations and use our custom confirm modal 2025-03-16 18:02:16 -05:00
Gabriel Gavrilov 394bf8cb70 Allow number types for payload metadata when updating books. (#4118)
* Allow number types for payload metadata

* cast numbers to string
2025-03-16 08:42:18 -05:00
advplyr 3f6609ab1b Merge pull request #4119 from jfrazx/master
ci: update actions
2025-03-15 17:43:54 -05:00
advplyr e29d3a3672 Cast OpenLibrary publishYear to string #4114 2025-03-15 17:41:07 -05:00
jfrazx ecd782c8a9 fix: docker action 2025-03-15 00:49:27 -07:00
jfrazx cb102deaed Merge pull request #1 from jfrazx/ci/update-actions
ci: update actions
2025-03-14 20:18:59 -07:00
jfrazx 9f883a5019 ci: update actions 2025-03-14 19:43:09 -07:00
advplyr 607f143861 Merge pull request #4113 from advplyr/parsing-opf-v3
Update opf parser to support refines meta elements
2025-03-14 17:39:20 -05:00
advplyr 804dafdfcb Add test for parseOpfMetadata OPF v3 author 2025-03-14 17:32:32 -05:00
advplyr de22177dbf Update opf parser to support refines meta elements 2025-03-13 17:49:05 -05:00
advplyr 2bd46eb67b Fix conflicts 2025-03-13 17:15:30 -05:00
advplyr 9960986e6e Remove old unused i18n strings 2025-03-12 17:22:12 -05:00
ejlaner 759efc0d7d Translated using Weblate (Japanese)
Currently translated at 1.7% (19 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ja/
2025-03-12 23:20:27 +01:00
Xeratone 72f2712a5f Translated using Weblate (Japanese)
Currently translated at 0.2% (3 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ja/
2025-03-12 23:20:26 +01:00
Jan-Eric Myhrgren 1f609e023d Translated using Weblate (Swedish)
Currently translated at 94.5% (1033 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-12 23:20:25 +01:00
Jan-Eric Myhrgren d2f506eefe Translated using Weblate (Swedish)
Currently translated at 94.5% (1033 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-12 23:20:24 +01:00
Ricky Tigg 78031b1a89 Translated using Weblate (Finnish)
Currently translated at 78.0% (853 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-03-12 23:20:23 +01:00
Ricky Tigg c3ce084aac Translated using Weblate (Finnish)
Currently translated at 76.3% (835 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-03-12 23:20:23 +01:00
Jan-Eric Myhrgren 3d6e50a099 Translated using Weblate (Swedish)
Currently translated at 94.5% (1033 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-12 23:20:22 +01:00
Максим Горпиніч 8820fac6a6 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1093 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-03-12 23:20:21 +01:00
Jan Schoenfeld d6950eab21 Translated using Weblate (German)
Currently translated at 100.0% (1093 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-03-12 23:20:20 +01:00
Miró Allard 03f5038882 Translated using Weblate (Swedish)
Currently translated at 94.6% (1034 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-12 23:20:19 +01:00
advplyr 2685d12ca3 Replace enable watcher setting strings, update enable automatic backups #4095 2025-03-12 17:20:11 -05:00
advplyr e504bb09eb Merge pull request #4106 from Roukanken42/fix/loading-epub-covers
Fix: Load epub covers via cover-image property
2025-03-12 17:06:00 -05:00
advplyr 90d1aab1de Merge pull request #4097 from Vito0912/master
fix updating progress not updating finishedAt
2025-03-12 17:01:10 -05:00
advplyr a3cd9e4440 Update confirm mark as finished to use translation #4017 2025-03-11 17:52:42 -05:00
Roukanken b86797a245 Fix: Load epub covers via cover-image property 2025-03-11 21:05:21 +01:00
Vito0912 953f21ed53 fix updating progress not updating finishedAt 2025-03-10 13:58:52 +01:00
advplyr ef77a88fce Merge pull request #4093 from gitting/master
Fix spelling
2025-03-09 17:09:54 -05:00
IUser e7ca6a4ea9 Fix spelling 2025-03-09 14:01:53 -07:00
advplyr e74b6982f9 Merge pull request #4046 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-03-08 18:04:15 -06:00
Troj@ 438364dafb Translated using Weblate (Belarusian)
Currently translated at 56.2% (615 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-03-09 00:04:04 +00:00
Troj@ c8f79dca6c Translated using Weblate (Belarusian)
Currently translated at 53.0% (580 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-03-09 00:04:04 +00:00
Phantomwise 9a2eb24d4b Translated using Weblate (French)
Currently translated at 99.7% (1090 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2025-03-09 00:04:03 +00:00
Andreas Morell-Reng e5f7f0812e Translated using Weblate (Danish)
Currently translated at 100.0% (1093 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2025-03-09 00:04:02 +00:00
Simple16 4705e73714 Translated using Weblate (Russian)
Currently translated at 100.0% (1093 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-03-09 00:04:01 +00:00
Troj@ 738d936243 Translated using Weblate (Belarusian)
Currently translated at 51.6% (564 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-03-09 00:03:59 +00:00
Troj@ 507338d906 Translated using Weblate (Belarusian)
Currently translated at 50.9% (557 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-03-09 00:03:59 +00:00
Troj@ 776819ad52 Translated using Weblate (Belarusian)
Currently translated at 48.3% (529 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-03-09 00:03:58 +00:00
Jan-Eric Myhrgren f8af265440 Translated using Weblate (Swedish)
Currently translated at 94.6% (1034 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-09 00:03:57 +00:00
Marcus skoding d426ed101e Translated using Weblate (Swedish)
Currently translated at 94.2% (1030 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-09 00:03:56 +00:00
Fredrik Lindqvist e4eead75b1 Translated using Weblate (Swedish)
Currently translated at 94.2% (1030 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-09 00:03:56 +00:00
Jan-Eric Myhrgren 4dd2f7cf18 Translated using Weblate (Swedish)
Currently translated at 94.2% (1030 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-09 00:03:55 +00:00
Troj@ 9c9c60a5bd Translated using Weblate (Belarusian)
Currently translated at 47.5% (520 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-03-09 00:03:54 +00:00
Prashant Mhatre 4d88deabd2 Translated using Weblate (Hindi)
Currently translated at 8.3% (91 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hi/
2025-03-09 00:03:54 +00:00
Troj@ 3a539a4dd3 Translated using Weblate (Belarusian)
Currently translated at 43.3% (474 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-03-09 00:03:53 +00:00
phewi a57addccae Translated using Weblate (Finnish)
Currently translated at 55.3% (605 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-03-09 00:03:52 +00:00
Troj@ a73372c51d Translated using Weblate (Belarusian)
Currently translated at 40.4% (442 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-03-09 00:03:51 +00:00
thehijacker dea457adcd Translated using Weblate (Slovenian)
Currently translated at 100.0% (1093 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2025-03-09 00:03:50 +00:00
phewi 20e007ecd4 Translated using Weblate (Finnish)
Currently translated at 51.6% (565 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-03-09 00:03:49 +00:00
Ricky Tigg 4ee6b6d49b Translated using Weblate (Finnish)
Currently translated at 51.6% (565 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-03-09 00:03:48 +00:00
Jan-Eric Myhrgren e5cb43bd75 Translated using Weblate (Swedish)
Currently translated at 92.6% (1013 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-09 00:03:47 +00:00
Troj@ fb877779d1 Translated using Weblate (Belarusian)
Currently translated at 35.4% (388 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-03-09 00:03:46 +00:00
Krissse10 abcc2eb22b Translated using Weblate (Swedish)
Currently translated at 92.6% (1013 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-03-09 00:03:46 +00:00
Ranforingus a21b6e1dec Translated using Weblate (Dutch)
Currently translated at 99.5% (1088 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/
2025-03-09 00:03:44 +00:00
advplyr ddd8f15f2b Merge pull request #4088 from nichwall/checkRemoveAuthors_fix
Fix empty series delete check
2025-03-08 18:03:36 -06:00
advplyr 8f308c6180 Close RSS feeds after removing empty series 2025-03-08 17:47:47 -06:00
advplyr e93b18745e Merge pull request #4089 from nichwall/logger_cleanup
Simplify log level determination
2025-03-08 17:25:42 -06:00
Nicholas Wallace 10acf28fa6 Simplify log level determination 2025-03-08 12:46:36 -07:00
Nicholas Wallace 84e20e041c Fix: empty series delete flakiness 2025-03-08 11:16:34 -07:00
Nicholas Wallace 167617cce0 Add: transaction to empty author remove 2025-03-08 10:43:27 -07:00
advplyr d3fd19da65 Fixes for screen readers on podcast page and episodes table 2025-03-07 17:23:18 -06:00
advplyr 31be775c32 Merge pull request #4082 from mikiher/fix-lazy-episode-row-rtl
Fix RTL issue in LazyEpisodeRow
2025-03-07 16:56:18 -06:00
mikiher 81cd6f6c7d Fix RTL issue in LazyEpisodeRow 2025-03-07 21:14:50 +02:00
advplyr 4fdb37c9dc Merge pull request #4078 from advplyr/validate_migration_files
Update migration manager to validate migration files #4042
2025-03-06 17:35:12 -06:00
advplyr c29935e57b Update migration manager to validate migration files #4042 2025-03-06 17:24:33 -06:00
advplyr d41b48c89a Merge pull request #4075 from Vito0912/feat/fixCrashCustomProvider
Fixes search not returning results if description field is not provided by a custom provider
2025-03-04 17:53:58 -06:00
advplyr b17e6010fd Add validation for custom metadata provider responses 2025-03-04 17:50:40 -06:00
Vito0912 a296ac6132 fix crash 2025-03-04 18:06:58 +01:00
advplyr 5746e848b0 Fix:Trim whitespace from custom metadata provider name & url #4069 2025-03-02 17:13:27 -06:00
advplyr c6b5d4aa26 Update author by string translation #4017 2025-03-01 17:48:11 -06:00
advplyr 43a507faa8 Merge pull request #4030 from 4ch1m/add_filename_sorting_for_podcasts-view
new sort option for podcasts view (-> sort by filename)
2025-02-28 17:45:43 -06:00
advplyr 828d5d2afc Update episode row to show filename when sorting by filename 2025-02-28 17:42:56 -06:00
advplyr 6075f2686f Merge pull request #3546 from justcallmelarry/master
API PATCH /me/progress/:id - allow providing createdAt and respect provided finishedAt when syncing progress
2025-02-28 17:25:46 -06:00
advplyr ae3517bcde Merge pull request #4055 from nichwall/2_15_0_migration_fix
Fix: flaky 2.15.0 migration test
2025-02-27 18:28:21 -06:00
Nicholas Wallace 0a00ebcde1 Fix: flaky 2.15.0 migration test 2025-02-26 21:40:56 -07:00
advplyr 68ef0f83e1 Update select all in feed modal to check downloading 2025-02-26 18:00:36 -06:00
advplyr e4a34b0145 Merge pull request #4041 from nichwall/podcast_queue_no_duplicates
Prevent duplicate episodes from being added to queue
2025-02-26 17:58:27 -06:00
advplyr 0ca65d1f79 Show download icon for queued/downloaded episodes in rss feed modal 2025-02-26 17:56:17 -06:00
advplyr bd3d396f37 Merge pull request #4035 from nichwall/podcast_episode_play_order
Play first podcast episode in table
2025-02-25 17:31:48 -06:00
advplyr fd1c8ee513 Update episode list to come from component ref, populate queue from table order when playing episode 2025-02-25 17:25:56 -06:00
advplyr b0045b5b8b Update browser confirm prompts to use confirm prompt modal instead 2025-02-24 17:44:17 -06:00
Nicholas Wallace 6674189acd Add: prevent duplicates from being added to queue 2025-02-23 19:23:26 -07:00
advplyr c7d8021a16 Version bump v2.19.5 2025-02-23 17:20:30 -06:00
advplyr 9e83ad25b9 Merge pull request #4015 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-02-23 17:18:50 -06:00
Troja 2eccb9465c Translated using Weblate (Belarusian)
Currently translated at 31.0% (339 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-02-24 00:01:03 +01:00
Максим Горпиніч 599b6bd6ad Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1093 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-02-24 00:01:03 +01:00
Jan-Eric Myhrgren e01ac489fb Translated using Weblate (Swedish)
Currently translated at 92.6% (1013 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-02-24 00:01:03 +01:00
biuklija 271dbc4764 Translated using Weblate (Croatian)
Currently translated at 100.0% (1093 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-02-24 00:01:03 +01:00
Vito0912 84c2931434 Translated using Weblate (German)
Currently translated at 99.9% (1092 of 1093 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-02-24 00:01:03 +01:00
burghy86 38483c9269 Translated using Weblate (Italian)
Currently translated at 100.0% (1092 of 1092 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2025-02-24 00:01:03 +01:00
mickeynos b2e97d70df Translated using Weblate (Czech)
Currently translated at 99.5% (1087 of 1092 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2025-02-24 00:01:03 +01:00
Troja 78aafe038d Translated using Weblate (Belarusian)
Currently translated at 28.6% (313 of 1092 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-02-24 00:01:03 +01:00
Максим Горпиніч 34f7ddfdd7 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1092 of 1092 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-02-24 00:01:03 +01:00
Jan-Eric Myhrgren 0e9777feec Translated using Weblate (Swedish)
Currently translated at 92.0% (1005 of 1092 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-02-24 00:01:03 +01:00
Troja 6351fd8d7b Translated using Weblate (Belarusian)
Currently translated at 25.6% (280 of 1091 strings)

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

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-02-24 00:01:03 +01:00
Michał Rączka-Dudek 2b36caf096 Translated using Weblate (Polish)
Currently translated at 74.5% (812 of 1089 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-02-24 00:01:03 +01:00
Charlie f87a0bfc2f Translated using Weblate (French)
Currently translated at 99.6% (1085 of 1089 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2025-02-24 00:01:03 +01:00
Nicholas W b109b2edee Added translation using Weblate (Romanian) 2025-02-24 00:01:03 +01:00
Milo Ivir 7795bf25d0 Translated using Weblate (Croatian)
Currently translated at 100.0% (1089 of 1089 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-02-24 00:01:03 +01:00
advplyr 3d5c02ae7c Merge pull request #4037 from mikiher/route-to-library-if-last-issue-removed
Route from Issues to Library page after last issue was removed
2025-02-23 17:00:57 -06:00
advplyr 373d14a49e Merge pull request #4034 from nichwall/custom-metadata-provider-logging
Add: log custom metadata provider to match other providers
2025-02-23 16:58:07 -06:00
advplyr a17127f078 Merge pull request #4031 from nichwall/temp_file_ignore_refactor
Refactor ignore file logic
2025-02-23 16:56:09 -06:00
advplyr 20f812403f Add fileUtils recurseFiles and shouldIgnoreFile tests 2025-02-23 16:53:11 -06:00
advplyr a864c6bcc6 Merge pull request #4020 from mikiher/invalidate-count-cache-on-entity-update
Invalidate count cache on entity update
2025-02-23 15:21:36 -06:00
mikiher 6c0e42db49 Route from Issues to Library if last issue is removed 2025-02-23 18:06:36 +02:00
mikiher 364ccd85fe Use count cache only when no filter is set 2025-02-23 08:53:57 +02:00
mikiher d6b58c2f10 Revert "Invalidate count cache on entity update"
This reverts commit e8b60defb6.
2025-02-23 08:03:10 +02:00
Nicholas Wallace 72169990ac Fix: double reverse of array 2025-02-22 22:06:51 -07:00
Nicholas Wallace 5f105dc6cc Change: Play button for podcast picks first episode in table 2025-02-22 21:50:37 -07:00
Nicholas Wallace 706b2d7d72 Add: store for filtered podcast episodes 2025-02-22 21:50:09 -07:00
advplyr 64185b7519 Add backup schedule string translation #4017 2025-02-22 17:53:05 -06:00
advplyr e1b3b657c4 Merge pull request #4027 from Alexshch09/Add-admin-auth-to-LibraryController
fix(auth): Add admin-level auth to LibraryController 'delete', 'update' and 'delete items with issues'
2025-02-22 17:45:38 -06:00
Nicholas Wallace 4662fc5244 Add: log custom metadata provider to match other providers 2025-02-22 14:48:13 -07:00
Nicholas Wallace 13c20e0cdd Add: generic function to ignor files 2025-02-22 12:28:51 -07:00
Achim 007691ffe5 add "sort by filename" 2025-02-22 17:08:29 +01:00
advplyr 19a65dba98 Update backup schedule description translations #4017 2025-02-21 18:18:54 -06:00
Mike Smith 799879d67d prevent long author strings from pushing the player controls down by truncating (#3944)
* prevent long author strings from pushing the player controls down by truncating

* move truncate to single author, instead of the main container
2025-02-21 17:45:29 -06:00
alexshch09 452d354b52 fix(auth): Add admin-level auth to LibraryController delete update and issue removal 2025-02-22 00:44:52 +01:00
advplyr 9d7f44f73a Fix RSS Feed Open query 2025-02-21 17:39:36 -06:00
mikiher e8b60defb6 Invalidate count cache on entity update 2025-02-21 09:45:10 +02:00
advplyr 0cc2e39367 Update en-us string order 2025-02-20 17:59:09 -06:00
advplyr a34b01fcb4 Add localization strings for Cover Provider and Activities #4017 2025-02-20 17:45:33 -06:00
advplyr 7919a8b581 Fix get podcast library items endpoint when not including a limit query param #4014 2025-02-20 17:40:54 -06:00
advplyr 565eb423ee Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2025-02-19 17:44:21 -06:00
advplyr 42b0e31b4a Version bump v2.19.4 2025-02-19 17:44:14 -06:00
advplyr 97a8959bf8 Merge pull request #3974 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-02-19 17:16:19 -06:00
advplyr b5b99cbaca Merge pull request #4008 from mikiher/resort-after-title-change
Re-sort title-sorted bookshelf after title change
2025-02-19 17:15:45 -06:00
advplyr f04ef320aa Restore scroll position on title change re-sort 2025-02-19 17:12:19 -06:00
polarwood 4e33059ac8 Translated using Weblate (Turkish)
Currently translated at 18.8% (205 of 1089 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/tr/
2025-02-19 23:59:53 +01:00
Jan-Eric Myhrgren 699644322b Translated using Weblate (Swedish)
Currently translated at 91.9% (1001 of 1089 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-02-19 23:59:52 +01:00
biuklija 49ba364b2a Translated using Weblate (Croatian)
Currently translated at 100.0% (1089 of 1089 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-02-19 23:59:52 +01:00
Armanc Keser adb3967f89 Translated using Weblate (Turkish)
Currently translated at 14.2% (155 of 1089 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/tr/
2025-02-19 23:59:51 +01:00
polarwood cfdcac9475 Translated using Weblate (Turkish)
Currently translated at 13.0% (142 of 1089 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/tr/
2025-02-19 23:59:51 +01:00
Jan-Eric Myhrgren b1d57bc0b3 Translated using Weblate (Swedish)
Currently translated at 90.6% (987 of 1089 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-02-19 23:59:50 +01:00
A L f7cea8ca12 Translated using Weblate (Bulgarian)
Currently translated at 77.2% (841 of 1089 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/bg/
2025-02-19 23:59:50 +01:00
Ivan Penchev 293440006b Translated using Weblate (Bulgarian)
Currently translated at 77.2% (841 of 1089 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/bg/
2025-02-19 23:59:49 +01:00
Ivan Penchev 45f7f54b6c Translated using Weblate (Bulgarian)
Currently translated at 70.8% (772 of 1089 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/bg/
2025-02-19 23:59:49 +01:00
advplyr bb5e16157c Merge pull request #4005 from mikiher/fix-triggers-for-new-databases
Add title triggers in new databases
2025-02-19 16:59:41 -06:00
mikiher 2e8cb46c57 Resort title-sorted bookshelf after title change 2025-02-19 21:04:07 +02:00
mikiher f9c0e52f18 Add title triggers in new databases 2025-02-19 17:39:32 +02:00
advplyr 6290cfaeb1 Auto format 2025-02-18 17:19:06 -06:00
advplyr fd3d4f5fcf Merge pull request #3978 from sloped/fix/detect-http-https-upgrades
fix: allow upgrading HTTP to HTTPS for redirects
2025-02-18 17:18:36 -06:00
advplyr 9f9bee2ddc Merge pull request #3996 from mikiher/optimize-podcast-queries
Improve podcast library page query performance on title, titleIgnorePrefix, and addedAt sort orders
2025-02-18 17:04:45 -06:00
mikiher 568bf0254d Change migration version to v2.19.4 2025-02-18 07:57:46 +02:00
advplyr 79f4db5ff3 Version bump v2.19.3 2025-02-16 17:01:45 -06:00
mikiher 7038f5730f Set title[IgnorePrefix] when a podcast libraryItem is created 2025-02-16 14:57:05 +02:00
mikiher 0a8186cbda Add ANALYZE to database init sequence 2025-02-16 13:38:54 +02:00
mikiher 659164003f Clear LibraryItemsPodcastFilters count cache after podcast[Episode] is created or destroryed 2025-02-16 13:27:47 +02:00
mikiher de5d8650e8 Add profiling to podcast library filterdata queries 2025-02-16 12:47:23 +02:00
mikiher bacefb5f6f Format PodcastScanner (Pretteier-only changes) 2025-02-16 12:41:47 +02:00
mikiher 0169bf5518 Update podcast.numEpisodes when episodes are created or destroyed 2025-02-16 12:38:44 +02:00
mikiher 8f192b1b17 Add profiling to podcasts and podcast episodes page queries 2025-02-16 09:46:32 +02:00
mikiher 21343b5aa0 Add count cache to libraryItemsPodcastQueries 2025-02-16 09:40:29 +02:00
mikiher a5508cdc4c Remove unnecessary 'distinct: true' from podcast episodes page query 2025-02-16 09:32:00 +02:00
mikiher bd4f48ec39 Add required: true to includes in podcast episodes page query 2025-02-16 09:29:57 +02:00
mikiher cb9fc3e0d1 Replace numEpisodesIncomplete subquery with cached user progress calculation 2025-02-16 09:22:06 +02:00
mikiher 707533df8f Remove numEpisodes subquery from podcasst page query 2025-02-16 09:15:54 +02:00
mikiher 2e48ec0dde Use libraryItem.title[IgnorePrefix] for sorting podcasts page query 2025-02-16 09:08:27 +02:00
mikiher f1e46a351b Separate feed query from podcasts page query 2025-02-16 09:05:54 +02:00
mikiher da8fd2d9d5 Set podcastId when mediaProgress is created 2025-02-16 08:57:10 +02:00
mikiher f1de307bf9 Update cached user whenever mediaProgress is removed 2025-02-16 08:52:33 +02:00
mikiher 7282afcfde Add podcastId to mediaProgress model 2025-02-16 08:42:09 +02:00
mikiher e2f1aeed75 Add numEpisodes to podcast model 2025-02-16 08:38:03 +02:00
mikiher 23a750214f Add migration in preparation for podcast query optimization 2025-02-16 08:35:51 +02:00
advplyr 6a7418ad41 Fix:Edit book cover tab local images overflowing #3986 2025-02-15 17:55:56 -06:00
advplyr 8b00c16062 Merge pull request #3993 from mikiher/fix-stringify-sequelize-query
fix stringifySequelizeQuery and add tests
2025-02-15 17:24:19 -06:00
mikiher 8ee5646d79 fix stringifySequelizeQuery and add tests 2025-02-15 23:57:27 +02:00
advplyr 373551fb74 Merge pull request #3985 from advplyr/fix-quick-match-all-crash
Fix server crash when quick match all updates series sequence #3961
2025-02-14 17:22:29 -06:00
advplyr d9b206fe1c Fix server crash when quick match all updates existing series sequence #3961 2025-02-14 16:56:37 -06:00
advplyr fe4e0145c9 Merge pull request #3984 from advplyr/fix-chapter-end-sleep-timer
Fix chapter end sleep timer sometimes not stopping #3969
2025-02-14 16:39:26 -06:00
advplyr c4d99a118f Fix chapter end sleep timer sometimes not stopping #3969 2025-02-14 16:24:39 -06:00
advplyr b96226966b Merge pull request #3980 from advplyr/stringify_sequelize_query
Fix count cache by stringify Symbols #3979
2025-02-13 18:24:36 -06:00
Conner McCall f460297daf fix: allow upgrading HTTP to HTTPS for redirects
Re: #3142 and #3658

When adding certain podcasts, the server encountered a redirect from an HTTP URL to an HTTPS domain, causing an error that was difficult for end users to diagnose without inspecting logs or HTML.

This issue arose due to SSRF security measures that blocked such redirects. Instead of failing in these cases, we now detect when the error is caused by an HTTP-to-HTTPS upgrade. If confirmed, we upgrade the initial URL to HTTPS and resend the request.

Since this change does not allow cross-protocol or cross-domain redirections, it remains secure while resolving most of the reported issues.

Affected podcasts that are now fixed:

- D&D is for Nerds
- The New Yorker: The Writer's Voice - New Fiction from The New Yorker
- Radiolab
2025-02-13 09:19:02 -06:00
Lauri Vuorela 2fdab39e27 Merge branch 'advplyr:master' into master 2024-10-29 22:08:01 +01:00
Lauri Vuorela 9b01d11b27 allow setting createdAt and respect set finishedAt when syncing progress 2024-10-22 23:58:09 +02:00
108 changed files with 4283 additions and 1054 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
steps: steps:
- name: Check issue headings - name: Check issue headings
uses: actions/github-script@v6 uses: actions/github-script@v7
with: with:
script: | script: |
const issueBody = context.payload.issue.body || ""; const issueBody = context.payload.issue.body || "";
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
+10 -11
View File
@@ -1,5 +1,4 @@
--- ---
name: Build and Push Docker Image name: Build and Push Docker Image
on: on:
@@ -11,7 +10,7 @@ on:
required: true required: true
default: 'latest' default: 'latest'
push: push:
branches: [main,master] branches: [main, master]
tags: tags:
- 'v*.*.*' - 'v*.*.*'
# Only build when files in these directories have been changed # Only build when files in these directories have been changed
@@ -23,16 +22,16 @@ on:
jobs: jobs:
build: build:
if: "!contains(github.event.head_commit.message, 'skip ci')" if: ${{ !contains(github.event.head_commit.message, 'skip ci') && github.repository == 'advplyr/audiobookshelf' }}
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- name: Check out - name: Check out
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v5
with: with:
images: advplyr/audiobookshelf,ghcr.io/${{ github.repository_owner }}/audiobookshelf images: advplyr/audiobookshelf,ghcr.io/${{ github.repository_owner }}/audiobookshelf
tags: | tags: |
@@ -40,13 +39,13 @@ jobs:
type=semver,pattern={{version}} type=semver,pattern={{version}}
- name: Setup QEMU - name: Setup QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Cache Docker layers - name: Cache Docker layers
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: /tmp/.buildx-cache path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }} key: ${{ runner.os }}-buildx-${{ github.sha }}
@@ -54,20 +53,20 @@ jobs:
${{ runner.os }}-buildx- ${{ runner.os }}-buildx-
- name: Login to Dockerhub - name: Login to Dockerhub
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }} password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Login to ghcr - name: Login to ghcr
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GHCR_PASSWORD }} password: ${{ secrets.GHCR_PASSWORD }}
- name: Build image - name: Build image
uses: docker/build-push-action@v3 uses: docker/build-push-action@v6
with: with:
tags: ${{ github.event.inputs.tags || steps.meta.outputs.tags }} tags: ${{ github.event.inputs.tags || steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
+2 -1
View File
@@ -20,7 +20,8 @@ jobs:
- name: Set up node - name: Set up node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20' node-version: 20
cache: 'npm'
# The only argument is the `directory`, which is where the i18n files are # The only argument is the `directory`, which is where the i18n files are
# stored. # stored.
+5 -4
View File
@@ -18,14 +18,15 @@ jobs:
name: build and test name: build and test
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: setup nade - name: setup node
uses: actions/setup-node@v3 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: 'npm'
- name: install pkg (using yao-pkg fork for targetting node20) - name: install pkg (using yao-pkg fork for targeting node20)
run: npm install -g @yao-pkg/pkg run: npm install -g @yao-pkg/pkg
- name: get client dependencies - name: get client dependencies
+7
View File
@@ -18,15 +18,22 @@ jobs:
# Check out the repository # Check out the repository
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
# Set up node to run the javascript # Set up node to run the javascript
- name: Set up node - name: Set up node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
# Install Redocly CLI # Install Redocly CLI
- name: Install Redocly CLI - name: Install Redocly CLI
run: npm install -g @redocly/cli@latest run: npm install -g @redocly/cli@latest
# Perform linting for exploded spec # Perform linting for exploded spec
- name: Run linting for exploded spec - name: Run linting for exploded spec
run: redocly lint docs/root.yaml --format=github-actions run: redocly lint docs/root.yaml --format=github-actions
# Perform linting for bundled spec # Perform linting for bundled spec
- name: Run linting for bundled spec - name: Run linting for bundled spec
run: redocly lint docs/openapi.json --format=github-actions run: redocly lint docs/openapi.json --format=github-actions
+1
View File
@@ -29,6 +29,7 @@ jobs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20 node-version: 20
cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
+31 -4
View File
@@ -419,7 +419,7 @@ export default {
this.postScrollTimeout = setTimeout(this.postScroll, 500) this.postScrollTimeout = setTimeout(this.postScroll, 500)
}, },
async resetEntities() { async resetEntities(scrollPositionToRestore) {
if (this.isFetchingEntities) { if (this.isFetchingEntities) {
this.pendingReset = true this.pendingReset = true
return return
@@ -437,6 +437,12 @@ export default {
await this.loadPage(0) await this.loadPage(0)
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf) var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
this.mountEntities(0, lastBookIndex) this.mountEntities(0, lastBookIndex)
if (scrollPositionToRestore) {
if (window.bookshelf) {
window.bookshelf.scrollTop = scrollPositionToRestore
}
}
}, },
async rebuild() { async rebuild() {
this.initSizeData() this.initSizeData()
@@ -444,9 +450,8 @@ export default {
var lastBookIndex = Math.min(this.totalEntities, this.booksPerFetch) var lastBookIndex = Math.min(this.totalEntities, this.booksPerFetch)
this.destroyEntityComponents() this.destroyEntityComponents()
await this.loadPage(0) await this.loadPage(0)
var bookshelfEl = document.getElementById('bookshelf') if (window.bookshelf) {
if (bookshelfEl) { window.bookshelf.scrollTop = 0
bookshelfEl.scrollTop = 0
} }
this.mountEntities(0, lastBookIndex) this.mountEntities(0, lastBookIndex)
}, },
@@ -547,6 +552,15 @@ export default {
if (this.entityName === 'items' || this.entityName === 'series-books') { if (this.entityName === 'items' || this.entityName === 'series-books') {
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id) var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
if (indexOf >= 0) { if (indexOf >= 0) {
if (this.entityName === 'items' && this.orderBy === 'media.metadata.title') {
const curTitle = this.entities[indexOf].media.metadata?.title
const newTitle = libraryItem.media.metadata?.title
if (curTitle != newTitle) {
console.log('Title changed. Re-sorting...')
this.resetEntities(this.currScrollTop)
return
}
}
this.entities[indexOf] = libraryItem this.entities[indexOf] = libraryItem
if (this.entityComponentRefs[indexOf]) { if (this.entityComponentRefs[indexOf]) {
this.entityComponentRefs[indexOf].setEntity(libraryItem) this.entityComponentRefs[indexOf].setEntity(libraryItem)
@@ -554,6 +568,18 @@ export default {
} }
} }
}, },
routeToBookshelfIfLastIssueRemoved() {
if (this.totalEntities === 0) {
const currentRouteQuery = this.$route.query
if (currentRouteQuery?.filter && currentRouteQuery.filter === 'issues') {
this.$nextTick(() => {
console.log('Last issue removed. Redirecting to library bookshelf')
this.$router.push(`/library/${this.currentLibraryId}/bookshelf`)
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
})
}
}
},
libraryItemRemoved(libraryItem) { libraryItemRemoved(libraryItem) {
if (this.entityName === 'items' || this.entityName === 'series-books') { if (this.entityName === 'items' || this.entityName === 'series-books') {
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id) var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
@@ -564,6 +590,7 @@ export default {
this.executeRebuild() this.executeRebuild()
} }
} }
this.routeToBookshelfIfLastIssueRemoved()
}, },
libraryItemsAdded(libraryItems) { libraryItemsAdded(libraryItems) {
console.log('items added', libraryItems) console.log('items added', libraryItems)
+13 -8
View File
@@ -13,7 +13,7 @@
</div> </div>
<div class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5"> <div class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5">
<span class="material-symbols text-sm">person</span> <span class="material-symbols text-sm">person</span>
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div> <div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate">{{ podcastAuthor }}</div>
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate"> <div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate">
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link> <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
</div> </div>
@@ -85,7 +85,8 @@ export default {
displayTitle: null, displayTitle: null,
currentPlaybackRate: 1, currentPlaybackRate: 1,
syncFailedToast: null, syncFailedToast: null,
coverAspectRatio: 1 coverAspectRatio: 1,
lastChapterId: null
} }
}, },
computed: { computed: {
@@ -236,12 +237,16 @@ export default {
} }
}, 1000) }, 1000)
}, },
checkChapterEnd(time) { checkChapterEnd() {
if (!this.currentChapter) return if (!this.currentChapter) return
const chapterEndTime = this.currentChapter.end
const tolerance = 0.75 // Track chapter transitions by comparing current chapter with last chapter
if (time >= chapterEndTime - tolerance) { if (this.lastChapterId !== this.currentChapter.id) {
this.sleepTimerEnd() // Chapter changed - if we had a previous chapter, this means we crossed a boundary
if (this.lastChapterId) {
this.sleepTimerEnd()
}
this.lastChapterId = this.currentChapter.id
} }
}, },
sleepTimerEnd() { sleepTimerEnd() {
@@ -301,7 +306,7 @@ export default {
} }
if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER && this.sleepTimerSet) { if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER && this.sleepTimerSet) {
this.checkChapterEnd(time) this.checkChapterEnd()
} }
}, },
setDuration(duration) { setDuration(duration) {
@@ -11,7 +11,7 @@
</template> </template>
</div> </div>
</div> </div>
<div v-if="publishedYear" class="flex py-0.5"> <div v-if="publishedYear" role="paragraph" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32"> <div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublishYear }}</span> <span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublishYear }}</span>
</div> </div>
@@ -19,7 +19,7 @@
{{ publishedYear }} {{ publishedYear }}
</div> </div>
</div> </div>
<div v-if="publisher" class="flex py-0.5"> <div v-if="publisher" role="paragraph" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32"> <div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublisher }}</span> <span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublisher }}</span>
</div> </div>
@@ -27,7 +27,7 @@
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=publishers.${$encode(publisher)}`" class="hover:underline">{{ publisher }}</nuxt-link> <nuxt-link :to="`/library/${libraryId}/bookshelf?filter=publishers.${$encode(publisher)}`" class="hover:underline">{{ publisher }}</nuxt-link>
</div> </div>
</div> </div>
<div v-if="podcastType" class="flex py-0.5"> <div v-if="podcastType" role="paragraph" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32"> <div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPodcastType }}</span> <span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPodcastType }}</span>
</div> </div>
@@ -65,7 +65,7 @@
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=languages.${$encode(language)}`" class="hover:underline">{{ language }}</nuxt-link> <nuxt-link :to="`/library/${libraryId}/bookshelf?filter=languages.${$encode(language)}`" class="hover:underline">{{ language }}</nuxt-link>
</div> </div>
</div> </div>
<div v-if="tracks.length || (isPodcast && totalPodcastDuration)" class="flex py-0.5"> <div v-if="tracks.length || (isPodcast && totalPodcastDuration)" role="paragraph" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32"> <div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span> <span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
</div> </div>
@@ -73,7 +73,7 @@
{{ durationPretty }} {{ durationPretty }}
</div> </div>
</div> </div>
<div class="flex py-0.5"> <div role="paragraph" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32"> <div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelSize }}</span> <span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelSize }}</span>
</div> </div>
+11 -9
View File
@@ -1,23 +1,25 @@
<template> <template>
<div ref="wrapper" class="relative" v-click-outside="clickOutside"> <div ref="wrapper" class="relative" v-click-outside="clickOutside">
<button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none cursor-pointer" aria-haspopup="listbox" aria-expanded="true" aria-labelledby="listbox-label" @click.prevent="showMenu = !showMenu"> <div class="relative h-9">
<span class="flex items-center justify-between"> <button type="button" class="relative w-full h-full border border-gray-500 hover:border-gray-400 rounded shadow-sm pl-3 pr-3 py-0 text-left focus:outline-none cursor-pointer" aria-haspopup="menu" :aria-expanded="showMenu" @click.prevent="showMenu = !showMenu">
<span class="block truncate text-xs">{{ selectedText }}</span> <span class="flex items-center justify-between">
</span> <span class="block truncate text-xs">{{ selectedText }}</span>
</span>
</button>
<span v-if="selected === 'all'" class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"> <span v-if="selected === 'all'" class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg> </svg>
</span> </span>
<div v-else class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-300" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected"> <button v-else type="button" :aria-label="$strings.ButtonClearFilter" class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer text-gray-400 hover:text-gray-300" @mousedown.stop @mouseup.stop @click.stop.prevent="clearSelected">
<span class="material-symbols" style="font-size: 1.1rem">close</span> <span class="material-symbols" style="font-size: 1.1rem">close</span>
</div> </button>
</button> </div>
<div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 text-sm ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none"> <div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 text-sm ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label"> <ul class="h-full w-full" role="menu">
<template v-for="item in items"> <template v-for="item in items">
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="option" @click="clickedOption(item)"> <li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="menuitem" @click="clickedOption(item)">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="font-normal ml-3 block truncate">{{ item.text }}</span> <span class="font-normal ml-3 block truncate">{{ item.text }}</span>
</div> </div>
@@ -10,14 +10,14 @@
<div class="w-full p-8"> <div class="w-full p-8">
<div class="flex mb-2"> <div class="flex mb-2">
<div class="w-3/4 p-1"> <div class="w-3/4 p-1">
<ui-text-input-with-label v-model="newName" :label="$strings.LabelName" /> <ui-text-input-with-label v-model="newName" :label="$strings.LabelName" trim-whitespace />
</div> </div>
<div class="w-1/4 p-1"> <div class="w-1/4 p-1">
<ui-text-input-with-label value="Book" readonly :label="$strings.LabelMediaType" /> <ui-text-input-with-label value="Book" readonly :label="$strings.LabelMediaType" />
</div> </div>
</div> </div>
<div class="w-full mb-2 p-1"> <div class="w-full mb-2 p-1">
<ui-text-input-with-label v-model="newUrl" label="URL" /> <ui-text-input-with-label v-model="newUrl" label="URL" trim-whitespace />
</div> </div>
<div class="w-full mb-2 p-1"> <div class="w-full mb-2 p-1">
<ui-text-input-with-label v-model="newAuthHeaderValue" :label="$strings.LabelProviderAuthorizationValue" type="password" /> <ui-text-input-with-label v-model="newAuthHeaderValue" :label="$strings.LabelProviderAuthorizationValue" type="password" />
@@ -65,7 +65,11 @@ export default {
} }
}, },
methods: { methods: {
submitForm() { async submitForm() {
// Remove focus from active input
document.activeElement?.blur?.()
await this.$nextTick()
if (!this.newName || !this.newUrl) { if (!this.newName || !this.newUrl) {
this.$toast.error(this.$strings.ToastProviderNameAndUrlRequired) this.$toast.error(this.$strings.ToastProviderNameAndUrlRequired)
return return
@@ -18,7 +18,7 @@
<ui-textarea-with-label v-model="newCollectionDescription" :label="$strings.LabelDescription" /> <ui-textarea-with-label v-model="newCollectionDescription" :label="$strings.LabelDescription" />
</div> </div>
</div> </div>
<div class="absolute bottom-0 left-0 right-0 w-full py-2 px-4 flex"> <div class="absolute bottom-0 left-0 right-0 w-full py-4 px-4 flex">
<ui-btn v-if="userCanDelete" small color="error" type="button" @click.stop="removeClick">{{ $strings.ButtonRemove }}</ui-btn> <ui-btn v-if="userCanDelete" small color="error" type="button" @click.stop="removeClick">{{ $strings.ButtonRemove }}</ui-btn>
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn color="success" type="submit">{{ $strings.ButtonSave }}</ui-btn> <ui-btn color="success" type="submit">{{ $strings.ButtonSave }}</ui-btn>
@@ -94,21 +94,32 @@ export default {
this.newCollectionDescription = this.collection.description || '' this.newCollectionDescription = this.collection.description || ''
}, },
removeClick() { removeClick() {
if (confirm(this.$getString('MessageConfirmRemoveCollection', [this.collectionName]))) { const payload = {
this.processing = true message: this.$getString('MessageConfirmRemoveCollection', [this.collectionName]),
this.$axios callback: (confirmed) => {
.$delete(`/api/collections/${this.collection.id}`) if (confirmed) {
.then(() => { this.deleteCollection()
this.processing = false }
this.show = false },
this.$toast.success(this.$strings.ToastCollectionRemoveSuccess) type: 'yesNo'
})
.catch((error) => {
console.error('Failed to remove collection', error)
this.processing = false
this.$toast.error(this.$strings.ToastRemoveFailed)
})
} }
this.$store.commit('globals/setConfirmPrompt', payload)
},
deleteCollection() {
this.processing = true
this.$axios
.$delete(`/api/collections/${this.collection.id}`)
.then(() => {
this.show = false
this.$toast.success(this.$strings.ToastCollectionRemoveSuccess)
})
.catch((error) => {
console.error('Failed to remove collection', error)
this.$toast.error(this.$strings.ToastRemoveFailed)
})
.finally(() => {
this.processing = false
})
}, },
submitForm() { submitForm() {
if (this.newCollectionName === this.collectionName && this.newCollectionDescription === this.collection.description) { if (this.newCollectionName === this.collectionName && this.newCollectionDescription === this.collection.description) {
+2 -2
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative"> <div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative">
<div class="flex flex-col sm:flex-row mb-4"> <div class="flex flex-col sm:flex-row mb-4">
<div class="relative self-center"> <div class="relative self-center md:self-start">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, libraryItemUpdatedAt, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, libraryItemUpdatedAt, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<!-- book cover overlay --> <!-- book cover overlay -->
@@ -36,7 +36,7 @@
<ui-btn small @click="showLocalCovers = !showLocalCovers">{{ showLocalCovers ? $strings.ButtonHide : $strings.ButtonShow }}</ui-btn> <ui-btn small @click="showLocalCovers = !showLocalCovers">{{ showLocalCovers ? $strings.ButtonHide : $strings.ButtonShow }}</ui-btn>
</div> </div>
<div v-if="showLocalCovers" class="flex items-center justify-center pb-2"> <div v-if="showLocalCovers" class="flex items-center justify-center flex-wrap pb-2">
<template v-for="localCoverFile in localCovers"> <template v-for="localCoverFile in localCovers">
<div :key="localCoverFile.ino" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="localCoverFile.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(localCoverFile)"> <div :key="localCoverFile.ino" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="localCoverFile.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(localCoverFile)">
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }"> <div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
@@ -74,21 +74,32 @@ export default {
this.newPlaylistDescription = this.playlist.description || '' this.newPlaylistDescription = this.playlist.description || ''
}, },
removeClick() { removeClick() {
if (confirm(this.$getString('MessageConfirmRemovePlaylist', [this.playlistName]))) { const payload = {
this.processing = true message: this.$getString('MessageConfirmRemovePlaylist', [this.playlistName]),
this.$axios callback: (confirmed) => {
.$delete(`/api/playlists/${this.playlist.id}`) if (confirmed) {
.then(() => { this.removePlaylist()
this.processing = false }
this.show = false },
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess) type: 'yesNo'
})
.catch((error) => {
console.error('Failed to remove playlist', error)
this.processing = false
this.$toast.error(this.$strings.ToastRemoveFailed)
})
} }
this.$store.commit('globals/setConfirmPrompt', payload)
},
removePlaylist() {
this.processing = true
this.$axios
.$delete(`/api/playlists/${this.playlist.id}`)
.then(() => {
this.show = false
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
})
.catch((error) => {
console.error('Failed to remove playlist', error)
this.$toast.error(this.$strings.ToastRemoveFailed)
})
.finally(() => {
this.processing = false
})
}, },
submitForm() { submitForm() {
if (this.newPlaylistName === this.playlistName && this.newPlaylistDescription === this.playlist.description) { if (this.newPlaylistName === this.playlistName && this.newPlaylistDescription === this.playlist.description) {
@@ -16,11 +16,12 @@
v-for="(episode, index) in episodesList" v-for="(episode, index) in episodesList"
:key="index" :key="index"
class="relative" class="relative"
:class="getIsEpisodeDownloaded(episode) ? 'bg-primary bg-opacity-40' : selectedEpisodes[episode.cleanUrl] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'" :class="episode.isDownloaded || episode.isDownloading ? 'bg-primary bg-opacity-40' : selectedEpisodes[episode.cleanUrl] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'"
@click="toggleSelectEpisode(episode)" @click="toggleSelectEpisode(episode)"
> >
<div class="absolute top-0 left-0 h-full flex items-center p-2"> <div class="absolute top-0 left-0 h-full flex items-center p-2">
<span v-if="getIsEpisodeDownloaded(episode)" class="material-symbols text-success text-xl">download_done</span> <span v-if="episode.isDownloaded" class="material-symbols text-success text-xl">download_done</span>
<span v-else-if="episode.isDownloading" class="material-symbols text-warning text-xl">download</span>
<ui-checkbox v-else v-model="selectedEpisodes[episode.cleanUrl]" small checkbox-bg="primary" border-color="gray-600" /> <ui-checkbox v-else v-model="selectedEpisodes[episode.cleanUrl]" small checkbox-bg="primary" border-color="gray-600" />
</div> </div>
<div class="px-8 py-2"> <div class="px-8 py-2">
@@ -58,6 +59,14 @@ export default {
episodes: { episodes: {
type: Array, type: Array,
default: () => [] default: () => []
},
downloadQueue: {
type: Array,
default: () => []
},
episodesDownloading: {
type: Array,
default: () => []
} }
}, },
data() { data() {
@@ -79,6 +88,21 @@ export default {
handler(newVal) { handler(newVal) {
if (newVal) this.init() if (newVal) this.init()
} }
},
episodes: {
handler(newVal) {
if (newVal) this.updateEpisodeDownloadStatuses()
}
},
episodesDownloading: {
handler(newVal) {
if (newVal) this.updateEpisodeDownloadStatuses()
}
},
downloadQueue: {
handler(newVal) {
if (newVal) this.updateEpisodeDownloadStatuses()
}
} }
}, },
computed: { computed: {
@@ -132,6 +156,13 @@ export default {
} }
return false return false
}, },
getIsEpisodeDownloadingOrQueued(episode) {
const episodesToCheck = [...this.episodesDownloading, ...this.downloadQueue]
if (episode.guid) {
return episodesToCheck.some((download) => download.guid === episode.guid)
}
return episodesToCheck.some((download) => this.getCleanEpisodeUrl(download.url) === episode.cleanUrl)
},
/** /**
* UPDATE: As of v2.4.5 guid is used for matching existing downloaded episodes if it is found on the RSS feed. * UPDATE: As of v2.4.5 guid is used for matching existing downloaded episodes if it is found on the RSS feed.
* Fallback to checking the clean url * Fallback to checking the clean url
@@ -173,13 +204,13 @@ export default {
}, },
toggleSelectAll(val) { toggleSelectAll(val) {
for (const episode of this.episodesList) { for (const episode of this.episodesList) {
if (this.getIsEpisodeDownloaded(episode)) this.selectedEpisodes[episode.cleanUrl] = false if (episode.isDownloaded || episode.isDownloading) this.selectedEpisodes[episode.cleanUrl] = false
else this.$set(this.selectedEpisodes, episode.cleanUrl, val) else this.$set(this.selectedEpisodes, episode.cleanUrl, val)
} }
}, },
checkSetIsSelectedAll() { checkSetIsSelectedAll() {
for (const episode of this.episodesList) { for (const episode of this.episodesList) {
if (!this.getIsEpisodeDownloaded(episode) && !this.selectedEpisodes[episode.cleanUrl]) { if (!episode.isDownloaded && !episode.isDownloading && !this.selectedEpisodes[episode.cleanUrl]) {
this.selectAll = false this.selectAll = false
return return
} }
@@ -187,7 +218,7 @@ export default {
this.selectAll = true this.selectAll = true
}, },
toggleSelectEpisode(episode) { toggleSelectEpisode(episode) {
if (this.getIsEpisodeDownloaded(episode)) return if (episode.isDownloaded || episode.isDownloading) return
this.$set(this.selectedEpisodes, episode.cleanUrl, !this.selectedEpisodes[episode.cleanUrl]) this.$set(this.selectedEpisodes, episode.cleanUrl, !this.selectedEpisodes[episode.cleanUrl])
this.checkSetIsSelectedAll() this.checkSetIsSelectedAll()
}, },
@@ -223,6 +254,23 @@ export default {
}) })
}, },
init() { init() {
this.updateDownloadedEpisodeMaps()
this.episodesCleaned = this.episodes
.filter((ep) => ep.enclosure?.url)
.map((_ep) => {
return {
..._ep,
cleanUrl: this.getCleanEpisodeUrl(_ep.enclosure.url),
isDownloading: this.getIsEpisodeDownloadingOrQueued(_ep),
isDownloaded: this.getIsEpisodeDownloaded(_ep)
}
})
this.episodesCleaned.sort((a, b) => (a.publishedAt < b.publishedAt ? 1 : -1))
this.selectAll = false
this.selectedEpisodes = {}
},
updateDownloadedEpisodeMaps() {
this.downloadedEpisodeGuidMap = {} this.downloadedEpisodeGuidMap = {}
this.downloadedEpisodeUrlMap = {} this.downloadedEpisodeUrlMap = {}
@@ -230,18 +278,16 @@ export default {
if (episode.guid) this.downloadedEpisodeGuidMap[episode.guid] = episode.id if (episode.guid) this.downloadedEpisodeGuidMap[episode.guid] = episode.id
if (episode.enclosure?.url) this.downloadedEpisodeUrlMap[this.getCleanEpisodeUrl(episode.enclosure.url)] = episode.id if (episode.enclosure?.url) this.downloadedEpisodeUrlMap[this.getCleanEpisodeUrl(episode.enclosure.url)] = episode.id
}) })
},
this.episodesCleaned = this.episodes updateEpisodeDownloadStatuses() {
.filter((ep) => ep.enclosure?.url) this.updateDownloadedEpisodeMaps()
.map((_ep) => { this.episodesCleaned = this.episodesCleaned.map((ep) => {
return { return {
..._ep, ...ep,
cleanUrl: this.getCleanEpisodeUrl(_ep.enclosure.url) isDownloading: this.getIsEpisodeDownloadingOrQueued(ep),
} isDownloaded: this.getIsEpisodeDownloaded(ep)
}) }
this.episodesCleaned.sort((a, b) => (a.publishedAt < b.publishedAt ? 1 : -1)) })
this.selectAll = false
this.selectedEpisodes = {}
} }
}, },
mounted() {} mounted() {}
+26 -15
View File
@@ -28,7 +28,7 @@
<button aria-label="Download Backup" class="inline-flex material-symbols text-xl mx-1 mt-1 text-white/70 hover:text-white/100" @click.stop="downloadBackup(backup)">download</button> <button aria-label="Download Backup" class="inline-flex material-symbols text-xl mx-1 mt-1 text-white/70 hover:text-white/100" @click.stop="downloadBackup(backup)">download</button>
<button aria-label="Delete Backup" class="inline-flex material-symbols text-xl mx-1 text-white/70 hover:text-error" @click="deleteBackupClick(backup)">delete</button> <button aria-label="Delete Backup" class="inline-flex material-symbols text-xl mx-1 text-white/70 hover:text-error" @click.stop="deleteBackupClick(backup)">delete</button>
</div> </div>
</td> </td>
</tr> </tr>
@@ -107,21 +107,32 @@ export default {
}) })
}, },
deleteBackupClick(backup) { deleteBackupClick(backup) {
if (confirm(this.$getString('MessageConfirmDeleteBackup', [this.$formatDatetime(backup.createdAt, this.dateFormat, this.timeFormat)]))) { const payload = {
this.processing = true message: this.$getString('MessageConfirmDeleteBackup', [this.$formatDatetime(backup.createdAt, this.dateFormat, this.timeFormat)]),
this.$axios callback: (confirmed) => {
.$delete(`/api/backups/${backup.id}`) if (confirmed) {
.then((data) => { this.deleteBackup(backup)
this.setBackups(data.backups || []) }
this.$toast.success(this.$strings.ToastBackupDeleteSuccess) },
this.processing = false type: 'yesNo'
})
.catch((error) => {
console.error(error)
this.$toast.error(this.$strings.ToastBackupDeleteFailed)
this.processing = false
})
} }
this.$store.commit('globals/setConfirmPrompt', payload)
},
deleteBackup(backup) {
this.processing = true
this.$axios
.$delete(`/api/backups/${backup.id}`)
.then((data) => {
this.setBackups(data.backups || [])
this.$toast.success(this.$strings.ToastBackupDeleteSuccess)
})
.catch((error) => {
console.error(error)
this.$toast.error(this.$strings.ToastBackupDeleteFailed)
})
.finally(() => {
this.processing = false
})
}, },
applyBackup(backup) { applyBackup(backup) {
this.selectedBackup = backup this.selectedBackup = backup
+29 -17
View File
@@ -91,24 +91,36 @@ export default {
}, },
deleteUserClick(user) { deleteUserClick(user) {
if (this.isDeletingUser) return if (this.isDeletingUser) return
if (confirm(this.$getString('MessageRemoveUserWarning', [user.username]))) {
this.isDeletingUser = true const payload = {
this.$axios message: this.$getString('MessageRemoveUserWarning', [user.username]),
.$delete(`/api/users/${user.id}`) callback: (confirmed) => {
.then((data) => { if (confirmed) {
this.isDeletingUser = false this.deleteUser(user)
if (data.error) { }
this.$toast.error(data.error) },
} else { type: 'yesNo'
this.$toast.success(this.$strings.ToastUserDeleteSuccess)
}
})
.catch((error) => {
console.error('Failed to delete user', error)
this.$toast.error(this.$strings.ToastUserDeleteFailed)
this.isDeletingUser = false
})
} }
this.$store.commit('globals/setConfirmPrompt', payload)
},
deleteUser(user) {
this.isDeletingUser = true
this.$axios
.$delete(`/api/users/${user.id}`)
.then((data) => {
if (data.error) {
this.$toast.error(data.error)
} else {
this.$toast.success(this.$strings.ToastUserDeleteSuccess)
}
})
.catch((error) => {
console.error('Failed to delete user', error)
this.$toast.error(this.$strings.ToastUserDeleteFailed)
})
.finally(() => {
this.isDeletingUser = false
})
}, },
editUser(user) { editUser(user) {
this.$emit('edit', user) this.$emit('edit', user)
@@ -8,10 +8,15 @@
</div> </div>
<div class="h-10 flex items-center mt-1.5 mb-0.5 overflow-hidden"> <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 dir="auto" class="text-sm text-gray-200 line-clamp-2" v-html="episodeSubtitle"></div>
</div> </div>
<div class="h-8 flex items-center"> <div class="h-8 flex items-center">
<div class="w-full inline-flex justify-between max-w-xl"> <p v-if="sortKey === 'audioFile.metadata.filename'" class="text-sm text-gray-300 truncate font-light">
<strong className="font-bold">{{ $strings.LabelFilename }}</strong
>: {{ episode.audioFile.metadata.filename }}
</p>
<div v-else class="w-full inline-flex justify-between max-w-xl">
<p v-if="episode?.season" class="text-sm text-gray-300">{{ $getString('LabelSeasonNumber', [episode.season]) }}</p> <p v-if="episode?.season" class="text-sm text-gray-300">{{ $getString('LabelSeasonNumber', [episode.season]) }}</p>
<p v-if="episode?.episode" class="text-sm text-gray-300">{{ $getString('LabelEpisodeNumber', [episode.episode]) }}</p> <p v-if="episode?.episode" class="text-sm text-gray-300">{{ $getString('LabelEpisodeNumber', [episode.episode]) }}</p>
<p v-if="episode?.chapters?.length" class="text-sm text-gray-300">{{ $getString('LabelChapterCount', [episode.chapters.length]) }}</p> <p v-if="episode?.chapters?.length" class="text-sm text-gray-300">{{ $getString('LabelChapterCount', [episode.chapters.length]) }}</p>
@@ -21,12 +26,13 @@
<div class="flex items-center pt-2"> <div class="flex items-center pt-2">
<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="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick"> <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="userIsFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick">
<span class="material-symbols fill text-2xl" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span> <span class="material-symbols fill text-2xl" aria-hidden="true" :class="streamIsPlaying ? '' : 'text-success'">{{ streamIsPlaying ? 'pause' : 'play_arrow' }}</span>
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p> <span class="sr-only">{{ streamIsPlaying ? $strings.ButtonPause : $strings.ButtonPlay }}</span>
<p class="pl-2 pr-1 text-sm font-semibold" aria-hidden="true">{{ timeRemaining }}</p>
</button> </button>
<ui-tooltip v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" :text="isQueued ? $strings.MessageRemoveFromPlayerQueue : $strings.MessageAddToPlayerQueue" :class="isQueued ? 'text-success' : ''" direction="top"> <ui-tooltip v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" :text="isQueued ? $strings.MessageRemoveFromPlayerQueue : $strings.MessageAddToPlayerQueue" :class="isQueued ? 'text-success' : ''" direction="top">
<ui-icon-btn :icon="isQueued ? 'playlist_add_check' : 'playlist_play'" borderless @click="queueBtnClick" /> <ui-icon-btn :icon="isQueued ? 'playlist_add_check' : 'playlist_play'" :aria-label="isQueued ? $strings.LabelRemoveFromPlayerQueue : $strings.LabelAddToPlayerQueue" borderless @click="queueBtnClick" />
</ui-tooltip> </ui-tooltip>
<ui-tooltip :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top"> <ui-tooltip :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
@@ -34,11 +40,11 @@
</ui-tooltip> </ui-tooltip>
<ui-tooltip :text="$strings.LabelYourPlaylists" direction="top"> <ui-tooltip :text="$strings.LabelYourPlaylists" direction="top">
<ui-icon-btn icon="playlist_add" borderless @click="clickAddToPlaylist" /> <ui-icon-btn icon="playlist_add" :aria-label="$strings.LabelYourPlaylists" borderless @click="clickAddToPlaylist" />
</ui-tooltip> </ui-tooltip>
<ui-icon-btn v-if="userCanUpdate" icon="edit" borderless @click="clickEdit" /> <ui-icon-btn v-if="userCanUpdate" icon="edit" borderless @click="clickEdit" />
<ui-icon-btn v-if="userCanDelete" icon="close" borderless @click="removeClick" /> <ui-icon-btn v-if="userCanDelete" icon="close" :aria-label="$strings.HeaderRemoveEpisode" borderless @click="removeClick" />
</div> </div>
</div> </div>
<div v-if="isHovering || isSelected || isSelectionMode" class="hidden md:block w-12 min-w-12" /> <div v-if="isHovering || isSelected || isSelectionMode" class="hidden md:block w-12 min-w-12" />
@@ -48,7 +54,7 @@
<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="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="flex h-full items-center">
<div class="mx-1"> <div class="mx-1">
<ui-checkbox v-model="isSelected" @input="selectedUpdated" checkbox-bg="bg" /> <ui-checkbox v-model="isSelected" @input="selectedUpdated" checkbox-bg="bg" aria-label="Select episode" />
</div> </div>
</div> </div>
</div> </div>
@@ -65,7 +71,8 @@ export default {
episode: { episode: {
type: Object, type: Object,
default: () => null default: () => null
} },
sortKey: String
}, },
data() { data() {
return { return {
@@ -1,3 +1,4 @@
<template> <template>
<div id="lazy-episodes-table" 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 flex-wrap flex-col md:flex-row md:items-center mb-4">
@@ -123,6 +124,10 @@ export default {
{ {
text: this.$strings.LabelEpisode, text: this.$strings.LabelEpisode,
value: 'episode' value: 'episode'
},
{
text: this.$strings.LabelFilename,
value: 'audioFile.metadata.filename'
} }
] ]
}, },
@@ -171,8 +176,17 @@ export default {
return episodeProgress && !episodeProgress.isFinished return episodeProgress && !episodeProgress.isFinished
}) })
.sort((a, b) => { .sort((a, b) => {
let aValue = a[this.sortKey] let aValue
let bValue = b[this.sortKey] let bValue
if (this.sortKey.includes('.')) {
const getNestedValue = (ob, s) => s.split('.').reduce((o, k) => o?.[k], ob)
aValue = getNestedValue(a, this.sortKey)
bValue = getNestedValue(b, this.sortKey)
} else {
aValue = a[this.sortKey]
bValue = b[this.sortKey]
}
// Sort episodes with no pub date as the oldest // Sort episodes with no pub date as the oldest
if (this.sortKey === 'publishedAt') { if (this.sortKey === 'publishedAt') {
@@ -361,20 +375,20 @@ export default {
playEpisode(episode) { playEpisode(episode) {
const queueItems = [] const queueItems = []
const episodesInListeningOrder = this.episodesCopy.map((ep) => ({ ...ep })).sort((a, b) => String(a.publishedAt).localeCompare(String(b.publishedAt), undefined, { numeric: true, sensitivity: 'base' })) const episodesInListeningOrder = this.episodesList
const episodeIndex = episodesInListeningOrder.findIndex((e) => e.id === episode.id) const episodeIndex = episodesInListeningOrder.findIndex((e) => e.id === episode.id)
for (let i = episodeIndex; i < episodesInListeningOrder.length; i++) { for (let i = episodeIndex; i < episodesInListeningOrder.length; i++) {
const episode = episodesInListeningOrder[i] const _episode = episodesInListeningOrder[i]
const podcastProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id) const podcastProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, _episode.id)
if (!podcastProgress || !podcastProgress.isFinished) { if (!podcastProgress?.isFinished || episode.id === _episode.id) {
queueItems.push({ queueItems.push({
libraryItemId: this.libraryItem.id, libraryItemId: this.libraryItem.id,
libraryId: this.libraryItem.libraryId, libraryId: this.libraryItem.libraryId,
episodeId: episode.id, episodeId: _episode.id,
title: episode.title, title: _episode.title,
subtitle: this.mediaMetadata.title, subtitle: this.mediaMetadata.title,
caption: episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate, caption: _episode.publishedAt ? this.$getString('LabelPublishedDate', [this.$formatDate(_episode.publishedAt, this.dateFormat)]) : this.$strings.LabelUnknownPublishDate,
duration: episode.audioFile.duration || null, duration: _episode.audioFile.duration || null,
coverPath: this.media.coverPath || null coverPath: this.media.coverPath || null
}) })
} }
@@ -440,7 +454,8 @@ export default {
propsData: { propsData: {
index, index,
libraryItemId: this.libraryItem.id, libraryItemId: this.libraryItem.id,
episode: this.episodesList[index] episode: this.episodesList[index],
sortKey: this.sortKey
}, },
created() { created() {
this.$on('selected', (payload) => { this.$on('selected', (payload) => {
+6 -2
View File
@@ -1,7 +1,7 @@
<template> <template>
<label class="flex justify-start items-center" :class="!disabled ? 'cursor-pointer' : ''"> <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"> <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' : ''" /> <input v-model="selected" :disabled="disabled" type="checkbox" :aria-label="ariaLabel" class="opacity-0 absolute" :class="!disabled ? 'cursor-pointer' : ''" />
<span v-if="partial" class="material-symbols text-base leading-none text-gray-400">remove</span> <span v-if="partial" class="material-symbols 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> <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>
@@ -33,7 +33,11 @@ export default {
default: '' default: ''
}, },
disabled: Boolean, disabled: Boolean,
partial: Boolean partial: Boolean,
ariaLabel: {
type: String,
default: ''
}
}, },
data() { data() {
return {} return {}
@@ -5,8 +5,8 @@
<ui-tooltip v-if="tasksRunning" :text="$strings.LabelTasks" direction="bottom" class="flex items-center"> <ui-tooltip v-if="tasksRunning" :text="$strings.LabelTasks" direction="bottom" class="flex items-center">
<widgets-loading-spinner /> <widgets-loading-spinner />
</ui-tooltip> </ui-tooltip>
<ui-tooltip v-else text="Activities" direction="bottom" class="flex items-center"> <ui-tooltip v-else :text="$strings.LabelActivities" direction="bottom" class="flex items-center">
<span class="material-symbols text-1.5xl" aria-label="Activities" role="button">notifications</span> <span class="material-symbols text-1.5xl" :aria-label="$strings.LabelActivities" role="button">notifications</span>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div v-if="showUnseenSuccessIndicator" class="w-2 h-2 rounded-full bg-success pointer-events-none absolute -top-1 -right-0.5" /> <div v-if="showUnseenSuccessIndicator" class="w-2 h-2 rounded-full bg-success pointer-events-none absolute -top-1 -right-0.5" />
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.19.2", "version": "2.20.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.19.2", "version": "2.20.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@nuxtjs/axios": "^5.13.6", "@nuxtjs/axios": "^5.13.6",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.19.2", "version": "2.20.0",
"buildNumber": 1, "buildNumber": 1,
"description": "Self-hosted audiobook and podcast client", "description": "Self-hosted audiobook and podcast client",
"main": "index.js", "main": "index.js",
+24 -14
View File
@@ -176,21 +176,31 @@ export default {
this.$store.commit('globals/setEditCollection', this.collection) this.$store.commit('globals/setEditCollection', this.collection)
}, },
removeClick() { removeClick() {
if (confirm(this.$getString('MessageConfirmRemoveCollection', [this.collectionName]))) { const payload = {
this.processing = true message: this.$getString('MessageConfirmRemoveCollection', [this.collectionName]),
this.$axios callback: (confirmed) => {
.$delete(`/api/collections/${this.collection.id}`) if (confirmed) {
.then(() => { this.deleteCollection()
this.$toast.success(this.$strings.ToastCollectionRemoveSuccess) }
}) },
.catch((error) => { type: 'yesNo'
console.error('Failed to remove collection', error)
this.$toast.error(this.$strings.ToastCollectionRemoveFailed)
})
.finally(() => {
this.processing = false
})
} }
this.$store.commit('globals/setConfirmPrompt', payload)
},
deleteCollection() {
this.processing = true
this.$axios
.$delete(`/api/collections/${this.collection.id}`)
.then(() => {
this.$toast.success(this.$strings.ToastCollectionRemoveSuccess)
})
.catch((error) => {
console.error('Failed to remove collection', error)
this.$toast.error(this.$strings.ToastCollectionRemoveFailed)
})
.finally(() => {
this.processing = false
})
}, },
clickPlay() { clickPlay() {
const queueItems = [] const queueItems = []
+1 -1
View File
@@ -122,7 +122,7 @@ export default {
}, },
scheduleDescription() { scheduleDescription() {
if (!this.cronExpression) return '' if (!this.cronExpression) return ''
const parsed = this.$parseCronExpression(this.cronExpression) const parsed = this.$parseCronExpression(this.cronExpression, this)
return parsed ? parsed.description : `${this.$strings.LabelCustomCronExpression} ${this.cronExpression}` return parsed ? parsed.description : `${this.$strings.LabelCustomCronExpression} ${this.cronExpression}`
}, },
nextBackupDate() { nextBackupDate() {
+1 -1
View File
@@ -67,7 +67,7 @@
<div class="flex-grow" /> <div class="flex-grow" />
</div> </div>
<div v-if="newServerSettings.scannerFindCovers" class="w-44 ml-14 mb-2"> <div v-if="newServerSettings.scannerFindCovers" class="w-44 ml-14 mb-2">
<ui-dropdown v-model="newServerSettings.scannerCoverProvider" small :items="providers" label="Cover Provider" @input="updateScannerCoverProvider" :disabled="updatingServerSettings" /> <ui-dropdown v-model="newServerSettings.scannerCoverProvider" small :items="providers" :label="$strings.LabelCoverProvider" @input="updateScannerCoverProvider" :disabled="updatingServerSettings" />
</div> </div>
<div role="article" :aria-label="$strings.LabelSettingsPreferMatchedMetadataHelp" class="flex items-center py-2"> <div role="article" :aria-label="$strings.LabelSettingsPreferMatchedMetadataHelp" class="flex items-center py-2">
+34 -22
View File
@@ -41,7 +41,7 @@
<p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">{{ $getString('LabelByAuthor', [podcastAuthor]) }}</p> <p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">{{ $getString('LabelByAuthor', [podcastAuthor]) }}</p>
<p v-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden overflow-ellipsis"> <p v-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden overflow-ellipsis">
by <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link> {{ $getString('LabelByAuthor', ['']) }}<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
</p> </p>
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p> <p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
@@ -123,7 +123,7 @@
</div> </div>
<div class="my-4 w-full"> <div class="my-4 w-full">
<div ref="description" id="item-description" dir="auto" class="default-style less-spacing text-base text-gray-100 whitespace-pre-line mb-1" :class="{ 'show-full': showFullDescription }" v-html="description" /> <div ref="description" id="item-description" dir="auto" role="paragraph" class="default-style less-spacing text-base text-gray-100 whitespace-pre-line mb-1" :class="{ 'show-full': showFullDescription }" v-html="description" />
<button v-if="isDescriptionClamped" class="py-0.5 flex items-center text-slate-300 hover:text-white" @click="showFullDescription = !showFullDescription">{{ showFullDescription ? $strings.ButtonReadLess : $strings.ButtonReadMore }} <span class="material-symbols text-xl pl-1" v-html="showFullDescription ? 'expand_less' : '&#xe313;'" /></button> <button v-if="isDescriptionClamped" class="py-0.5 flex items-center text-slate-300 hover:text-white" @click="showFullDescription = !showFullDescription">{{ showFullDescription ? $strings.ButtonReadLess : $strings.ButtonReadMore }} <span class="material-symbols text-xl pl-1" v-html="showFullDescription ? 'expand_less' : '&#xe313;'" /></button>
</div> </div>
@@ -132,7 +132,7 @@
<tables-tracks-table v-if="tracks.length" :title="$strings.LabelStatsAudioTracks" :tracks="tracksWithAudioFile" :is-file="isFile" :library-item-id="libraryItemId" class="mt-6" /> <tables-tracks-table v-if="tracks.length" :title="$strings.LabelStatsAudioTracks" :tracks="tracksWithAudioFile" :is-file="isFile" :library-item-id="libraryItemId" class="mt-6" />
<tables-podcast-lazy-episodes-table v-if="isPodcast" :library-item="libraryItem" /> <tables-podcast-lazy-episodes-table ref="episodesTable" v-if="isPodcast" :library-item="libraryItem" />
<tables-ebook-files-table v-if="ebookFiles.length" :library-item="libraryItem" class="mt-6" /> <tables-ebook-files-table v-if="ebookFiles.length" :library-item="libraryItem" class="mt-6" />
@@ -141,7 +141,7 @@
</div> </div>
</div> </div>
<modals-podcast-episode-feed v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" /> <modals-podcast-episode-feed v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" :download-queue="episodeDownloadsQueued" :episodes-downloading="episodesDownloading" />
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :playback-rate="1" :library-item-id="libraryItemId" hide-create @select="selectBookmark" /> <modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :playback-rate="1" :library-item-id="libraryItemId" hide-create @select="selectBookmark" />
</div> </div>
</template> </template>
@@ -503,7 +503,7 @@ export default {
toggleFinished(confirmed = false) { toggleFinished(confirmed = false) {
if (!this.userIsFinished && this.progressPercent > 0 && !confirmed) { if (!this.userIsFinished && this.progressPercent > 0 && !confirmed) {
const payload = { const payload = {
message: `Are you sure you want to mark "${this.title}" as finished?`, message: this.$getString('MessageConfirmMarkItemFinished', [this.title]),
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.toggleFinished(true) this.toggleFinished(true)
@@ -534,13 +534,15 @@ export default {
let episodeId = null let episodeId = null
const queueItems = [] const queueItems = []
if (this.isPodcast) { if (this.isPodcast) {
const episodesInListeningOrder = this.podcastEpisodes.map((ep) => ({ ...ep })).sort((a, b) => String(a.publishedAt).localeCompare(String(b.publishedAt), undefined, { numeric: true, sensitivity: 'base' })) // Uses the sorting and filtering from the episode table component
const episodesInListeningOrder = this.$refs.episodesTable?.episodesList || []
// Find most recent episode unplayed // Find the first unplayed episode from the table
let episodeIndex = episodesInListeningOrder.findLastIndex((ep) => { let episodeIndex = episodesInListeningOrder.findIndex((ep) => {
const podcastProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, ep.id) const podcastProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, ep.id)
return !podcastProgress || !podcastProgress.isFinished return !podcastProgress || !podcastProgress.isFinished
}) })
// If all episodes are played, use the first episode
if (episodeIndex < 0) episodeIndex = 0 if (episodeIndex < 0) episodeIndex = 0
episodeId = episodesInListeningOrder[episodeIndex].id episodeId = episodesInListeningOrder[episodeIndex].id
@@ -599,19 +601,31 @@ export default {
}, },
clearProgressClick() { clearProgressClick() {
if (!this.userMediaProgress) return if (!this.userMediaProgress) return
if (confirm(this.$strings.MessageConfirmResetProgress)) {
this.resettingProgress = true const payload = {
this.$axios message: this.$strings.MessageConfirmResetProgress,
.$delete(`/api/me/progress/${this.userMediaProgress.id}`) callback: (confirmed) => {
.then(() => { if (confirmed) {
console.log('Progress reset complete') this.clearProgress()
this.resettingProgress = false }
}) },
.catch((error) => { type: 'yesNo'
console.error('Progress reset failed', error)
this.resettingProgress = false
})
} }
this.$store.commit('globals/setConfirmPrompt', payload)
},
clearProgress() {
this.resettingProgress = true
this.$axios
.$delete(`/api/me/progress/${this.userMediaProgress.id}`)
.then(() => {
console.log('Progress reset complete')
})
.catch((error) => {
console.error('Progress reset failed', error)
})
.finally(() => {
this.resettingProgress = false
})
}, },
clickRSSFeed() { clickRSSFeed() {
this.$store.commit('globals/setRSSFeedOpenCloseModal', { this.$store.commit('globals/setRSSFeedOpenCloseModal', {
@@ -646,13 +660,11 @@ export default {
}, },
rssFeedOpen(data) { rssFeedOpen(data) {
if (data.entityId === this.libraryItemId) { if (data.entityId === this.libraryItemId) {
console.log('RSS Feed Opened', data)
this.rssFeed = data this.rssFeed = data
} }
}, },
rssFeedClosed(data) { rssFeedClosed(data) {
if (data.entityId === this.libraryItemId) { if (data.entityId === this.libraryItemId) {
console.log('RSS Feed Closed', data)
this.rssFeed = null this.rssFeed = null
} }
}, },
@@ -155,7 +155,7 @@ export default {
const itemProgressPercent = episode.progress?.progress || 0 const itemProgressPercent = episode.progress?.progress || 0
if (!isFinished && itemProgressPercent > 0 && !confirmed) { if (!isFinished && itemProgressPercent > 0 && !confirmed) {
const payload = { const payload = {
message: `Are you sure you want to mark "${episode.title}" as finished?`, message: this.$getString('MessageConfirmMarkItemFinished', [episode.title]),
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.toggleEpisodeFinished(episode, true) this.toggleEpisodeFinished(episode, true)
+24 -14
View File
@@ -109,21 +109,31 @@ export default {
this.$store.commit('globals/setEditPlaylist', this.playlist) this.$store.commit('globals/setEditPlaylist', this.playlist)
}, },
removeClick() { removeClick() {
if (confirm(`Are you sure you want to remove playlist "${this.playlistName}"?`)) { const payload = {
this.processingRemove = true message: this.$getString('MessageConfirmRemovePlaylist', [this.playlistName]),
var playlistName = this.playlistName callback: (confirmed) => {
this.$axios if (confirmed) {
.$delete(`/api/playlists/${this.playlist.id}`) this.removePlaylist()
.then(() => { }
this.processingRemove = false },
this.$toast.success(`Playlist "${playlistName}" Removed`) type: 'yesNo'
})
.catch((error) => {
console.error('Failed to remove playlist', error)
this.processingRemove = false
this.$toast.error(`Failed to remove playlist`)
})
} }
this.$store.commit('globals/setConfirmPrompt', payload)
},
removePlaylist() {
this.processingRemove = true
this.$axios
.$delete(`/api/playlists/${this.playlist.id}`)
.then(() => {
this.$toast.success(this.$strings.ToastPlaylistRemoveSuccess)
})
.catch((error) => {
console.error('Failed to remove playlist', error)
this.$toast.error(this.$strings.ToastRemoveFailed)
})
.finally(() => {
this.processingRemove = false
})
}, },
clickPlay() { clickPlay() {
const queueItems = [] const queueItems = []
+14
View File
@@ -107,6 +107,19 @@ Vue.prototype.$formatNumber = (num) => {
return Intl.NumberFormat(Vue.prototype.$languageCodes.current).format(num) return Intl.NumberFormat(Vue.prototype.$languageCodes.current).format(num)
} }
/**
* Get the days of the week for the current language
* Starts with Sunday
* @returns {string[]}
*/
Vue.prototype.$getDaysOfWeek = () => {
const days = []
for (let i = 0; i < 7; i++) {
days.push(new Date(2025, 0, 5 + i).toLocaleString(Vue.prototype.$languageCodes.current, { weekday: 'long' }))
}
return days
}
const translations = { const translations = {
[defaultCode]: enUsStrings [defaultCode]: enUsStrings
} }
@@ -148,6 +161,7 @@ async function loadi18n(code) {
Vue.prototype.$setDateFnsLocale(languageCodeMap[code].dateFnsLocale) Vue.prototype.$setDateFnsLocale(languageCodeMap[code].dateFnsLocale)
this?.$eventBus?.$emit('change-lang', code) this?.$eventBus?.$emit('change-lang', code)
return true return true
} }
+10 -10
View File
@@ -93,7 +93,7 @@ Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true, showSeconds = t
return strs.join(' ') return strs.join(' ')
} }
Vue.prototype.$parseCronExpression = (expression) => { Vue.prototype.$parseCronExpression = (expression, context) => {
if (!expression) return null if (!expression) return null
const pieces = expression.split(' ') const pieces = expression.split(' ')
if (pieces.length !== 5) { if (pieces.length !== 5) {
@@ -102,31 +102,31 @@ Vue.prototype.$parseCronExpression = (expression) => {
const commonPatterns = [ const commonPatterns = [
{ {
text: 'Every 12 hours', text: context.$strings.LabelIntervalEvery12Hours,
value: '0 */12 * * *' value: '0 */12 * * *'
}, },
{ {
text: 'Every 6 hours', text: context.$strings.LabelIntervalEvery6Hours,
value: '0 */6 * * *' value: '0 */6 * * *'
}, },
{ {
text: 'Every 2 hours', text: context.$strings.LabelIntervalEvery2Hours,
value: '0 */2 * * *' value: '0 */2 * * *'
}, },
{ {
text: 'Every hour', text: context.$strings.LabelIntervalEveryHour,
value: '0 * * * *' value: '0 * * * *'
}, },
{ {
text: 'Every 30 minutes', text: context.$strings.LabelIntervalEvery30Minutes,
value: '*/30 * * * *' value: '*/30 * * * *'
}, },
{ {
text: 'Every 15 minutes', text: context.$strings.LabelIntervalEvery15Minutes,
value: '*/15 * * * *' value: '*/15 * * * *'
}, },
{ {
text: 'Every minute', text: context.$strings.LabelIntervalEveryMinute,
value: '* * * * *' value: '* * * * *'
} }
] ]
@@ -147,7 +147,7 @@ Vue.prototype.$parseCronExpression = (expression) => {
return null return null
} }
const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] const weekdays = context.$getDaysOfWeek()
var weekdayText = 'day' var weekdayText = 'day'
if (pieces[4] !== '*') if (pieces[4] !== '*')
weekdayText = pieces[4] weekdayText = pieces[4]
@@ -156,7 +156,7 @@ Vue.prototype.$parseCronExpression = (expression) => {
.join(', ') .join(', ')
return { return {
description: `Run every ${weekdayText} at ${pieces[1]}:${pieces[0].padStart(2, '0')}` description: context.$getString('MessageScheduleRunEveryWeekdayAtTime', [weekdayText, `${pieces[1]}:${pieces[0].padStart(2, '0')}`])
} }
} }
+404 -4
View File
@@ -11,9 +11,10 @@
"ButtonAuthors": "Аўтары", "ButtonAuthors": "Аўтары",
"ButtonBack": "Назад", "ButtonBack": "Назад",
"ButtonBatchEditPopulateFromExisting": "Запоўніць з існуючага", "ButtonBatchEditPopulateFromExisting": "Запоўніць з існуючага",
"ButtonBatchEditPopulateMapDetails": "Запоўніць падрабязнасці карты",
"ButtonBrowseForFolder": "Знайсці тэчку", "ButtonBrowseForFolder": "Знайсці тэчку",
"ButtonCancel": "Адмяніць", "ButtonCancel": "Скасаваць",
"ButtonCancelEncode": "Адмяніць кадзіраванне", "ButtonCancelEncode": "Скасаваць кадзіраванне",
"ButtonChangeRootPassword": "Зменіце Root пароль", "ButtonChangeRootPassword": "Зменіце Root пароль",
"ButtonCheckAndDownloadNewEpisodes": "Праверыць і спампаваць новыя эпізоды", "ButtonCheckAndDownloadNewEpisodes": "Праверыць і спампаваць новыя эпізоды",
"ButtonChooseAFolder": "Выбраць тэчку", "ButtonChooseAFolder": "Выбраць тэчку",
@@ -43,7 +44,7 @@
"ButtonLatest": "Апошняе", "ButtonLatest": "Апошняе",
"ButtonLibrary": "Бібліятэка", "ButtonLibrary": "Бібліятэка",
"ButtonLogout": "Выйсці", "ButtonLogout": "Выйсці",
"ButtonLookup": "", "ButtonLookup": "Пошук",
"ButtonManageTracks": "Кіраванне дарожкамі", "ButtonManageTracks": "Кіраванне дарожкамі",
"ButtonMapChapterTitles": "Супаставіць назвы раздзелаў", "ButtonMapChapterTitles": "Супаставіць назвы раздзелаў",
"ButtonMatchAllAuthors": "Супадзенне ўсіх аўтараў", "ButtonMatchAllAuthors": "Супадзенне ўсіх аўтараў",
@@ -59,7 +60,7 @@
"ButtonPlay": "Прайграць", "ButtonPlay": "Прайграць",
"ButtonPlayAll": "Прайграць усё", "ButtonPlayAll": "Прайграць усё",
"ButtonPlaying": "Прайграваецца", "ButtonPlaying": "Прайграваецца",
"ButtonPlaylists": "Плэйлісты", "ButtonPlaylists": "Спісы прайгравання",
"ButtonPrevious": "Папярэдні", "ButtonPrevious": "Папярэдні",
"ButtonPreviousChapter": "Папярэдні раздзел", "ButtonPreviousChapter": "Папярэдні раздзел",
"ButtonProbeAudioFile": "Праверыць аўдыяфайл", "ButtonProbeAudioFile": "Праверыць аўдыяфайл",
@@ -72,6 +73,8 @@
"ButtonQuickMatch": "Хуткі пошук", "ButtonQuickMatch": "Хуткі пошук",
"ButtonReScan": "Паўторнае сканаванне", "ButtonReScan": "Паўторнае сканаванне",
"ButtonRead": "Чытаць", "ButtonRead": "Чытаць",
"ButtonReadLess": "Чытаць менш",
"ButtonReadMore": "Чытаць больш",
"ButtonRefresh": "Абнавіць", "ButtonRefresh": "Абнавіць",
"ButtonRemove": "Выдаліць", "ButtonRemove": "Выдаліць",
"ButtonRemoveAll": "Выдаліць усе", "ButtonRemoveAll": "Выдаліць усе",
@@ -94,6 +97,7 @@
"ButtonSeries": "Серыі", "ButtonSeries": "Серыі",
"ButtonSetChaptersFromTracks": "Усталяваць раздзелы з трэкаў", "ButtonSetChaptersFromTracks": "Усталяваць раздзелы з трэкаў",
"ButtonShare": "Падзяліцца", "ButtonShare": "Падзяліцца",
"ButtonShiftTimes": "Карэкцыя часу",
"ButtonStartM4BEncode": "Пачаць кадзіраванне ў M4B", "ButtonStartM4BEncode": "Пачаць кадзіраванне ў M4B",
"ButtonStartMetadataEmbed": "Пачаць убудаванне метаданых", "ButtonStartMetadataEmbed": "Пачаць убудаванне метаданых",
"ButtonStats": "Статыстыка", "ButtonStats": "Статыстыка",
@@ -153,63 +157,459 @@
"HeaderManageGenres": "Кіраванне жанрамі", "HeaderManageGenres": "Кіраванне жанрамі",
"HeaderManageTags": "Кіраванне тэгамі", "HeaderManageTags": "Кіраванне тэгамі",
"HeaderMapDetails": "Падрабязнасці адлюстравання", "HeaderMapDetails": "Падрабязнасці адлюстравання",
"HeaderMetadataOrderOfPrecedence": "Парадак прыярытэтнасці метададзеных",
"HeaderMetadataToEmbed": "Метададзеныя для ўбудавання",
"HeaderNewAccount": "Новы ўліковы запіс", "HeaderNewAccount": "Новы ўліковы запіс",
"HeaderNewLibrary": "Новая бібліятэка", "HeaderNewLibrary": "Новая бібліятэка",
"HeaderNotificationCreate": "Стварыць апавяшчэнне", "HeaderNotificationCreate": "Стварыць апавяшчэнне",
"HeaderNotificationUpdate": "Абнавіць апавяшчэнне", "HeaderNotificationUpdate": "Абнавіць апавяшчэнне",
"HeaderNotifications": "Апавяшчэнні", "HeaderNotifications": "Апавяшчэнні",
"HeaderOpenIDConnectAuthentication": "Аўтэнтыфікацыя праз OpenID Connect",
"HeaderOpenListeningSessions": "Адкрыць сеансы праслухоўвання", "HeaderOpenListeningSessions": "Адкрыць сеансы праслухоўвання",
"HeaderOpenRSSFeed": "Адкрыць RSS-стужку",
"HeaderOtherFiles": "Іншыя файлы",
"HeaderPasswordAuthentication": "Аўтэнтыфікацыя паролем",
"HeaderPermissions": "Дазволы",
"HeaderPlayerQueue": "Чарга прайгравання",
"HeaderPlayerSettings": "Налады прайгравальніка",
"HeaderPlaylist": "Спіс прайгравання",
"HeaderPlaylistItems": "Элементы спіса прайгравання",
"HeaderPodcastsToAdd": "Падкасты для дадання",
"HeaderPreviewCover": "Прадпрагляд вокладкі",
"HeaderRSSFeedGeneral": "Падрабязнасці RSS",
"HeaderRSSFeedIsOpen": "RSS-стужка адкрыта",
"HeaderRSSFeeds": "RSS-стужкі",
"HeaderRemoveEpisode": "Выдаліць эпізод",
"HeaderRemoveEpisodes": "Выдаліць {0} эпізодаў",
"HeaderSavedMediaProgress": "Захаваны прагрэс медыя",
"HeaderSchedule": "Расклад",
"HeaderScheduleEpisodeDownloads": "Расклад аўтаматычных спамповак эпізодаў", "HeaderScheduleEpisodeDownloads": "Расклад аўтаматычных спамповак эпізодаў",
"HeaderScheduleLibraryScans": "Расклад аўтаматычнага сканавання бібліятэкі",
"HeaderSession": "Сеанс",
"HeaderSetBackupSchedule": "Наладзіць расклад рэзервовага капіравання",
"HeaderSettings": "Налады", "HeaderSettings": "Налады",
"HeaderSettingsDisplay": "Дысплей", "HeaderSettingsDisplay": "Дысплей",
"HeaderSettingsExperimental": "Эксперыментальныя функцыі", "HeaderSettingsExperimental": "Эксперыментальныя функцыі",
"HeaderSettingsGeneral": "Агульныя", "HeaderSettingsGeneral": "Агульныя",
"HeaderSettingsScanner": "Сканер", "HeaderSettingsScanner": "Сканер",
"HeaderSettingsWebClient": "Вэб-кліент", "HeaderSettingsWebClient": "Вэб-кліент",
"HeaderSleepTimer": "Таймер сну",
"HeaderStatsLargestItems": "Найбуйнейшыя элементы",
"HeaderStatsLongestItems": "Найдаўжэйшыя элементы (гадзіны)",
"HeaderStatsMinutesListeningChart": "Хвілін праслухоўвання (апошнія 7 дзён)",
"HeaderStatsRecentSessions": "Апошнія сеансы",
"HeaderStatsTop10Authors": "10 лепшых аўтараў", "HeaderStatsTop10Authors": "10 лепшых аўтараў",
"HeaderStatsTop5Genres": "5 лепшых жанраў", "HeaderStatsTop5Genres": "5 лепшых жанраў",
"HeaderTableOfContents": "Змест", "HeaderTableOfContents": "Змест",
"HeaderTools": "Інструменты", "HeaderTools": "Інструменты",
"HeaderUpdateAccount": "Абнавіць уліковы запіс", "HeaderUpdateAccount": "Абнавіць уліковы запіс",
"HeaderUpdateAuthor": "Абнавіць аўтара",
"HeaderUpdateDetails": "Абнавіць падрабязнасці",
"HeaderUpdateLibrary": "Абнавіць бібліятэку",
"HeaderUsers": "Карыстальнікі",
"HeaderYearReview": "Вынікі {0} года",
"HeaderYourStats": "Ваша статыстыка",
"LabelAbridged": "Скарочаная версія",
"LabelAbridgedChecked": "Скарочаная версія (праверана)",
"LabelAbridgedUnchecked": "Поўная версія (неправерана)",
"LabelAccessibleBy": "Даступна для",
"LabelAccountType": "Тып уліковага запіса", "LabelAccountType": "Тып уліковага запіса",
"LabelAccountTypeAdmin": "Адміністратар", "LabelAccountTypeAdmin": "Адміністратар",
"LabelAccountTypeGuest": "Госць", "LabelAccountTypeGuest": "Госць",
"LabelAccountTypeUser": "Карыстальнік", "LabelAccountTypeUser": "Карыстальнік",
"LabelActivities": "Дзеянні",
"LabelActivity": "Дзеянне",
"LabelAddToCollection": "Дадаць у калекцыю",
"LabelAddToCollectionBatch": "Дадаць {0} кніг у калекцыю",
"LabelAddToPlaylist": "Дадаць у спіс прайгравання",
"LabelAddToPlaylistBatch": "Дадаць {0} элементаў у спіс прайгравання",
"LabelAddedAt": "Дата дабаўлення",
"LabelAddedDate": "Дададзена {0}",
"LabelAdminUsersOnly": "Толькі для адміністратараў",
"LabelAll": "Усе",
"LabelAllUsers": "Усе карыстальнікі",
"LabelAllUsersExcludingGuests": "Усе карыстальнікі, акрамя гасцей",
"LabelAllUsersIncludingGuests": "Усе карыстальнікі, уключаючы гасцей",
"LabelAlreadyInYourLibrary": "Ужо ў вашай бібліятэцы",
"LabelApiToken": "Токен API",
"LabelAppend": "Дадаць",
"LabelAudioBitrate": "Бітрэйт аўдыё (напрыклад, 128к)", "LabelAudioBitrate": "Бітрэйт аўдыё (напрыклад, 128к)",
"LabelAudioChannels": "Аўдыёканалы (1 або 2)", "LabelAudioChannels": "Аўдыёканалы (1 або 2)",
"LabelAudioCodec": "Аўдыёкодэк", "LabelAudioCodec": "Аўдыёкодэк",
"LabelAuthor": "Аўтар",
"LabelAuthorFirstLast": "Аўтар (Імя Прозвішча)",
"LabelAuthorLastFirst": "Аўтар (Прозвішча, Імя)",
"LabelAuthors": "Аўтары",
"LabelAutoDownloadEpisodes": "Аўтаматычнае спампаванне эпізодаў", "LabelAutoDownloadEpisodes": "Аўтаматычнае спампаванне эпізодаў",
"LabelAutoFetchMetadata": "Аўтаматычнае атрыманне метададзеных",
"LabelBackupAudioFiles": "Рэзервовае капіраванне аўдыёфайлаў", "LabelBackupAudioFiles": "Рэзервовае капіраванне аўдыёфайлаў",
"LabelBackupLocation": "Месцазнаходжанне рэзервовых копій",
"LabelBackupsNumberToKeepHelp": "Адначасова будзе выдаляцца толькі 1 рэзервовая копія, таму, калі ў вас іх больш, вам варта выдаліць іх уручную.",
"LabelBooks": "Кнігі",
"LabelChapters": "Раздзелы",
"LabelClosePlayer": "Зачыніць прайгравальнік",
"LabelCollapseSeries": "Згарнуць серыі",
"LabelComplete": "Завершана",
"LabelContinueListening": "Працягваць слухаць", "LabelContinueListening": "Працягваць слухаць",
"LabelContinueReading": "Працягнуць чытанне",
"LabelContinueSeries": "Працягнуць серыі",
"LabelDatetime": "Дата і час",
"LabelDescription": "Апісанне",
"LabelDiscFromFilename": "Дыск з імя файла",
"LabelDiscover": "Знайсці",
"LabelDownload": "Спампаваць", "LabelDownload": "Спампаваць",
"LabelDownloadNEpisodes": "Спампована {0} эпізодаў", "LabelDownloadNEpisodes": "Спампована {0} эпізодаў",
"LabelDownloadable": "Спампоўваецца", "LabelDownloadable": "Спампоўваецца",
"LabelDuration": "Працягласць",
"LabelEbook": "Электронная кніга",
"LabelEbooks": "Электронныя кнігі",
"LabelEnable": "Уключыць",
"LabelEncodingBackupLocation": "Рэзервовая копія вашых арыгінальных аўдыёфайлаў будзе захавана ў:", "LabelEncodingBackupLocation": "Рэзервовая копія вашых арыгінальных аўдыёфайлаў будзе захавана ў:",
"LabelEncodingChaptersNotEmbedded": "Раздзелы не ўбудаваны ў шматдарожкавыя аўдыякнігі.", "LabelEncodingChaptersNotEmbedded": "Раздзелы не ўбудаваны ў шматдарожкавыя аўдыякнігі.",
"LabelEncodingFinishedM4B": "Гатовы файл M4B будзе змешчаны ў вашу тэчку з аўдыякнігамі па адрасе:", "LabelEncodingFinishedM4B": "Гатовы файл M4B будзе змешчаны ў вашу тэчку з аўдыякнігамі па адрасе:",
"LabelEncodingInfoEmbedded": "Метаданыя будуць убудаваны ў аўдыядарожкі ўнутры вашай тэчкі з аўдыякнігамі.", "LabelEncodingInfoEmbedded": "Метаданыя будуць убудаваны ў аўдыядарожкі ўнутры вашай тэчкі з аўдыякнігамі.",
"LabelEncodingTimeWarning": "Кадаванне можа заняць да 30 хвілін.",
"LabelEnd": "Канец",
"LabelEndOfChapter": "Канец раздзела",
"LabelEpisode": "Эпізод",
"LabelEpisodeNotLinkedToRssFeed": "Эпізод не звязаны з RSS-стужкай",
"LabelEpisodeUrlFromRssFeed": "URL эпізоду з RSS-стужкі",
"LabelFeedURL": "URL стужкі",
"LabelFile": "Файл",
"LabelFileBirthtime": "Час стварэння файла",
"LabelFileModified": "Час змянення файла",
"LabelFilename": "Імя файла",
"LabelFinished": "Скончана",
"LabelFolder": "Тэчка",
"LabelFontBoldness": "Таўшчыня шрыфта",
"LabelFontScale": "Памер шрыфту",
"LabelGenre": "Жанр",
"LabelGenres": "Жанры",
"LabelHasEbook": "Мае электронную кнігу",
"LabelHasSupplementaryEbook": "Мае дадатковую электронную кнігу",
"LabelHideSubtitles": "Схаваць падзагалоўкі",
"LabelHost": "Хост",
"LabelInProgress": "У працэсе",
"LabelIncomplete": "Незавершана",
"LabelIntervalCustomDailyWeekly": "Карыстальніцкі штодзённы/штотыднёвы",
"LabelIntervalEvery12Hours": "Кожныя 12 гадзін",
"LabelIntervalEvery15Minutes": "Кожныя 15 хвілін",
"LabelIntervalEvery2Hours": "Кожныя 2 гадзіны",
"LabelIntervalEvery30Minutes": "Кожныя 30 хвілін",
"LabelIntervalEvery6Hours": "Кожныя 6 гадзін",
"LabelIntervalEveryDay": "Кожны дзень",
"LabelIntervalEveryHour": "Кожную гадзіну",
"LabelIntervalEveryMinute": "Кожную хвіліну",
"LabelInvert": "Інвертаваць",
"LabelItem": "Элемент",
"LabelLanguage": "Мова",
"LabelLanguageDefaultServer": "Мова сервера па змаўчанні",
"LabelLanguages": "Мовы",
"LabelLastBookAdded": "Апошняя дададзеная кніга",
"LabelLastBookUpdated": "Апошняя абноўленая кніга",
"LabelLastSeen": "Апошні прагляд",
"LabelLastTime": "Апошні раз",
"LabelLastUpdate": "Апошняе абнаўленне",
"LabelLayout": "Знешні выгляд",
"LabelLayoutSinglePage": "Аднабаковы",
"LabelLayoutSplitPage": "Падзяліць старонку",
"LabelLess": "Менш",
"LabelLibrariesAccessibleToUser": "Бібліятэкі, даступныя карыстальніку",
"LabelLibrary": "Бібліятэка",
"LabelLibraryFilterSublistEmpty": "Не {0}",
"LabelLibraryItem": "Элемент бібліятэкі",
"LabelLibraryName": "Імя бібліятэкі",
"LabelLimit": "Абмежаванне",
"LabelLineSpacing": "Міжрадковы інтэрвал",
"LabelListenAgain": "Паслухаць зноў",
"LabelMaxEpisodesToDownload": "Максімальная колькасць эпізодаў для спампоўкі. Выкарыстоўвайце 0 для неабмежаванай колькасці.", "LabelMaxEpisodesToDownload": "Максімальная колькасць эпізодаў для спампоўкі. Выкарыстоўвайце 0 для неабмежаванай колькасці.",
"LabelMaxEpisodesToDownloadPerCheck": "Максімальная колькасць новых эпізодаў для спампоўкі за праверку", "LabelMaxEpisodesToDownloadPerCheck": "Максімальная колькасць новых эпізодаў для спампоўкі за праверку",
"LabelMaxEpisodesToKeepHelp": "Значэнне 0 не ўстанаўлівае максімальнага абмежавання. Пасля аўтаматычнай спампоўкі новага эпізоду будзе выдалены самы стары эпізод, калі ў вас больш за X эпізодаў. Пры кожнай новай спампоўцы будзе выдаляцца толькі 1 эпізод.", "LabelMaxEpisodesToKeepHelp": "Значэнне 0 не ўстанаўлівае максімальнага абмежавання. Пасля аўтаматычнай спампоўкі новага эпізоду будзе выдалены самы стары эпізод, калі ў вас больш за X эпізодаў. Пры кожнай новай спампоўцы будзе выдаляцца толькі 1 эпізод.",
"LabelMediaPlayer": "Медыяпрайгравальнік",
"LabelMediaType": "Тып медыя",
"LabelMissing": "Адсутнічае",
"LabelMore": "Больш",
"LabelMoreInfo": "Больш інфармацыі",
"LabelName": "Імя",
"LabelNarrator": "Чытальнік",
"LabelNarrators": "Чытальнікі",
"LabelNewestAuthors": "Новыя аўтары",
"LabelNewestEpisodes": "Новыя эпізоды",
"LabelNotFinished": "Не скончана",
"LabelNotStarted": "Не пачата",
"LabelNotificationsMaxFailedAttemptsHelp": "Апавяшчэнні адключаюцца пасля таго, як не ўдаецца іх адправіць гэтулькі разоў",
"LabelNumberOfEpisodes": "# з эпізодаў",
"LabelOpenRSSFeed": "Адкрыць RSS-стужку",
"LabelPassword": "Пароль",
"LabelPath": "Шлях",
"LabelPermissionsDownload": "Можна спампаваць", "LabelPermissionsDownload": "Можна спампаваць",
"LabelPlaylists": "Cпісs прайгравання",
"LabelPodcast": "Падкаст",
"LabelPodcasts": "Падкасты",
"LabelPreventIndexing": "Прадухіліць індэксацыю вашай стужкі каталогамі падкастаў iTunes і Google",
"LabelProgress": "Прагрэс",
"LabelPubDate": "Дата публікацыі",
"LabelPublishYear": "Год публікацыі",
"LabelPublishedDate": "Апублікавана {0}",
"LabelRSSFeedCustomOwnerEmail": "Карыстальніцкая электронная пошта ўладальніка",
"LabelRSSFeedCustomOwnerName": "Карыстальніцкае імя ўладальніка",
"LabelRSSFeedOpen": "RSS-стужка адкрытая",
"LabelRSSFeedPreventIndexing": "Прадухіліць індэксацыю",
"LabelRSSFeedSlug": "Ідэнтыфікатар RSS-стужкі",
"LabelRSSFeedURL": "URL RSS-стужкі",
"LabelRandomly": "Выпадкова",
"LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працягваць слухаць", "LabelReAddSeriesToContinueListening": "Дадаць серыю зноў у Працягваць слухаць",
"LabelRead": "Чытаць",
"LabelReadAgain": "Чытаць зноў",
"LabelRecentSeries": "Апошнія серыі",
"LabelRecentlyAdded": "Нядаўна дададзеныя",
"LabelRemoveAllMetadataAbs": "Выдаліць усе файлы metadata.abs",
"LabelRemoveAllMetadataJson": "Выдаліць усе файлы metadata.json",
"LabelRemoveCover": "Выдаліць вокладку",
"LabelRemoveMetadataFile": "Выдаліць файлы метададзеных у тэчках элементаў бібліятэкі",
"LabelRemoveMetadataFileHelp": "Выдаліць усе файлы metadata.json і metadata.abs у вашых {0} тэчках.",
"LabelRowsPerPage": "Радкоў на старонку",
"LabelSearchTerm": "Пошукавы запыт",
"LabelSearchTitle": "Пошук па загалоўку",
"LabelSearchTitleOrASIN": "Пошук па загалоўку або ASIN",
"LabelSeason": "Сезон",
"LabelSeasonNumber": "Сезон #{0}",
"LabelSelectAll": "Выбраць усё",
"LabelSelectAllEpisodes": "Выбраць усе эпізоды",
"LabelSelectEpisodesShowing": "Выбраць {0} эпізодаў для паказу",
"LabelSelectUsers": "Выбраць карыстальнікаў",
"LabelSendEbookToDevice": "Адправіць электронную кнігу на...",
"LabelSequence": "Паслядоўнасць",
"LabelSerial": "Серыйны",
"LabelSeries": "Серыі",
"LabelSeriesName": "Назва серыі",
"LabelSeriesProgress": "Прагрэс серыі",
"LabelServerLogLevel": "Узровень журнала сервера",
"LabelServerYearReview": "Вынікі года сервера ({0})",
"LabelSetEbookAsPrimary": "Зрабіць асноўным",
"LabelSetEbookAsSupplementary": "Зрабіць дадатковым",
"LabelSettingsAllowIframe": "Дазволіць убудоўванне ў iframe",
"LabelSettingsAudiobooksOnly": "Толькі аўдыякнігі",
"LabelSettingsAudiobooksOnlyHelp": "Уключэнне гэтай налады будзе ігнараваць файлы электронных кніг, калі толькі яны не знаходзяцца ў тэчцы з аўдыякнігамі. У такім выпадку яны будуць пазначаны як дадатковыя электронныя кнігі.",
"LabelSettingsBookshelfViewHelp": "Рэалістычны дызайн з драўлянымі паліцамі",
"LabelSettingsEnableWatcherHelp": "Адключае аўтаматычнае дадаванне/абнаўленне элементаў пры выяўленні змен у файлах. *Патрабуецца перазапуск сервера",
"LabelSettingsEpubsAllowScriptedContent": "Дазволіць скрыптавы кантэнт у EPUB",
"LabelSettingsEpubsAllowScriptedContentHelp": "Дазволіць EPUB-файлам выконваць скрыпты. Рэкамендуецца пакінуць гэтую наладу выключанай, калі вы не давяраеце крыніцы EPUB-файлаў.",
"LabelSettingsExperimentalFeatures": "Эксперыментальныя функцыі",
"LabelSettingsExperimentalFeaturesHelp": "Функцыі ў распрацоўцы, для якіх вашы водгукі і дапамога ў тэставанні будуць карыснымі. Націсніце, каб адкрыць абмеркаванне на GitHub.",
"LabelSettingsFindCovers": "Знайсці вокладкі",
"LabelSettingsFindCoversHelp": "Калі ў вашай аўдыякнізе няма ўбудаванай вокладкі або выявы вокладкі ў тэчцы, сканер паспрабуе знайсці вокладку.<br>Заўвага: гэта павялічыць час сканавання",
"LabelSettingsHideSingleBookSeries": "Схаваць серыі з адной кнігай",
"LabelSettingsHideSingleBookSeriesHelp": "Серыі, якія змяшчаюць толькі адну кнігу, будуць схаваны са старонкі серый і паліц на галоўнай старонцы.",
"LabelSettingsHomePageBookshelfView": "Галоўная старонка выкарыстоўвае выгляд кніжнай паліцы",
"LabelSettingsLibraryBookshelfView": "Бібліятэка выкарыстоўвае выгляд кніжнай паліцы",
"LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Час, што застаўся, менш за (секунды)",
"LabelSettingsLibraryMarkAsFinishedWhen": "Пазначыць элемент медыя як скончаны, калі",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропусціць папярэднія кнігі ў \"Працягнуць серыю\"",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Палка \"Працягнуць серыю\" на галоўнай старонцы паказвае першую не пачатую кнігу ў серыях, дзе завершана хаця б адна кніга і няма кніг у працэсе чытання. Уключэнне гэтай налады дазволіць працягваць серыю з самай апошняй завершанай кнігі замест першай не пачатай.",
"LabelSettingsParseSubtitles": "Разабраць падзагалоўкі",
"LabelSettingsParseSubtitlesHelp": "Выдзяляць падзагаловак з назваў тэчак аўдыякніг.<br>Падзагаловак павінен быць аддзелены знакам \" - \".<br>Напрыклад, \"Назва кнігі - Падзагаловак тут\" мае падзагаловак \"Падзагаловак тут\"",
"LabelSettingsTimeFormat": "Фармат часу",
"LabelShareDownloadableHelp": "Дазваляе карыстальнікам, якія маюць спасылку на доступ, спампаваць ZIP-файл элемента бібліятэкі.", "LabelShareDownloadableHelp": "Дазваляе карыстальнікам, якія маюць спасылку на доступ, спампаваць ZIP-файл элемента бібліятэкі.",
"LabelShowAll": "Паказаць усё",
"LabelShowSubtitles": "Паказаць падзагалоўкі",
"LabelSize": "Памер",
"LabelSleepTimer": "Таймер сну",
"LabelStart": "Пачаць",
"LabelStartTime": "Час пачатку",
"LabelStatsAudioTracks": "Аўдыядарожкі", "LabelStatsAudioTracks": "Аўдыядарожкі",
"LabelStatsAuthors": "Аўтары",
"LabelStatsBestDay": "Лепшы дзень",
"LabelStatsDailyAverage": "У сярэднім за дзень",
"LabelStatsDays": "Дзён",
"LabelStatsDaysListened": "Дзён праслухана",
"LabelStatsHours": "Гадзін",
"LabelStatsInARow": "без перапынку",
"LabelStatsItemsFinished": "Скончаныя элементы",
"LabelStatsItemsInLibrary": "Элементаў у бібліятэцы",
"LabelStatsMinutes": "хвілін",
"LabelStatsMinutesListening": "Хвілін праслухоўвання",
"LabelStatsOverallDays": "Агульная колькасць дзён",
"LabelStatsOverallHours": "Агульная колькасць гадзін",
"LabelStatsWeekListening": "Праслухана за тыдзень",
"LabelSubtitle": "Падзагаловак",
"LabelSupportedFileTypes": "Падтрымліваемыя тыпы файлаў",
"LabelTag": "Метка",
"LabelTags": "Меткі",
"LabelTagsAccessibleToUser": "Меткі, даступныя карыстальніку",
"LabelTagsNotAccessibleToUser": "Меткі, недаступныя карыстальніку",
"LabelTasks": "Выконваюцца задачы",
"LabelTextEditorBulletedList": "Маркіраваны спіс",
"LabelTextEditorLink": "Спасылка",
"LabelTextEditorNumberedList": "Нумараваны спіс",
"LabelTextEditorUnlink": "Адключыць спасылку",
"LabelTheme": "Тэма",
"LabelThemeDark": "Цёмная",
"LabelThemeLight": "Светлая",
"LabelTimeBase": "Часавая база",
"LabelTimeDurationXHours": "{0} гадзін",
"LabelTimeDurationXMinutes": "{0} хвілін",
"LabelTimeDurationXSeconds": "{0} секунд",
"LabelTimeInMinutes": "Час у хвілінах",
"LabelTimeLeft": "{0} засталося",
"LabelTimeListened": "Час праслухоўвання",
"LabelTimeListenedToday": "Час праслухоўвання сёння",
"LabelTimeRemaining": "Засталося {0}",
"LabelTimeToShift": "Час зрушэння ў секундах",
"LabelTitle": "Назва",
"LabelToolsSplitM4bDescription": "Стварэнне MP3 з M4B, падзеленага па раздзелах, з убудаванымі метаданымі, вокладкай і раздзеламі.",
"LabelTotalDuration": "Агульная працягласць",
"LabelTotalTimeListened": "Агульны час праслухоўвання",
"LabelTrackFromFilename": "Дарожка з імя файла",
"LabelTrackFromMetadata": "Дарожка з метаданых",
"LabelTracks": "Дарожкі", "LabelTracks": "Дарожкі",
"LabelTracksMultiTrack": "Шматдарожкавы",
"LabelTracksNone": "Няма дарожак",
"LabelTracksSingleTrack": "Аднадарожкавы",
"LabelType": "Тып",
"LabelUndo": "Адмяніць",
"LabelUnknown": "Невядома",
"LabelUnknownPublishDate": "Невядомая дата публікацыі",
"LabelUpdateCover": "Абнавіць вокладку",
"LabelUpdateCoverHelp": "Дазволіць замену існуючых вокладак для выбраных кніг пры выяўленні адпаведнасці",
"LabelUpdateDetails": "Абнавіць падрабязнасці",
"LabelUpdateDetailsHelp": "Дазволіць замену існуючых падрабязнасцей для выбраных кніг пры выяўленні адпаведнасці",
"LabelUpdatedAt": "Абноўлена ў",
"LabelUploaderDragAndDrop": "Перацягвайце і скідайце файлы або тэчкі",
"LabelUploaderDragAndDropFilesOnly": "Перацягвайце і скідайце файлы",
"LabelUploaderDropFiles": "Скідайце файлы",
"LabelUploaderItemFetchMetadataHelp": "Аўтаматычна атрымліваць назву, аўтара і серыю",
"LabelUseAdvancedOptions": "Выкарыстоўваць пашыраныя параметры",
"LabelUseChapterTrack": "Выкарыстоўваць дарожку раздзелаў",
"LabelUseFullTrack": "Выкарыстоўваць поўную дарожку",
"LabelUser": "Карыстальнік",
"LabelUsername": "Імя карыстальніка",
"LabelValue": "Значэнне",
"LabelVersion": "Версія",
"LabelViewBookmarks": "Праглядзець закладкі",
"LabelViewChapters": "Праглядзець раздзелы",
"LabelViewPlayerSettings": "Праглядзець налады прайгравальніка",
"LabelViewQueue": "Праглядзець чаргу прайгравальніка",
"LabelVolume": "Гучнасць",
"LabelWebRedirectURLsDescription": "Аўтарызуйце гэтыя URL у вашым OAuth-правайдары для перанакіравання ў вэб-дадатак пасля ўваходу:",
"LabelWebRedirectURLsSubfolder": "Падтэчка для URL-перанакіраванняў",
"LabelWeekdaysToRun": "Дні тыдня для запуску",
"LabelXBooks": "{0} кніг",
"LabelXItems": "{0} элементаў",
"LabelYearReviewHide": "Схаваць вынікі года",
"LabelYearReviewShow": "Азнаёміцца з вынікамі года",
"LabelYourAudiobookDuration": "Працягласць вашай аўдыякнігі",
"LabelYourBookmarks": "Вашы закладкі",
"LabelYourPlaylists": "Вашы спісы прайгравання",
"LabelYourProgress": "Ваш прагрэс",
"MessageAddToPlayerQueue": "Дадаць у чаргу прайгравальніка",
"MessageAppriseDescription": "Каб выкарыстоўваць гэтую функцыю, вам спатрэбіцца запусціць асобнік <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> або API, які будзе апрацоўваць тыя ж запыты.<br />URL Apprise API павінен быць поўным шляхам для адпраўкі апавяшчэння, напрыклад, калі ваш API працуе па адрасе <code>http://192.168.1.1:8337</code>, то вы павінны ўвесці <code>http://192.168.1.1:8337/notify</code>.",
"MessageBackupsDescription": "Рэзервовыя копіі ўключаюць карыстальнікаў, іх прагрэс, падрабязнасці элементаў бібліятэкі, налады сервера і выявы, якія захоўваюцца ў <code>/metadata/items</code> і <code>/metadata/authors</code>. Рэзервовыя копіі <strong>не</strong> ўключаюць файлы, якія захоўваюцца ў вашых тэчках бібліятэкі.",
"MessageBackupsLocationEditNote": "Заўвага: Абнаўленне месцазнаходжання рэзервовых копій не перамяшчае і не змяняе існуючыя рэзервовыя копіі",
"MessageBackupsLocationNoEditNote": "Заўвага: Месцазнаходжанне рэзервовых копій задаецца праз зменную асяроддзя і не можа быць зменена тут.",
"MessageBackupsLocationPathEmpty": "Шлях да месцазнаходжання рэзервовых копій не можа быць пустым",
"MessageBatchEditPopulateMapDetailsAllHelp": "Запоўніце ўключаныя палі дадзенымі з усіх элементаў. Палі з некалькімі значэннямі будуць аб'яднаны",
"MessageBatchEditPopulateMapDetailsItemHelp": "Запоўніце ўключаныя палі падрабязнасцей карты дадзенымі з гэтага элемента",
"MessageBookshelfNoRSSFeeds": "Няма адкрытых RSS-стужак",
"MessageChapterErrorStartGteDuration": "Няправільны час пачатку: ён павінен быць меншым за працягласць аўдыякнігі",
"MessageChapterErrorStartLtPrev": "Няправільны час пачатку: ён павінен быць большым або роўным часу пачатку папярэдняга раздзела",
"MessageConfirmCloseFeed": "Вы ўпэўнены, што жадаеце закрыць гэтую стужку?",
"MessageConfirmRemoveListeningSessions": "Вы ўпэўнены, што жадаеце выдаліць {0} сеансаў праслухоўвання?", "MessageConfirmRemoveListeningSessions": "Вы ўпэўнены, што жадаеце выдаліць {0} сеансаў праслухоўвання?",
"MessageConfirmRemovePlaylist": "Вы ўпэўненыя, што жадаеце выдаліць свой спіс прайгравання \"{0}\"?",
"MessageConfirmSendEbookToDevice": "Вы ўпэўнены, што хочаце адправіць {0} электронную кнігу \"{1}\" на прыладу \"{2}\"?",
"MessageDownloadingEpisode": "Спампоўка эпізоду", "MessageDownloadingEpisode": "Спампоўка эпізоду",
"MessageEpisodesQueuedForDownload": "{0} эпізод(аў) у чарзе для спампоўкі", "MessageEpisodesQueuedForDownload": "{0} эпізод(аў) у чарзе для спампоўкі",
"MessageEreaderDevices": "Каб забяспечыць дастаўку электронных кніг, вам можа спатрэбіцца дадаць вышэйзгаданы адрас электроннай пошты як дазволенага адпраўніка для кожнай прылады, пералічанай ніжэй.",
"MessageFeedURLWillBe": "URL стужкі будзе {0}",
"MessageFetching": "Атрыманне...",
"MessageLoading": "Загрузка...",
"MessageMapChapterTitles": "Супаставіць назвы раздзелаў з вашымі існуючымі раздзеламі аўдыякнігі без змянення часовых метак",
"MessageMarkAsFinished": "Пазначыць як скончана",
"MessageNoBookmarks": "Няма закладак",
"MessageNoChapters": "Няма раздзелаў",
"MessageNoCollections": "Няма калекцый",
"MessageNoDownloadsInProgress": "Зараз няма актыўных спамповак", "MessageNoDownloadsInProgress": "Зараз няма актыўных спамповак",
"MessageNoDownloadsQueued": "Няма спамповак у чарзе", "MessageNoDownloadsQueued": "Няма спамповак у чарзе",
"MessageNoItems": "Няма элементаў",
"MessageNoItemsFound": "Элементы не знойдзены",
"MessageNoListeningSessions": "Няма сеансаў праслухоўвання", "MessageNoListeningSessions": "Няма сеансаў праслухоўвання",
"MessageNoMediaProgress": "Няма прагрэсу медыя",
"MessageNoPodcastFeed": "Няправільны падкаст: Няма стужкі",
"MessageNoPodcastsFound": "Падкасты не знойдзены",
"MessageNoUpdatesWereNecessary": "Абнаўленні не патрабаваліся",
"MessageNoUserPlaylists": "У вас няма спісаў прайгравання",
"MessageNoUserPlaylistsHelp": "Спісы прайгравання прыватныя. Толькі карыстальнік, які іх стварыў, можа іх бачыць.",
"MessageOpmlPreviewNote": "Заўвага: гэта папярэдні прагляд разабранага OPML-файла. Фактычная назва падкаста будзе ўзятая з RSS-стужкі.",
"MessagePlaylistCreateFromCollection": "Стварыць спіс прайгравання з калекцыі",
"MessagePodcastHasNoRSSFeedForMatching": "У падкаста няма URL RSS-стужкі для супадзення",
"MessagePodcastSearchField": "Увядзіце пошукавы запыт або URL RSS-стужкі",
"MessageReportBugsAndContribute": "Паведамляйце пра памылкі, прапануйце новыя функцыі і ўдзельнічайце на",
"MessageScheduleRunEveryWeekdayAtTime": "Выконваць кожныя {0} у {1}",
"MessageStartPlaybackAtTime": "Пачаць прайграванне для \"{0}\" з {1}?",
"MessageTaskCanceledByUser": "Задача скасавана карыстальнікам",
"MessageTaskDownloadingEpisodeDescription": "Спампоўка эпізоду \"{0}\"", "MessageTaskDownloadingEpisodeDescription": "Спампоўка эпізоду \"{0}\"",
"MessageTaskOpmlImportDescription": "Стварэнне падкастаў з {0} RSS-стужак",
"MessageTaskOpmlImportFeed": "Імпарт стужкі з OPML",
"MessageTaskOpmlImportFeedDescription": "Імпартаванне RSS-стужкі \"{0}\"",
"MessageTaskOpmlImportFeedFailed": "Не ўдалося атрымаць стужку падкаста",
"MessageTaskOpmlImportFeedPodcastDescription": "Стварэнне падкаста \"{0}\"",
"MessageTaskOpmlImportFeedPodcastExists": "Падкаст ужо існуе па гэтым шляху",
"MessageTaskOpmlImportFeedPodcastFailed": "Не ўдалося стварыць падкаст",
"MessageTaskOpmlParseNoneFound": "У OPML-файле не знойдзена стужак",
"NoteChapterEditorTimes": "Заўвага: Час пачатку першага раздзела павінен заставацца 0:00, а час пачатку апошняга раздзела не можа перавышаць працягласць гэтай аўдыякнігі.",
"NoteRSSFeedPodcastAppsHttps": "Папярэджанне: большасць праграм для падкастаў патрабуюць, каб URL RSS-стужкі выкарыстоўваў HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Папярэджанне: адзін ці больш вашых эпізодаў не маюць даты публікацыі. Некаторыя праграмы для падкастаў патрабуюць гэтага.",
"NoteUploaderFoldersWithMediaFiles": "Тэчкі з медыяфайламі будуць апрацоўвацца як асобныя элементы бібліятэкі.",
"NotificationOnEpisodeDownloadedDescription": "Выклікаецца, калі эпізод падкаста аўтаматычна спампоўваецца", "NotificationOnEpisodeDownloadedDescription": "Выклікаецца, калі эпізод падкаста аўтаматычна спампоўваецца",
"PlaceholderNewPlaylist": "Імя новага спіса прайгравання",
"StatsBooksFinished": "кнігі скончаны",
"StatsBooksFinishedThisYear": "Некаторыя кнігі скончаны ў гэтым годзе…",
"StatsBooksListenedTo": "кнігі, якія былі праслуханы",
"StatsCollectionGrewTo": "Ваша калекцыя кніг павялічылася да…",
"ToastAccountUpdateSuccess": "Уліковы запіс абноўлены", "ToastAccountUpdateSuccess": "Уліковы запіс абноўлены",
"ToastBookmarkCreateFailed": "Не ўдалося стварыць закладку",
"ToastDateTimeInvalidOrIncomplete": "Дата і час указаны некарэктна або не цалкам",
"ToastDeviceTestEmailFailed": "Не ўдалося адправіць тэставае электроннае пісьмо",
"ToastEncodeCancelFailed": "Не ўдалося скасаваць кадаванне",
"ToastEncodeCancelSucces": "Кадаванне скасавана",
"ToastEpisodeDownloadQueueClearFailed": "Не ўдалося ачысціць чаргу", "ToastEpisodeDownloadQueueClearFailed": "Не ўдалося ачысціць чаргу",
"ToastEpisodeDownloadQueueClearSuccess": "Чарга спампоўкі эпізодаў ачышчана", "ToastEpisodeDownloadQueueClearSuccess": "Чарга спампоўкі эпізодаў ачышчана",
"ToastInvalidMaxEpisodesToDownload": "Няправільная максімальная колькасць эпізодаў для спампоўкі", "ToastInvalidMaxEpisodesToDownload": "Няправільная максімальная колькасць эпізодаў для спампоўкі",
"ToastItemMarkedAsFinishedFailed": "Не ўдалося пазначыць як Скончана",
"ToastItemMarkedAsFinishedSuccess": "Элемент пазначаны як Завершаны",
"ToastItemMarkedAsNotFinishedFailed": "Не ўдалося пазначыць як Незавершанае",
"ToastItemMarkedAsNotFinishedSuccess": "Элемент пазначаны як Незавершаны",
"ToastItemUpdateSuccess": "Элемент абноўлены",
"ToastLibraryCreateFailed": "Не ўдалося стварыць бібліятэку",
"ToastLibraryCreateSuccess": "Бібліятэка \"{0}\" створана",
"ToastLibraryDeleteFailed": "Не ўдалося выдаліць бібліятэку",
"ToastLibraryDeleteSuccess": "Бібліятэка выдалена",
"ToastLibraryScanFailedToStart": "Не ўдалося запусціць сканаванне",
"ToastLibraryScanStarted": "Сканаванне бібліятэкі запушчана",
"ToastLibraryUpdateSuccess": "Бібліятэка \"{0}\" абноўлена",
"ToastMatchAllAuthorsFailed": "Не ўдалося знайсці адпаведнасць для ўсіх аўтараў",
"ToastMetadataFilesRemovedError": "Памылка пры выдаленні metadata.{0} файлаў",
"ToastMetadataFilesRemovedNoneFound": "У бібліятэцы не знойдзены metadata.{0} файлаў",
"ToastMetadataFilesRemovedNoneRemoved": "Не выдалена metadata.{0} файлаў",
"ToastMetadataFilesRemovedSuccess": "{0} metadata.{1} файлаў выдалена",
"ToastMustHaveAtLeastOnePath": "Павінен быць хаця б адзін шлях",
"ToastNameEmailRequired": "Імя і электронная пошта абавязковыя",
"ToastNameRequired": "Імя абавязковае",
"ToastNewUserCreatedFailed": "Не ўдалося стварыць уліковы запіс: \"{0}\"", "ToastNewUserCreatedFailed": "Не ўдалося стварыць уліковы запіс: \"{0}\"",
"ToastNewUserCreatedSuccess": "Новы ўліковы запіс створаны", "ToastNewUserCreatedSuccess": "Новы ўліковы запіс створаны",
"ToastNoRSSFeed": "У падкаста няма RSS-стужкі",
"ToastPlaylistCreateFailed": "Не ўдалося стварыць спіс прайгравання",
"ToastPlaylistCreateSuccess": "Спіс прайгравання створаны",
"ToastPlaylistRemoveSuccess": "Спіс прайгравання выдалены",
"ToastPlaylistUpdateSuccess": "Спіс прайгравання абноўлены",
"ToastPodcastGetFeedFailed": "Не ўдалося атрымаць стужку падкаста",
"ToastPodcastNoEpisodesInFeed": "У RSS-стужцы не знойдзена эпізодаў",
"ToastPodcastNoRssFeed": "У падкаста няма RSS-стужкі",
"ToastRSSFeedCloseFailed": "Не ўдалося закрыць RSS-стужку",
"ToastRSSFeedCloseSuccess": "RSS-стужка закрыта",
"ToastSendEbookToDeviceFailed": "Не ўдалося адправіць электронную кнігу на прыладу",
"ToastSendEbookToDeviceSuccess": "Электронная кніга адпраўлена на прыладу \"{0}\"",
"ToastSleepTimerDone": "Таймер сну скончыўся... Хр-р-р",
"ToastUserPasswordMustChange": "Новы пароль не можа супадаць са старым", "ToastUserPasswordMustChange": "Новы пароль не можа супадаць са старым",
"ToastUserRootRequireName": "Неабходна ўвесці імя карыстальніка адміністратара" "ToastUserRootRequireName": "Неабходна ўвесці імя карыстальніка адміністратара"
} }
+186 -113
View File
@@ -1,5 +1,5 @@
{ {
"ButtonAdd": "Добави", "ButtonAdd": "Създай",
"ButtonAddChapters": "Добави Глави", "ButtonAddChapters": "Добави Глави",
"ButtonAddDevice": "Добави Устройство", "ButtonAddDevice": "Добави Устройство",
"ButtonAddLibrary": "Добави Библиотека", "ButtonAddLibrary": "Добави Библиотека",
@@ -10,15 +10,18 @@
"ButtonApplyChapters": "Приложи Глави", "ButtonApplyChapters": "Приложи Глави",
"ButtonAuthors": "Автори", "ButtonAuthors": "Автори",
"ButtonBack": "Назад", "ButtonBack": "Назад",
"ButtonBatchEditPopulateFromExisting": "Попълни от съществуващи",
"ButtonBatchEditPopulateMapDetails": "Попълни подробности за картата",
"ButtonBrowseForFolder": "Прегледай за папка", "ButtonBrowseForFolder": "Прегледай за папка",
"ButtonCancel": "Откажи", "ButtonCancel": "Отказ",
"ButtonCancelEncode": "Откажи закодирането", "ButtonCancelEncode": "Откажи закодирането",
"ButtonChangeRootPassword": "Промени паролата за Root", "ButtonChangeRootPassword": "Промени паролата за Root",
"ButtonCheckAndDownloadNewEpisodes": "Провери и Свали Нови Епизоди", "ButtonCheckAndDownloadNewEpisodes": "Провери и Свали Нови Епизоди",
"ButtonChooseAFolder": "Избери Папка", "ButtonChooseAFolder": "Избери Папка",
"ButtonChooseFiles": "Избери Файлове", "ButtonChooseFiles": "Избери Файлове",
"ButtonClearFilter": "Изчисти Филтър", "ButtonClearFilter": "Изчисти филтър",
"ButtonCloseFeed": "Затвори Feed", "ButtonCloseFeed": "Затвори стената",
"ButtonCloseSession": "Затвори отворената сесия",
"ButtonCollections": "Колекции", "ButtonCollections": "Колекции",
"ButtonConfigureScanner": "Конфигурирай Скенера", "ButtonConfigureScanner": "Конфигурирай Скенера",
"ButtonCreate": "Създай", "ButtonCreate": "Създай",
@@ -28,6 +31,9 @@
"ButtonEdit": "Редактирай", "ButtonEdit": "Редактирай",
"ButtonEditChapters": "Редактирай Глави", "ButtonEditChapters": "Редактирай Глави",
"ButtonEditPodcast": "Редактирай Подкаст", "ButtonEditPodcast": "Редактирай Подкаст",
"ButtonEnable": "Активирай",
"ButtonFireAndFail": "Задействай и неуспей",
"ButtonFireOnTest": "Задействай събитие onTest",
"ButtonForceReScan": "Принудително Пресканиране", "ButtonForceReScan": "Принудително Пресканиране",
"ButtonFullPath": "Пълен Път", "ButtonFullPath": "Пълен Път",
"ButtonHide": "Скрий", "ButtonHide": "Скрий",
@@ -44,24 +50,31 @@
"ButtonMatchAllAuthors": "Съвпадение на Всички Автори", "ButtonMatchAllAuthors": "Съвпадение на Всички Автори",
"ButtonMatchBooks": "Съвпадение на Книги", "ButtonMatchBooks": "Съвпадение на Книги",
"ButtonNevermind": "Няма значение", "ButtonNevermind": "Няма значение",
"ButtonNext": "Следващо",
"ButtonNextChapter": "Следваща Глава", "ButtonNextChapter": "Следваща Глава",
"ButtonOk": "Добре", "ButtonNextItemInQueue": "Следващият елемент в опашката",
"ButtonOpenFeed": "Отвори Feed", "ButtonOk": "Приемам",
"ButtonOpenFeed": "Отвори стената",
"ButtonOpenManager": "Отвори Мениджър", "ButtonOpenManager": "Отвори Мениджър",
"ButtonPause": "Пауза", "ButtonPause": "Паузирай",
"ButtonPlay": "Пусни", "ButtonPlay": "Пусни",
"ButtonPlayAll": "Пусни всички",
"ButtonPlaying": "Пуска се", "ButtonPlaying": "Пуска се",
"ButtonPlaylists": "Плейлисти", "ButtonPlaylists": "Плейлисти",
"ButtonPrevious": "Предишен",
"ButtonPreviousChapter": "Предишна Глава", "ButtonPreviousChapter": "Предишна Глава",
"ButtonProbeAudioFile": "Провери аудио файла",
"ButtonPurgeAllCache": "Изчисти Всички Кешове", "ButtonPurgeAllCache": "Изчисти Всички Кешове",
"ButtonPurgeItemsCache": "Изчисти Кеша на Елементи", "ButtonPurgeItemsCache": "Изчисти Кеша на Елементи",
"ButtonQueueAddItem": "Добави към опашката", "ButtonQueueAddItem": "Добави към опашката",
"ButtonQueueRemoveItem": "Премахни от опашката", "ButtonQueueRemoveItem": "Премахни от опашката",
"ButtonQuickEmbed": "Бързо вграждане",
"ButtonQuickEmbedMetadata": "Бързо вграждане метадата",
"ButtonQuickMatch": "Бързо Съпоставяне", "ButtonQuickMatch": "Бързо Съпоставяне",
"ButtonReScan": "Пресканирай", "ButtonReScan": "Пресканирай",
"ButtonRead": "Прочети", "ButtonRead": "Прочети",
"ButtonReadLess": "Покажи по-малко", "ButtonReadLess": "Изчети по-малко",
"ButtonReadMore": окажи повече", "ButtonReadMore": рочети дълго",
"ButtonRefresh": "Обнови", "ButtonRefresh": "Обнови",
"ButtonRemove": "Премахни", "ButtonRemove": "Премахни",
"ButtonRemoveAll": "Премахни Всички", "ButtonRemoveAll": "Премахни Всички",
@@ -77,7 +90,9 @@
"ButtonSaveTracklist": "Запази Списък с Канали", "ButtonSaveTracklist": "Запази Списък с Канали",
"ButtonScan": "Сканирай", "ButtonScan": "Сканирай",
"ButtonScanLibrary": "Сканирай Библиотека", "ButtonScanLibrary": "Сканирай Библиотека",
"ButtonSearch": "Търси", "ButtonScrollLeft": "Скролни наляво",
"ButtonScrollRight": "Скролни надясно",
"ButtonSearch": "Търси в",
"ButtonSelectFolderPath": "Избери Път на Папка", "ButtonSelectFolderPath": "Избери Път на Папка",
"ButtonSeries": "Серии", "ButtonSeries": "Серии",
"ButtonSetChaptersFromTracks": "Задай Глави от Песни", "ButtonSetChaptersFromTracks": "Задай Глави от Песни",
@@ -86,8 +101,10 @@
"ButtonShow": "Покажи", "ButtonShow": "Покажи",
"ButtonStartM4BEncode": "Започни M4B Кодиране", "ButtonStartM4BEncode": "Започни M4B Кодиране",
"ButtonStartMetadataEmbed": "Започни Вграждане на Метаданни", "ButtonStartMetadataEmbed": "Започни Вграждане на Метаданни",
"ButtonStats": "Статистики",
"ButtonSubmit": "Изпрати", "ButtonSubmit": "Изпрати",
"ButtonTest": "Тест", "ButtonTest": "Тест",
"ButtonUnlinkOpenId": "Премахни връзката с OpenID",
"ButtonUpload": "Качи", "ButtonUpload": "Качи",
"ButtonUploadBackup": "Качи Backup", "ButtonUploadBackup": "Качи Backup",
"ButtonUploadCover": "Качи Корица", "ButtonUploadCover": "Качи Корица",
@@ -100,9 +117,10 @@
"ErrorUploadFetchMetadataNoResults": "Метаданните не могат да бъдат взети - опитайте да обновите заглавието и/или автора", "ErrorUploadFetchMetadataNoResults": "Метаданните не могат да бъдат взети - опитайте да обновите заглавието и/или автора",
"ErrorUploadLacksTitle": "Трябва да има Заглавие", "ErrorUploadLacksTitle": "Трябва да има Заглавие",
"HeaderAccount": "Профил", "HeaderAccount": "Профил",
"HeaderAdvanced": "Разширени", "HeaderAddCustomMetadataProvider": "Добави персонализиран доставчик на метаданни",
"HeaderAdvanced": "Разширени настройки",
"HeaderAppriseNotificationSettings": "Apprise Notification Опции", "HeaderAppriseNotificationSettings": "Apprise Notification Опции",
"HeaderAudioTracks": "Звуков Канал", "HeaderAudioTracks": "Песни",
"HeaderAudiobookTools": "Инструмент за Менижиране на Аудиокниги", "HeaderAudiobookTools": "Инструмент за Менижиране на Аудиокниги",
"HeaderAuthentication": "Аутентикация", "HeaderAuthentication": "Аутентикация",
"HeaderBackups": "Архив", "HeaderBackups": "Архив",
@@ -110,26 +128,26 @@
"HeaderChapters": "Глави", "HeaderChapters": "Глави",
"HeaderChooseAFolder": "Избети Папка", "HeaderChooseAFolder": "Избети Папка",
"HeaderCollection": "Колекция", "HeaderCollection": "Колекция",
"HeaderCollectionItems": "Елементи на Колекция", "HeaderCollectionItems": "Елемент в колекция",
"HeaderCover": "Корица", "HeaderCover": "Корица",
"HeaderCurrentDownloads": "Текущи Сваляния", "HeaderCurrentDownloads": "Текущи Сваляния",
"HeaderCustomMessageOnLogin": "Потребителско съобщение при влизане", "HeaderCustomMessageOnLogin": "Потребителско съобщение при влизане",
"HeaderCustomMetadataProviders": "Потребителски Доставчици на Метаданни", "HeaderCustomMetadataProviders": "Потребителски Доставчици на Метаданни",
"HeaderDetails": "Детайли", "HeaderDetails": "Детайли",
"HeaderDownloadQueue": "Опашка за Сваляне", "HeaderDownloadQueue": "Опашка за Сваляне",
"HeaderEbookFiles": "Файлове на Електронни книги", "HeaderEbookFiles": "Е-книги файлове",
"HeaderEmail": "Емейл", "HeaderEmail": "Емейл",
"HeaderEmailSettings": "Настройки Емайл", "HeaderEmailSettings": "Настройки Емайл",
"HeaderEpisodes": "Епизоди", "HeaderEpisodes": "Епизоди",
"HeaderEreaderDevices": "Елктронни Четци", "HeaderEreaderDevices": "Елктронни Четци",
"HeaderEreaderSettings": "Настройки на Електронни Четци", "HeaderEreaderSettings": "Настройки на Е-четецът",
"HeaderFiles": "Файлове", "HeaderFiles": "Файлове",
"HeaderFindChapters": "Намери Глави", "HeaderFindChapters": "Намери Глави",
"HeaderIgnoredFiles": "Игнорирани Файлове", "HeaderIgnoredFiles": "Игнорирани Файлове",
"HeaderItemFiles": "Файлове на Елемент", "HeaderItemFiles": "Файлове на Елемент",
"HeaderItemMetadataUtils": "Инструменти за Метаданни на Елемент", "HeaderItemMetadataUtils": "Инструменти за Метаданни на Елемент",
"HeaderLastListeningSession": "Последна Сесия на Слушане", "HeaderLastListeningSession": "Последна Сесия на Слушане",
"HeaderLatestEpisodes": "Последни Епизоди", "HeaderLatestEpisodes": "Последни епизоди",
"HeaderLibraries": "Библиотеки", "HeaderLibraries": "Библиотеки",
"HeaderLibraryFiles": "Файлове на Библиотека", "HeaderLibraryFiles": "Файлове на Библиотека",
"HeaderLibraryStats": "Статистика на Библиотека", "HeaderLibraryStats": "Статистика на Библиотека",
@@ -145,24 +163,29 @@
"HeaderMetadataToEmbed": "Метаданни за Вграждане", "HeaderMetadataToEmbed": "Метаданни за Вграждане",
"HeaderNewAccount": "Нов Профил", "HeaderNewAccount": "Нов Профил",
"HeaderNewLibrary": "Нова Библиотека", "HeaderNewLibrary": "Нова Библиотека",
"HeaderNotificationCreate": "Създай нотификация",
"HeaderNotificationUpdate": "Обнови нотификация",
"HeaderNotifications": "Известия", "HeaderNotifications": "Известия",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Аутентикация", "HeaderOpenIDConnectAuthentication": "OpenID Connect Аутентикация",
"HeaderOpenRSSFeed": "Отвори RSS Feed", "HeaderOpenListeningSessions": "Отвори сесия",
"HeaderOpenRSSFeed": "Отвори RSS емисията",
"HeaderOtherFiles": "Други Файлове", "HeaderOtherFiles": "Други Файлове",
"HeaderPasswordAuthentication": "Паролна Аутентикация", "HeaderPasswordAuthentication": "Паролна Аутентикация",
"HeaderPermissions": "Права", "HeaderPermissions": "Права",
"HeaderPlayerQueue": "Опашка на Плейъра", "HeaderPlayerQueue": "Опашка на Плейъра",
"HeaderPlayerSettings": "Настройки на плейъра",
"HeaderPlaylist": "Плейлист", "HeaderPlaylist": "Плейлист",
"HeaderPlaylistItems": "Елементи на Плейлист", "HeaderPlaylistItems": "Елементи от плейлист",
"HeaderPodcastsToAdd": "Подкасти за Добавяне", "HeaderPodcastsToAdd": "Подкасти за Добавяне",
"HeaderPreviewCover": "Преглед на Корица", "HeaderPreviewCover": "Преглед на Корица",
"HeaderRSSFeedGeneral": "RSS Детайли", "HeaderRSSFeedGeneral": "RSS подробности",
"HeaderRSSFeedIsOpen": "RSS Feed е Отворен", "HeaderRSSFeedIsOpen": "RSS емисията е отворена",
"HeaderRSSFeeds": "RSS Feed-ове", "HeaderRSSFeeds": "RSS Feed-ове",
"HeaderRemoveEpisode": "Премахни Епизод", "HeaderRemoveEpisode": "Премахни Епизод",
"HeaderRemoveEpisodes": "Премахни {0} Епизоди", "HeaderRemoveEpisodes": "Премахни {0} Епизоди",
"HeaderSavedMediaProgress": "Запазен Прогрес на Медията", "HeaderSavedMediaProgress": "Запазен Прогрес на Медията",
"HeaderSchedule": "График", "HeaderSchedule": "График",
"HeaderScheduleEpisodeDownloads": "Планирай автоматично изтегляне на епизоди",
"HeaderScheduleLibraryScans": "График за Автоматично Сканиране на Библиотека", "HeaderScheduleLibraryScans": "График за Автоматично Сканиране на Библиотека",
"HeaderSession": "Сесия", "HeaderSession": "Сесия",
"HeaderSetBackupSchedule": "Задай График за Backup", "HeaderSetBackupSchedule": "Задай График за Backup",
@@ -171,11 +194,12 @@
"HeaderSettingsExperimental": "Експериментални Функции", "HeaderSettingsExperimental": "Експериментални Функции",
"HeaderSettingsGeneral": "Общи", "HeaderSettingsGeneral": "Общи",
"HeaderSettingsScanner": "Скенер", "HeaderSettingsScanner": "Скенер",
"HeaderSleepTimer": "Таймер за Сън", "HeaderSettingsWebClient": "Уеб клиент",
"HeaderSleepTimer": "Таймер за заспиване",
"HeaderStatsLargestItems": "Най-Големите Елементи", "HeaderStatsLargestItems": "Най-Големите Елементи",
"HeaderStatsLongestItems": "Най-Дългите Елементи (часове)", "HeaderStatsLongestItems": "Най-Дългите Елементи (часове)",
"HeaderStatsMinutesListeningChart": "Минути на Слушане (последни 7 дни)", "HeaderStatsMinutesListeningChart": "Изслушани минути (последните 7 дни)",
"HeaderStatsRecentSessions": "Скорошни Сесии", "HeaderStatsRecentSessions": "Последни сесии",
"HeaderStatsTop10Authors": "Топ 10 Автори", "HeaderStatsTop10Authors": "Топ 10 Автори",
"HeaderStatsTop5Genres": "Топ 5 Жанрове", "HeaderStatsTop5Genres": "Топ 5 Жанрове",
"HeaderTableOfContents": "Съдържание", "HeaderTableOfContents": "Съдържание",
@@ -186,7 +210,7 @@
"HeaderUpdateLibrary": "Обнови Библиотека", "HeaderUpdateLibrary": "Обнови Библиотека",
"HeaderUsers": "Потребители", "HeaderUsers": "Потребители",
"HeaderYearReview": "Преглед на {0} Година", "HeaderYearReview": "Преглед на {0} Година",
"HeaderYourStats": "Твоята Статистика", "HeaderYourStats": "Вашата статистика",
"LabelAbridged": "Съкратен", "LabelAbridged": "Съкратен",
"LabelAbridgedChecked": "Съкратена (отбелязано)", "LabelAbridgedChecked": "Съкратена (отбелязано)",
"LabelAbridgedUnchecked": "Несъкратена (не отбелязано)", "LabelAbridgedUnchecked": "Несъкратена (не отбелязано)",
@@ -198,21 +222,26 @@
"LabelActivity": "Дейност", "LabelActivity": "Дейност",
"LabelAddToCollection": "Добави в Колекция", "LabelAddToCollection": "Добави в Колекция",
"LabelAddToCollectionBatch": "Добави {0} Книги в Колекция", "LabelAddToCollectionBatch": "Добави {0} Книги в Колекция",
"LabelAddToPlaylist": "Добави в Плейлист", "LabelAddToPlaylist": "Добави в плейлист",
"LabelAddToPlaylistBatch": "Добави {0} Елемент в Плейлист", "LabelAddToPlaylistBatch": "Добави {0} Елемент в Плейлист",
"LabelAddedAt": "Добавени На", "LabelAddedAt": "Добавено в",
"LabelAddedDate": "Добавено",
"LabelAdminUsersOnly": "Само за Администратори", "LabelAdminUsersOnly": "Само за Администратори",
"LabelAll": "Всички", "LabelAll": "Всичко",
"LabelAllUsers": "Всички Потребители", "LabelAllUsers": "Всички Потребители",
"LabelAllUsersExcludingGuests": "Всички потребители без гости", "LabelAllUsersExcludingGuests": "Всички потребители без гости",
"LabelAllUsersIncludingGuests": "Всички потребители включително гости", "LabelAllUsersIncludingGuests": "Всички потребители включително гости",
"LabelAlreadyInYourLibrary": "Вече е в твоята библиотека", "LabelAlreadyInYourLibrary": "Вече е в твоята библиотека",
"LabelApiToken": "АПИ Токен",
"LabelAppend": "Добави", "LabelAppend": "Добави",
"LabelAudioBitrate": "Аудио битрейт (напр. 128k)",
"LabelAudioChannels": "Аудио канали (1 или 2)",
"LabelAudioCodec": "Аудио кодек",
"LabelAuthor": "Автор", "LabelAuthor": "Автор",
"LabelAuthorFirstLast": "Автор (Първо Име, Фамилия)", "LabelAuthorFirstLast": "Автор (Първи, Последен)",
"LabelAuthorLastFirst": "Автор (Фамилия, Първо Име)", "LabelAuthorLastFirst": "Автор (Последен, Първи)",
"LabelAuthors": "Автори", "LabelAuthors": "Автори",
"LabelAutoDownloadEpisodes": "Автоматично Сваляне на Епизоди", "LabelAutoDownloadEpisodes": "Автоматично изтегляне на епизоди",
"LabelAutoFetchMetadata": "Автоматично Взимане на Метаданни", "LabelAutoFetchMetadata": "Автоматично Взимане на Метаданни",
"LabelAutoFetchMetadataHelp": "Взима метаданни за заглвие, автор и серии за да опрости качването. Допълнителни метаданни може да трябва да бъде взера след качване.", "LabelAutoFetchMetadataHelp": "Взима метаданни за заглвие, автор и серии за да опрости качването. Допълнителни метаданни може да трябва да бъде взера след качване.",
"LabelAutoLaunch": "Автоматично Стартиране", "LabelAutoLaunch": "Автоматично Стартиране",
@@ -220,6 +249,7 @@
"LabelAutoRegister": "Автоматична Регистрация", "LabelAutoRegister": "Автоматична Регистрация",
"LabelAutoRegisterDescription": "Автоматично създаване на нови потребители след вход", "LabelAutoRegisterDescription": "Автоматично създаване на нови потребители след вход",
"LabelBackToUser": "Обратно към Потребител", "LabelBackToUser": "Обратно към Потребител",
"LabelBackupAudioFiles": "Създай резервно копие на аудио файлове",
"LabelBackupLocation": "Местоположение на Архив", "LabelBackupLocation": "Местоположение на Архив",
"LabelBackupsEnableAutomaticBackups": "Включи автоматично архивиране", "LabelBackupsEnableAutomaticBackups": "Включи автоматично архивиране",
"LabelBackupsEnableAutomaticBackupsHelp": "Архиви запазени в /metadata/backups", "LabelBackupsEnableAutomaticBackupsHelp": "Архиви запазени в /metadata/backups",
@@ -228,31 +258,38 @@
"LabelBackupsNumberToKeep": "Брой архиви за запазване", "LabelBackupsNumberToKeep": "Брой архиви за запазване",
"LabelBackupsNumberToKeepHelp": "Само 1 архив ще бъде премахнат на веднъж, така че ако вече имате повече архиви от това трябва да ги премахнете ръчно.", "LabelBackupsNumberToKeepHelp": "Само 1 архив ще бъде премахнат на веднъж, така че ако вече имате повече архиви от това трябва да ги премахнете ръчно.",
"LabelBitrate": "Битрейт", "LabelBitrate": "Битрейт",
"LabelBonus": "Бонус",
"LabelBooks": "Книги", "LabelBooks": "Книги",
"LabelButtonText": "Текст на Бутон", "LabelButtonText": "Текст на Бутон",
"LabelByAuthor": "от {0}",
"LabelChangePassword": "Промени Парола", "LabelChangePassword": "Промени Парола",
"LabelChannels": "Канали", "LabelChannels": "Канали",
"LabelChapterCount": "{0} Глави",
"LabelChapterTitle": "Заглавие на Глава", "LabelChapterTitle": "Заглавие на Глава",
"LabelChapters": "Глави", "LabelChapters": "Глави",
"LabelChaptersFound": "намерени глави", "LabelChaptersFound": "намерени глави",
"LabelClickForMoreInfo": "Кликни за повече информация", "LabelClickForMoreInfo": "Кликни за повече информация",
"LabelClosePlayer": "Затвори Плейъра", "LabelClickToUseCurrentValue": "Натисни да ползваш сегашната стойност",
"LabelClosePlayer": "Затвори",
"LabelCodec": "Кодек", "LabelCodec": "Кодек",
"LabelCollapseSeries": "Свий Серия", "LabelCollapseSeries": "Скрий сериите",
"LabelCollapseSubSeries": "Свий подсерии",
"LabelCollection": "Колекция", "LabelCollection": "Колекция",
"LabelCollections": "Колекции", "LabelCollections": "Колекции",
"LabelComplete": "Завършено", "LabelComplete": "Приключено",
"LabelConfirmPassword": "Потвърди Парола", "LabelConfirmPassword": "Потвърди Парола",
"LabelContinueListening": "Продължи Слушане", "LabelContinueListening": "Продължи слушане",
"LabelContinueReading": "Продължи Четене", "LabelContinueReading": "Продължи четене",
"LabelContinueSeries": "Продължи Серия", "LabelContinueSeries": "Продължи серии",
"LabelCover": "Корица", "LabelCover": "Корица",
"LabelCoverImageURL": "URL на Корица", "LabelCoverImageURL": "URL на Корица",
"LabelCreatedAt": "Създадено на", "LabelCreatedAt": "Създадено на",
"LabelCronExpression": "Cron израз",
"LabelCurrent": "Текущо", "LabelCurrent": "Текущо",
"LabelCurrently": "Текущо:", "LabelCurrently": "Текущо:",
"LabelCustomCronExpression": "Потребителски Cron Expression:", "LabelCustomCronExpression": "Потребителски Cron Expression:",
"LabelDatetime": "Дата и Време", "LabelDatetime": "Дата и Време",
"LabelDays": "Дни",
"LabelDeleteFromFileSystemCheckbox": "Изтрий от файловата система (отмени за да бъдат премахни само от базата данни)", "LabelDeleteFromFileSystemCheckbox": "Изтрий от файловата система (отмени за да бъдат премахни само от базата данни)",
"LabelDescription": "Описание", "LabelDescription": "Описание",
"LabelDeselectAll": "Премахни всички", "LabelDeselectAll": "Премахни всички",
@@ -263,16 +300,18 @@
"LabelDiscFromFilename": "Диск от Име на Файл", "LabelDiscFromFilename": "Диск от Име на Файл",
"LabelDiscFromMetadata": "Диск от Метаданни", "LabelDiscFromMetadata": "Диск от Метаданни",
"LabelDiscover": "Открий", "LabelDiscover": "Открий",
"LabelDownload": "Сваляне", "LabelDownload": "Свали",
"LabelDownloadNEpisodes": "Свали {0} епизоди", "LabelDownloadNEpisodes": "Свали {0} епизоди",
"LabelDownloadable": "Може да се изтегли",
"LabelDuration": "Продължителност", "LabelDuration": "Продължителност",
"LabelDurationComparisonExactMatch": "(точно съвпадение)", "LabelDurationComparisonExactMatch": "(точно съвпадение)",
"LabelDurationComparisonLonger": "({0} по-дълго)", "LabelDurationComparisonLonger": "({0} по-дълго)",
"LabelDurationComparisonShorter": "({0} по-късо)", "LabelDurationComparisonShorter": "({0} по-късо)",
"LabelDurationFound": "Намерена продължителност:", "LabelDurationFound": "Намерена продължителност:",
"LabelEbook": "Електронна книга", "LabelEbook": "Е-Книга",
"LabelEbooks": "Електронни книги", "LabelEbooks": "Е-книги",
"LabelEdit": "Редакция", "LabelEdit": "Редакция",
"LabelEmail": "Имейл",
"LabelEmailSettingsFromAddress": "От Адрес", "LabelEmailSettingsFromAddress": "От Адрес",
"LabelEmailSettingsRejectUnauthorized": "Отхвърли неавторизирани сертификати", "LabelEmailSettingsRejectUnauthorized": "Отхвърли неавторизирани сертификати",
"LabelEmailSettingsRejectUnauthorizedHelp": "Спирането на валидацията на SSL сертификате може да изложи връзката ви на рискове, като man-in-the-middle атака. Спираите тази опция само ако знете имоликацийте от това и се доверявате на mail сървъра към който се свързвате.", "LabelEmailSettingsRejectUnauthorizedHelp": "Спирането на валидацията на SSL сертификате може да изложи връзката ви на рискове, като man-in-the-middle атака. Спираите тази опция само ако знете имоликацийте от това и се доверявате на mail сървъра към който се свързвате.",
@@ -280,41 +319,53 @@
"LabelEmailSettingsSecureHelp": "Ако е вярно възката ще изполва TLS когате се свързва със сървъра. Ако не е то TLS ще се използва ако сървъра поддържа разширението STARTTLS. В повечето случаи задайте тази стойност на истина ако се свързвате към порт 465. За порт 587 или 25 оставете я на лъжа. (от nodemailer.com/smtp/#authentication)", "LabelEmailSettingsSecureHelp": "Ако е вярно възката ще изполва TLS когате се свързва със сървъра. Ако не е то TLS ще се използва ако сървъра поддържа разширението STARTTLS. В повечето случаи задайте тази стойност на истина ако се свързвате към порт 465. За порт 587 или 25 оставете я на лъжа. (от nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Тестов Адрес", "LabelEmailSettingsTestAddress": "Тестов Адрес",
"LabelEmbeddedCover": "Вградена Корица", "LabelEmbeddedCover": "Вградена Корица",
"LabelEnable": "Включи", "LabelEnable": "Активирай",
"LabelEncodingBackupLocation": "Резервно копие на вашите оригинални аудио файлове ще бъде съхранено в:",
"LabelEncodingChaptersNotEmbedded": "Главите не са вградени в аудиокнигите с множество тракове.",
"LabelEncodingClearItemCache": "Уверете се, че периодично изчиствате кеша на елементите.",
"LabelEncodingFinishedM4B": "Завършеният M4B файл ще бъде поставен в папката на вашите аудиокниги на:",
"LabelEncodingInfoEmbedded": "Метаданните ще бъдат вградени в аудио траковете в папката на вашите аудиокниги.",
"LabelEnd": "Край", "LabelEnd": "Край",
"LabelEndOfChapter": "Край на глава",
"LabelEpisode": "Епизод", "LabelEpisode": "Епизод",
"LabelEpisodeTitle": "Заглавие на Епизод", "LabelEpisodeTitle": "Заглавие на Епизод",
"LabelEpisodeType": "Тип на Епизод", "LabelEpisodeType": "Тип на Епизод",
"LabelExample": "Пример", "LabelExample": "Пример",
"LabelExplicit": "Експлицитно", "LabelExpandSeries": "Покажи сериите",
"LabelExpandSubSeries": "Покажи съб сериите",
"LabelExplicit": "С нецензурно съдържание",
"LabelExplicitChecked": "С нецензурно съдържание (проверено)",
"LabelExplicitUnchecked": "Без нецензурно съдържание (непроверено)",
"LabelExportOPML": "Експортирай OPML",
"LabelFeedURL": "URL на емисия",
"LabelFetchingMetadata": "Взимане на Метаданни", "LabelFetchingMetadata": "Взимане на Метаданни",
"LabelFile": "Файл", "LabelFile": "Файл",
"LabelFileBirthtime": "Дата на създаване на файла", "LabelFileBirthtime": "Дата на създаване на файла",
"LabelFileModified": "Файлът променен", "LabelFileModified": "Дата на модификация на файла",
"LabelFilename": "Име на Файл", "LabelFilename": "Име на файла",
"LabelFilterByUser": "Филтриране по Потребител", "LabelFilterByUser": "Филтриране по Потребител",
"LabelFindEpisodes": "Намери Епизоди", "LabelFindEpisodes": "Намери Епизоди",
"LabelFinished": "Завършено", "LabelFinished": "Дата на приключване",
"LabelFolder": "Папка", "LabelFolder": "Папка",
"LabelFolders": "Папки", "LabelFolders": "Папки",
"LabelFontBold": "Получерно", "LabelFontBold": "Получерно",
"LabelFontBoldness": "Плътност на шрифта", "LabelFontBoldness": "Дебелина на шрифта",
"LabelFontFamily": "Шрифт", "LabelFontFamily": "Шрифт",
"LabelFontItalic": "Курсив", "LabelFontItalic": "Курсив",
"LabelFontScale": "Мащаб на Шрифта", "LabelFontScale": "Мащаб на шрифта",
"LabelFontStrikethrough": "Зачертан", "LabelFontStrikethrough": "Зачертан",
"LabelFormat": "Формат", "LabelFormat": "Формат",
"LabelGenre": "Жанр", "LabelGenre": "Жанр",
"LabelGenres": "Жанрове", "LabelGenres": "Жанрове",
"LabelHardDeleteFile": "Пълно Изтриване на Файл", "LabelHardDeleteFile": "Пълно Изтриване на Файл",
"LabelHasEbook": "Има електронна книга", "LabelHasEbook": "Има е-книга",
"LabelHasSupplementaryEbook": "Има допълнителна електронна книга", "LabelHasSupplementaryEbook": "Има допълнителна е-книга",
"LabelHighestPriority": "Най-висок Приоритет", "LabelHighestPriority": "Най-висок Приоритет",
"LabelHost": "Хост", "LabelHost": "Хост",
"LabelHour": "Час", "LabelHour": "Час",
"LabelIcon": "Икона", "LabelIcon": "Икона",
"LabelImageURLFromTheWeb": "URL на Изображение от Интернет", "LabelImageURLFromTheWeb": "URL на Изображение от Интернет",
"LabelInProgress": "В Прогрес", "LabelInProgress": "В процес на изпълнение",
"LabelIncludeInTracklist": "Включи в Списъка с Канали", "LabelIncludeInTracklist": "Включи в Списъка с Канали",
"LabelIncomplete": "Незавършено", "LabelIncomplete": "Незавършено",
"LabelInterval": "Интервал", "LabelInterval": "Интервал",
@@ -337,7 +388,7 @@
"LabelLastTime": "Последно Време", "LabelLastTime": "Последно Време",
"LabelLastUpdate": "Последно Обновяване", "LabelLastUpdate": "Последно Обновяване",
"LabelLayout": "Оформление", "LabelLayout": "Оформление",
"LabelLayoutSinglePage": "Една Страница", "LabelLayoutSinglePage": "Единична страница",
"LabelLayoutSplitPage": "Разделена Страница", "LabelLayoutSplitPage": "Разделена Страница",
"LabelLess": "По-малко", "LabelLess": "По-малко",
"LabelLibrariesAccessibleToUser": "Библиотеки Достъпни за Потребителя", "LabelLibrariesAccessibleToUser": "Библиотеки Достъпни за Потребителя",
@@ -345,8 +396,8 @@
"LabelLibraryItem": "Елемент на Библиотека", "LabelLibraryItem": "Елемент на Библиотека",
"LabelLibraryName": "Име на Библиотека", "LabelLibraryName": "Име на Библиотека",
"LabelLimit": "Лимит", "LabelLimit": "Лимит",
"LabelLineSpacing": "Линейно Разстояние", "LabelLineSpacing": "Междуредие",
"LabelListenAgain": "Слушай Отново", "LabelListenAgain": "Слушай отново",
"LabelLogLevelDebug": "Дебъг", "LabelLogLevelDebug": "Дебъг",
"LabelLogLevelInfo": "Информация", "LabelLogLevelInfo": "Информация",
"LabelLogLevelWarn": "Предупреждение", "LabelLogLevelWarn": "Предупреждение",
@@ -355,7 +406,7 @@
"LabelMatchExistingUsersBy": "Съпостави съществуващи потребители по", "LabelMatchExistingUsersBy": "Съпостави съществуващи потребители по",
"LabelMatchExistingUsersByDescription": "Използва се за свързване на съществуващи потребители. След свързване потребителите ще бъдат съпоставени по уникален идентификатор от вашия доставчик на SSO", "LabelMatchExistingUsersByDescription": "Използва се за свързване на съществуващи потребители. След свързване потребителите ще бъдат съпоставени по уникален идентификатор от вашия доставчик на SSO",
"LabelMediaPlayer": "Медия Плейър", "LabelMediaPlayer": "Медия Плейър",
"LabelMediaType": "Тип на Медията", "LabelMediaType": "Тип медия",
"LabelMetaTag": "Мета Таг", "LabelMetaTag": "Мета Таг",
"LabelMetaTags": "Мета Тагове", "LabelMetaTags": "Мета Тагове",
"LabelMetadataOrderOfPrecedenceDescription": "По-високите източници на метаданни ще заменят по-ниските", "LabelMetadataOrderOfPrecedenceDescription": "По-високите източници на метаданни ще заменят по-ниските",
@@ -367,19 +418,19 @@
"LabelMobileRedirectURIs": "Позволени URI за Мобилно Пренасочване", "LabelMobileRedirectURIs": "Позволени URI за Мобилно Пренасочване",
"LabelMobileRedirectURIsDescription": "Това е whitelist на валидни URI за пренасочване за мобилни приложения. По подразбиране е <code>audiobookshelf://oauth</code>, който може да премахнете или допълните с допълнителни URI за интеграция на приложения от трети страни. Използването на звезда (<code>*</code>) като единствен запис позволява всеки URI.", "LabelMobileRedirectURIsDescription": "Това е whitelist на валидни URI за пренасочване за мобилни приложения. По подразбиране е <code>audiobookshelf://oauth</code>, който може да премахнете или допълните с допълнителни URI за интеграция на приложения от трети страни. Използването на звезда (<code>*</code>) като единствен запис позволява всеки URI.",
"LabelMore": "Повече", "LabelMore": "Повече",
"LabelMoreInfo": "Повече Информация", "LabelMoreInfo": "Повече информация",
"LabelName": "Име", "LabelName": "Име",
"LabelNarrator": "Разказвач", "LabelNarrator": "Разказвач",
"LabelNarrators": "Разказвачи", "LabelNarrators": "Разказвачи",
"LabelNew": "Нови", "LabelNew": "Нови",
"LabelNewPassword": "Нова Парола", "LabelNewPassword": "Нова Парола",
"LabelNewestAuthors": "Най-нови Автори", "LabelNewestAuthors": "Най-новите автори",
"LabelNewestEpisodes": "Най-нови Епизоди", "LabelNewestEpisodes": "Най-новите епизоди",
"LabelNextBackupDate": "Следваща Дата на Архивиране", "LabelNextBackupDate": "Следваща Дата на Архивиране",
"LabelNextScheduledRun": "Следващо Планирано Изпълнение", "LabelNextScheduledRun": "Следващо Планирано Изпълнение",
"LabelNoCustomMetadataProviders": "Няма потребителски доставчици на метаданни", "LabelNoCustomMetadataProviders": "Няма потребителски доставчици на метаданни",
"LabelNoEpisodesSelected": "Няма избрани епизоди", "LabelNoEpisodesSelected": "Няма избрани епизоди",
"LabelNotFinished": "Не е завършено", "LabelNotFinished": "Не е приключено",
"LabelNotStarted": "Не е започнато", "LabelNotStarted": "Не е започнато",
"LabelNotes": "Бележки", "LabelNotes": "Бележки",
"LabelNotificationAppriseURL": "Apprise URL-и", "LabelNotificationAppriseURL": "Apprise URL-и",
@@ -392,7 +443,10 @@
"LabelNotificationsMaxQueueSize": "Максимален размер на опашката за известия", "LabelNotificationsMaxQueueSize": "Максимален размер на опашката за известия",
"LabelNotificationsMaxQueueSizeHelp": "Събитията са ограничени до изстрелване на 1 на секунда. Събитията ще бъдат игнорирани ако опашката е на максимален размер. Това предотвратява спамирането на известия.", "LabelNotificationsMaxQueueSizeHelp": "Събитията са ограничени до изстрелване на 1 на секунда. Събитията ще бъдат игнорирани ако опашката е на максимален размер. Това предотвратява спамирането на известия.",
"LabelNumberOfBooks": "Брой на Книги", "LabelNumberOfBooks": "Брой на Книги",
"LabelNumberOfEpisodes": "# Епизоди", "LabelNumberOfEpisodes": "Брой епизоди",
"LabelOpenIDAdvancedPermsClaimDescription": "Име на OpenID твърдението, което съдържа разширени права за достъп до потребителски действия в приложението, които ще се прилагат за роли, различни от администраторските (<b>ако е конфигурирано</b>). Ако твърдението липсва в отговора, достъпът до ABS ще бъде отказан. Ако липсва една опция, тя ще се третира като <code>false</code>. Уверете се, че твърдението на доставчика на идентичност съответства на очакваната структура:",
"LabelOpenIDClaims": "Оставете следните опции празни, за да деактивирате разширеното присвояване на групи, като автоматично ще бъде присвоена групата 'Потребител'.",
"LabelOpenIDGroupClaimDescription": "Име на OpenID твърдението, което съдържа списък с групите на потребителя. Обикновено се нарича <code>groups</code>. <b>Ако е конфигурирано</b>, приложението автоматично ще присвоява роли въз основа на членството на потребителя в групи, при условие че тези групи са наименувани без чувствителност към регистъра като 'admin', 'user' или 'guest' в твърдението. Твърдението трябва да съдържа списък и ако потребителят принадлежи към множество групи, приложението ще присвои ролята, съответстваща на най-високото ниво на достъп. Ако няма съвпадение с група, достъпът ще бъде отказан.",
"LabelOpenRSSFeed": "Отвори RSS Feed", "LabelOpenRSSFeed": "Отвори RSS Feed",
"LabelOverwrite": "Презапиши", "LabelOverwrite": "Презапиши",
"LabelPassword": "Парола", "LabelPassword": "Парола",
@@ -414,24 +468,27 @@
"LabelPodcasts": "Подкасти", "LabelPodcasts": "Подкасти",
"LabelPort": "Порт", "LabelPort": "Порт",
"LabelPrefixesToIgnore": "Префикси за Игнориране (без значение за главни/малки букви)", "LabelPrefixesToIgnore": "Префикси за Игнориране (без значение за главни/малки букви)",
"LabelPreventIndexing": "Предотврати индексирането на вашия feed от iTunes и Google podcast директории", "LabelPreventIndexing": "Предотвратете индексирането на вашата емисия от директориите на iTunes и Google за подкасти",
"LabelPrimaryEbook": "Основна Електронна Книга", "LabelPrimaryEbook": "Основна Електронна Книга",
"LabelProgress": "Прогрес", "LabelProgress": "Прогрес",
"LabelProvider": "Доставчик", "LabelProvider": "Доставчик",
"LabelPubDate": "Дата на Издаване", "LabelPubDate": "Дата на публикуване",
"LabelPublishYear": "Година на Издаване", "LabelPublishYear": "Година на публикуване",
"LabelPublishedDate": "Публикувани {0}",
"LabelPublisher": "Издател", "LabelPublisher": "Издател",
"LabelPublishers": "Издателство", "LabelPublishers": "Издателство",
"LabelRSSFeedCustomOwnerEmail": отребителски собственик Email", "LabelRSSFeedCustomOwnerEmail": ерсонализиран имейл на собственика",
"LabelRSSFeedCustomOwnerName": отребителски собственик Име", "LabelRSSFeedCustomOwnerName": ерсонализирано име на собственика",
"LabelRSSFeedOpen": "RSS Feed Оптворен", "LabelRSSFeedOpen": "RSS Feed Оптворен",
"LabelRSSFeedPreventIndexing": "Предотврати индексиране", "LabelRSSFeedPreventIndexing": "Предотвратете индексиране",
"LabelRSSFeedSlug": "RSS Feed слъг", "LabelRSSFeedSlug": "идентификатор на RSS емисия",
"LabelRSSFeedURL": "URL на RSS емисия",
"LabelRandomly": "Случайно",
"LabelRead": "Прочети", "LabelRead": "Прочети",
"LabelReadAgain": "Прочети Отново", "LabelReadAgain": "Прочети отново",
"LabelReadEbookWithoutProgress": "Прочети електронната книга без записване прогрес", "LabelReadEbookWithoutProgress": "Прочети електронната книга без записване прогрес",
"LabelRecentSeries": "Скорошни Серии", "LabelRecentSeries": "Скорошни серии",
"LabelRecentlyAdded": "Наскоро Добавени", "LabelRecentlyAdded": "Скорошно добавени",
"LabelRecommended": "Препоръчано", "LabelRecommended": "Препоръчано",
"LabelRedo": "Повтори", "LabelRedo": "Повтори",
"LabelRegion": "Регион", "LabelRegion": "Регион",
@@ -448,22 +505,17 @@
"LabelSelectUsers": "Избери Потребители", "LabelSelectUsers": "Избери Потребители",
"LabelSendEbookToDevice": "Изпрати електронна книга до ...", "LabelSendEbookToDevice": "Изпрати електронна книга до ...",
"LabelSequence": "Последователност", "LabelSequence": "Последователност",
"LabelSeries": "Серия", "LabelSeries": "От сериите",
"LabelSeriesName": "Име на Серия", "LabelSeriesName": "Име на Серия",
"LabelSeriesProgress": "Прогрес на Серия", "LabelSeriesProgress": "Прогрес на Серия",
"LabelServerYearReview": "Преглед на годината на сървъра ({0})", "LabelServerYearReview": "Преглед на годината на сървъра ({0})",
"LabelSetEbookAsPrimary": "Задай като основна", "LabelSetEbookAsPrimary": "Направи главен",
"LabelSetEbookAsSupplementary": "Задай като допълнителна", "LabelSetEbookAsSupplementary": "Направи второстепенен",
"LabelSettingsAudiobooksOnly": "Само аудиокниги", "LabelSettingsAudiobooksOnly": "Само аудиокниги",
"LabelSettingsAudiobooksOnlyHelp": "Активирането на тази настройка ще игнорира файловете на електронни книги, освен ако не са в папка с аудиокниги, в което случай ще бъдат зададени като допълнителни електронни книги", "LabelSettingsAudiobooksOnlyHelp": "Активирането на тази настройка ще игнорира файловете на електронни книги, освен ако не са в папка с аудиокниги, в което случай ще бъдат зададени като допълнителни електронни книги",
"LabelSettingsBookshelfViewHelp": "Скеуморфен дизайн с дървени рафтове", "LabelSettingsBookshelfViewHelp": "Скеуморфен дизайн с дървени рафтове",
"LabelSettingsChromecastSupport": "Chromecast поддръжка", "LabelSettingsChromecastSupport": "Chromecast поддръжка",
"LabelSettingsDateFormat": "Формат на Дата", "LabelSettingsDateFormat": "Формат на Дата",
"LabelSettingsDisableWatcher": "Изключи наблюдателя",
"LabelSettingsDisableWatcherForLibrary": "Изключи наблюдателя за библиотека",
"LabelSettingsDisableWatcherHelp": "Изключва автоматичното добавяне/обновяване на елементи, когато се открият промени във файловете. *Изисква рестарт на сървъра",
"LabelSettingsEnableWatcher": "Включи наблюдателя",
"LabelSettingsEnableWatcherForLibrary": "Включи наблюдателя за библиотека",
"LabelSettingsEnableWatcherHelp": "Включва автоматичното добавяне/обновяване на елементи, когато се открият промени във файловете. *Изисква рестарт на сървъра", "LabelSettingsEnableWatcherHelp": "Включва автоматичното добавяне/обновяване на елементи, когато се открият промени във файловете. *Изисква рестарт на сървъра",
"LabelSettingsEpubsAllowScriptedContent": "Позволи скриптово съдържание в epub-и", "LabelSettingsEpubsAllowScriptedContent": "Позволи скриптово съдържание в epub-и",
"LabelSettingsEpubsAllowScriptedContentHelp": "Позволи epub файловете да изпълняват скриптове. Препоръчително е да бъде изключено освен ако не се доверявате на източника на epub файловете.", "LabelSettingsEpubsAllowScriptedContentHelp": "Позволи epub файловете да изпълняват скриптове. Препоръчително е да бъде изключено освен ако не се доверявате на източника на epub файловете.",
@@ -476,6 +528,7 @@
"LabelSettingsHomePageBookshelfView": "Начална страница изглед на рафт", "LabelSettingsHomePageBookshelfView": "Начална страница изглед на рафт",
"LabelSettingsLibraryBookshelfView": "Библиотека изглед на рафт", "LabelSettingsLibraryBookshelfView": "Библиотека изглед на рафт",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропусни предишни книги в Продължи Поредица", "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропусни предишни книги в Продължи Поредица",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Рафтът на началната страница 'Продължи поредицата' показва първата книга, която не е започната в поредици, в които има поне една завършена книга и няма книги в процес на четене. Активирането на тази настройка ще продължи поредицата от най-далечната завършена книга вместо от първата незапочната книга.",
"LabelSettingsParseSubtitles": "Извлечи подзаглавия", "LabelSettingsParseSubtitles": "Извлечи подзаглавия",
"LabelSettingsParseSubtitlesHelp": "Извлича подзаглавия от имената на папките на аудиокнигите.<br>Подзаглавията трябва да бъдат разделени с \" - \"<br>например \"Заглавие на Книга - Тук е Подзаглавито\" има подзаглавие \"Тук е Подзаглавито\"", "LabelSettingsParseSubtitlesHelp": "Извлича подзаглавия от имената на папките на аудиокнигите.<br>Подзаглавията трябва да бъдат разделени с \" - \"<br>например \"Заглавие на Книга - Тук е Подзаглавито\" има подзаглавие \"Тук е Подзаглавито\"",
"LabelSettingsPreferMatchedMetadata": "Предпочети съвпадащи метаданни", "LabelSettingsPreferMatchedMetadata": "Предпочети съвпадащи метаданни",
@@ -491,9 +544,10 @@
"LabelSettingsStoreMetadataWithItem": "Запази метаданните с елемента", "LabelSettingsStoreMetadataWithItem": "Запази метаданните с елемента",
"LabelSettingsStoreMetadataWithItemHelp": "По подразбиране метаданните се съхраняват в /metadata/items, като активирате тази настройка метаданните ще се съхраняват в папката на елемента на вашата библиотека", "LabelSettingsStoreMetadataWithItemHelp": "По подразбиране метаданните се съхраняват в /metadata/items, като активирате тази настройка метаданните ще се съхраняват в папката на елемента на вашата библиотека",
"LabelSettingsTimeFormat": "Формат на Време", "LabelSettingsTimeFormat": "Формат на Време",
"LabelShowAll": "Покажи Всички", "LabelShowAll": "Покажи всички",
"LabelShowSeconds": "Покажи секунди",
"LabelSize": "Размер", "LabelSize": "Размер",
"LabelSleepTimer": "Таймер за Сън", "LabelSleepTimer": "Таймер за изключване",
"LabelSlug": "Слъг", "LabelSlug": "Слъг",
"LabelStart": "Старт", "LabelStart": "Старт",
"LabelStartTime": "Начално Време", "LabelStartTime": "Начално Време",
@@ -501,19 +555,19 @@
"LabelStartedAt": "Стартирано на", "LabelStartedAt": "Стартирано на",
"LabelStatsAudioTracks": "Аудио Канали", "LabelStatsAudioTracks": "Аудио Канали",
"LabelStatsAuthors": "Автори", "LabelStatsAuthors": "Автори",
"LabelStatsBestDay": "Най-добър Ден", "LabelStatsBestDay": "Най-добър ден",
"LabelStatsDailyAverage": "Дневна Средна Стойност", "LabelStatsDailyAverage": "Средно дневно",
"LabelStatsDays": "Дни", "LabelStatsDays": "Общо дни",
"LabelStatsDaysListened": "Дни Слушани", "LabelStatsDaysListened": "Общо слушани дни",
"LabelStatsHours": "Часове", "LabelStatsHours": "Часове",
"LabelStatsInARow": "подред", "LabelStatsInARow": "последователно",
"LabelStatsItemsFinished": "Завършени Елементи", "LabelStatsItemsFinished": "Приключени елементи",
"LabelStatsItemsInLibrary": "Елементи в Библиотеката", "LabelStatsItemsInLibrary": "Елементи в Библиотеката",
"LabelStatsMinutes": "минути", "LabelStatsMinutes": "минути",
"LabelStatsMinutesListening": "Минути Слушани", "LabelStatsMinutesListening": "Общо слушани минути",
"LabelStatsOverallDays": "Общо Дни", "LabelStatsOverallDays": "Общо Дни",
"LabelStatsOverallHours": "Общо Часове", "LabelStatsOverallHours": "Общо Часове",
"LabelStatsWeekListening": "Седмица Слушане", "LabelStatsWeekListening": "Общо слушани седмици",
"LabelSubtitle": "Подзаглавие", "LabelSubtitle": "Подзаглавие",
"LabelSupportedFileTypes": "Поддържани Типове Файлове", "LabelSupportedFileTypes": "Поддържани Типове Файлове",
"LabelTag": "Таг", "LabelTag": "Таг",
@@ -531,7 +585,7 @@
"LabelTimeBase": "Времева Основа", "LabelTimeBase": "Времева Основа",
"LabelTimeListened": "Време Слушано", "LabelTimeListened": "Време Слушано",
"LabelTimeListenedToday": "Време Слушано Днес", "LabelTimeListenedToday": "Време Слушано Днес",
"LabelTimeRemaining": "{0} оставащо време", "LabelTimeRemaining": "{0} оставащи",
"LabelTimeToShift": "Време за изместване в секунди", "LabelTimeToShift": "Време за изместване в секунди",
"LabelTitle": "Заглавие", "LabelTitle": "Заглавие",
"LabelToolsEmbedMetadata": "Вграждане на Метаданни", "LabelToolsEmbedMetadata": "Вграждане на Метаданни",
@@ -544,14 +598,14 @@
"LabelTotalTimeListened": "Общо Време Слушано", "LabelTotalTimeListened": "Общо Време Слушано",
"LabelTrackFromFilename": "Канал от Име на Файл", "LabelTrackFromFilename": "Канал от Име на Файл",
"LabelTrackFromMetadata": "Канал от Метаданни", "LabelTrackFromMetadata": "Канал от Метаданни",
"LabelTracks": "Канали", "LabelTracks": "Тракове",
"LabelTracksMultiTrack": "Многоканален", "LabelTracksMultiTrack": "Многоканален",
"LabelTracksNone": "Няма канали", "LabelTracksNone": "Няма канали",
"LabelTracksSingleTrack": "Единичен канал", "LabelTracksSingleTrack": "Единичен канал",
"LabelType": "Тип", "LabelType": "Тип",
"LabelUnabridged": "Несъкратен", "LabelUnabridged": "Несъкратен",
"LabelUndo": "Отмени", "LabelUndo": "Отмени",
"LabelUnknown": "Неизвестно", "LabelUnknown": "Неизвестен",
"LabelUpdateCover": "Обнови Корица", "LabelUpdateCover": "Обнови Корица",
"LabelUpdateCoverHelp": "Позволи презаписване на съществуващите корици за избраните книги, когато се намери съвпадение", "LabelUpdateCoverHelp": "Позволи презаписване на съществуващите корици за избраните книги, когато се намери съвпадение",
"LabelUpdateDetails": "Обнови Детайли", "LabelUpdateDetails": "Обнови Детайли",
@@ -563,7 +617,7 @@
"LabelUseChapterTrack": "Използвай канал за глава", "LabelUseChapterTrack": "Използвай канал за глава",
"LabelUseFullTrack": "Използвай пълен канал", "LabelUseFullTrack": "Използвай пълен канал",
"LabelUser": "Потребител", "LabelUser": "Потребител",
"LabelUsername": "Потребителско Име", "LabelUsername": "Потребителско име",
"LabelValue": "Стойност", "LabelValue": "Стойност",
"LabelVersion": "Версия", "LabelVersion": "Версия",
"LabelViewBookmarks": "Виж Отметки", "LabelViewBookmarks": "Виж Отметки",
@@ -571,16 +625,20 @@
"LabelViewQueue": "Виж Опашка", "LabelViewQueue": "Виж Опашка",
"LabelVolume": "Сила на Звука", "LabelVolume": "Сила на Звука",
"LabelWeekdaysToRun": "Делници за изпълнение", "LabelWeekdaysToRun": "Делници за изпълнение",
"LabelYearReviewHide": "Скрий ревю на годината ти",
"LabelYearReviewShow": "Виж ревю на годината ти",
"LabelYourAudiobookDuration": "Продължителност на вашата аудиокнига", "LabelYourAudiobookDuration": "Продължителност на вашата аудиокнига",
"LabelYourBookmarks": "Вашите Отметки", "LabelYourBookmarks": "Твойте отметки",
"LabelYourPlaylists": "Вашите Плейлисти", "LabelYourPlaylists": "Вашите Плейлисти",
"LabelYourProgress": "Вашият Прогрес", "LabelYourProgress": "Твоят прогрес",
"MessageAddToPlayerQueue": "Добави към опашката на плейъра", "MessageAddToPlayerQueue": "Добави към опашката на плейъра",
"MessageAppriseDescription": "За да ползвате тази функция трябва да имате активна инстанция на <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> или на друго АПИ което да обработва тези заявки. <br />The Apprise API Url-а трябва дае пълния URL път за изпращане на известията, например, ако вашето АПИ ве подава от <code>http://192.168.1.1:8337</code> трябва да сложитев <code>http://192.168.1.1:8337/notify</code>.", "MessageAppriseDescription": "За да ползвате тази функция трябва да имате активна инстанция на <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> или на друго АПИ което да обработва тези заявки. <br />The Apprise API Url-а трябва дае пълния URL път за изпращане на известията, например, ако вашето АПИ ве подава от <code>http://192.168.1.1:8337</code> трябва да сложитев <code>http://192.168.1.1:8337/notify</code>.",
"MessageBackupsDescription": "Резервните копия включват потребители, напредък на потребителите, подробности за елементите в библиотеката, настройки на сървъра и изображения, съхранени в <code>/metadata/items</code> и <code>/metadata/authors</code>. Резервните копия <strong>не</strong> включват никакви файлове, съхранени в папките на вашата библиотека.",
"MessageBatchQuickMatchDescription": "Бързото Съпоставяне ще опита да добави липсващи корици и метаданни за избраните елементи. Активирайте опциите по-долу, за да позволите на Бързото съпоставяне да презапише съществуващите корици и/или метаданни.", "MessageBatchQuickMatchDescription": "Бързото Съпоставяне ще опита да добави липсващи корици и метаданни за избраните елементи. Активирайте опциите по-долу, за да позволите на Бързото съпоставяне да презапише съществуващите корици и/или метаданни.",
"MessageBookshelfNoCollections": "Все още нямате създадени колекции", "MessageBookshelfNoCollections": "Все още нямате създадени колекции",
"MessageBookshelfNoRSSFeeds": "Няма отворени RSS feed-ове", "MessageBookshelfNoRSSFeeds": "Няма отворени RSS feed-ове",
"MessageBookshelfNoResultsForFilter": "Няма резултат за филтер \"{0}: {1}\"", "MessageBookshelfNoResultsForFilter": "Няма резултат за филтер \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Няма резултати от заявката",
"MessageBookshelfNoSeries": "Нямаш сеЗЙ", "MessageBookshelfNoSeries": "Нямаш сеЗЙ",
"MessageChapterEndIsAfter": "Краят на главата е след края на вашата аудиокнига", "MessageChapterEndIsAfter": "Краят на главата е след края на вашата аудиокнига",
"MessageChapterErrorFirstNotZero": "Първата глава трябва да започва от 0", "MessageChapterErrorFirstNotZero": "Първата глава трябва да започва от 0",
@@ -600,6 +658,8 @@
"MessageConfirmMarkAllEpisodesNotFinished": "Сигурни ли сте, че искате да маркирате всички епизоди като незавършени?", "MessageConfirmMarkAllEpisodesNotFinished": "Сигурни ли сте, че искате да маркирате всички епизоди като незавършени?",
"MessageConfirmMarkSeriesFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като завършени?", "MessageConfirmMarkSeriesFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като завършени?",
"MessageConfirmMarkSeriesNotFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като незавършени?", "MessageConfirmMarkSeriesNotFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като незавършени?",
"MessageConfirmPurgeCache": "Изчистването на кеша ще изтрие цялата директория в <code>/metadata/cache</code>. <br /><br />Сигурни ли сте, че искате да премахнете директорията на кеша?",
"MessageConfirmPurgeItemsCache": "Изчистването на кеша на елементите ще изтрие цялата директория в <code>/metadata/cache/items</code>. <br />Сигурни ли сте?",
"MessageConfirmQuickEmbed": "Внимание! Бързото вграждане няма да архивира вашите аудио файлове. Уверете се, че имате резервно копие на вашите аудио файлове. <br><br>Искате ли да продължите?", "MessageConfirmQuickEmbed": "Внимание! Бързото вграждане няма да архивира вашите аудио файлове. Уверете се, че имате резервно копие на вашите аудио файлове. <br><br>Искате ли да продължите?",
"MessageConfirmReScanLibraryItems": "Сигурни ли сте, че искате да сканирате отново {0} елемента?", "MessageConfirmReScanLibraryItems": "Сигурни ли сте, че искате да сканирате отново {0} елемента?",
"MessageConfirmRemoveAllChapters": "Сигурни ли сте, че искате да премахнете всички глави?", "MessageConfirmRemoveAllChapters": "Сигурни ли сте, че искате да премахнете всички глави?",
@@ -617,34 +677,36 @@
"MessageConfirmRenameTagMergeNote": "Забележка: Този таг вече съществува и ще бъде слято.", "MessageConfirmRenameTagMergeNote": "Забележка: Този таг вече съществува и ще бъде слято.",
"MessageConfirmRenameTagWarning": "Внимание! Вече съществува подобен таг с различно писане \"{0}\".", "MessageConfirmRenameTagWarning": "Внимание! Вече съществува подобен таг с различно писане \"{0}\".",
"MessageConfirmSendEbookToDevice": "Сигурни ли сте, че искате да изпратите {0} електронна книга \"{1}\" до устройство \"{2}\"?", "MessageConfirmSendEbookToDevice": "Сигурни ли сте, че искате да изпратите {0} електронна книга \"{1}\" до устройство \"{2}\"?",
"MessageDownloadingEpisode": "Изтегляне на епизод", "MessageDownloadingEpisode": "Сваля епизод",
"MessageDragFilesIntoTrackOrder": "Плъзнете файлове в правилния ред на каналите", "MessageDragFilesIntoTrackOrder": "Плъзнете файлове в правилния ред на каналите",
"MessageEmbedFinished": "Вграждането завърши!", "MessageEmbedFinished": "Вграждането завърши!",
"MessageEpisodesQueuedForDownload": "{0} епизод(и) в опашка за изтегляне", "MessageEpisodesQueuedForDownload": "{0} Епизод(и) са сложени за сваляне",
"MessageFeedURLWillBe": "Feed URL-a ще бъде {0}", "MessageEreaderDevices": "За да осигурите доставката на е-книги, може да се наложи да добавите горепосочения имейл адрес като валиден подател за всяко устройство, изброено по-долу.",
"MessageFetching": "Взимане...", "MessageFeedURLWillBe": "Адресът на емисията ще бъде {0}",
"MessageFetching": "Извличане...",
"MessageForceReScanDescription": "ще сканира всички файлове отново като прясно сканиране. Аудио файлове ID3 тагове, OPF файлове и текстови файлове ще бъдат сканирани като нови.", "MessageForceReScanDescription": "ще сканира всички файлове отново като прясно сканиране. Аудио файлове ID3 тагове, OPF файлове и текстови файлове ще бъдат сканирани като нови.",
"MessageImportantNotice": "Важно Съобщение!", "MessageImportantNotice": "Важно Съобщение!",
"MessageInsertChapterBelow": "Вмъкни глава под", "MessageInsertChapterBelow": "Вмъкни глава под",
"MessageItemsSelected": "{0} избрани", "MessageItemsSelected": "{0} избрани",
"MessageItemsUpdated": "{0} елемента обновени", "MessageItemsUpdated": "{0} елемента обновени",
"MessageJoinUsOn": "Присъединете се към нас", "MessageJoinUsOn": "Присъединете се към нас",
"MessageLoading": "Зареждане...", "MessageLoading": "Зарежда...",
"MessageLoadingFolders": "Зареждане на Папки...", "MessageLoadingFolders": "Зареждане на Папки...",
"MessageLogsDescription": "Логовете се съхраняват в <code>/metadata/logs</code> като JSON файлове. Дневниците за сривове се съхраняват в <code>/metadata/logs/crash_logs.txt</code>.",
"MessageM4BFailed": "M4B Провалено!", "MessageM4BFailed": "M4B Провалено!",
"MessageM4BFinished": "M4B Завършено!", "MessageM4BFinished": "M4B Завършено!",
"MessageMapChapterTitles": "Съпостави заглавията на главите със съществуващите глави на аудиокнигата без да променяш времената", "MessageMapChapterTitles": "Съпостави заглавията на главите със съществуващите глави на аудиокнигата без да променяш времената",
"MessageMarkAllEpisodesFinished": "Маркирай всички епизоди като завършени", "MessageMarkAllEpisodesFinished": "Маркирай всички епизоди като завършени",
"MessageMarkAllEpisodesNotFinished": "Маркирай всички епизоди като незавършени", "MessageMarkAllEpisodesNotFinished": "Маркирай всички епизоди като незавършени",
"MessageMarkAsFinished": "Маркирай като Завършено", "MessageMarkAsFinished": "Маркирай като завършено",
"MessageMarkAsNotFinished": "Маркирай като Незавършено", "MessageMarkAsNotFinished": "Маркирай като Незавършено",
"MessageMatchBooksDescription": "ще се опита да съпостави книги в библиотеката с книга от избрания доставчик за търсене и ще попълни празни детайли и корици. Не презаписва детайлите.", "MessageMatchBooksDescription": "ще се опита да съпостави книги в библиотеката с книга от избрания доставчик за търсене и ще попълни празни детайли и корици. Не презаписва детайлите.",
"MessageNoAudioTracks": "Няма аудио канали", "MessageNoAudioTracks": "Няма аудио канали",
"MessageNoAuthors": "Няма Автори", "MessageNoAuthors": "Няма Автори",
"MessageNoBackups": "Няма архиви", "MessageNoBackups": "Няма архиви",
"MessageNoBookmarks": "Няма Отметки", "MessageNoBookmarks": "Няма отметки",
"MessageNoChapters": "Няма Глави", "MessageNoChapters": "Няма глави",
"MessageNoCollections": "Няма Колекции", "MessageNoCollections": "Няма колекции",
"MessageNoCoversFound": "Не са намерени корици", "MessageNoCoversFound": "Не са намерени корици",
"MessageNoDescription": "Няма описание", "MessageNoDescription": "Няма описание",
"MessageNoDownloadsInProgress": "Няма изтегляния в прогрес", "MessageNoDownloadsInProgress": "Няма изтегляния в прогрес",
@@ -654,9 +716,9 @@
"MessageNoFoldersAvailable": "Няма налични папки", "MessageNoFoldersAvailable": "Няма налични папки",
"MessageNoGenres": "Няма Жанрове", "MessageNoGenres": "Няма Жанрове",
"MessageNoIssues": "Няма проблеми", "MessageNoIssues": "Няма проблеми",
"MessageNoItems": "Няма Елементи", "MessageNoItems": "Няма елементи",
"MessageNoItemsFound": "Няма намерени елементи", "MessageNoItemsFound": "Няма намерени елементи",
"MessageNoListeningSessions": "Няма слушателски сесии", "MessageNoListeningSessions": "Няма сесии за слушане",
"MessageNoLogs": "Няма логове", "MessageNoLogs": "Няма логове",
"MessageNoMediaProgress": "Няма прогрес на медията", "MessageNoMediaProgress": "Няма прогрес на медията",
"MessageNoNotifications": "Няма известия", "MessageNoNotifications": "Няма известия",
@@ -666,20 +728,21 @@
"MessageNoSeries": "Няма Серии", "MessageNoSeries": "Няма Серии",
"MessageNoTags": "Няма Тагове", "MessageNoTags": "Няма Тагове",
"MessageNoTasksRunning": "Няма вършещи се задачи", "MessageNoTasksRunning": "Няма вършещи се задачи",
"MessageNoUpdatesWereNecessary": "Не бяха необходими обновления", "MessageNoUpdatesWereNecessary": "Няма нужда от обновяване",
"MessageNoUserPlaylists": "Няма плейлисти на потребителя", "MessageNoUserPlaylists": "Нямате създадени плейлисти",
"MessageNotYetImplemented": "Още не е изпълнено", "MessageNotYetImplemented": "Още не е изпълнено",
"MessageOr": "или", "MessageOr": "или",
"MessagePauseChapter": "Пауза на глава", "MessagePauseChapter": "Пауза на глава",
"MessagePlayChapter": "Пусни налчалото на глава", "MessagePlayChapter": "Пусни налчалото на глава",
"MessagePlaylistCreateFromCollection": "Създай плейлист от колекция", "MessagePlaylistCreateFromCollection": "Създай плейлист от колекция",
"MessagePodcastHasNoRSSFeedForMatching": "Подкастът няма URL адрес на RSS feed за използване за съпоставяне", "MessagePodcastHasNoRSSFeedForMatching": "Подкастът няма URL адрес на RSS feed за използване за съпоставяне",
"MessagePodcastSearchField": "Въведи какво да търся или RSS емисия адрес",
"MessageQuickMatchDescription": "Попълни празните детайли и корици с първия резултат от '{0}'. Не презаписва детайлите, освен ако не е активирана настройката 'Предпочети съвпадащи метаданни' на сървъра.", "MessageQuickMatchDescription": "Попълни празните детайли и корици с първия резултат от '{0}'. Не презаписва детайлите, освен ако не е активирана настройката 'Предпочети съвпадащи метаданни' на сървъра.",
"MessageRemoveChapter": "Премахни глава", "MessageRemoveChapter": "Премахни глава",
"MessageRemoveEpisodes": "Премахни {0} епизод(и)", "MessageRemoveEpisodes": "Премахни {0} епизод(и)",
"MessageRemoveFromPlayerQueue": "Премахни от опашката на плейъра", "MessageRemoveFromPlayerQueue": "Премахни от опашката на плейъра",
"MessageRemoveUserWarning": "Сигурни ли сте, че искате да изтриете потребител \"{0}\" завинаги?", "MessageRemoveUserWarning": "Сигурни ли сте, че искате да изтриете потребител \"{0}\" завинаги?",
"MessageReportBugsAndContribute": "Съобщавайте за грешки, заявявайте функции и допринасяйте на", "MessageReportBugsAndContribute": "Докладвайте грешки, поискайте нови функции и допринасяйте на",
"MessageResetChaptersConfirm": "Сигурни ли сте, че искате да нулирате главите и да отмените промените, които сте направили?", "MessageResetChaptersConfirm": "Сигурни ли сте, че искате да нулирате главите и да отмените промените, които сте направили?",
"MessageRestoreBackupConfirm": "Сигурни ли сте, че искате да възстановите архива създаден на", "MessageRestoreBackupConfirm": "Сигурни ли сте, че искате да възстановите архива създаден на",
"MessageRestoreBackupWarning": "Възстановяването на архив ще презапише цялата база данни, намираща се в /config и кориците в /metadata/items & /metadata/authors.<br /><br />Архивите не променят файловете в папките на вашата библиотека. Ако сте активирали настройките на сървъра за съхранение на корици и метаданни в папките на вашата библиотека, те няма да бъдат архивирани или презаписани.<br /><br />Всички клиенти, използващи вашия сървър, ще бъдат автоматично обновени.", "MessageRestoreBackupWarning": "Възстановяването на архив ще презапише цялата база данни, намираща се в /config и кориците в /metadata/items & /metadata/authors.<br /><br />Архивите не променят файловете в папките на вашата библиотека. Ако сте активирали настройките на сървъра за съхранение на корици и метаданни в папките на вашата библиотека, те няма да бъдат архивирани или презаписани.<br /><br />Всички клиенти, използващи вашия сървър, ще бъдат автоматично обновени.",
@@ -700,8 +763,8 @@
"NoteChangeRootPassword": "Root потребителят е единственият потребител, който може да има празна парола", "NoteChangeRootPassword": "Root потребителят е единственият потребител, който може да има празна парола",
"NoteChapterEditorTimes": "Забележка: Първото време на начало на главата трябва да остане на 0:00, а последното време на начало на главата не може да надвишава продължителността на тази аудиокнига.", "NoteChapterEditorTimes": "Забележка: Първото време на начало на главата трябва да остане на 0:00, а последното време на начало на главата не може да надвишава продължителността на тази аудиокнига.",
"NoteFolderPicker": "Забележка: папките, които вече са картографирани, няма да бъдат показани", "NoteFolderPicker": "Забележка: папките, които вече са картографирани, няма да бъдат показани",
"NoteRSSFeedPodcastAppsHttps": "Внимание: Повечето приложения за подкасти изискват URL адреса на RSS feed да използва HTTPS", "NoteRSSFeedPodcastAppsHttps": "Предупреждение: Повечето приложения за подкасти изискват URL адресът на RSS емисията да използва HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Внимание: 1 или повече от вашите епизоди нямат дата на публикуване. Някои приложения за подкасти изискват това", "NoteRSSFeedPodcastAppsPubDate": "Предупреждение: Един или повече от вашите епизоди нямат дата на публикуване. Някои приложения за подкасти изискват това.",
"NoteUploaderFoldersWithMediaFiles": "Папките с медийни файлове ще бъдат обработени като отделни елементи на библиотеката.", "NoteUploaderFoldersWithMediaFiles": "Папките с медийни файлове ще бъдат обработени като отделни елементи на библиотеката.",
"NoteUploaderOnlyAudioFiles": "Ако качвате само аудио файлове, то всеки аудио файл ще бъде обработен като отделна аудиокнига.", "NoteUploaderOnlyAudioFiles": "Ако качвате само аудио файлове, то всеки аудио файл ще бъде обработен като отделна аудиокнига.",
"NoteUploaderUnsupportedFiles": "Неподдържаните файлове се игнорират. При избор или пускане на папка, други файлове, които не са в папка на елемент, се игнорират.", "NoteUploaderUnsupportedFiles": "Неподдържаните файлове се игнорират. При избор или пускане на папка, други файлове, които не са в папка на елемент, се игнорират.",
@@ -722,18 +785,25 @@
"ToastBackupRestoreFailed": "Неуспешно възстановяване на архив", "ToastBackupRestoreFailed": "Неуспешно възстановяване на архив",
"ToastBackupUploadFailed": "Неуспешно качване на архив", "ToastBackupUploadFailed": "Неуспешно качване на архив",
"ToastBackupUploadSuccess": "Архивът е качен", "ToastBackupUploadSuccess": "Архивът е качен",
"ToastBatchUpdateFailed": "Неуспешно групово актуализиране",
"ToastBatchUpdateSuccess": "Успешно групово актуализиране",
"ToastBookmarkCreateFailed": "Неуспешно създаване на отметка", "ToastBookmarkCreateFailed": "Неуспешно създаване на отметка",
"ToastBookmarkCreateSuccess": "Отметката е създадена", "ToastBookmarkCreateSuccess": "Отметката е създадена",
"ToastBookmarkRemoveSuccess": "Отметката е премахната", "ToastBookmarkRemoveSuccess": "Отметката е премахната",
"ToastCachePurgeFailed": "Неуспешно изчистване на кеша",
"ToastCachePurgeSuccess": "Успешно изчистване на кеша",
"ToastChaptersHaveErrors": "Главите имат грешки", "ToastChaptersHaveErrors": "Главите имат грешки",
"ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия", "ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия",
"ToastCollectionRemoveSuccess": "Колекцията е премахната", "ToastCollectionRemoveSuccess": "Колекцията е премахната",
"ToastCollectionUpdateSuccess": "Колекцията е обновена", "ToastCollectionUpdateSuccess": "Колекцията е обновена",
"ToastDeleteFileFailed": "Неуспешно изтриване на файла",
"ToastDeleteFileSuccess": "Успешно изтриване на файла",
"ToastFailedToLoadData": "Неуспешно зареждане на данни",
"ToastItemCoverUpdateSuccess": "Корицата на елемента е обновена", "ToastItemCoverUpdateSuccess": "Корицата на елемента е обновена",
"ToastItemDetailsUpdateSuccess": "Детайлите на елемента са обновени", "ToastItemDetailsUpdateSuccess": "Детайлите на елемента са обновени",
"ToastItemMarkedAsFinishedFailed": "Неуспешно маркиране като завършено", "ToastItemMarkedAsFinishedFailed": "Неуспешно маркиране като Завършено",
"ToastItemMarkedAsFinishedSuccess": "Елементът е маркиран като завършен", "ToastItemMarkedAsFinishedSuccess": "Елементът е маркиран като завършен",
"ToastItemMarkedAsNotFinishedFailed": "Неуспешно маркиране като незавършено", "ToastItemMarkedAsNotFinishedFailed": "Неуспешно маркиране като Незавършено",
"ToastItemMarkedAsNotFinishedSuccess": "Елементът е маркиран като незавършен", "ToastItemMarkedAsNotFinishedSuccess": "Елементът е маркиран като незавършен",
"ToastLibraryCreateFailed": "Неуспешно създаване на библиотека", "ToastLibraryCreateFailed": "Неуспешно създаване на библиотека",
"ToastLibraryCreateSuccess": "Библиотеката \"{0}\" е създадена", "ToastLibraryCreateSuccess": "Библиотеката \"{0}\" е създадена",
@@ -747,20 +817,23 @@
"ToastPlaylistRemoveSuccess": "Плейлистът е премахнат", "ToastPlaylistRemoveSuccess": "Плейлистът е премахнат",
"ToastPlaylistUpdateSuccess": "Плейлистът е обновен", "ToastPlaylistUpdateSuccess": "Плейлистът е обновен",
"ToastPodcastCreateFailed": "Неуспешно създаване на подкаст", "ToastPodcastCreateFailed": "Неуспешно създаване на подкаст",
"ToastPodcastCreateSuccess": "Подкастът е създаден", "ToastPodcastCreateSuccess": "Подкаст успешно създаден",
"ToastRSSFeedCloseFailed": "Неуспешно затваряне на RSS feed", "ToastRSSFeedCloseFailed": "Неуспешно затваряне на RSS емисията",
"ToastRSSFeedCloseSuccess": "RSS feed затворен", "ToastRSSFeedCloseSuccess": "RSS емисията е затворена",
"ToastRemoveItemFromCollectionFailed": "Неуспешно премахване на елемент от колекция", "ToastRemoveItemFromCollectionFailed": "Неуспешно премахване на елемент от колекция",
"ToastRemoveItemFromCollectionSuccess": "Елементът е премахнат от колекция", "ToastRemoveItemFromCollectionSuccess": "Елементът е премахнат от колекция",
"ToastSendEbookToDeviceFailed": "Неуспешно изпращане на електронна книга до устройство", "ToastSendEbookToDeviceFailed": "Неуспешно изпращане на електронна книга до устройство",
"ToastSendEbookToDeviceSuccess": "Електронната книга е изпратена до устройство \"{0}\"", "ToastSendEbookToDeviceSuccess": "Електронната книга е изпратена до устройство \"{0}\"",
"ToastSeriesUpdateFailed": "Неуспешно обновяване на серия", "ToastSeriesUpdateFailed": "Неуспешно обновяване на серия",
"ToastSeriesUpdateSuccess": "Серията е обновена", "ToastSeriesUpdateSuccess": "Серията е обновена",
"ToastServerSettingsUpdateSuccess": "Настройките на сървъра са актуализирани",
"ToastSessionDeleteFailed": "Неуспешно изтриване на сесия", "ToastSessionDeleteFailed": "Неуспешно изтриване на сесия",
"ToastSessionDeleteSuccess": "Сесията е изтрита", "ToastSessionDeleteSuccess": "Сесията е изтрита",
"ToastSocketConnected": "Свързан сокет", "ToastSocketConnected": "Свързан сокет",
"ToastSocketDisconnected": "Сокетът е прекъснат", "ToastSocketDisconnected": "Сокетът е прекъснат",
"ToastSocketFailedToConnect": "Неуспешно свързване на сокет", "ToastSocketFailedToConnect": "Неуспешно свързване на сокет",
"ToastSortingPrefixesEmptyError": "Трябва да има поне 1 префикс за сортиране",
"ToastSortingPrefixesUpdateSuccess": "Префиксите за сортиране са актуализирани ({0} елемента)",
"ToastUserDeleteFailed": "Неуспешно изтриване на потребител", "ToastUserDeleteFailed": "Неуспешно изтриване на потребител",
"ToastUserDeleteSuccess": "Потребителят е изтрит" "ToastUserDeleteSuccess": "Потребителят е изтрит"
} }
-5
View File
@@ -552,11 +552,6 @@
"LabelSettingsBookshelfViewHelp": "কাঠের তাক সহ স্কুমরফিক ডিজাইন", "LabelSettingsBookshelfViewHelp": "কাঠের তাক সহ স্কুমরফিক ডিজাইন",
"LabelSettingsChromecastSupport": "ক্রোমকাস্ট সমর্থন", "LabelSettingsChromecastSupport": "ক্রোমকাস্ট সমর্থন",
"LabelSettingsDateFormat": "তারিখ বিন্যাস", "LabelSettingsDateFormat": "তারিখ বিন্যাস",
"LabelSettingsDisableWatcher": "প্রহরী নিষ্ক্রিয় করুন",
"LabelSettingsDisableWatcherForLibrary": "লাইব্রেরির জন্য ফোল্ডার প্রহরী নিষ্ক্রিয় করুন",
"LabelSettingsDisableWatcherHelp": "ফাইলের পরিবর্তন শনাক্ত হলে স্বয়ংক্রিয়ভাবে আইটেম যোগ/আপডেট করা অক্ষম করবে। *সার্ভার পুনরায় চালু করতে হবে",
"LabelSettingsEnableWatcher": "প্রহরী সক্ষম করুন",
"LabelSettingsEnableWatcherForLibrary": "লাইব্রেরির জন্য ফোল্ডার প্রহরী সক্ষম করুন",
"LabelSettingsEnableWatcherHelp": "ফাইলের পরিবর্তন শনাক্ত হলে আইটেমগুলির স্বয়ংক্রিয় যোগ/আপডেট সক্ষম করবে। *সার্ভার পুনরায় চালু করতে হবে", "LabelSettingsEnableWatcherHelp": "ফাইলের পরিবর্তন শনাক্ত হলে আইটেমগুলির স্বয়ংক্রিয় যোগ/আপডেট সক্ষম করবে। *সার্ভার পুনরায় চালু করতে হবে",
"LabelSettingsEpubsAllowScriptedContent": "ইপাবে স্ক্রিপ্ট করা বিষয়বস্তুর অনুমতি দিন", "LabelSettingsEpubsAllowScriptedContent": "ইপাবে স্ক্রিপ্ট করা বিষয়বস্তুর অনুমতি দিন",
"LabelSettingsEpubsAllowScriptedContentHelp": "ইপাব ফাইলগুলিকে স্ক্রিপ্ট চালানোর অনুমতি দিন। আপনি ইপাব ফাইলগুলির উৎসকে বিশ্বাস না করলে এই সেটিংটি নিষ্ক্রিয় রাখার সুপারিশ করা হলো।", "LabelSettingsEpubsAllowScriptedContentHelp": "ইপাব ফাইলগুলিকে স্ক্রিপ্ট চালানোর অনুমতি দিন। আপনি ইপাব ফাইলগুলির উৎসকে বিশ্বাস না করলে এই সেটিংটি নিষ্ক্রিয় রাখার সুপারিশ করা হলো।",
-5
View File
@@ -548,11 +548,6 @@
"LabelSettingsBookshelfViewHelp": "Disseny esqueomorf amb prestatgeries de fusta", "LabelSettingsBookshelfViewHelp": "Disseny esqueomorf amb prestatgeries de fusta",
"LabelSettingsChromecastSupport": "Compatibilitat amb Chromecast", "LabelSettingsChromecastSupport": "Compatibilitat amb Chromecast",
"LabelSettingsDateFormat": "Format de Data", "LabelSettingsDateFormat": "Format de Data",
"LabelSettingsDisableWatcher": "Desactivar Watcher",
"LabelSettingsDisableWatcherForLibrary": "Desactivar Watcher de Carpetes per a aquesta biblioteca",
"LabelSettingsDisableWatcherHelp": "Desactiva la funció d'afegir/actualitzar elements automàticament quan es detectin canvis en els fitxers. *Requereix reiniciar el servidor",
"LabelSettingsEnableWatcher": "Habilitar Watcher",
"LabelSettingsEnableWatcherForLibrary": "Habilitar Watcher per a la carpeta d'aquesta biblioteca",
"LabelSettingsEnableWatcherHelp": "Permet afegir/actualitzar elements automàticament quan es detectin canvis en els fitxers. *Requereix reiniciar el servidor", "LabelSettingsEnableWatcherHelp": "Permet afegir/actualitzar elements automàticament quan es detectin canvis en els fitxers. *Requereix reiniciar el servidor",
"LabelSettingsEpubsAllowScriptedContent": "Permetre scripts en epubs", "LabelSettingsEpubsAllowScriptedContent": "Permetre scripts en epubs",
"LabelSettingsEpubsAllowScriptedContentHelp": "Permetre que els fitxers epub executin scripts. Es recomana mantenir aquesta opció desactivada tret que confiïs en l'origen dels fitxers epub.", "LabelSettingsEpubsAllowScriptedContentHelp": "Permetre que els fitxers epub executin scripts. Es recomana mantenir aquesta opció desactivada tret que confiïs en l'origen dels fitxers epub.",
+5 -5
View File
@@ -217,6 +217,7 @@
"LabelAccountTypeAdmin": "Správce", "LabelAccountTypeAdmin": "Správce",
"LabelAccountTypeGuest": "Host", "LabelAccountTypeGuest": "Host",
"LabelAccountTypeUser": "Uživatel", "LabelAccountTypeUser": "Uživatel",
"LabelActivities": "Aktivity",
"LabelActivity": "Aktivita", "LabelActivity": "Aktivita",
"LabelAddToCollection": "Přidat do kolekce", "LabelAddToCollection": "Přidat do kolekce",
"LabelAddToCollectionBatch": "Přidat {0} knihy do kolekce", "LabelAddToCollectionBatch": "Přidat {0} knihy do kolekce",
@@ -389,6 +390,7 @@
"LabelIntervalEvery6Hours": "Každých 6 hodin", "LabelIntervalEvery6Hours": "Každých 6 hodin",
"LabelIntervalEveryDay": "Každý den", "LabelIntervalEveryDay": "Každý den",
"LabelIntervalEveryHour": "Každou hodinu", "LabelIntervalEveryHour": "Každou hodinu",
"LabelIntervalEveryMinute": "Každou minutu",
"LabelInvert": "Invertovat", "LabelInvert": "Invertovat",
"LabelItem": "Položka", "LabelItem": "Položka",
"LabelJumpBackwardAmount": "Přeskočit zpět o", "LabelJumpBackwardAmount": "Přeskočit zpět o",
@@ -484,6 +486,7 @@
"LabelPersonalYearReview": "Váš přehled roku ({0})", "LabelPersonalYearReview": "Váš přehled roku ({0})",
"LabelPhotoPathURL": "Cesta k fotografii/URL", "LabelPhotoPathURL": "Cesta k fotografii/URL",
"LabelPlayMethod": "Metoda přehrávání", "LabelPlayMethod": "Metoda přehrávání",
"LabelPlaybackRateIncrementDecrement": "Velikost kroku pro změnu rychlosti přehrávání",
"LabelPlayerChapterNumberMarker": "{0} z {1}", "LabelPlayerChapterNumberMarker": "{0} z {1}",
"LabelPlaylists": "Seznamy skladeb", "LabelPlaylists": "Seznamy skladeb",
"LabelPodcast": "Podcast", "LabelPodcast": "Podcast",
@@ -552,11 +555,6 @@
"LabelSettingsBookshelfViewHelp": "Skeumorfní design s dřevěnými policemi", "LabelSettingsBookshelfViewHelp": "Skeumorfní design s dřevěnými policemi",
"LabelSettingsChromecastSupport": "Podpora Chromecastu", "LabelSettingsChromecastSupport": "Podpora Chromecastu",
"LabelSettingsDateFormat": "Formát data", "LabelSettingsDateFormat": "Formát data",
"LabelSettingsDisableWatcher": "Zakázat sledování",
"LabelSettingsDisableWatcherForLibrary": "Zakázat sledování složky pro knihovnu",
"LabelSettingsDisableWatcherHelp": "Zakáže automatické přidávání/aktualizaci položek při zjištění změn v souboru. *Vyžaduje restart serveru",
"LabelSettingsEnableWatcher": "Povolit sledování",
"LabelSettingsEnableWatcherForLibrary": "Povolit sledování složky pro knihovnu",
"LabelSettingsEnableWatcherHelp": "Povoluje automatické přidávání/aktualizaci položek, když jsou zjištěny změny souborů. *Vyžaduje restart serveru", "LabelSettingsEnableWatcherHelp": "Povoluje automatické přidávání/aktualizaci položek, když jsou zjištěny změny souborů. *Vyžaduje restart serveru",
"LabelSettingsEpubsAllowScriptedContent": "Povolení skriptovaného obsahu v epubu", "LabelSettingsEpubsAllowScriptedContent": "Povolení skriptovaného obsahu v epubu",
"LabelSettingsEpubsAllowScriptedContentHelp": "Povolení spouštění skriptů v souborech epub. Doporučujeme toto nastavení vypnout, pokud nedůvěřujete zdroji souborů epub.", "LabelSettingsEpubsAllowScriptedContentHelp": "Povolení spouštění skriptů v souborech epub. Doporučujeme toto nastavení vypnout, pokud nedůvěřujete zdroji souborů epub.",
@@ -706,6 +704,7 @@
"MessageBackupsLocationPathEmpty": "Umístění záloh nemůže být prázdné", "MessageBackupsLocationPathEmpty": "Umístění záloh nemůže být prázdné",
"MessageBatchQuickMatchDescription": "Rychlá párování se pokusí přidat chybějící obálky a metadata pro vybrané položky. Povolením níže uvedených možností umožníte funkci Rychlé párování přepsat stávající obálky a/nebo metadata.", "MessageBatchQuickMatchDescription": "Rychlá párování se pokusí přidat chybějící obálky a metadata pro vybrané položky. Povolením níže uvedených možností umožníte funkci Rychlé párování přepsat stávající obálky a/nebo metadata.",
"MessageBookshelfNoCollections": "Ještě jste nevytvořili žádnou sbírku", "MessageBookshelfNoCollections": "Ještě jste nevytvořili žádnou sbírku",
"MessageBookshelfNoCollectionsHelp": "Kolekce jsou veřejné. Mohou je zobrazit všichni uživatelé s přístupem do knihovny.",
"MessageBookshelfNoRSSFeeds": "Nejsou otevřeny žádné RSS kanály", "MessageBookshelfNoRSSFeeds": "Nejsou otevřeny žádné RSS kanály",
"MessageBookshelfNoResultsForFilter": "Filtr \"{0}: {1}\"", "MessageBookshelfNoResultsForFilter": "Filtr \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Žádné výsledky pro dotaz", "MessageBookshelfNoResultsForQuery": "Žádné výsledky pro dotaz",
@@ -816,6 +815,7 @@
"MessageNoTasksRunning": "Nejsou spuštěny žádné úlohy", "MessageNoTasksRunning": "Nejsou spuštěny žádné úlohy",
"MessageNoUpdatesWereNecessary": "Nebyly nutné žádné aktualizace", "MessageNoUpdatesWereNecessary": "Nebyly nutné žádné aktualizace",
"MessageNoUserPlaylists": "Nemáte žádné seznamy skladeb", "MessageNoUserPlaylists": "Nemáte žádné seznamy skladeb",
"MessageNoUserPlaylistsHelp": "Seznamy skladeb jsou soukromé. Zobrazit je může pouze uživatel, který je vytvořil.",
"MessageNotYetImplemented": "Ještě není implementováno", "MessageNotYetImplemented": "Ještě není implementováno",
"MessageOpmlPreviewNote": "Poznámka: Toto je náhled načteného OMPL souboru. Aktuální název podcastu bude načten z RSS feedu.", "MessageOpmlPreviewNote": "Poznámka: Toto je náhled načteného OMPL souboru. Aktuální název podcastu bude načten z RSS feedu.",
"MessageOr": "nebo", "MessageOr": "nebo",
+4 -5
View File
@@ -219,6 +219,7 @@
"LabelAccountTypeAdmin": "Administrator", "LabelAccountTypeAdmin": "Administrator",
"LabelAccountTypeGuest": "Gæst", "LabelAccountTypeGuest": "Gæst",
"LabelAccountTypeUser": "Bruger", "LabelAccountTypeUser": "Bruger",
"LabelActivities": "Aktiviteter",
"LabelActivity": "Aktivitet", "LabelActivity": "Aktivitet",
"LabelAddToCollection": "Tilføj til Samling", "LabelAddToCollection": "Tilføj til Samling",
"LabelAddToCollectionBatch": "Tilføj {0} Bøger til Samling", "LabelAddToCollectionBatch": "Tilføj {0} Bøger til Samling",
@@ -283,6 +284,7 @@
"LabelContinueSeries": "Fortsæt Serien", "LabelContinueSeries": "Fortsæt Serien",
"LabelCover": "Omslag", "LabelCover": "Omslag",
"LabelCoverImageURL": "Omslagsbillede URL", "LabelCoverImageURL": "Omslagsbillede URL",
"LabelCoverProvider": "Cover billede udbyder",
"LabelCreatedAt": "Oprettet Kl.", "LabelCreatedAt": "Oprettet Kl.",
"LabelCronExpression": "Cron Udtryk", "LabelCronExpression": "Cron Udtryk",
"LabelCurrent": "Aktuel", "LabelCurrent": "Aktuel",
@@ -391,6 +393,7 @@
"LabelIntervalEvery6Hours": "Hver 6. time", "LabelIntervalEvery6Hours": "Hver 6. time",
"LabelIntervalEveryDay": "Hver dag", "LabelIntervalEveryDay": "Hver dag",
"LabelIntervalEveryHour": "Hver time", "LabelIntervalEveryHour": "Hver time",
"LabelIntervalEveryMinute": "Hvert minut",
"LabelInvert": "Inverter", "LabelInvert": "Inverter",
"LabelItem": "Element", "LabelItem": "Element",
"LabelJumpBackwardAmount": "Spring bagud mængde", "LabelJumpBackwardAmount": "Spring bagud mængde",
@@ -555,11 +558,6 @@
"LabelSettingsBookshelfViewHelp": "Skeumorfisk design med træhylder", "LabelSettingsBookshelfViewHelp": "Skeumorfisk design med træhylder",
"LabelSettingsChromecastSupport": "Chromecast-understøttelse", "LabelSettingsChromecastSupport": "Chromecast-understøttelse",
"LabelSettingsDateFormat": "Datoformat", "LabelSettingsDateFormat": "Datoformat",
"LabelSettingsDisableWatcher": "Deaktiver overvågning",
"LabelSettingsDisableWatcherForLibrary": "Deaktiver mappeovervågning for bibliotek",
"LabelSettingsDisableWatcherHelp": "Deaktiverer automatisk tilføjelse/opdatering af elementer, når der registreres filændringer. *Kræver servergenstart",
"LabelSettingsEnableWatcher": "Aktiver overvågning",
"LabelSettingsEnableWatcherForLibrary": "Aktiver mappeovervågning for bibliotek",
"LabelSettingsEnableWatcherHelp": "Aktiverer automatisk tilføjelse/opdatering af elementer, når filændringer registreres. *Kræver servergenstart", "LabelSettingsEnableWatcherHelp": "Aktiverer automatisk tilføjelse/opdatering af elementer, når filændringer registreres. *Kræver servergenstart",
"LabelSettingsEpubsAllowScriptedContent": "Tillad scriptet indhold i epub", "LabelSettingsEpubsAllowScriptedContent": "Tillad scriptet indhold i epub",
"LabelSettingsEpubsAllowScriptedContentHelp": "Tillad epub filer at køre scripts. Det anbefales at holde denne indstilling deaktiveret med mindre du stoler på kilderne af epub filerne.", "LabelSettingsEpubsAllowScriptedContentHelp": "Tillad epub filer at køre scripts. Det anbefales at holde denne indstilling deaktiveret med mindre du stoler på kilderne af epub filerne.",
@@ -845,6 +843,7 @@
"MessageRestoreBackupConfirm": "Er du sikker på, at du vil gendanne sikkerhedskopien oprettet den", "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.", "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.",
"MessageScheduleLibraryScanNote": "For de fleste brugere, er det anbefalet at efterlade denne funktion deaktiveret for at holde mappe lurer indstilling aktiveret. Mappe lureren vil automatisk opdage ændringer i biblioteksmapper. Mappe lureren virker ikke for alle filsystemer (så som NFS) så schedulerede biblioteksscans vil blive anvendt.", "MessageScheduleLibraryScanNote": "For de fleste brugere, er det anbefalet at efterlade denne funktion deaktiveret for at holde mappe lurer indstilling aktiveret. Mappe lureren vil automatisk opdage ændringer i biblioteksmapper. Mappe lureren virker ikke for alle filsystemer (så som NFS) så schedulerede biblioteksscans vil blive anvendt.",
"MessageScheduleRunEveryWeekdayAtTime": "Kør hvert {0} af {1}",
"MessageSearchResultsFor": "Søgeresultater for", "MessageSearchResultsFor": "Søgeresultater for",
"MessageSelected": "{0} valgt", "MessageSelected": "{0} valgt",
"MessageServerCouldNotBeReached": "Serveren kunne ikke nås", "MessageServerCouldNotBeReached": "Serveren kunne ikke nås",
+6 -6
View File
@@ -219,7 +219,8 @@
"LabelAccountTypeAdmin": "Admin", "LabelAccountTypeAdmin": "Admin",
"LabelAccountTypeGuest": "Gast", "LabelAccountTypeGuest": "Gast",
"LabelAccountTypeUser": "Benutzer", "LabelAccountTypeUser": "Benutzer",
"LabelActivity": "Aktivitäten", "LabelActivities": "Aktivitäten",
"LabelActivity": "Aktivität",
"LabelAddToCollection": "Zur Sammlung hinzufügen", "LabelAddToCollection": "Zur Sammlung hinzufügen",
"LabelAddToCollectionBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Sammlung hinzu", "LabelAddToCollectionBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Sammlung hinzu",
"LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen", "LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen",
@@ -283,6 +284,7 @@
"LabelContinueSeries": "Serien fortsetzen", "LabelContinueSeries": "Serien fortsetzen",
"LabelCover": "Titelbild", "LabelCover": "Titelbild",
"LabelCoverImageURL": "URL des Titelbildes", "LabelCoverImageURL": "URL des Titelbildes",
"LabelCoverProvider": "Titelbildanbieter",
"LabelCreatedAt": "Erstellt am", "LabelCreatedAt": "Erstellt am",
"LabelCronExpression": "Cron-Ausdruck", "LabelCronExpression": "Cron-Ausdruck",
"LabelCurrent": "Aktuell", "LabelCurrent": "Aktuell",
@@ -391,6 +393,7 @@
"LabelIntervalEvery6Hours": "Alle 6 Stunden", "LabelIntervalEvery6Hours": "Alle 6 Stunden",
"LabelIntervalEveryDay": "Jeden Tag", "LabelIntervalEveryDay": "Jeden Tag",
"LabelIntervalEveryHour": "Jede Stunde", "LabelIntervalEveryHour": "Jede Stunde",
"LabelIntervalEveryMinute": "Jede Minute",
"LabelInvert": "Umkehren", "LabelInvert": "Umkehren",
"LabelItem": "Medium", "LabelItem": "Medium",
"LabelJumpBackwardAmount": "Zurückspringen Zeit", "LabelJumpBackwardAmount": "Zurückspringen Zeit",
@@ -555,11 +558,6 @@
"LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden", "LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden",
"LabelSettingsChromecastSupport": "Chromecastunterstützung", "LabelSettingsChromecastSupport": "Chromecastunterstützung",
"LabelSettingsDateFormat": "Datumsformat", "LabelSettingsDateFormat": "Datumsformat",
"LabelSettingsDisableWatcher": "Überwachung deaktivieren",
"LabelSettingsDisableWatcherForLibrary": "Ordnerüberwachung für die Bibliothek deaktivieren",
"LabelSettingsDisableWatcherHelp": "Deaktiviert das automatische Hinzufügen/Aktualisieren von Elementen, wenn Dateiänderungen erkannt werden. *Erfordert einen Server-Neustart",
"LabelSettingsEnableWatcher": "Überwachung aktivieren",
"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", "LabelSettingsEnableWatcherHelp": "Aktiviert das automatische Hinzufügen/Aktualisieren von Elementen, wenn Dateiänderungen erkannt werden. *Erfordert einen Server-Neustart",
"LabelSettingsEpubsAllowScriptedContent": "Skriptinhalte in Epubs zulassen", "LabelSettingsEpubsAllowScriptedContent": "Skriptinhalte in Epubs zulassen",
"LabelSettingsEpubsAllowScriptedContentHelp": "Erlaube Epub-Dateien, Skripte auszuführen. Es wird empfohlen, diese Einstellung deaktiviert zu lassen, es sei denn, du vertraust der Quelle der Epub-Dateien.", "LabelSettingsEpubsAllowScriptedContentHelp": "Erlaube Epub-Dateien, Skripte auszuführen. Es wird empfohlen, diese Einstellung deaktiviert zu lassen, es sei denn, du vertraust der Quelle der Epub-Dateien.",
@@ -708,6 +706,7 @@
"MessageBackupsLocationNoEditNote": "Hinweis: Der Sicherungsspeicherort wird über eine Umgebungsvariable festgelegt und kann hier nicht geändert werden.", "MessageBackupsLocationNoEditNote": "Hinweis: Der Sicherungsspeicherort wird über eine Umgebungsvariable festgelegt und kann hier nicht geändert werden.",
"MessageBackupsLocationPathEmpty": "Der Backup-Pfad darf nicht leer sein", "MessageBackupsLocationPathEmpty": "Der Backup-Pfad darf nicht leer sein",
"MessageBatchEditPopulateMapDetailsAllHelp": "Fülle die aktivierten Felder mit Daten aus allen Elementen. Felder mit mehreren Werten werden zusammengeführt", "MessageBatchEditPopulateMapDetailsAllHelp": "Fülle die aktivierten Felder mit Daten aus allen Elementen. Felder mit mehreren Werten werden zusammengeführt",
"MessageBatchEditPopulateMapDetailsItemHelp": "Aktivierte Felder für Kartendetails mit Daten aus diesem Element füllen",
"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.", "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", "MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt",
"MessageBookshelfNoCollectionsHelp": "Sammlungen sind öffentlich. Alle Benutzer mit Zugriff auf die Bibliothek können sie sehen.", "MessageBookshelfNoCollectionsHelp": "Sammlungen sind öffentlich. Alle Benutzer mit Zugriff auf die Bibliothek können sie sehen.",
@@ -844,6 +843,7 @@
"MessageRestoreBackupConfirm": "Bist du dir sicher, dass du die Sicherung wiederherstellen willst, welche am", "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.", "MessageRestoreBackupWarning": "Bei der Wiederherstellung einer Sicherung wird die gesamte Datenbank unter /config und die Titelbilder in /metadata/items und /metadata/authors überschrieben.<br /><br />Bei der Sicherung werden keine Dateien in deinen Bibliotheksordnern verändert. Wenn du die Servereinstellungen aktiviert hast, um Cover und Metadaten in deinen Bibliotheksordnern zu speichern, werden diese nicht gesichert oder überschrieben.<br /><br />Alle Clients, die Ihren Server nutzen, werden automatisch aktualisiert.",
"MessageScheduleLibraryScanNote": "Für die meisten Nutzer wird empfohlen, diese Funktion deaktiviert zu lassen und stattdessen die Ordnerüberwachung aktiviert zu lassen. Die Ordnerüberwachung erkennt automatisch Änderungen in deinen Bibliotheksordnern. Da die Ordnerüberwachung jedoch nicht mit jedem Dateisystem (z.B. NFS) funktioniert, können alternativ hier geplante Bibliotheks-Scans aktiviert werden.", "MessageScheduleLibraryScanNote": "Für die meisten Nutzer wird empfohlen, diese Funktion deaktiviert zu lassen und stattdessen die Ordnerüberwachung aktiviert zu lassen. Die Ordnerüberwachung erkennt automatisch Änderungen in deinen Bibliotheksordnern. Da die Ordnerüberwachung jedoch nicht mit jedem Dateisystem (z.B. NFS) funktioniert, können alternativ hier geplante Bibliotheks-Scans aktiviert werden.",
"MessageScheduleRunEveryWeekdayAtTime": "Immer {0} um {1} ausführen",
"MessageSearchResultsFor": "Suchergebnisse für", "MessageSearchResultsFor": "Suchergebnisse für",
"MessageSelected": "{0} ausgewählt", "MessageSelected": "{0} ausgewählt",
"MessageServerCouldNotBeReached": "Server kann nicht erreicht werden", "MessageServerCouldNotBeReached": "Server kann nicht erreicht werden",
+8 -7
View File
@@ -219,6 +219,7 @@
"LabelAccountTypeAdmin": "Admin", "LabelAccountTypeAdmin": "Admin",
"LabelAccountTypeGuest": "Guest", "LabelAccountTypeGuest": "Guest",
"LabelAccountTypeUser": "User", "LabelAccountTypeUser": "User",
"LabelActivities": "Activities",
"LabelActivity": "Activity", "LabelActivity": "Activity",
"LabelAddToCollection": "Add to Collection", "LabelAddToCollection": "Add to Collection",
"LabelAddToCollectionBatch": "Add {0} Books to Collection", "LabelAddToCollectionBatch": "Add {0} Books to Collection",
@@ -251,7 +252,7 @@
"LabelBackToUser": "Back to User", "LabelBackToUser": "Back to User",
"LabelBackupAudioFiles": "Backup Audio Files", "LabelBackupAudioFiles": "Backup Audio Files",
"LabelBackupLocation": "Backup Location", "LabelBackupLocation": "Backup Location",
"LabelBackupsEnableAutomaticBackups": "Enable automatic backups", "LabelBackupsEnableAutomaticBackups": "Automatic backups",
"LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups", "LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups",
"LabelBackupsMaxBackupSize": "Maximum backup size (in GB) (0 for unlimited)", "LabelBackupsMaxBackupSize": "Maximum backup size (in GB) (0 for unlimited)",
"LabelBackupsMaxBackupSizeHelp": "As a safeguard against misconfiguration, backups will fail if they exceed the configured size.", "LabelBackupsMaxBackupSizeHelp": "As a safeguard against misconfiguration, backups will fail if they exceed the configured size.",
@@ -283,6 +284,7 @@
"LabelContinueSeries": "Continue Series", "LabelContinueSeries": "Continue Series",
"LabelCover": "Cover", "LabelCover": "Cover",
"LabelCoverImageURL": "Cover Image URL", "LabelCoverImageURL": "Cover Image URL",
"LabelCoverProvider": "Cover Provider",
"LabelCreatedAt": "Created At", "LabelCreatedAt": "Created At",
"LabelCronExpression": "Cron Expression", "LabelCronExpression": "Cron Expression",
"LabelCurrent": "Current", "LabelCurrent": "Current",
@@ -391,6 +393,7 @@
"LabelIntervalEvery6Hours": "Every 6 hours", "LabelIntervalEvery6Hours": "Every 6 hours",
"LabelIntervalEveryDay": "Every day", "LabelIntervalEveryDay": "Every day",
"LabelIntervalEveryHour": "Every hour", "LabelIntervalEveryHour": "Every hour",
"LabelIntervalEveryMinute": "Every minute",
"LabelInvert": "Invert", "LabelInvert": "Invert",
"LabelItem": "Item", "LabelItem": "Item",
"LabelJumpBackwardAmount": "Jump backward amount", "LabelJumpBackwardAmount": "Jump backward amount",
@@ -555,11 +558,8 @@
"LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves", "LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves",
"LabelSettingsChromecastSupport": "Chromecast support", "LabelSettingsChromecastSupport": "Chromecast support",
"LabelSettingsDateFormat": "Date Format", "LabelSettingsDateFormat": "Date Format",
"LabelSettingsDisableWatcher": "Disable Watcher", "LabelSettingsEnableWatcher": "Automatically scan libraries for changes",
"LabelSettingsDisableWatcherForLibrary": "Disable folder watcher for library", "LabelSettingsEnableWatcherForLibrary": "Automatically scan library for changes",
"LabelSettingsDisableWatcherHelp": "Disables the automatic adding/updating of items when file changes are detected. *Requires server restart",
"LabelSettingsEnableWatcher": "Enable Watcher",
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
"LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart", "LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart",
"LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs", "LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs",
"LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.", "LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
@@ -577,7 +577,7 @@
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", "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.", "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", "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\"", "LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be separated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"",
"LabelSettingsPreferMatchedMetadata": "Prefer matched metadata", "LabelSettingsPreferMatchedMetadata": "Prefer matched metadata",
"LabelSettingsPreferMatchedMetadataHelp": "Matched data will override item details when using Quick Match. By default Quick Match will only fill in missing details.", "LabelSettingsPreferMatchedMetadataHelp": "Matched data will override item details when using Quick Match. By default Quick Match will only fill in missing details.",
"LabelSettingsSkipMatchingBooksWithASIN": "Skip matching books that already have an ASIN", "LabelSettingsSkipMatchingBooksWithASIN": "Skip matching books that already have an ASIN",
@@ -845,6 +845,7 @@
"MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on", "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.", "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.",
"MessageScheduleLibraryScanNote": "For most users, it is recommended to leave this feature disabled and keep the folder watcher setting enabled. The folder watcher will automatically detect changes in your library folders. The folder watcher doesn't work for every file system (like NFS) so scheduled library scans can be used instead.", "MessageScheduleLibraryScanNote": "For most users, it is recommended to leave this feature disabled and keep the folder watcher setting enabled. The folder watcher will automatically detect changes in your library folders. The folder watcher doesn't work for every file system (like NFS) so scheduled library scans can be used instead.",
"MessageScheduleRunEveryWeekdayAtTime": "Run every {0} at {1}",
"MessageSearchResultsFor": "Search results for", "MessageSearchResultsFor": "Search results for",
"MessageSelected": "{0} selected", "MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server could not be reached", "MessageServerCouldNotBeReached": "Server could not be reached",
-5
View File
@@ -552,11 +552,6 @@
"LabelSettingsBookshelfViewHelp": "Diseño Esqueuomorfo con Estantes de Madera", "LabelSettingsBookshelfViewHelp": "Diseño Esqueuomorfo con Estantes de Madera",
"LabelSettingsChromecastSupport": "Soporte para Chromecast", "LabelSettingsChromecastSupport": "Soporte para Chromecast",
"LabelSettingsDateFormat": "Formato de Fecha", "LabelSettingsDateFormat": "Formato de Fecha",
"LabelSettingsDisableWatcher": "Deshabilitar Watcher",
"LabelSettingsDisableWatcherForLibrary": "Deshabilitar Watcher de Carpetas para esta biblioteca",
"LabelSettingsDisableWatcherHelp": "Deshabilitar la función de agregar/actualizar elementos automáticamente cuando se detectan cambios en los archivos. *Require Reiniciar el Servidor",
"LabelSettingsEnableWatcher": "Habilitar Watcher",
"LabelSettingsEnableWatcherForLibrary": "Habilitar Watcher para la carpeta de esta biblioteca",
"LabelSettingsEnableWatcherHelp": "Permite agregar/actualizar elementos automáticamente cuando se detectan cambios en los archivos. *Requiere reiniciar el servidor", "LabelSettingsEnableWatcherHelp": "Permite agregar/actualizar elementos automáticamente cuando se detectan cambios en los archivos. *Requiere reiniciar el servidor",
"LabelSettingsEpubsAllowScriptedContent": "Permitir scripts en epubs", "LabelSettingsEpubsAllowScriptedContent": "Permitir scripts en epubs",
"LabelSettingsEpubsAllowScriptedContentHelp": "Permitir que los archivos epub ejecuten scripts. Se recomienda mantener esta opción desactivada a menos que confíe en el origen de los archivos epub.", "LabelSettingsEpubsAllowScriptedContentHelp": "Permitir que los archivos epub ejecuten scripts. Se recomienda mantener esta opción desactivada a menos que confíe en el origen de los archivos epub.",
-5
View File
@@ -445,11 +445,6 @@
"LabelSettingsBookshelfViewHelp": "Skeumorfne kujundus puidust riiulitega", "LabelSettingsBookshelfViewHelp": "Skeumorfne kujundus puidust riiulitega",
"LabelSettingsChromecastSupport": "Chromecasti tugi", "LabelSettingsChromecastSupport": "Chromecasti tugi",
"LabelSettingsDateFormat": "Kuupäeva vorming", "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", "LabelSettingsEnableWatcherHelp": "Lubab automaatset lisamist/uuendamist, kui tuvastatakse failimuudatused. *Nõuab serveri taaskäivitamist",
"LabelSettingsExperimentalFeatures": "Eksperimentaalsed funktsioonid", "LabelSettingsExperimentalFeatures": "Eksperimentaalsed funktsioonid",
"LabelSettingsExperimentalFeaturesHelp": "Arengus olevad funktsioonid, mis vajavad teie tagasisidet ja abi testimisel. Klõpsake GitHubi arutelu avamiseks.", "LabelSettingsExperimentalFeaturesHelp": "Arengus olevad funktsioonid, mis vajavad teie tagasisidet ja abi testimisel. Klõpsake GitHubi arutelu avamiseks.",
+302 -17
View File
@@ -10,6 +10,8 @@
"ButtonApplyChapters": "Käytä lukuihin", "ButtonApplyChapters": "Käytä lukuihin",
"ButtonAuthors": "Tekijät", "ButtonAuthors": "Tekijät",
"ButtonBack": "Takaisin", "ButtonBack": "Takaisin",
"ButtonBatchEditPopulateFromExisting": "Täydennä olemassa olevista",
"ButtonBatchEditPopulateMapDetails": "Täydennä karttatiedot",
"ButtonBrowseForFolder": "Selaa (kansio)", "ButtonBrowseForFolder": "Selaa (kansio)",
"ButtonCancel": "Peruuta", "ButtonCancel": "Peruuta",
"ButtonCancelEncode": "Lopeta enkoodaus", "ButtonCancelEncode": "Lopeta enkoodaus",
@@ -51,7 +53,7 @@
"ButtonNext": "Seuraava", "ButtonNext": "Seuraava",
"ButtonNextChapter": "Seuraava luku", "ButtonNextChapter": "Seuraava luku",
"ButtonNextItemInQueue": "Seuraava jonossa", "ButtonNextItemInQueue": "Seuraava jonossa",
"ButtonOk": "Ok", "ButtonOk": "Hyvä on",
"ButtonOpenFeed": "Avaa syöte", "ButtonOpenFeed": "Avaa syöte",
"ButtonOpenManager": "Avaa hallinta", "ButtonOpenManager": "Avaa hallinta",
"ButtonPause": "Pysäytä", "ButtonPause": "Pysäytä",
@@ -61,13 +63,14 @@
"ButtonPlaylists": "Soittolistat", "ButtonPlaylists": "Soittolistat",
"ButtonPrevious": "Edellinen", "ButtonPrevious": "Edellinen",
"ButtonPreviousChapter": "Edellinen luku", "ButtonPreviousChapter": "Edellinen luku",
"ButtonProbeAudioFile": "Luotaa äänitiedosto",
"ButtonPurgeAllCache": "Tyhjennä kaikki välimuistit", "ButtonPurgeAllCache": "Tyhjennä kaikki välimuistit",
"ButtonPurgeItemsCache": "Tyhjennä kohteiden välimuisti", "ButtonPurgeItemsCache": "Tyhjennä kohteiden välimuisti",
"ButtonQueueAddItem": "Lisää jonoon", "ButtonQueueAddItem": "Lisää jonoon",
"ButtonQueueRemoveItem": "Poista jonosta", "ButtonQueueRemoveItem": "Poista jonosta",
"ButtonQuickEmbed": "Pikaupota", "ButtonQuickEmbed": "Pikaupota",
"ButtonQuickEmbedMetadata": "Upota kuvailutiedot nopeasti", "ButtonQuickEmbedMetadata": "Upota kuvailutiedot nopeasti",
"ButtonQuickMatch": "Pikatäsmää", "ButtonQuickMatch": "Pikatäsmäys",
"ButtonReScan": "Uudelleenskannaa", "ButtonReScan": "Uudelleenskannaa",
"ButtonRead": "Lue", "ButtonRead": "Lue",
"ButtonReadLess": "Lue vähemmän", "ButtonReadLess": "Lue vähemmän",
@@ -132,7 +135,7 @@
"HeaderCustomMetadataProviders": "Mukautetut metadatan tarjoajat", "HeaderCustomMetadataProviders": "Mukautetut metadatan tarjoajat",
"HeaderDetails": "Yksityiskohdat", "HeaderDetails": "Yksityiskohdat",
"HeaderDownloadQueue": "Latausjono", "HeaderDownloadQueue": "Latausjono",
"HeaderEbookFiles": "E-kirjatiedostot", "HeaderEbookFiles": "S-kirjatiedostot",
"HeaderEmail": "Sähköposti", "HeaderEmail": "Sähköposti",
"HeaderEmailSettings": "Sähköpostiasetukset", "HeaderEmailSettings": "Sähköpostiasetukset",
"HeaderEpisodes": "Jaksot", "HeaderEpisodes": "Jaksot",
@@ -141,6 +144,8 @@
"HeaderFiles": "Tiedostot", "HeaderFiles": "Tiedostot",
"HeaderFindChapters": "Etsi kappaleet", "HeaderFindChapters": "Etsi kappaleet",
"HeaderIgnoredFiles": "Ohitetut tiedostot", "HeaderIgnoredFiles": "Ohitetut tiedostot",
"HeaderItemFiles": "Kohteen tiedostot",
"HeaderItemMetadataUtils": "Metadatan hallinta",
"HeaderLastListeningSession": "Edellinen kuuntelukerta", "HeaderLastListeningSession": "Edellinen kuuntelukerta",
"HeaderLatestEpisodes": "Viimeisimmät jaksot", "HeaderLatestEpisodes": "Viimeisimmät jaksot",
"HeaderLibraries": "Kirjastot", "HeaderLibraries": "Kirjastot",
@@ -152,6 +157,8 @@
"HeaderLogs": "Lokit", "HeaderLogs": "Lokit",
"HeaderManageGenres": "Hallitse lajityyppejä", "HeaderManageGenres": "Hallitse lajityyppejä",
"HeaderManageTags": "Hallitse tageja", "HeaderManageTags": "Hallitse tageja",
"HeaderMapDetails": "Karttatiedot",
"HeaderMatch": "Täsmää",
"HeaderMetadataOrderOfPrecedence": "Metadatan tärkeysjärjestys", "HeaderMetadataOrderOfPrecedence": "Metadatan tärkeysjärjestys",
"HeaderMetadataToEmbed": "Sisällytettävä metadata", "HeaderMetadataToEmbed": "Sisällytettävä metadata",
"HeaderNewAccount": "Uusi tili", "HeaderNewAccount": "Uusi tili",
@@ -159,6 +166,8 @@
"HeaderNotificationCreate": "Luo ilmoitus", "HeaderNotificationCreate": "Luo ilmoitus",
"HeaderNotificationUpdate": "Päivitä ilmoitus", "HeaderNotificationUpdate": "Päivitä ilmoitus",
"HeaderNotifications": "Ilmoitukset", "HeaderNotifications": "Ilmoitukset",
"HeaderOpenIDConnectAuthentication": "OpenID Connect -todennus",
"HeaderOpenListeningSessions": "Avoimet kuuntelusessiot",
"HeaderOpenRSSFeed": "Avaa RSS-syöte", "HeaderOpenRSSFeed": "Avaa RSS-syöte",
"HeaderOtherFiles": "Muut tiedostot", "HeaderOtherFiles": "Muut tiedostot",
"HeaderPasswordAuthentication": "Salasanan todentaminen", "HeaderPasswordAuthentication": "Salasanan todentaminen",
@@ -174,6 +183,7 @@
"HeaderRSSFeeds": "RSS syötteet", "HeaderRSSFeeds": "RSS syötteet",
"HeaderRemoveEpisode": "Poista jakso", "HeaderRemoveEpisode": "Poista jakso",
"HeaderRemoveEpisodes": "Poista {0} jaksoa", "HeaderRemoveEpisodes": "Poista {0} jaksoa",
"HeaderSavedMediaProgress": "Tallennettu median edistyminen",
"HeaderSchedule": "Ajoita", "HeaderSchedule": "Ajoita",
"HeaderScheduleEpisodeDownloads": "Ajoita automaattiset jaksolataukset", "HeaderScheduleEpisodeDownloads": "Ajoita automaattiset jaksolataukset",
"HeaderScheduleLibraryScans": "Ajoita automaattiset kirjastoskannaukset", "HeaderScheduleLibraryScans": "Ajoita automaattiset kirjastoskannaukset",
@@ -184,17 +194,18 @@
"HeaderSettingsExperimental": "Kokeelliset ominaisuudet", "HeaderSettingsExperimental": "Kokeelliset ominaisuudet",
"HeaderSettingsGeneral": "Yleiset", "HeaderSettingsGeneral": "Yleiset",
"HeaderSettingsScanner": "Skannaaja", "HeaderSettingsScanner": "Skannaaja",
"HeaderSettingsWebClient": "Webasiakasohjelma",
"HeaderSleepTimer": "Uniajastin", "HeaderSleepTimer": "Uniajastin",
"HeaderStatsLargestItems": "Suurimmat kohteet", "HeaderStatsLargestItems": "Suurimmat kohteet",
"HeaderStatsLongestItems": "Pisimmät kohteet (h)", "HeaderStatsLongestItems": "Pisimmät kohteet (h)",
"HeaderStatsMinutesListeningChart": "Kuunteluminuutit (viim. 7 pv)", "HeaderStatsMinutesListeningChart": "Kuunteluminuutit (viim. 7 pv)",
"HeaderStatsRecentSessions": "Viimeaikaiset istunnot", "HeaderStatsRecentSessions": "Viimeaikaiset istunnot",
"HeaderStatsTop10Authors": "Suosituimmat 10 kirjailijaa", "HeaderStatsTop10Authors": "Suosituimmat 10 tekijää",
"HeaderStatsTop5Genres": "Suosituimmat 5 lajityyppiä", "HeaderStatsTop5Genres": "Suosituimmat 5 lajityyppiä",
"HeaderTableOfContents": "Sisällysluettelo", "HeaderTableOfContents": "Sisällysluettelo",
"HeaderTools": "Työkalut", "HeaderTools": "Työkalut",
"HeaderUpdateAccount": "Päivitä tili", "HeaderUpdateAccount": "Päivitä tili",
"HeaderUpdateAuthor": "Päivitä kirjailija", "HeaderUpdateAuthor": "Päivitä tekijä",
"HeaderUpdateDetails": "Päivitä yksityiskohdat", "HeaderUpdateDetails": "Päivitä yksityiskohdat",
"HeaderUpdateLibrary": "Päivitä kirjasto", "HeaderUpdateLibrary": "Päivitä kirjasto",
"HeaderUsers": "Käyttäjät", "HeaderUsers": "Käyttäjät",
@@ -203,10 +214,12 @@
"LabelAbridged": "Lyhennetty", "LabelAbridged": "Lyhennetty",
"LabelAbridgedChecked": "Lyhennetty (tarkistettu)", "LabelAbridgedChecked": "Lyhennetty (tarkistettu)",
"LabelAbridgedUnchecked": "Lyhentämätön (tarkistamaton)", "LabelAbridgedUnchecked": "Lyhentämätön (tarkistamaton)",
"LabelAccessibleBy": "Saavutettavissa:",
"LabelAccountType": "Tilin tyyppi", "LabelAccountType": "Tilin tyyppi",
"LabelAccountTypeAdmin": "Järjestelmänvalvoja", "LabelAccountTypeAdmin": "Järjestelmänvalvoja",
"LabelAccountTypeGuest": "Vieras", "LabelAccountTypeGuest": "Vieras",
"LabelAccountTypeUser": "Käyttäjä", "LabelAccountTypeUser": "Käyttäjä",
"LabelActivities": "Toiminnot",
"LabelActivity": "Toiminta", "LabelActivity": "Toiminta",
"LabelAddToCollection": "Lisää kokoelmaan", "LabelAddToCollection": "Lisää kokoelmaan",
"LabelAddToCollectionBatch": "Lisää {0} kirjaa kokoelmaan", "LabelAddToCollectionBatch": "Lisää {0} kirjaa kokoelmaan",
@@ -221,6 +234,7 @@
"LabelAllUsersIncludingGuests": "Kaikki käyttäjät mukaan lukien vieraat", "LabelAllUsersIncludingGuests": "Kaikki käyttäjät mukaan lukien vieraat",
"LabelAlreadyInYourLibrary": "Jo kirjastossasi", "LabelAlreadyInYourLibrary": "Jo kirjastossasi",
"LabelApiToken": "Sovellusliittymätunnus", "LabelApiToken": "Sovellusliittymätunnus",
"LabelAppend": "Lisää loppuun",
"LabelAudioBitrate": "Äänen bittinopeus (esim. 128k)", "LabelAudioBitrate": "Äänen bittinopeus (esim. 128k)",
"LabelAudioChannels": "Äänikanavat (1 tai 2)", "LabelAudioChannels": "Äänikanavat (1 tai 2)",
"LabelAudioCodec": "Äänikoodekki", "LabelAudioCodec": "Äänikoodekki",
@@ -230,7 +244,9 @@
"LabelAuthors": "Tekijät", "LabelAuthors": "Tekijät",
"LabelAutoDownloadEpisodes": "Lataa jaksot automaattisesti", "LabelAutoDownloadEpisodes": "Lataa jaksot automaattisesti",
"LabelAutoFetchMetadata": "Etsi metadata automaattisesti", "LabelAutoFetchMetadata": "Etsi metadata automaattisesti",
"LabelAutoFetchMetadataHelp": "Hakee metatiedot kohteille, kirjailijoille ja sarjoille lähetyksen nopeuttamiseksi. Joitain metatietoja voidaan joutua täsmäämään lähetyksen jälkeen.",
"LabelAutoLaunch": "Automaattinen käynnistys", "LabelAutoLaunch": "Automaattinen käynnistys",
"LabelAutoLaunchDescription": "Uudelleenohjaa automaattisesti kirjautumisen tarjoajaan kirjautumissivulle saavuttaessa. (ohitettavissa käyttämällä polkua <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Automaattinen rekisteröinti", "LabelAutoRegister": "Automaattinen rekisteröinti",
"LabelAutoRegisterDescription": "Luo automaattisesti uusia käyttäjiä kirjautumisen jälkeen", "LabelAutoRegisterDescription": "Luo automaattisesti uusia käyttäjiä kirjautumisen jälkeen",
"LabelBackToUser": "Takaisin käyttäjään", "LabelBackToUser": "Takaisin käyttäjään",
@@ -246,17 +262,19 @@
"LabelBonus": "Bonus", "LabelBonus": "Bonus",
"LabelBooks": "Kirjat", "LabelBooks": "Kirjat",
"LabelButtonText": "Painikkeen teksti", "LabelButtonText": "Painikkeen teksti",
"LabelByAuthor": "tekijältä {0}",
"LabelChangePassword": "Vaihda salasana", "LabelChangePassword": "Vaihda salasana",
"LabelChannels": "Kanavat", "LabelChannels": "Kanavat",
"LabelChapterCount": "{0} lukua", "LabelChapterCount": "{0} lukua",
"LabelChapterTitle": "Luvun nimi", "LabelChapterTitle": "Luvun nimi",
"LabelChapters": "Luvut", "LabelChapters": "Luvut",
"LabelChaptersFound": "lukua löydetty", "LabelChaptersFound": "lukuja löydetty",
"LabelClickForMoreInfo": "Napsauta saadaksesi lisätietoja", "LabelClickForMoreInfo": "Napsauta saadaksesi lisätietoja",
"LabelClickToUseCurrentValue": "Käytä nykyistä arvoa napsauttamalla", "LabelClickToUseCurrentValue": "Käytä nykyistä arvoa napsauttamalla",
"LabelClosePlayer": "Sulje soitin", "LabelClosePlayer": "Sulje soitin",
"LabelCodec": "Koodekki", "LabelCodec": "Koodekki",
"LabelCollapseSeries": "Pienennä sarja", "LabelCollapseSeries": "Pienennä sarja",
"LabelCollapseSubSeries": "Tiivistä alisarjat",
"LabelCollection": "Kokoelma", "LabelCollection": "Kokoelma",
"LabelCollections": "Kokoelmat", "LabelCollections": "Kokoelmat",
"LabelComplete": "Valmis", "LabelComplete": "Valmis",
@@ -266,9 +284,13 @@
"LabelContinueSeries": "Jatka sarjoja", "LabelContinueSeries": "Jatka sarjoja",
"LabelCover": "Kansikuva", "LabelCover": "Kansikuva",
"LabelCoverImageURL": "Kansikuvan URL-osoite", "LabelCoverImageURL": "Kansikuvan URL-osoite",
"LabelCoverProvider": "Kansikuvan tarjoaja",
"LabelCreatedAt": "Luotu", "LabelCreatedAt": "Luotu",
"LabelCronExpression": "Cron ajastin",
"LabelCurrent": "Nykyinen", "LabelCurrent": "Nykyinen",
"LabelCurrently": "Nyt:", "LabelCurrently": "Nyt:",
"LabelCustomCronExpression": "Mukautettu Cron-ajastin:",
"LabelDatetime": "Päivämäärä/Aika",
"LabelDays": "Päivää", "LabelDays": "Päivää",
"LabelDeleteFromFileSystemCheckbox": "Poista tiedostojärjestelmästä (poista merkintä, jos haluat poistaa vain tietokannasta)", "LabelDeleteFromFileSystemCheckbox": "Poista tiedostojärjestelmästä (poista merkintä, jos haluat poistaa vain tietokannasta)",
"LabelDescription": "Kuvaus", "LabelDescription": "Kuvaus",
@@ -277,6 +299,8 @@
"LabelDeviceInfo": "Laitteen tiedot", "LabelDeviceInfo": "Laitteen tiedot",
"LabelDeviceIsAvailableTo": "Laite on saatavilla...", "LabelDeviceIsAvailableTo": "Laite on saatavilla...",
"LabelDirectory": "Kansio", "LabelDirectory": "Kansio",
"LabelDiscFromFilename": "Levyn numero tiedostonimestä",
"LabelDiscFromMetadata": "Levyn numero metatiedoista",
"LabelDiscover": "Löydä", "LabelDiscover": "Löydä",
"LabelDownload": "Lataa", "LabelDownload": "Lataa",
"LabelDownloadNEpisodes": "Lataa {0} jaksoa", "LabelDownloadNEpisodes": "Lataa {0} jaksoa",
@@ -286,23 +310,27 @@
"LabelDurationComparisonLonger": "({0} pidempi)", "LabelDurationComparisonLonger": "({0} pidempi)",
"LabelDurationComparisonShorter": "({0} lyhyempi)", "LabelDurationComparisonShorter": "({0} lyhyempi)",
"LabelDurationFound": "Kesto löydetty:", "LabelDurationFound": "Kesto löydetty:",
"LabelEbook": "E-kirja", "LabelEbook": "S-kirja",
"LabelEbooks": "E-kirjat", "LabelEbooks": "S-kirjat",
"LabelEdit": "Muokkaa", "LabelEdit": "Muokkaa",
"LabelEmail": "Sähköposti", "LabelEmail": "Sähköposti",
"LabelEmailSettingsFromAddress": "Osoitteesta", "LabelEmailSettingsFromAddress": "Osoitteesta",
"LabelEmailSettingsRejectUnauthorized": "Hylkää luvattomat sertifikaatit", "LabelEmailSettingsRejectUnauthorized": "Hylkää luvattomat sertifikaatit",
"LabelEmailSettingsRejectUnauthorizedHelp": "SSL-sertifikaatin varmentamisen käytöstä poistaminen saattaa vaarantaa yhteytesti turvallisuusriskeihin, kuten man-in-the-middle hyökkäyksiin. Poista käytöstä vain jos ymmärrät vaaran ja luotat yhdistämääsi sähköpostipalvelimeen.", "LabelEmailSettingsRejectUnauthorizedHelp": "SSL-sertifikaatin varmentamisen käytöstä poistaminen saattaa vaarantaa yhteytesti turvallisuusriskeihin, kuten man-in-the-middle hyökkäyksiin. Poista käytöstä vain jos ymmärrät vaaran ja luotat yhdistämääsi sähköpostipalvelimeen.",
"LabelEmailSettingsSecure": "Turvallinen", "LabelEmailSettingsSecure": "Turvallinen",
"LabelEmailSettingsSecureHelp": "Jos tosi, niin yhteys käyttää TLS:ää yhdistäessään palvelimeen. Jos epätosi, niin TSL käytetään jos palvelin tukee STARTTLS-lisäosaa. Yleensä tämä arvo on tosi jos yhdistät porttiin 465. Porteille 587 tai 25 käytä arvoa epätosi (Lähde: nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Testiosoite", "LabelEmailSettingsTestAddress": "Testiosoite",
"LabelEmbeddedCover": "Upotettu kansikuva", "LabelEmbeddedCover": "Upotettu kansikuva",
"LabelEnable": "Ota käyttöön", "LabelEnable": "Ota käyttöön",
"LabelEncodingBackupLocation": "Alkuperäisistä audiotiedostoistasi tallennetaan varmuuskopio osoitteessa:", "LabelEncodingBackupLocation": "Alkuperäisistä audiotiedostoistasi tallennetaan varmuuskopio osoitteessa:",
"LabelEncodingChaptersNotEmbedded": "Lukuja ei upoteta moniraitaisiin äänikirjoihin.", "LabelEncodingChaptersNotEmbedded": "Lukuja ei ole upotettu moniraitaisiin äänikirjoihin.",
"LabelEncodingClearItemCache": "Varmista, että kohteiden välimuisti tyhjennetään säännöllisesti.",
"LabelEncodingFinishedM4B": "Valmistunut M4B tullaan viemään äänikirjakansioosi:",
"LabelEncodingInfoEmbedded": "Kuvailutiedot upotetaan äänikirjakansion ääniraitoihin.", "LabelEncodingInfoEmbedded": "Kuvailutiedot upotetaan äänikirjakansion ääniraitoihin.",
"LabelEncodingStartedNavigation": "Voit poistua sivulta kun tehtävä on aloitettu.", "LabelEncodingStartedNavigation": "Voit poistua sivulta kun tehtävä on aloitettu.",
"LabelEncodingTimeWarning": "Koodaus saattaa kestää 30 minuuttiin asti.", "LabelEncodingTimeWarning": "Koodaus saattaa kestää 30 minuuttiin asti.",
"LabelEncodingWarningAdvancedSettings": "Varoitus: Älä päivitä näitä asetuksia ellet ymmärrä ffmpeg-koodausasetuksia.", "LabelEncodingWarningAdvancedSettings": "Varoitus: Älä päivitä näitä asetuksia ellet ymmärrä ffmpeg-koodausasetuksia.",
"LabelEncodingWatcherDisabled": "Jos kansiotarkkailu on poistettu käytöstä, tämä äänikirja pitää skannata uudestaan myöhemmin.",
"LabelEnd": "Loppu", "LabelEnd": "Loppu",
"LabelEndOfChapter": "Luvun loppu", "LabelEndOfChapter": "Luvun loppu",
"LabelEpisode": "Jakso", "LabelEpisode": "Jakso",
@@ -312,6 +340,7 @@
"LabelEpisodeType": "Jakson tyyppi", "LabelEpisodeType": "Jakson tyyppi",
"LabelEpisodeUrlFromRssFeed": "Jakson URL RSS-syötteestä", "LabelEpisodeUrlFromRssFeed": "Jakson URL RSS-syötteestä",
"LabelEpisodes": "Jaksot", "LabelEpisodes": "Jaksot",
"LabelEpisodic": "Jaksollinen",
"LabelExample": "Esimerkki", "LabelExample": "Esimerkki",
"LabelExpandSeries": "Laajenna sarja", "LabelExpandSeries": "Laajenna sarja",
"LabelExpandSubSeries": "Laajenna alisarja", "LabelExpandSubSeries": "Laajenna alisarja",
@@ -335,15 +364,22 @@
"LabelFontItalic": "Kursiivi", "LabelFontItalic": "Kursiivi",
"LabelFontScale": "Kirjasintyyppien skaalautuminen", "LabelFontScale": "Kirjasintyyppien skaalautuminen",
"LabelFontStrikethrough": "Yliviivattu", "LabelFontStrikethrough": "Yliviivattu",
"LabelFormat": "Muoto",
"LabelFull": "Täynnä", "LabelFull": "Täynnä",
"LabelGenre": "Lajityyppi", "LabelGenre": "Lajityyppi",
"LabelGenres": "Lajityypit", "LabelGenres": "Lajityypit",
"LabelHardDeleteFile": "Kova tiedostojen poisto",
"LabelHasEbook": "Sillä on s-kirja",
"LabelHasSupplementaryEbook": "Sillä on täydentävän s-kirjan",
"LabelHideSubtitles": "Piilota tekstitykset",
"LabelHighestPriority": "Tärkein", "LabelHighestPriority": "Tärkein",
"LabelHost": "Isäntä", "LabelHost": "Isäntä",
"LabelHour": "Tunti",
"LabelHours": "Tunnit", "LabelHours": "Tunnit",
"LabelIcon": "Kuvake", "LabelIcon": "Kuvake",
"LabelImageURLFromTheWeb": "Kuvan verkko-osoite", "LabelImageURLFromTheWeb": "Kuvan verkko-osoite",
"LabelInProgress": "Kesken", "LabelInProgress": "Kesken",
"LabelIncludeInTracklist": "Sisällytä kappalelistaan",
"LabelIncomplete": "Keskeneräinen", "LabelIncomplete": "Keskeneräinen",
"LabelInterval": "Väli", "LabelInterval": "Väli",
"LabelIntervalCustomDailyWeekly": "Mukautettu päivittäinen/viikoittainen", "LabelIntervalCustomDailyWeekly": "Mukautettu päivittäinen/viikoittainen",
@@ -354,6 +390,8 @@
"LabelIntervalEvery6Hours": "6 tunnin välein", "LabelIntervalEvery6Hours": "6 tunnin välein",
"LabelIntervalEveryDay": "Joka päivä", "LabelIntervalEveryDay": "Joka päivä",
"LabelIntervalEveryHour": "Joka tunti", "LabelIntervalEveryHour": "Joka tunti",
"LabelIntervalEveryMinute": "Joka minuutti",
"LabelInvert": "Saa käänteiseksi",
"LabelItem": "Kohde", "LabelItem": "Kohde",
"LabelLanguage": "Kieli", "LabelLanguage": "Kieli",
"LabelLanguageDefaultServer": "Palvelimen oletuskieli", "LabelLanguageDefaultServer": "Palvelimen oletuskieli",
@@ -361,6 +399,7 @@
"LabelLastBookAdded": "Viimeisin lisätty kirja", "LabelLastBookAdded": "Viimeisin lisätty kirja",
"LabelLastBookUpdated": "Viimeisin päivitetty kirja", "LabelLastBookUpdated": "Viimeisin päivitetty kirja",
"LabelLastSeen": "Nähty viimeksi", "LabelLastSeen": "Nähty viimeksi",
"LabelLastTime": "Viimeinen kerta",
"LabelLastUpdate": "Viimeisin päivitys", "LabelLastUpdate": "Viimeisin päivitys",
"LabelLayout": "Asettelu", "LabelLayout": "Asettelu",
"LabelLayoutSinglePage": "Yksi sivu", "LabelLayoutSinglePage": "Yksi sivu",
@@ -368,15 +407,21 @@
"LabelLess": "Vähemmän", "LabelLess": "Vähemmän",
"LabelLibrariesAccessibleToUser": "Käyttäjälle saatavilla olevat kirjastot", "LabelLibrariesAccessibleToUser": "Käyttäjälle saatavilla olevat kirjastot",
"LabelLibrary": "Kirjasto", "LabelLibrary": "Kirjasto",
"LabelLibraryFilterSublistEmpty": "Ei {0}",
"LabelLibraryItem": "Kirjaston kohde",
"LabelLibraryName": "Kirjaston nimi", "LabelLibraryName": "Kirjaston nimi",
"LabelLimit": "Raja", "LabelLimit": "Raja",
"LabelLineSpacing": "Riviväli", "LabelLineSpacing": "Riviväli",
"LabelListenAgain": "Kuuntele uudelleen", "LabelListenAgain": "Kuuntele uudelleen",
"LabelLogLevelDebug": "Viankorjaus",
"LabelLogLevelInfo": "Tiedot", "LabelLogLevelInfo": "Tiedot",
"LabelLogLevelWarn": "Varoita", "LabelLogLevelWarn": "Varoitus",
"LabelLookForNewEpisodesAfterDate": "Etsi uusia jaksoja tämän päivämäärän jälkeen", "LabelLookForNewEpisodesAfterDate": "Etsi uusia jaksoja tämän päivämäärän jälkeen",
"LabelLowestPriority": "Vähiten tärkeä", "LabelLowestPriority": "Vähiten tärkeä",
"LabelMatchExistingUsersBy": "Vastaa olemassa olevia käyttäjiä mukaan",
"LabelMatchExistingUsersByDescription": "Käytetään olemassa olevien käyttäjien yhdistämiseen. Kun yhteys on muodostettu, käyttäjät saavat yksilöllisen tunnuksen SSO-palveluntarjoajaltasi",
"LabelMaxEpisodesToDownload": "Jaksojen maksimilatausmäärä. 0 poistaa rajoituksen.", "LabelMaxEpisodesToDownload": "Jaksojen maksimilatausmäärä. 0 poistaa rajoituksen.",
"LabelMaxEpisodesToDownloadPerCheck": "Enintään # ladattavia uusia jaksoja tarkistusta kohden",
"LabelMaxEpisodesToKeep": "Säilytettävien jaksojen enimmäismäärä", "LabelMaxEpisodesToKeep": "Säilytettävien jaksojen enimmäismäärä",
"LabelMaxEpisodesToKeepHelp": "Jos arvona on 0, enimmäisrajaa ei ole. Kun uusi jakso ladataan automaattisesti, vanhin jakso poistetaan, jos jaksoja on yli X. Tämä poistaa vain yhden jakson uutta latauskertaa kohden.", "LabelMaxEpisodesToKeepHelp": "Jos arvona on 0, enimmäisrajaa ei ole. Kun uusi jakso ladataan automaattisesti, vanhin jakso poistetaan, jos jaksoja on yli X. Tämä poistaa vain yhden jakson uutta latauskertaa kohden.",
"LabelMediaPlayer": "Mediasoitin", "LabelMediaPlayer": "Mediasoitin",
@@ -387,8 +432,11 @@
"LabelMetadataProvider": "Kuvailutietojen toimittaja", "LabelMetadataProvider": "Kuvailutietojen toimittaja",
"LabelMinute": "Minuutti", "LabelMinute": "Minuutti",
"LabelMinutes": "Minuutit", "LabelMinutes": "Minuutit",
"LabelMissing": "Puuttuu", "LabelMissing": "Puuttuva",
"LabelMissingEbook": "Ei e-kirjaa", "LabelMissingEbook": "Sillä ei ole s-kirjaa",
"LabelMissingSupplementaryEbook": "Ei täydentävää s-kirjaa",
"LabelMobileRedirectURIs": "Sallitut mobiiliuudelleenohjaus-URI:t",
"LabelMobileRedirectURIsDescription": "Tämä on valkoluettelo kelvollisista uudelleenohjaus-URI:ista mobiilisovelluksille. Oletusarvo on <code>äänikirjahylly://oauth</code>, jonka voit poistaa tai täydentää ylimääräisillä URI:lla kolmannen osapuolen sovellusten integrointia varten. Asteriskin (<code>*</code>) käyttäminen ainoana merkintänä sallii minkä tahansa URI:n.",
"LabelMore": "Lisää", "LabelMore": "Lisää",
"LabelMoreInfo": "Lisätietoja", "LabelMoreInfo": "Lisätietoja",
"LabelName": "Nimi", "LabelName": "Nimi",
@@ -396,7 +444,7 @@
"LabelNarrators": "Lukijat", "LabelNarrators": "Lukijat",
"LabelNew": "Uusi", "LabelNew": "Uusi",
"LabelNewPassword": "Uusi salasana", "LabelNewPassword": "Uusi salasana",
"LabelNewestAuthors": "Uusimmat kirjailijat", "LabelNewestAuthors": "Uusimmat tekijät",
"LabelNewestEpisodes": "Uusimmat jaksot", "LabelNewestEpisodes": "Uusimmat jaksot",
"LabelNextBackupDate": "Seuraava varmuuskopiointipäivämäärä", "LabelNextBackupDate": "Seuraava varmuuskopiointipäivämäärä",
"LabelNextScheduledRun": "Seuraava ajastettu suorittaminen", "LabelNextScheduledRun": "Seuraava ajastettu suorittaminen",
@@ -405,25 +453,36 @@
"LabelNotFinished": "Ei valmis", "LabelNotFinished": "Ei valmis",
"LabelNotStarted": "Ei aloitettu", "LabelNotStarted": "Ei aloitettu",
"LabelNotes": "Muistiinpanoja", "LabelNotes": "Muistiinpanoja",
"LabelNotificationAppriseURL": "Apprise osoitteet (URL)",
"LabelNotificationAvailableVariables": "Käytettävissä olevat muuttujat", "LabelNotificationAvailableVariables": "Käytettävissä olevat muuttujat",
"LabelNotificationBodyTemplate": "Runkomalli",
"LabelNotificationEvent": "Ilmoitustapahtuma", "LabelNotificationEvent": "Ilmoitustapahtuma",
"LabelNotificationTitleTemplate": "Otsikkomalli",
"LabelNotificationsMaxFailedAttempts": "Epäonnistuneiden yritysten enimmäismäärä", "LabelNotificationsMaxFailedAttempts": "Epäonnistuneiden yritysten enimmäismäärä",
"LabelNotificationsMaxFailedAttemptsHelp": "Ilmoitukset poistetaan käytöstä, jos niiden lähettäminen epäonnistuu näin monta kertaa", "LabelNotificationsMaxFailedAttemptsHelp": "Ilmoitukset poistetaan käytöstä, jos niiden lähettäminen epäonnistuu näin monta kertaa",
"LabelNotificationsMaxQueueSize": "Ilmoitustapahtumajonon enimmäispituus", "LabelNotificationsMaxQueueSize": "Ilmoitustapahtumajonon enimmäispituus",
"LabelNotificationsMaxQueueSizeHelp": "Tapahtumat on rajoitettu ampumaan yksi sekunnissa. Tapahtumat ohitetaan, jos jono on enimmäiskoko. Tämä estää ilmoitusten roskapostin.",
"LabelNumberOfBooks": "Kirjojen määrä", "LabelNumberOfBooks": "Kirjojen määrä",
"LabelNumberOfEpisodes": "Jaksojen määrä", "LabelNumberOfEpisodes": "# jaksoja",
"LabelOpenIDAdvancedPermsClaimDescription": "OpenID-vaatimuksen nimi, joka sisältää lisäoikeudet sovelluksen käyttäjän toimiin, joita sovelletaan muihin kuin järjestelmänvalvojan rooleihin (<b>jos määritetty</b>). Jos vaatimus puuttuu vastauksesta, pääsy ABS:iin evätään. Jos yksittäinen vaihtoehto puuttuu, sitä käsitellään <code>false</code>-arvona. Varmista, että identiteetin tarjoajan vaatimus vastaa odotettua rakennetta:",
"LabelOpenIDClaims": "Jätä seuraavat vaihtoehdot tyhjiksi, jos haluat poistaa edistyneen ryhmän ja lupien määrityksen käytöstä ja määrittää sitten automaattisesti käyttäjäryhmän.",
"LabelOpenIDGroupClaimDescription": "Sen OpenID-vaatimuksen nimi, joka sisältää luettelon käyttäjäryhmistä. Kutsutaan yleisesti <code>ryhmiksi</code>. <b>Jos se on määritetty</b>, sovellus jakaa automaattisesti roolit käyttäjän ryhmäjäsenyyksien perusteella, jos näiden ryhmien nimet eivät erota kirjainkoosta \"admin\", \"user\" tai \"guest\" vaatimuksessa. Vaatimuksen tulee sisältää luettelo, ja jos käyttäjä kuuluu useisiin ryhmiin, sovellus määrittää korkeinta pääsytasoa vastaavan roolin. Jos mikään ryhmä ei täsmää, pääsy evätään.",
"LabelOpenRSSFeed": "Avaa RSS-syöte",
"LabelOverwrite": "Korvaa", "LabelOverwrite": "Korvaa",
"LabelPaginationPageXOfY": "Sivu {0}/{1}", "LabelPaginationPageXOfY": "Sivu {0}/{1}",
"LabelPassword": "Salasana", "LabelPassword": "Salasana",
"LabelPath": "Polku", "LabelPath": "Polku",
"LabelPermanent": "Pysyvä", "LabelPermanent": "Pysyvä",
"LabelPermissionsAccessAllLibraries": "Käyttöoikeudet kaikkiin kirjastoihin", "LabelPermissionsAccessAllLibraries": "Käyttöoikeudet kaikkiin kirjastoihin",
"LabelPermissionsAccessAllTags": "Saa käyttää kaikkia tunnisteita", "LabelPermissionsAccessAllTags": "On pääsy kaikkiin tunnisteihin",
"LabelPermissionsAccessExplicitContent": "Saa käyttää aikuisille tarkoitettua sisältöä", "LabelPermissionsAccessExplicitContent": "Saa käyttää aikuisille tarkoitettua sisältöä",
"LabelPermissionsCreateEreader": "Voi luoda e-lukijan",
"LabelPermissionsDelete": "Voi poistaa", "LabelPermissionsDelete": "Voi poistaa",
"LabelPermissionsDownload": "Voi ladata", "LabelPermissionsDownload": "Voi ladata",
"LabelPermissionsUpdate": "Voi päivittää", "LabelPermissionsUpdate": "Voi päivittää",
"LabelPermissionsUpload": "Voi lähettää", "LabelPermissionsUpload": "Voi lähettää",
"LabelPersonalYearReview": "Vuotesi katsauksessa ({0})",
"LabelPhotoPathURL": "Valokuvan polku/URL-osoite",
"LabelPlayMethod": "Toistotapa", "LabelPlayMethod": "Toistotapa",
"LabelPlayerChapterNumberMarker": "{0}/{1}", "LabelPlayerChapterNumberMarker": "{0}/{1}",
"LabelPlaylists": "Soittolistat", "LabelPlaylists": "Soittolistat",
@@ -432,82 +491,296 @@
"LabelPodcastType": "Podcastien tyyppi", "LabelPodcastType": "Podcastien tyyppi",
"LabelPodcasts": "Podcastit", "LabelPodcasts": "Podcastit",
"LabelPort": "Portti", "LabelPort": "Portti",
"LabelPrimaryEbook": "Ensisijainen e-kirja", "LabelPrefixesToIgnore": "Ohitettavat etuliitteet (kirjainkoolla ei väliä)",
"LabelPreventIndexing": "Estä syötteesi olemasta iTunesin ja Googlen podcast-hakemistojen indeksoinnin kohteena",
"LabelPrimaryEbook": "Ensisijainen s-kirja",
"LabelProgress": "Edistyminen", "LabelProgress": "Edistyminen",
"LabelProvider": "Toimittaja", "LabelProvider": "Toimittaja",
"LabelProviderAuthorizationValue": "Valtuutusotsikon arvo",
"LabelPubDate": "Julkaisupäivä", "LabelPubDate": "Julkaisupäivä",
"LabelPublishYear": "Julkaisuvuosi", "LabelPublishYear": "Julkaisuvuosi",
"LabelPublishedDate": "Julkaistu {0}", "LabelPublishedDate": "Julkaistu {0}",
"LabelPublishedDecade": "Julkaistu vuosikymmen",
"LabelPublishedDecades": "Julkaistu vuosikymmenet",
"LabelPublisher": "Julkaisija", "LabelPublisher": "Julkaisija",
"LabelPublishers": "Julkaisijat", "LabelPublishers": "Julkaisijat",
"LabelRSSFeedCustomOwnerEmail": "Mukautettu omistajan sähköposti",
"LabelRSSFeedCustomOwnerName": "Mukautettu omistajan nimi",
"LabelRSSFeedOpen": "RSS-syöte avoin",
"LabelRSSFeedPreventIndexing": "Estä indeksointi", "LabelRSSFeedPreventIndexing": "Estä indeksointi",
"LabelRSSFeedSlug": "RSS-syöte Slug",
"LabelRSSFeedURL": "RSS-syötteen URL-osoite",
"LabelRandomly": "Satunnaisesti", "LabelRandomly": "Satunnaisesti",
"LabelReAddSeriesToContinueListening": "Lisää sarja uudelleen jatkaaksesi kuuntelua",
"LabelRead": "Lue", "LabelRead": "Lue",
"LabelReadAgain": "Lue uudelleen", "LabelReadAgain": "Lue uudelleen",
"LabelReadEbookWithoutProgress": "Lue e-kirja tallentamatta edistymistietoja", "LabelReadEbookWithoutProgress": "Lue s-kirja tallentamatta edistymistietoja",
"LabelRecentSeries": "Viimeisimmät sarjat", "LabelRecentSeries": "Viimeisimmät sarjat",
"LabelRecentlyAdded": "Viimeeksi lisätyt", "LabelRecentlyAdded": "Viimeeksi lisätyt",
"LabelRecommended": "Suositeltu", "LabelRecommended": "Suositeltu",
"LabelRedo": "Tee uudelleen", "LabelRedo": "Tee uudelleen",
"LabelRegion": "Alue", "LabelRegion": "Alue",
"LabelReleaseDate": "Julkaisupäivä", "LabelReleaseDate": "Julkaisupäivä",
"LabelRemoveAllMetadataAbs": "Poista kaikki metadata.abs-tiedostot",
"LabelRemoveAllMetadataJson": "Poista kaikki metadata.json-tiedostot",
"LabelRemoveCover": "Poista kansikuva", "LabelRemoveCover": "Poista kansikuva",
"LabelRemoveMetadataFile": "Poista metatietotiedostot kirjaston kohdekansioista",
"LabelRemoveMetadataFileHelp": "Poista kaikki metadata.json- ja metadata.abs-tiedostot {0} kansiostasi.",
"LabelRowsPerPage": "Rivejä sivulla", "LabelRowsPerPage": "Rivejä sivulla",
"LabelSearchTerm": "Hakusana", "LabelSearchTerm": "Hakusana",
"LabelSearchTitle": "Etsi otsikko",
"LabelSearchTitleOrASIN": "Etsi otsikko tai ASIN",
"LabelSeason": "Kausi", "LabelSeason": "Kausi",
"LabelSeasonNumber": "Kausi #{0}",
"LabelSelectAll": "Valitse kaikki", "LabelSelectAll": "Valitse kaikki",
"LabelSelectAllEpisodes": "Valitse kaikki jaksot",
"LabelSelectEpisodesShowing": "Valitse {0} näytettävää jaksoa",
"LabelSelectUsers": "Valitse käyttäjät", "LabelSelectUsers": "Valitse käyttäjät",
"LabelSendEbookToDevice": "Lähetä s-kirja kohteeseen...",
"LabelSequence": "Sekvenssi",
"LabelSerial": "Sarja",
"LabelSeries": "Sarja", "LabelSeries": "Sarja",
"LabelSeriesName": "Sarjan nimi", "LabelSeriesName": "Sarjan nimi",
"LabelSeriesProgress": "Sarjan edistyminen",
"LabelServerLogLevel": "Palvelimen lokitaso",
"LabelServerYearReview": "Palvelimen vuosi katsauksessa ({0})",
"LabelSetEbookAsPrimary": "Aseta ensisijaiseksi", "LabelSetEbookAsPrimary": "Aseta ensisijaiseksi",
"LabelSetEbookAsSupplementary": "Aseta täydentäväksi", "LabelSetEbookAsSupplementary": "Aseta täydentäväksi",
"LabelSettingsAllowIframe": "Salli upottaminen iframe-kehykseen",
"LabelSettingsAudiobooksOnly": "Vain äänikirjat", "LabelSettingsAudiobooksOnly": "Vain äänikirjat",
"LabelSettingsAudiobooksOnlyHelp": "Tämän asetuksen käyttöönotto ohittaa s-kirjatiedostot, elleivät ne ole äänikirjakansiossa, jolloin ne asetetaan täydentäviksi s-kirjoiksi",
"LabelSettingsBookshelfViewHelp": "Skeuomorfinen muotoilu puisilla hyllyillä",
"LabelSettingsChromecastSupport": "Chromecast-tuki", "LabelSettingsChromecastSupport": "Chromecast-tuki",
"LabelSettingsDateFormat": "Päivämäärän muoto",
"LabelSettingsEnableWatcherHelp": "Ottaa käyttöön kohteiden automaattisen lisäämisen ja päivityksen kun tiedostomuutoksia havaitaan. *Tarvitsee palvelimen uudelleenkäynnistyksen",
"LabelSettingsEpubsAllowScriptedContent": "Salli komentosarjamuotoinen sisältö epubissa",
"LabelSettingsEpubsAllowScriptedContentHelp": "Salli epub-tiedostojen suorittaa komentosarjoja. On suositeltavaa pitää tämä asetus pois käytöstä, ellet luota epub-tiedostojen lähteeseen.",
"LabelSettingsExperimentalFeatures": "Kokeelliset ominaisuudet", "LabelSettingsExperimentalFeatures": "Kokeelliset ominaisuudet",
"LabelSettingsExperimentalFeaturesHelp": "Kehitettävissä olevat ominaisuudet, jotka voivat hyödyntää palautettasi ja auttaa testaamisessa. Napsauta avataksesi github-keskustelun.",
"LabelSettingsFindCovers": "Etsi kansikuvia", "LabelSettingsFindCovers": "Etsi kansikuvia",
"LabelSettingsFindCoversHelp": "Jos äänikirjassasi ei ole kansion sisällä upotettua kantta tai kansikuvaa, skanneri yrittää löytää kannen.<br>Huomaa: Tämä pidentää skannausaikaa",
"LabelSettingsHideSingleBookSeries": "Piilota yksittäinen kirjasarja",
"LabelSettingsHideSingleBookSeriesHelp": "Sarjat, joissa on yksi kirja, piilotetaan sarjasivulta ja kotisivujen hyllyiltä.",
"LabelSettingsHomePageBookshelfView": "Kotisivu käyttää kirjahyllynäkymää",
"LabelSettingsLibraryBookshelfView": "Kirjasto käyttää kirjahyllynäkymää",
"LabelSettingsLibraryMarkAsFinishedPercentComplete": "Valmistumisprosentti on suurempi kuin",
"LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Jäljellä oleva aika on alle (sekuntia)",
"LabelSettingsLibraryMarkAsFinishedWhen": "Merkitse mediakohde valmiiksi, kun",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Ohita aiemmat kirjat Jatka sarjassa",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Jatka sarja -kotisivun hyllyssä näkyy ensimmäinen kirja, jota ei ole aloitettu sarjoissa, joissa on vähintään yksi kirja valmiina eikä yhtään kirjaa kesken. Tämän asetuksen ottaminen käyttöön jatkaa sarjaa kauimpana valmistuneesta kirjasta ensimmäisen aloittamattoman kirjan sijaan.",
"LabelSettingsParseSubtitles": "Jäsennä tekstitykset",
"LabelSettingsParseSubtitlesHelp": "Pura tekstitykset äänikirjojen kansioiden nimistä.<br>Tekstitys on erotettava toisistaan merkillä \"-\"<br>ts. \"Kirjan otsikko - Tekstitys täällä\" on alaotsikko \"Tekstitys täällä\"",
"LabelSettingsPreferMatchedMetadata": "Pidä mieluummin täsmäävät metatiedot",
"LabelSettingsPreferMatchedMetadataHelp": "Täsmäävät tiedot ohittavat kohteen tiedot käytettäessä Pikatäsmäystä. Oletuksena Pikatäsmäys täyttää vain puuttuvat tiedot.",
"LabelSettingsSkipMatchingBooksWithASIN": "Ohita täsmäävät kirjat, joilla on jo ASIN",
"LabelSettingsSkipMatchingBooksWithISBN": "Ohita täsmäävät kirjat, joilla on jo ISBN",
"LabelSettingsSortingIgnorePrefixes": "Jätä etuliitteet huomioimatta lajittelussa",
"LabelSettingsSortingIgnorePrefixesHelp": "eli etuliitteelle \"tämän\" kirjan nimi \"Tämän kirjan nimi\" lajitellaan muodossa \"Kirjan nimi, Tämän\"",
"LabelSettingsSquareBookCovers": "Käytä neliömäisiä kirjankansia",
"LabelSettingsSquareBookCoversHelp": "Käytä mieluummin neliömäisiä kansia kuin tavallisia 1,6:1 kirjankansia",
"LabelSettingsStoreCoversWithItem": "Säilytyskannet esineen kanssa",
"LabelSettingsStoreCoversWithItemHelp": "Oletusarvoisesti kannet tallennetaan kansioon /metadata/items, ja tämän asetuksen ottaminen käyttöön tallentaa kannet kirjaston kohdekansioon. Vain yksi tiedosto nimeltä \"cover\" säilytetään",
"LabelSettingsStoreMetadataWithItem": "Tallenna metatiedot kohteen kanssa",
"LabelSettingsStoreMetadataWithItemHelp": "Oletuksena metatietotiedostot tallennetaan kansioon /metadata/items, ja tämän asetuksen ottaminen käyttöön tallentaa metatietotiedostot kirjastosi kohdekansioihin",
"LabelSettingsTimeFormat": "Aikamuoto",
"LabelShare": "Jaa", "LabelShare": "Jaa",
"LabelShareDownloadableHelp": "Antaa käyttäjien, joilla on jakolinkki, ladata kirjastokohteen zip-tiedoston.",
"LabelShareOpen": "Jaa Avoin",
"LabelShareURL": "Jaa URL-osoite",
"LabelShowAll": "Näytä kaikki", "LabelShowAll": "Näytä kaikki",
"LabelShowSeconds": "Näytä sekunnit", "LabelShowSeconds": "Näytä sekunnit",
"LabelShowSubtitles": "Näytä tekstitykset",
"LabelSize": "Koko", "LabelSize": "Koko",
"LabelSleepTimer": "Uniajastin", "LabelSleepTimer": "Uniajastin",
"LabelSlug": "Slug",
"LabelSortAscending": "Nouseva",
"LabelSortDescending": "Laskeva",
"LabelStart": "Aloita", "LabelStart": "Aloita",
"LabelStartTime": "Aloitusaika", "LabelStartTime": "Aloitusaika",
"LabelStarted": "Aloitettu",
"LabelStartedAt": "Aloitettu",
"LabelStatsAudioTracks": "Ääniraidat", "LabelStatsAudioTracks": "Ääniraidat",
"LabelStatsAuthors": "Tekijät",
"LabelStatsBestDay": "Paras päivä", "LabelStatsBestDay": "Paras päivä",
"LabelStatsDailyAverage": "Päivittäinen keskiarvo", "LabelStatsDailyAverage": "Päivittäinen keskiarvo",
"LabelStatsDays": "Päivää", "LabelStatsDays": "Päivää",
"LabelStatsDaysListened": "Päivää kuunneltu", "LabelStatsDaysListened": "Päivää kuunneltu",
"LabelStatsHours": "Tunnit", "LabelStatsHours": "Tunnit",
"LabelStatsInARow": "peräjälkeen", "LabelStatsInARow": "peräjälkeen",
"LabelStatsItemsFinished": "Valmiit tuotteet",
"LabelStatsItemsInLibrary": "Kohteet kirjastossa",
"LabelStatsMinutes": "minuuttia", "LabelStatsMinutes": "minuuttia",
"LabelStatsMinutesListening": "Minuuttia kuunneltu", "LabelStatsMinutesListening": "Minuuttia kuunneltu",
"LabelStatsOverallDays": "Päivät kokonaisuudessaan",
"LabelStatsOverallHours": "Tunnit kokonaisuudessaan",
"LabelStatsWeekListening": "Viikon aikana kuunneltu", "LabelStatsWeekListening": "Viikon aikana kuunneltu",
"LabelSubtitle": "Tekstitys",
"LabelSupportedFileTypes": "Tuetut tiedostotyypit",
"LabelTag": "Tägi", "LabelTag": "Tägi",
"LabelTags": "Tägit", "LabelTags": "Tägit",
"LabelTagsAccessibleToUser": "Tunnisteet käyttäjän käytettävissä",
"LabelTagsNotAccessibleToUser": "Tunnisteet ei käyttäjien käytettävissä",
"LabelTasks": "Tehtävät käynnissä",
"LabelTextEditorBulletedList": "Luettelomerkitty luettelo",
"LabelTextEditorLink": "Linkki",
"LabelTextEditorNumberedList": "Numeroitu luettelo",
"LabelTextEditorUnlink": "Poista linkitys",
"LabelTheme": "Teema", "LabelTheme": "Teema",
"LabelThemeDark": "Tumma", "LabelThemeDark": "Tumma",
"LabelThemeLight": "Kirkas", "LabelThemeLight": "Kirkas",
"LabelTimeBase": "Aika-alusta",
"LabelTimeDurationXHours": "{0} tuntia",
"LabelTimeDurationXMinutes": "{0} minuuttia",
"LabelTimeDurationXSeconds": "{0} sekuntia",
"LabelTimeInMinutes": "Aika minuutteina",
"LabelTimeLeft": "{0} jäljellä",
"LabelTimeListened": "Aika kuunneltu",
"LabelTimeListenedToday": "Kuunneltu aika tänään",
"LabelTimeRemaining": "{0} jäljellä", "LabelTimeRemaining": "{0} jäljellä",
"LabelTimeToShift": "Vaihtoaika sekunteina",
"LabelTitle": "Nimi", "LabelTitle": "Nimi",
"LabelToolsEmbedMetadata": "Upota metatiedot",
"LabelToolsEmbedMetadataDescription": "Upota metatiedot äänitiedostoihin, mukaan lukien kansikuva ja luvut.",
"LabelToolsM4bEncoder": "M4B Enkooderi",
"LabelToolsMakeM4b": "Tee M4B-äänikirjatiedosto",
"LabelToolsMakeM4bDescription": "Luo .M4B-äänikirjatiedosto, joka sisältää upotetut metatiedot, kansikuvan ja luvut.",
"LabelToolsSplitM4b": "Jaa M4B MP3:ksi",
"LabelToolsSplitM4bDescription": "Luo MP3-tiedostoja M4B:stä, jaettuna lukujen mukaan, upotetulla metatiedolla, kansikuvalla ja luvuilla.",
"LabelTotalDuration": "Kokonaiskesto", "LabelTotalDuration": "Kokonaiskesto",
"LabelTotalTimeListened": "Yhteensä kuunneltu aika",
"LabelTrackFromFilename": "Raita tiedostonimestä",
"LabelTrackFromMetadata": "Raita metatiedoista",
"LabelTracks": "Raidat", "LabelTracks": "Raidat",
"LabelTracksMultiTrack": "Moniraitainen",
"LabelTracksNone": "Ei raitoja",
"LabelTracksSingleTrack": "Yksiraitainen",
"LabelTrailer": "Traileri",
"LabelType": "Tyyppi", "LabelType": "Tyyppi",
"LabelUnabridged": "Lyhentämätön",
"LabelUndo": "Kumoa",
"LabelUnknown": "Tuntematon", "LabelUnknown": "Tuntematon",
"LabelUnknownPublishDate": "Tuntematon julkaisupäivämäärä",
"LabelUpdateCover": "Päivitä kansikuva", "LabelUpdateCover": "Päivitä kansikuva",
"LabelUpdateCoverHelp": "Salli valittujen kirjojen olemassa olevien kansien päällekirjoittaminen, kun osuma löytyy",
"LabelUpdateDetails": "Päivitä yksityiskohdat",
"LabelUpdateDetailsHelp": "Salli valittujen kirjojen olemassa olevien tietojen korvaaminen, kun osuma löytyy",
"LabelUpdatedAt": "Päivitetty",
"LabelUploaderDragAndDrop": "Vedä ja pudota tiedostoja tai kansioita",
"LabelUploaderDragAndDropFilesOnly": "Vedä ja pudota tiedostoja",
"LabelUploaderDropFiles": "Pudota tiedostot",
"LabelUploaderItemFetchMetadataHelp": "Nouda automaattisesti otsikko, tekijä ja sarja",
"LabelUseAdvancedOptions": "Käytä edistyneitä vaihtoehtoja",
"LabelUseChapterTrack": "Käytä luvunraitaa",
"LabelUseFullTrack": "Käytä täyttä raitaa",
"LabelUseZeroForUnlimited": "Käytä 0 rajatonta varten",
"LabelUser": "Käyttäjä", "LabelUser": "Käyttäjä",
"LabelUsername": "Käyttäjätunnus", "LabelUsername": "Käyttäjätunnus",
"LabelValue": "Arvo", "LabelValue": "Arvo",
"LabelVersion": "Versio", "LabelVersion": "Versio",
"LabelViewBookmarks": "Katso kirjanmerkit",
"LabelViewChapters": "Katso luvut",
"LabelViewPlayerSettings": "Katso soittimen asetukset",
"LabelViewQueue": "Katso soittimen jono",
"LabelVolume": "Äänenvoimakkuus",
"LabelWebRedirectURLsDescription": "Valtuuta nämä URL-osoitteet OAuth-palveluntarjoajassasi sallimaan uudelleenohjauksen takaisin verkkosovellukseen sisäänkirjautumisen jälkeen:",
"LabelWebRedirectURLsSubfolder": "Alikansio URL-osoitteiden uudelleenohjaukselle",
"LabelWeekdaysToRun": "Ajettavat arkipäivät",
"LabelXBooks": "{0} kirjaa",
"LabelXItems": "{0} kohdetta",
"LabelYearReviewHide": "Piilota vuosi arvostelussa",
"LabelYearReviewShow": "Näytä vuosi arvostelussa",
"LabelYourAudiobookDuration": "Äänikirjan kesto",
"LabelYourBookmarks": "Kirjanmerkkisi", "LabelYourBookmarks": "Kirjanmerkkisi",
"LabelYourPlaylists": "Soittolistasi",
"LabelYourProgress": "Edistymisesi", "LabelYourProgress": "Edistymisesi",
"MessageAddToPlayerQueue": "Lisää soittimen jonoon",
"MessageAppriseDescription": "Käyttääksesi tätä toimintoa tarvitset <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> -instanssin tai rajapinnan joka käsittelee samoja pyyntöjä. <br />Apprise rajapinnan osoite tulee olla täysi URL polku ilmoituksen lähetykseen, esim. jos rajapinta on osoitteessa <code>http://192.168.1.1:8337</code>,niin arvoksi tulee antaa <code>http://192.168.1.1:8337/notify</code>.",
"MessageBackupsDescription": "Varmuuskopiot sisältävät käyttäjät, käyttäjien edistymisen, kirjastokohteiden tiedot, palvelinasetukset ja <code>/metadata/items</code>- ja <code>/metadata/authors</code> -kansioihin tallennetut kuvat. Varmuuskopiot <strong>eivät sisällä</strong> kirjastosi kansioihin tallennettuja tiedostoja.",
"MessageBackupsLocationEditNote": "Huomautus: Varmuuskopion sijainnin päivittäminen ei siirrä tai muokkaa olemassa olevia varmuuskopioita",
"MessageBackupsLocationNoEditNote": "Huomautus: Varmuuskopion sijainti asetetaan ympäristömuuttujan kautta, eikä sitä voi muuttaa tässä.",
"MessageBackupsLocationPathEmpty": "Varmuuskopiointisijainnin polku ei voi olla tyhjä",
"MessageBatchEditPopulateMapDetailsAllHelp": "Täytä käytössä olevat kentät tiedoilla kaikista kohteista. Kentät, joilla on useita arvoja, yhdistetään",
"MessageBatchEditPopulateMapDetailsItemHelp": "Täytä käytössä olevat karttayksityiskohtakentät tämän kohteen tiedoilla",
"MessageBatchQuickMatchDescription": "Pikatäsmäys yrittää lisätä puuttuvat kannet ja metatiedot valituille kohteille. Ota käyttöön alla olevat vaihtoehdot, jotta Pikatäsmäys korvaa olemassa olevat kannet ja/tai metatiedot.",
"MessageBookshelfNoCollections": "Et ole vielä tehnyt kokoelmia",
"MessageBookshelfNoCollectionsHelp": "Kokoelmat ovat julkisia. Kaikki käyttäjät, joilla on pääsy kirjastoon, voivat nähdä ne.",
"MessageBookshelfNoRSSFeeds": "RSS-syötteitä ei ole auki",
"MessageBookshelfNoResultsForFilter": "Ei tuloksia suodattimelle \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Ei tuloksia kyselylle",
"MessageBookshelfNoSeries": "Sinulla ei ole sarjoja",
"MessageChapterEndIsAfter": "Luvun loppu sijaitsee äänikirjan lopun jälkeen",
"MessageChapterErrorFirstNotZero": "Ensimmäisen luvun tulee alkaa nollasta",
"MessageChapterErrorStartGteDuration": "Epäkelvollinen aloitusaika; on oltava lyhyempi kuin äänikirjan kesto",
"MessageChapterErrorStartLtPrev": "Epäkelvollinen aloitusaika; on oltava suurempi tai yhtä suuri kuin edellisen luvun aloitusaika",
"MessageChapterStartIsAfter": "Luku alkaa äänikirjan lopun jälkeen",
"MessageCheckingCron": "Tarkistetaan cronia...",
"MessageConfirmCloseFeed": "Oletko varma, että haluat sulkea tämän syötteen?",
"MessageConfirmDeleteBackup": "Oletko varma, että haluat poistaa varmuuskopion {0}:lle?",
"MessageConfirmDeleteDevice": "Oletko varma, että haluat poistaa s-lukulaitteen \"{0}\"?",
"MessageConfirmDeleteFile": "Tämä poistaa tiedoston tiedostojärjestelmästäsi. Oletko varma?",
"MessageConfirmDeleteLibrary": "Oletko varma, että haluat poistaa kirjaston \"{0}\" pysyvästi?",
"MessageConfirmDeleteLibraryItem": "Tämä poistaa kirjastokohteen tietokannasta ja tiedostojärjestelmästäsi. Oletko varma?",
"MessageConfirmDeleteLibraryItems": "Tämä poistaa {0} kirjastokohdetta tietokannasta ja tiedostojärjestelmästäsi. Oletko varma?",
"MessageConfirmDeleteMetadataProvider": "Oletko varma, että haluat poistaa mukautetun metatietojen tarjoajan \"{0}\"?",
"MessageConfirmDeleteNotification": "Oletko varma, että haluat poistaa tämän ilmoituksen?",
"MessageConfirmDeleteSession": "Oletko varma, että haluat poistaa tämän istunnon?",
"MessageConfirmEmbedMetadataInAudioFiles": "Oletko varma, että haluat upottaa metatiedot {0} äänitiedostoihin?",
"MessageConfirmForceReScan": "Oletko varma, että haluat pakottaa uudelleenskannauksen?",
"MessageConfirmMarkAllEpisodesFinished": "Oletko varma, että haluat merkitä kaikki jaksot päättyneiksi?",
"MessageConfirmMarkAllEpisodesNotFinished": "Oletko varma, että haluat merkitä kaikki jaksot ei-valmiiksi?",
"MessageConfirmMarkItemFinished": "Oletko varma, että haluat merkitä \"{0}\":n valmiiksi?",
"MessageConfirmMarkItemNotFinished": "Oletko varma, että haluat merkitä \"{0}\":n ei-valmiiksi?",
"MessageConfirmMarkSeriesFinished": "Oletko varma, että haluat merkitä kaikki tämän sarjan kirjat valmiiksi?",
"MessageConfirmMarkSeriesNotFinished": "Oletko varma, että haluat merkitä kaikki tämän sarjan kirjat ei-valmiiksi?",
"MessageConfirmNotificationTestTrigger": "Käynnistetäänkö tämä ilmoitus testitiedoilla?",
"MessageConfirmPurgeCache": "'Tyhjennä välimuisti' poistaa koko hakemiston sijainnilla <code>/metadata/cache</code>. <br /><br />Oletko varma, että haluat poistaa välimuistihakemiston?",
"MessageConfirmPurgeItemsCache": "'Tyhjennä kohteiden välimuisti' poistaa koko hakemiston sijainnilla <code>/metadata/cache/items</code>.<br />Oletko varma?",
"MessageConfirmQuickEmbed": "Varoitus! Pikaupottaminen ei varmuuskopioi äänitiedostojasi. Varmista, että sinulla on varmuuskopio äänitiedostoistasi. <br><br>Haluatko jatkaa?",
"MessageConfirmQuickMatchEpisodes": "Jaksojen pikatäsmääminen korvaa tiedot, jos vastaavuus löytyy. Vain täsmäämättömät jaksot päivitetään. Oletko varma?",
"MessageConfirmReScanLibraryItems": "Oletko varma, että haluat skannata uudelleen {0} kohdetta?",
"MessageConfirmRemoveAllChapters": "Oletko varma, että haluat poistaa kaikki jaksot?",
"MessageConfirmRemoveAuthor": "Oletko varma, että haluat poistaa tekijän \"{0}\"?",
"MessageConfirmRemoveCollection": "Oletko varma, että haluat poistaa kokoelman \"{0}\"?",
"MessageConfirmRemoveEpisode": "Oletko varma, että haluat poistaa jakson \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Oletko varma, että haluat poistaa {0} jaksoa?",
"MessageConfirmRemoveListeningSessions": "Oletko varma, että haluat poistaa {0} kuuntelukertaa?",
"MessageConfirmRemoveMetadataFiles": "Oletko varma, että haluat poistaa kaikki metadata.{0}-tiedostot kirjaston kohdekansioista?",
"MessageConfirmRemoveNarrator": "Oletko varma, että haluat poistaa kertojan \"{0}\"?",
"MessageConfirmRemovePlaylist": "Oletko varma, että haluat poistaa soittolistan \"{0}\"?",
"MessageConfirmRenameGenre": "Oletko varma, että haluat nimetä lajityypin \"{0}\" uudelleen \"{1}\":ksi kaikille kohteille?",
"MessageConfirmRenameGenreMergeNote": "Huomautus: Tämä lajityyppi on jo olemassa, joten ne yhdistetään.",
"MessageConfirmRenameGenreWarning": "Varoitus! Samanlainen lajityyppi eri kotelolla on jo olemassa \"{0}\".",
"MessageConfirmRenameTag": "Oletko varma, että haluat nimetä tunnisteen \"{0}\" uudelleen \"{1}\":ksi kaikille kohteille?",
"MessageConfirmRenameTagMergeNote": "Huomautus: Tämä tunniste on jo olemassa, joten ne yhdistetään.",
"MessageConfirmRenameTagWarning": "Varoitus! Samanlainen tunniste eri kotelolla on jo olemassa \"{0}\".",
"MessageConfirmResetProgress": "Oletko varma, että haluat nollata edistymisesi?",
"MessageConfirmSendEbookToDevice": "Oletko varma, että haluat lähettää {0} s-kirjan \"{1}\" laitteeseen \"{2}\"?",
"MessageConfirmUnlinkOpenId": "Oletko varma, että haluat poistaa tämän käyttäjän linkityksen OpenID:stä?",
"MessageDaysListenedInTheLastYear": "{0} kuunneltua päivää viime vuonna",
"MessageDownloadingEpisode": "Ladataan jaksoa", "MessageDownloadingEpisode": "Ladataan jaksoa",
"MessageDragFilesIntoTrackOrder": "Vedä tiedostot oikeaan raitojen järjestykseen",
"MessageEmbedFailed": "Upotus epäonnistui!",
"MessageEmbedFinished": "Upotus valmis!",
"MessageEmbedQueue": "Jonossa metatietojen upottamista varten ({0} jonossa)",
"MessageEpisodesQueuedForDownload": "{0} jaksoa on latausjonossa", "MessageEpisodesQueuedForDownload": "{0} jaksoa on latausjonossa",
"MessageEreaderDevices": "S-kirjojen toimituksen varmistamiseksi sinun on ehkä lisättävä yllä oleva sähköpostiosoite kelvolliseksi lähettäjäksi jokaiselle alla luetellulle laitteelle.",
"MessageFeedURLWillBe": "Syötteen URL tulee olemaan {0}", "MessageFeedURLWillBe": "Syötteen URL tulee olemaan {0}",
"MessageFetching": "Haetaan...", "MessageFetching": "Haetaan...",
"MessageForceReScanDescription": "skannaa kaikki tiedostot uudelleen kuten uusi tarkistus. Äänitiedoston ID3-tunnisteet, OPF-tiedostot ja tekstitiedostot skannataan uusina.",
"MessageImportantNotice": "Tärkeä huomautus!",
"MessageInsertChapterBelow": "Syötä luku alle",
"MessageItemsSelected": "{0} kohdetta valittu",
"MessageItemsUpdated": "{0} kohdetta päivitetty",
"MessageJoinUsOn": "Liity meihin",
"MessageLoading": "Ladataan...", "MessageLoading": "Ladataan...",
"MessageLoadingFolders": "Ladataan kansioita...",
"MessageLogsDescription": "Lokitiedot tallennetaan kansioon <code>/metadata/logs</code> JSON-tiedostoina. Kaatumislokit tallennetaan kansioon <code>/metadata/logs/crash_logs.txt</code>.",
"MessageM4BFailed": "M4B epäonnistui!",
"MessageM4BFinished": "M4B valmis!",
"MessageMarkAsFinished": "Merkitse valmiiksi", "MessageMarkAsFinished": "Merkitse valmiiksi",
"MessageNoBookmarks": "Ei kirjanmerkkejä", "MessageNoBookmarks": "Ei kirjanmerkkejä",
"MessageNoChapters": "Ei kappaleita", "MessageNoChapters": "Ei kappaleita",
"MessageNoCollections": "Ei kokoelmia",
"MessageNoCoversFound": "Kansikuvia ei löydetty", "MessageNoCoversFound": "Kansikuvia ei löydetty",
"MessageNoGenres": "Ei lajityyppejä", "MessageNoGenres": "Ei lajityyppejä",
"MessageNoItems": "Ei kohteita", "MessageNoItems": "Ei kohteita",
@@ -517,10 +790,22 @@
"MessageNoUpdatesWereNecessary": "Päivityksiä ei tarvittu", "MessageNoUpdatesWereNecessary": "Päivityksiä ei tarvittu",
"MessageNoUserPlaylists": "Sinulla ei ole soittolistoja", "MessageNoUserPlaylists": "Sinulla ei ole soittolistoja",
"MessageOr": "tai", "MessageOr": "tai",
"MessagePodcastSearchField": "Syötä hakutermi tai RSS-syötteen URL-osoite",
"MessageQuickMatchAllEpisodes": "Pikatäsmää kaikki jaksot",
"MessageRemoveUserWarning": "Oletko varma, että haluat poistaa käyttäjän \"{0}\" pysyvästi?",
"MessageReportBugsAndContribute": "Ilmoita virheistä, toivo ominaisuuksia ja osallistu", "MessageReportBugsAndContribute": "Ilmoita virheistä, toivo ominaisuuksia ja osallistu",
"MessageResetChaptersConfirm": "Oletko varma, että haluat nollata luvut ja kumota tekemäsi muutokset?",
"MessageRestoreBackupConfirm": "Oletko varma, että haluat palauttaa varmuuskopion, joka on luotu",
"MessageScheduleLibraryScanNote": "Suurimmalle osaa käyttäjistä on suositeltavaa jättää tämä ominaisuus pois päältä ja säilyttää kansiotarkkailu päällä. Kansiotarkkailu havaitsee automaattisesti tiedostomuutokset kirjaston kansioissa. Kansiotarkkailu ei toimi kaikille tiedostojärjestelmille (kuten NFS), jolloin voidaan käyttää ajastettuja kirjastoskannauksia.",
"MessageTaskFailed": "Epäonnistunut", "MessageTaskFailed": "Epäonnistunut",
"MessageWatcherIsDisabledGlobally": "Kansiotarkkailu on poistettu käytöstä kaikkialla palvelimen asetuksissa",
"NoteRSSFeedPodcastAppsHttps": "Varoitus: Useimmat podcast-sovellukset edellyttävät, että RSS-syötteen URL-osoite käyttää HTTPS:a",
"NoteRSSFeedPodcastAppsPubDate": "Varoitus: yhdellä tai useammalla jaksollasi ei ole julkaisupäivämäärää. Jotkut podcast-sovellukset vaativat tämän.",
"StatsSessions": "istunnot", "StatsSessions": "istunnot",
"ToastAccountUpdateSuccess": "Tili päivitetty", "ToastAccountUpdateSuccess": "Tili päivitetty",
"ToastAppriseUrlRequired": "Arvon tulee olla Apprise URL",
"ToastBatchQuickMatchFailed": "Erän pikatäsmäys epäonnistui!",
"ToastBatchQuickMatchStarted": "{0} kirjan erän pikatäsmäys aloitettu!",
"ToastBookmarkCreateFailed": "Kirjanmerkin luominen epäonnistui", "ToastBookmarkCreateFailed": "Kirjanmerkin luominen epäonnistui",
"ToastCoverUpdateFailed": "Kansikuvan päivitys epäonnistui", "ToastCoverUpdateFailed": "Kansikuvan päivitys epäonnistui",
"ToastItemCoverUpdateSuccess": "Kohteen kansikuva päivitetty", "ToastItemCoverUpdateSuccess": "Kohteen kansikuva päivitetty",
+8 -7
View File
@@ -76,7 +76,7 @@
"ButtonReadLess": "Lire moins", "ButtonReadLess": "Lire moins",
"ButtonReadMore": "Lire la suite", "ButtonReadMore": "Lire la suite",
"ButtonRefresh": "Rafraîchir", "ButtonRefresh": "Rafraîchir",
"ButtonRemove": "Supprimer", "ButtonRemove": "Retirer",
"ButtonRemoveAll": "Supprimer tout", "ButtonRemoveAll": "Supprimer tout",
"ButtonRemoveAllLibraryItems": "Supprimer tous les éléments de la bibliothèque", "ButtonRemoveAllLibraryItems": "Supprimer tous les éléments de la bibliothèque",
"ButtonRemoveFromContinueListening": "Ne plus continuer à écouter", "ButtonRemoveFromContinueListening": "Ne plus continuer à écouter",
@@ -219,6 +219,7 @@
"LabelAccountTypeAdmin": "Admin", "LabelAccountTypeAdmin": "Admin",
"LabelAccountTypeGuest": "Invité", "LabelAccountTypeGuest": "Invité",
"LabelAccountTypeUser": "Utilisateur", "LabelAccountTypeUser": "Utilisateur",
"LabelActivities": "Activités",
"LabelActivity": "Activité", "LabelActivity": "Activité",
"LabelAddToCollection": "Ajouter à la collection", "LabelAddToCollection": "Ajouter à la collection",
"LabelAddToCollectionBatch": "Ajout de {0} livres à la collection", "LabelAddToCollectionBatch": "Ajout de {0} livres à la collection",
@@ -283,6 +284,7 @@
"LabelContinueSeries": "Continuer les séries", "LabelContinueSeries": "Continuer les séries",
"LabelCover": "Couverture", "LabelCover": "Couverture",
"LabelCoverImageURL": "URL vers limage de couverture", "LabelCoverImageURL": "URL vers limage de couverture",
"LabelCoverProvider": "Source des couvertures",
"LabelCreatedAt": "Créé le", "LabelCreatedAt": "Créé le",
"LabelCronExpression": "Expression cron", "LabelCronExpression": "Expression cron",
"LabelCurrent": "Actuel", "LabelCurrent": "Actuel",
@@ -391,6 +393,7 @@
"LabelIntervalEvery6Hours": "Toutes les 6 heures", "LabelIntervalEvery6Hours": "Toutes les 6 heures",
"LabelIntervalEveryDay": "Tous les jours", "LabelIntervalEveryDay": "Tous les jours",
"LabelIntervalEveryHour": "Toutes les heures", "LabelIntervalEveryHour": "Toutes les heures",
"LabelIntervalEveryMinute": "Toutes les minutes",
"LabelInvert": "Inverser", "LabelInvert": "Inverser",
"LabelItem": "Élément", "LabelItem": "Élément",
"LabelJumpBackwardAmount": "Dans le lecteur, reculer de", "LabelJumpBackwardAmount": "Dans le lecteur, reculer de",
@@ -555,11 +558,6 @@
"LabelSettingsBookshelfViewHelp": "Interface skeumorphique avec étagères en bois", "LabelSettingsBookshelfViewHelp": "Interface skeumorphique avec étagères en bois",
"LabelSettingsChromecastSupport": "Support du Chromecast", "LabelSettingsChromecastSupport": "Support du Chromecast",
"LabelSettingsDateFormat": "Format de date", "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",
"LabelSettingsEnableWatcher": "Activer la veille",
"LabelSettingsEnableWatcherForLibrary": "Activer la surveillance des dossiers pour la bibliothèque",
"LabelSettingsEnableWatcherHelp": "Active la mise à jour automatique d'éléments lorsque des modifications de fichiers sont détectées. * Nécessite le redémarrage du serveur", "LabelSettingsEnableWatcherHelp": "Active la mise à jour automatique d'éléments lorsque des modifications de fichiers sont détectées. * Nécessite le redémarrage du serveur",
"LabelSettingsEpubsAllowScriptedContent": "Autoriser le contenu scénarisé pour les fichiers EPUB", "LabelSettingsEpubsAllowScriptedContent": "Autoriser le contenu scénarisé pour les fichiers EPUB",
"LabelSettingsEpubsAllowScriptedContentHelp": "Autoriser les fichiers EPUB à exécuter des scripts. Il est recommandé de laisser ce paramètre désactivé, sauf si vous faites confiance à la source des fichiers EPUB.", "LabelSettingsEpubsAllowScriptedContentHelp": "Autoriser les fichiers EPUB à exécuter des scripts. Il est recommandé de laisser ce paramètre désactivé, sauf si vous faites confiance à la source des fichiers EPUB.",
@@ -707,9 +705,10 @@
"MessageBackupsLocationEditNote": "Remarque: Mettre à jour l'emplacement de sauvegarde ne déplacera pas ou ne modifiera pas les sauvegardes existantes", "MessageBackupsLocationEditNote": "Remarque: Mettre à jour l'emplacement de sauvegarde ne déplacera pas ou ne modifiera pas les sauvegardes existantes",
"MessageBackupsLocationNoEditNote": "Remarque: lemplacement de sauvegarde est défini via une variable denvironnement et ne peut pas être modifié ici.", "MessageBackupsLocationNoEditNote": "Remarque: lemplacement de sauvegarde est défini via une variable denvironnement et ne peut pas être modifié ici.",
"MessageBackupsLocationPathEmpty": "L'emplacement de secours ne peut pas être vide", "MessageBackupsLocationPathEmpty": "L'emplacement de secours ne peut pas être vide",
"MessageBatchEditPopulateMapDetailsAllHelp": "Remplir les champs disponibles avec les données de tous les éléments. les champs avec des valeurs multiples seront fusionnés", "MessageBatchEditPopulateMapDetailsAllHelp": "Remplir les champs disponibles avec les données de tous les éléments. Les champs avec des valeurs multiples seront fusionnés.",
"MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera dajouter les couvertures et métadonnées manquantes pour les éléments sélectionnés. Activez les options ci-dessous pour permettre la Recherche par correspondance d’écraser les couvertures et/ou métadonnées existantes.", "MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera dajouter les couvertures et métadonnées manquantes pour les éléments sélectionnés. Activez les options ci-dessous pour permettre la Recherche par correspondance d’écraser les couvertures et/ou métadonnées existantes.",
"MessageBookshelfNoCollections": "Vous navez pas encore de collections", "MessageBookshelfNoCollections": "Vous navez pas encore de collections",
"MessageBookshelfNoCollectionsHelp": "Les collections sont publiques. Tous les utilisateurs ayant accès à la bibliothèque pourront les voir.",
"MessageBookshelfNoRSSFeeds": "Aucun flux RSS nest ouvert", "MessageBookshelfNoRSSFeeds": "Aucun flux RSS nest ouvert",
"MessageBookshelfNoResultsForFilter": "Aucun résultat pour le filtre « {0} : {1} »", "MessageBookshelfNoResultsForFilter": "Aucun résultat pour le filtre « {0} : {1} »",
"MessageBookshelfNoResultsForQuery": "Aucun résultat pour la requête", "MessageBookshelfNoResultsForQuery": "Aucun résultat pour la requête",
@@ -820,6 +819,7 @@
"MessageNoTasksRunning": "Aucune tâche en cours", "MessageNoTasksRunning": "Aucune tâche en cours",
"MessageNoUpdatesWereNecessary": "Aucune mise à jour n’était nécessaire", "MessageNoUpdatesWereNecessary": "Aucune mise à jour n’était nécessaire",
"MessageNoUserPlaylists": "Vous navez aucune liste de lecture", "MessageNoUserPlaylists": "Vous navez aucune liste de lecture",
"MessageNoUserPlaylistsHelp": "Les playlists sont privées. Seul l'utilisateur qui les a créées peut les voir.",
"MessageNotYetImplemented": "Non implémenté", "MessageNotYetImplemented": "Non implémenté",
"MessageOpmlPreviewNote": "Remarque: Il sagit dun aperçu du fichier OPML analysé. Le titre réel du podcast provient du flux RSS.", "MessageOpmlPreviewNote": "Remarque: Il sagit dun aperçu du fichier OPML analysé. Le titre réel du podcast provient du flux RSS.",
"MessageOr": "ou", "MessageOr": "ou",
@@ -842,6 +842,7 @@
"MessageRestoreBackupConfirm": "Êtes-vous sûr·e de vouloir restaurer la sauvegarde créée le", "MessageRestoreBackupConfirm": "Êtes-vous sûr·e 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.", "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.",
"MessageScheduleLibraryScanNote": "Pour la plupart des utilisateurs, il est recommandé de laisser cette fonctionnalité désactivée et de maintenir le réglage du moniteur de dossier activé. Le moniteur de dossier détectera automatiquement les changements dans vos dossiers de bibliothèque. Le moniteur de dossier ne fonctionne pas pour chaque système de fichiers (comme NFS) afin que les scans de bibliothèques programmés puissent être utilisés à la place.", "MessageScheduleLibraryScanNote": "Pour la plupart des utilisateurs, il est recommandé de laisser cette fonctionnalité désactivée et de maintenir le réglage du moniteur de dossier activé. Le moniteur de dossier détectera automatiquement les changements dans vos dossiers de bibliothèque. Le moniteur de dossier ne fonctionne pas pour chaque système de fichiers (comme NFS) afin que les scans de bibliothèques programmés puissent être utilisés à la place.",
"MessageScheduleRunEveryWeekdayAtTime": "Exécuté tous les {0} à {1}",
"MessageSearchResultsFor": "Résultats de recherche pour", "MessageSearchResultsFor": "Résultats de recherche pour",
"MessageSelected": "{0} sélectionnés", "MessageSelected": "{0} sélectionnés",
"MessageServerCouldNotBeReached": "Serveur inaccessible", "MessageServerCouldNotBeReached": "Serveur inaccessible",
-5
View File
@@ -471,11 +471,6 @@
"LabelSettingsBookshelfViewHelp": "עיצוב סקאומורפי עם מדפי עץ", "LabelSettingsBookshelfViewHelp": "עיצוב סקאומורפי עם מדפי עץ",
"LabelSettingsChromecastSupport": "תמיכה ב-Chromecast", "LabelSettingsChromecastSupport": "תמיכה ב-Chromecast",
"LabelSettingsDateFormat": "פורמט תאריך", "LabelSettingsDateFormat": "פורמט תאריך",
"LabelSettingsDisableWatcher": "השבת עוקב",
"LabelSettingsDisableWatcherForLibrary": "השבת עוקב תיקייה עבור ספרייה",
"LabelSettingsDisableWatcherHelp": "מבטל את הוספת/עדכון אוטומטי של פריטים כאשר שינויי קבצים זוהים. *דורש איתחול שרת",
"LabelSettingsEnableWatcher": "הפעל עוקב",
"LabelSettingsEnableWatcherForLibrary": "הפעל עוקב תיקייה עבור ספרייה",
"LabelSettingsEnableWatcherHelp": "מאפשר הוספת/עדכון אוטומטי של פריטים כאשר שינויי קבצים זוהים. *דורש איתחול שרת", "LabelSettingsEnableWatcherHelp": "מאפשר הוספת/עדכון אוטומטי של פריטים כאשר שינויי קבצים זוהים. *דורש איתחול שרת",
"LabelSettingsExperimentalFeatures": "תכונות ניסיוניות", "LabelSettingsExperimentalFeatures": "תכונות ניסיוניות",
"LabelSettingsExperimentalFeaturesHelp": "תכונות בפיתוח שדורשות משובך ובדיקה. לחץ לפתיחת דיון ב-GitHub.", "LabelSettingsExperimentalFeaturesHelp": "תכונות בפיתוח שדורשות משובך ובדיקה. לחץ לפתיחת דיון ב-GitHub.",
+1
View File
@@ -6,6 +6,7 @@
"ButtonApply": "लागू करें", "ButtonApply": "लागू करें",
"ButtonApplyChapters": "अध्यायों में परिवर्तन लागू करें", "ButtonApplyChapters": "अध्यायों में परिवर्तन लागू करें",
"ButtonAuthors": "लेखक", "ButtonAuthors": "लेखक",
"ButtonBack": "पीछे",
"ButtonBrowseForFolder": "फ़ोल्डर खोजें", "ButtonBrowseForFolder": "फ़ोल्डर खोजें",
"ButtonCancel": "रद्द करें", "ButtonCancel": "रद्द करें",
"ButtonCancelEncode": "एनकोड रद्द करें", "ButtonCancelEncode": "एनकोड रद्द करें",
+13 -12
View File
@@ -16,7 +16,7 @@
"ButtonCancel": "Odustani", "ButtonCancel": "Odustani",
"ButtonCancelEncode": "Otkaži kodiranje", "ButtonCancelEncode": "Otkaži kodiranje",
"ButtonChangeRootPassword": "Promijeni zaporku root korisnika", "ButtonChangeRootPassword": "Promijeni zaporku root korisnika",
"ButtonCheckAndDownloadNewEpisodes": "Provjeri i preuzmi nove epizode", "ButtonCheckAndDownloadNewEpisodes": "Provjeri i preuzmi nove nastavke",
"ButtonChooseAFolder": "Odaberi mapu", "ButtonChooseAFolder": "Odaberi mapu",
"ButtonChooseFiles": "Odaberi datoteke", "ButtonChooseFiles": "Odaberi datoteke",
"ButtonClearFilter": "Poništi filter", "ButtonClearFilter": "Poništi filter",
@@ -219,6 +219,7 @@
"LabelAccountTypeAdmin": "Administrator", "LabelAccountTypeAdmin": "Administrator",
"LabelAccountTypeGuest": "Gost", "LabelAccountTypeGuest": "Gost",
"LabelAccountTypeUser": "Korisnik", "LabelAccountTypeUser": "Korisnik",
"LabelActivities": "Aktivnosti",
"LabelActivity": "Aktivnost", "LabelActivity": "Aktivnost",
"LabelAddToCollection": "Dodaj u zbirku", "LabelAddToCollection": "Dodaj u zbirku",
"LabelAddToCollectionBatch": "Dodaj {0} knjiga u zbirku", "LabelAddToCollectionBatch": "Dodaj {0} knjiga u zbirku",
@@ -251,7 +252,7 @@
"LabelBackToUser": "Povratak na korisnika", "LabelBackToUser": "Povratak na korisnika",
"LabelBackupAudioFiles": "Sigurnosno kopiranje zvučnih datoteka", "LabelBackupAudioFiles": "Sigurnosno kopiranje zvučnih datoteka",
"LabelBackupLocation": "Lokacija sigurnosnih kopija", "LabelBackupLocation": "Lokacija sigurnosnih kopija",
"LabelBackupsEnableAutomaticBackups": "Omogući automatsku izradu sigurnosnih kopija", "LabelBackupsEnableAutomaticBackups": "Automatske sigurnosne kopije",
"LabelBackupsEnableAutomaticBackupsHelp": "Sigurnosne kopije spremaju se u /metadata/backups", "LabelBackupsEnableAutomaticBackupsHelp": "Sigurnosne kopije spremaju se u /metadata/backups",
"LabelBackupsMaxBackupSize": "Maksimalna veličina sigurnosne kopije (u GB) (0 za neograničeno)", "LabelBackupsMaxBackupSize": "Maksimalna veličina sigurnosne kopije (u GB) (0 za neograničeno)",
"LabelBackupsMaxBackupSizeHelp": "U svrhu sprečavanja izrade krive konfiguracije, sigurnosne kopije neće se izraditi ako su veće od zadane veličine.", "LabelBackupsMaxBackupSizeHelp": "U svrhu sprečavanja izrade krive konfiguracije, sigurnosne kopije neće se izraditi ako su veće od zadane veličine.",
@@ -283,6 +284,7 @@
"LabelContinueSeries": "Nastavi serijal", "LabelContinueSeries": "Nastavi serijal",
"LabelCover": "Naslovnica", "LabelCover": "Naslovnica",
"LabelCoverImageURL": "URL naslovnice", "LabelCoverImageURL": "URL naslovnice",
"LabelCoverProvider": "Pružatelj naslovnica",
"LabelCreatedAt": "Izrađen", "LabelCreatedAt": "Izrađen",
"LabelCronExpression": "Cron izraz", "LabelCronExpression": "Cron izraz",
"LabelCurrent": "Trenutan", "LabelCurrent": "Trenutan",
@@ -355,7 +357,7 @@
"LabelFileModifiedDate": "Izmijenjeno {0}", "LabelFileModifiedDate": "Izmijenjeno {0}",
"LabelFilename": "Naziv datoteke", "LabelFilename": "Naziv datoteke",
"LabelFilterByUser": "Filtriraj po korisniku", "LabelFilterByUser": "Filtriraj po korisniku",
"LabelFindEpisodes": "Pronađi epizode", "LabelFindEpisodes": "Pronađi nastavke",
"LabelFinished": "Dovršeno", "LabelFinished": "Dovršeno",
"LabelFolder": "Mapa", "LabelFolder": "Mapa",
"LabelFolders": "Mape", "LabelFolders": "Mape",
@@ -391,6 +393,7 @@
"LabelIntervalEvery6Hours": "Svakih 6 sati", "LabelIntervalEvery6Hours": "Svakih 6 sati",
"LabelIntervalEveryDay": "Svaki dan", "LabelIntervalEveryDay": "Svaki dan",
"LabelIntervalEveryHour": "Svaki sat", "LabelIntervalEveryHour": "Svaki sat",
"LabelIntervalEveryMinute": "Svaku minutu",
"LabelInvert": "Obrni", "LabelInvert": "Obrni",
"LabelItem": "Stavka", "LabelItem": "Stavka",
"LabelJumpBackwardAmount": "Dužina skoka unatrag", "LabelJumpBackwardAmount": "Dužina skoka unatrag",
@@ -401,7 +404,7 @@
"LabelLastBookAdded": "Zadnja dodana knjiga", "LabelLastBookAdded": "Zadnja dodana knjiga",
"LabelLastBookUpdated": "Zadnja ažurirana knjiga", "LabelLastBookUpdated": "Zadnja ažurirana knjiga",
"LabelLastSeen": "Zadnji puta viđen", "LabelLastSeen": "Zadnji puta viđen",
"LabelLastTime": "Zadnje vrijeme", "LabelLastTime": "Zadnje doslušano vrijeme",
"LabelLastUpdate": "Zadnje ažuriranje", "LabelLastUpdate": "Zadnje ažuriranje",
"LabelLayout": "Prikaz", "LabelLayout": "Prikaz",
"LabelLayoutSinglePage": "Jedna stranica", "LabelLayoutSinglePage": "Jedna stranica",
@@ -418,7 +421,7 @@
"LabelLogLevelDebug": "Debug", "LabelLogLevelDebug": "Debug",
"LabelLogLevelInfo": "Info", "LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Warn", "LabelLogLevelWarn": "Warn",
"LabelLookForNewEpisodesAfterDate": "Traži nove epizode nakon ovog datuma", "LabelLookForNewEpisodesAfterDate": "Traži nove nastavke nakon ovog datuma",
"LabelLowestPriority": "Najniži prioritet", "LabelLowestPriority": "Najniži prioritet",
"LabelMatchExistingUsersBy": "Prepoznaj postojeće korisnike pomoću", "LabelMatchExistingUsersBy": "Prepoznaj postojeće korisnike pomoću",
"LabelMatchExistingUsersByDescription": "Rabi se za povezivanje postojećih korisnika. Nakon što se spoje, korisnike se prepoznaje temeljem jedinstvene oznake vašeg pružatelja SSO usluga", "LabelMatchExistingUsersByDescription": "Rabi se za povezivanje postojećih korisnika. Nakon što se spoje, korisnike se prepoznaje temeljem jedinstvene oznake vašeg pružatelja SSO usluga",
@@ -447,7 +450,7 @@
"LabelNew": "Novo", "LabelNew": "Novo",
"LabelNewPassword": "Nova zaporka", "LabelNewPassword": "Nova zaporka",
"LabelNewestAuthors": "Najnoviji autori", "LabelNewestAuthors": "Najnoviji autori",
"LabelNewestEpisodes": "Najnovije epizode", "LabelNewestEpisodes": "Najnoviji nastavci",
"LabelNextBackupDate": "Sljedeća izrada sigurnosne kopije", "LabelNextBackupDate": "Sljedeća izrada sigurnosne kopije",
"LabelNextScheduledRun": "Sljedeće zakazano izvođenje", "LabelNextScheduledRun": "Sljedeće zakazano izvođenje",
"LabelNoCustomMetadataProviders": "Nema prilagođenih pružatelja meta-podataka", "LabelNoCustomMetadataProviders": "Nema prilagođenih pružatelja meta-podataka",
@@ -555,11 +558,8 @@
"LabelSettingsBookshelfViewHelp": "Skeumorfni dizajn sa drvenim policama", "LabelSettingsBookshelfViewHelp": "Skeumorfni dizajn sa drvenim policama",
"LabelSettingsChromecastSupport": "Podrška za Chromecast", "LabelSettingsChromecastSupport": "Podrška za Chromecast",
"LabelSettingsDateFormat": "Format datuma", "LabelSettingsDateFormat": "Format datuma",
"LabelSettingsDisableWatcher": "Isključi praćenje datotečnog sustava", "LabelSettingsEnableWatcher": "Automatski pretražuj ima li promjena u knjižnicama",
"LabelSettingsDisableWatcherForLibrary": "Onemogući praćenje datotečnog sustava za ovu knjižnicu", "LabelSettingsEnableWatcherForLibrary": "Automatski traži promjene u knjižnicama",
"LabelSettingsDisableWatcherHelp": "Onemogućuje automatsko dodavanje ili ažuriranje stavki kod uočenih promjena datoteka. *Potrebno je ponovno pokrenuti poslužitelj",
"LabelSettingsEnableWatcher": "Omogući praćenje promjena",
"LabelSettingsEnableWatcherForLibrary": "Omogući praćenje promjena u mapi knjižnice",
"LabelSettingsEnableWatcherHelp": "Omogućuje automatsko dodavanje/ažuriranje stavki kada se uoče izmjene datoteka. *Potrebno je ponovno pokretanje poslužitelja", "LabelSettingsEnableWatcherHelp": "Omogućuje automatsko dodavanje/ažuriranje stavki kada se uoče izmjene datoteka. *Potrebno je ponovno pokretanje poslužitelja",
"LabelSettingsEpubsAllowScriptedContent": "Omogući skripte u epub datotekama", "LabelSettingsEpubsAllowScriptedContent": "Omogući skripte u epub datotekama",
"LabelSettingsEpubsAllowScriptedContentHelp": "Omogućuje epub datotekama izvođenje skripti. Preporučamo isključiti ovu mogućnost ukoliko nemate povjerenja u izvore epub datoteka.", "LabelSettingsEpubsAllowScriptedContentHelp": "Omogućuje epub datotekama izvođenje skripti. Preporučamo isključiti ovu mogućnost ukoliko nemate povjerenja u izvore epub datoteka.",
@@ -678,7 +678,7 @@
"LabelUploaderDropFiles": "Ispusti datoteke", "LabelUploaderDropFiles": "Ispusti datoteke",
"LabelUploaderItemFetchMetadataHelp": "Automatski dohvati naslov, autora i serijal", "LabelUploaderItemFetchMetadataHelp": "Automatski dohvati naslov, autora i serijal",
"LabelUseAdvancedOptions": "Koristi se naprednim opcijama", "LabelUseAdvancedOptions": "Koristi se naprednim opcijama",
"LabelUseChapterTrack": "Koristi zvučni zapis poglavlja", "LabelUseChapterTrack": "Upravljaj trakom poglavlja",
"LabelUseFullTrack": "Koristi cijeli zvučni zapis", "LabelUseFullTrack": "Koristi cijeli zvučni zapis",
"LabelUseZeroForUnlimited": "0 za neograničeno", "LabelUseZeroForUnlimited": "0 za neograničeno",
"LabelUser": "Korisnik", "LabelUser": "Korisnik",
@@ -845,6 +845,7 @@
"MessageRestoreBackupConfirm": "Sigurno želite vratiti sigurnosnu kopiju izrađenu", "MessageRestoreBackupConfirm": "Sigurno želite vratiti sigurnosnu kopiju izrađenu",
"MessageRestoreBackupWarning": "Vraćanjem sigurnosne kopije prepisat ćete cijelu bazu podataka koja se nalazi u /config i slike naslovnice u /metadata/items i /metadata/authors.<br /><br />Sigurnosne kopije ne mijenjaju datoteke koje se nalaze u mapama vaših knjižnica. Ako ste u postavkama poslužitelja uključili mogućnost spremanja naslovnica i meta-podataka u mape knjižnice, te se datoteke neće niti sigurnosno pohraniti niti prepisati. <br /><br />Svi klijenti koji se spajaju na vaš poslužitelj automatski će se osvježiti.", "MessageRestoreBackupWarning": "Vraćanjem sigurnosne kopije prepisat ćete cijelu bazu podataka koja se nalazi u /config i slike naslovnice u /metadata/items i /metadata/authors.<br /><br />Sigurnosne kopije ne mijenjaju datoteke koje se nalaze u mapama vaših knjižnica. Ako ste u postavkama poslužitelja uključili mogućnost spremanja naslovnica i meta-podataka u mape knjižnice, te se datoteke neće niti sigurnosno pohraniti niti prepisati. <br /><br />Svi klijenti koji se spajaju na vaš poslužitelj automatski će se osvježiti.",
"MessageScheduleLibraryScanNote": "Za većinu korisnika se preporučuje ostaviti ovu funkciju deaktiviranom i ostaviti postavku promatrača mape aktiviranom. Promatrač mapa će automatski otkriti promjene u mapama vaše knjižnice. Promatrač mapa ne radi na svakom datotečnom sustavu (kao što je NFS) pa se umjesto njega mogu koristiti planirana pretraživanja knjižnice.", "MessageScheduleLibraryScanNote": "Za većinu korisnika se preporučuje ostaviti ovu funkciju deaktiviranom i ostaviti postavku promatrača mape aktiviranom. Promatrač mapa će automatski otkriti promjene u mapama vaše knjižnice. Promatrač mapa ne radi na svakom datotečnom sustavu (kao što je NFS) pa se umjesto njega mogu koristiti planirana pretraživanja knjižnice.",
"MessageScheduleRunEveryWeekdayAtTime": "Pokreni svaki {0} u {1}",
"MessageSearchResultsFor": "Rezultati pretrage za", "MessageSearchResultsFor": "Rezultati pretrage za",
"MessageSelected": "{0} odabrano", "MessageSelected": "{0} odabrano",
"MessageServerCouldNotBeReached": "Nije moguće pristupiti poslužitelju", "MessageServerCouldNotBeReached": "Nije moguće pristupiti poslužitelju",
-5
View File
@@ -552,11 +552,6 @@
"LabelSettingsBookshelfViewHelp": "Skeuomorfikus dizájn fa polcokkal", "LabelSettingsBookshelfViewHelp": "Skeuomorfikus dizájn fa polcokkal",
"LabelSettingsChromecastSupport": "Chromecast támogatás", "LabelSettingsChromecastSupport": "Chromecast támogatás",
"LabelSettingsDateFormat": "Dátumformátum", "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", "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",
"LabelSettingsEpubsAllowScriptedContent": "Szkriptelt tartalmak engedélyezése epub-okban", "LabelSettingsEpubsAllowScriptedContent": "Szkriptelt tartalmak engedélyezése epub-okban",
"LabelSettingsEpubsAllowScriptedContentHelp": "Megengedi, hogy az epub fájlok szkripteket hajtsanak végre. Ezt a beállítást kikapcsolva ajánlott tartani, kivéve, ha megbízik az epub fájlok forrásában.", "LabelSettingsEpubsAllowScriptedContentHelp": "Megengedi, hogy az epub fájlok szkripteket hajtsanak végre. Ezt a beállítást kikapcsolva ajánlott tartani, kivéve, ha megbízik az epub fájlok forrásában.",
+3 -5
View File
@@ -219,6 +219,7 @@
"LabelAccountTypeAdmin": "Amministratore", "LabelAccountTypeAdmin": "Amministratore",
"LabelAccountTypeGuest": "Ospite", "LabelAccountTypeGuest": "Ospite",
"LabelAccountTypeUser": "Utente", "LabelAccountTypeUser": "Utente",
"LabelActivities": "Attività",
"LabelActivity": "Attività", "LabelActivity": "Attività",
"LabelAddToCollection": "Aggiungi alla Raccolta", "LabelAddToCollection": "Aggiungi alla Raccolta",
"LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta", "LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta",
@@ -283,6 +284,7 @@
"LabelContinueSeries": "Continua serie", "LabelContinueSeries": "Continua serie",
"LabelCover": "Copertina", "LabelCover": "Copertina",
"LabelCoverImageURL": "Indirizzo della cover URL", "LabelCoverImageURL": "Indirizzo della cover URL",
"LabelCoverProvider": "Cover Provider",
"LabelCreatedAt": "Creato A", "LabelCreatedAt": "Creato A",
"LabelCronExpression": "Espressione Cron", "LabelCronExpression": "Espressione Cron",
"LabelCurrent": "Attuale", "LabelCurrent": "Attuale",
@@ -391,6 +393,7 @@
"LabelIntervalEvery6Hours": "Ogni 6 ore", "LabelIntervalEvery6Hours": "Ogni 6 ore",
"LabelIntervalEveryDay": "Ogni Giorno", "LabelIntervalEveryDay": "Ogni Giorno",
"LabelIntervalEveryHour": "Ogni ora", "LabelIntervalEveryHour": "Ogni ora",
"LabelIntervalEveryMinute": "Ogni minuto",
"LabelInvert": "Inverti", "LabelInvert": "Inverti",
"LabelItem": "Oggetti", "LabelItem": "Oggetti",
"LabelJumpBackwardAmount": "secondi di avvolgimento", "LabelJumpBackwardAmount": "secondi di avvolgimento",
@@ -555,11 +558,6 @@
"LabelSettingsBookshelfViewHelp": "Design con scaffali in legno", "LabelSettingsBookshelfViewHelp": "Design con scaffali in legno",
"LabelSettingsChromecastSupport": "Supporto a Chromecast", "LabelSettingsChromecastSupport": "Supporto a Chromecast",
"LabelSettingsDateFormat": "Formato Data", "LabelSettingsDateFormat": "Formato Data",
"LabelSettingsDisableWatcher": "Disattiva Watcher",
"LabelSettingsDisableWatcherForLibrary": "Disattiva Watcher per le librerie",
"LabelSettingsDisableWatcherHelp": "Disattiva il controllo automatico libri nelle cartelle delle librerie. *Richiede il Riavvio del Server",
"LabelSettingsEnableWatcher": "Abilita Watcher",
"LabelSettingsEnableWatcherForLibrary": "Abilita il controllo cartelle per la libreria",
"LabelSettingsEnableWatcherHelp": "Abilita l'aggiunta/aggiornamento automatico degli elementi quando vengono rilevate modifiche ai file. *Richiede il riavvio del Server", "LabelSettingsEnableWatcherHelp": "Abilita l'aggiunta/aggiornamento automatico degli elementi quando vengono rilevate modifiche ai file. *Richiede il riavvio del Server",
"LabelSettingsEpubsAllowScriptedContent": "Consenti contenuti con script negli epub", "LabelSettingsEpubsAllowScriptedContent": "Consenti contenuti con script negli epub",
"LabelSettingsEpubsAllowScriptedContentHelp": "Consenti ai file epub di eseguire script. Si consiglia di mantenere questa impostazione disabilitata a meno che non si ritenga attendibile l'origine dei file epub.", "LabelSettingsEpubsAllowScriptedContentHelp": "Consenti ai file epub di eseguire script. Si consiglia di mantenere questa impostazione disabilitata a meno che non si ritenga attendibile l'origine dei file epub.",
+19 -1
View File
@@ -1,3 +1,21 @@
{ {
"ButtonAdd": "追加" "ButtonAdd": "追加",
"ButtonAddChapters": "チャプターの追加",
"ButtonCancel": "キャンセル",
"ButtonOk": "はい",
"ButtonPlay": "プレイ",
"ButtonPlaying": "プレイ中",
"ButtonPrevious": "先",
"ButtonRead": "野村",
"ButtonYes": "はい",
"HeaderPlayerSettings": "プレーヤーの設定",
"LabelBooks": "ほん",
"LabelLanguage": "言語",
"LabelLanguages": "言語",
"LabelName": "名",
"LabelNew": "新しい",
"LabelNewPassword": "新しいのパスワード",
"LabelPassword": "パスワード",
"LabelPlaylists": "プレイリスト",
"LabelPodcast": "ポッドキャスト"
} }
-3
View File
@@ -413,9 +413,6 @@
"LabelSettingsBookshelfViewHelp": "Knygų lentynos dizainas su medinėmis lentynomis", "LabelSettingsBookshelfViewHelp": "Knygų lentynos dizainas su medinėmis lentynomis",
"LabelSettingsChromecastSupport": "„Chromecast“ palaikymas", "LabelSettingsChromecastSupport": "„Chromecast“ palaikymas",
"LabelSettingsDateFormat": "Datos formatas", "LabelSettingsDateFormat": "Datos formatas",
"LabelSettingsDisableWatcher": "Išjungti stebėtoją",
"LabelSettingsDisableWatcherForLibrary": "Išjungti aplankų stebėtoją bibliotekai",
"LabelSettingsDisableWatcherHelp": "Išjungia automatinį elementų pridėjimą/atnaujinimą, jei pastebėti failų pokyčiai. *Reikalingas serverio paleidimas iš naujo",
"LabelSettingsExperimentalFeatures": "Eksperimentiniai funkcionalumai", "LabelSettingsExperimentalFeatures": "Eksperimentiniai funkcionalumai",
"LabelSettingsExperimentalFeaturesHelp": "Funkcijos, kurios yra kuriamos ir laukiami jūsų komentarai. Spustelėkite, kad atidarytumėte „GitHub“ diskusiją.", "LabelSettingsExperimentalFeaturesHelp": "Funkcijos, kurios yra kuriamos ir laukiami jūsų komentarai. Spustelėkite, kad atidarytumėte „GitHub“ diskusiją.",
"LabelSettingsFindCovers": "Rasti viršelius", "LabelSettingsFindCovers": "Rasti viršelius",
+1 -5
View File
@@ -10,6 +10,7 @@
"ButtonApplyChapters": "Hoofdstukken toepassen", "ButtonApplyChapters": "Hoofdstukken toepassen",
"ButtonAuthors": "Auteurs", "ButtonAuthors": "Auteurs",
"ButtonBack": "Terug", "ButtonBack": "Terug",
"ButtonBatchEditPopulateMapDetails": "Kaartgegevens invullen",
"ButtonBrowseForFolder": "Bladeren naar map", "ButtonBrowseForFolder": "Bladeren naar map",
"ButtonCancel": "Annuleren", "ButtonCancel": "Annuleren",
"ButtonCancelEncode": "Encoding annuleren", "ButtonCancelEncode": "Encoding annuleren",
@@ -553,11 +554,6 @@
"LabelSettingsBookshelfViewHelp": "Skeumorphisch design met houten planken", "LabelSettingsBookshelfViewHelp": "Skeumorphisch design met houten planken",
"LabelSettingsChromecastSupport": "Chromecast ondersteuning", "LabelSettingsChromecastSupport": "Chromecast ondersteuning",
"LabelSettingsDateFormat": "Datum format", "LabelSettingsDateFormat": "Datum format",
"LabelSettingsDisableWatcher": "Watcher uitschakelen",
"LabelSettingsDisableWatcherForLibrary": "Map-watcher voor bibliotheek uitschakelen",
"LabelSettingsDisableWatcherHelp": "Schakelt het automatisch toevoegen/bijwerken van onderdelen wanneer bestandswijzigingen gedetecteerd zijn uit. *Vereist herstart server",
"LabelSettingsEnableWatcher": "Watcher inschakelen",
"LabelSettingsEnableWatcherForLibrary": "Map-watcher voor bibliotheek inschakelen",
"LabelSettingsEnableWatcherHelp": "Zorgt voor het automatisch toevoegen/bijwerken van onderdelen als bestandswijzigingen worden gedetecteerd. *Vereist herstarten van server", "LabelSettingsEnableWatcherHelp": "Zorgt voor het automatisch toevoegen/bijwerken van onderdelen als bestandswijzigingen worden gedetecteerd. *Vereist herstarten van server",
"LabelSettingsEpubsAllowScriptedContent": "Sta scripted content toe in epubs", "LabelSettingsEpubsAllowScriptedContent": "Sta scripted content toe in epubs",
"LabelSettingsEpubsAllowScriptedContentHelp": "Sta toe dat epub-bestanden scripts uitvoeren. Het wordt aanbevolen om deze instelling uitgeschakeld te houden, tenzij u de bron van de epub-bestanden vertrouwt.", "LabelSettingsEpubsAllowScriptedContentHelp": "Sta toe dat epub-bestanden scripts uitvoeren. Het wordt aanbevolen om deze instelling uitgeschakeld te houden, tenzij u de bron van de epub-bestanden vertrouwt.",
-5
View File
@@ -550,11 +550,6 @@
"LabelSettingsBookshelfViewHelp": "Skeuomorf design med hyller av ved", "LabelSettingsBookshelfViewHelp": "Skeuomorf design med hyller av ved",
"LabelSettingsChromecastSupport": "Chromecast støtte", "LabelSettingsChromecastSupport": "Chromecast støtte",
"LabelSettingsDateFormat": "Dato Format", "LabelSettingsDateFormat": "Dato Format",
"LabelSettingsDisableWatcher": "Deaktiver overvåker",
"LabelSettingsDisableWatcherForLibrary": "Deaktiver mappe overvåker for bibliotek",
"LabelSettingsDisableWatcherHelp": "Deaktiverer automatisk opprettelse/oppdatering av enheter når filendringer er oppdaget. *Krever restart av server*",
"LabelSettingsEnableWatcher": "Aktiver overvåker",
"LabelSettingsEnableWatcherForLibrary": "Aktiver mappe overvåker for bibliotek",
"LabelSettingsEnableWatcherHelp": "Aktiverer automatisk opprettelse/oppdatering av enheter når filendringer er oppdaget. *Krever restart av server*", "LabelSettingsEnableWatcherHelp": "Aktiverer automatisk opprettelse/oppdatering av enheter når filendringer er oppdaget. *Krever restart av server*",
"LabelSettingsEpubsAllowScriptedContent": "Tillat scripting i innholdet i ebub-bøker", "LabelSettingsEpubsAllowScriptedContent": "Tillat scripting i innholdet i ebub-bøker",
"LabelSettingsEpubsAllowScriptedContentHelp": "Tillat epub-filer å kjøre script. Det er anbefalt å slå av denne innstillingen med mindre du stoler på kilden til epub-filene.", "LabelSettingsEpubsAllowScriptedContentHelp": "Tillat epub-filer å kjøre script. Det er anbefalt å slå av denne innstillingen med mindre du stoler på kilden til epub-filene.",
+9 -7
View File
@@ -10,6 +10,8 @@
"ButtonApplyChapters": "Zatwierdź rozdziały", "ButtonApplyChapters": "Zatwierdź rozdziały",
"ButtonAuthors": "Autorzy", "ButtonAuthors": "Autorzy",
"ButtonBack": "Wstecz", "ButtonBack": "Wstecz",
"ButtonBatchEditPopulateFromExisting": "Powiel z poprzednich",
"ButtonBatchEditPopulateMapDetails": "Powiel szczegóły mapy",
"ButtonBrowseForFolder": "Wyszukaj folder", "ButtonBrowseForFolder": "Wyszukaj folder",
"ButtonCancel": "Anuluj", "ButtonCancel": "Anuluj",
"ButtonCancelEncode": "Anuluj enkodowanie", "ButtonCancelEncode": "Anuluj enkodowanie",
@@ -31,6 +33,7 @@
"ButtonEditPodcast": "Edytuj podcast", "ButtonEditPodcast": "Edytuj podcast",
"ButtonEnable": "Włącz", "ButtonEnable": "Włącz",
"ButtonFireAndFail": "Fail start", "ButtonFireAndFail": "Fail start",
"ButtonFireOnTest": "Uruchom po zdarzeniu testowym",
"ButtonForceReScan": "Wymuś ponowne skanowanie", "ButtonForceReScan": "Wymuś ponowne skanowanie",
"ButtonFullPath": "Pełna ścieżka", "ButtonFullPath": "Pełna ścieżka",
"ButtonHide": "Ukryj", "ButtonHide": "Ukryj",
@@ -87,6 +90,8 @@
"ButtonSaveTracklist": "Zapisz listę odtwarzania", "ButtonSaveTracklist": "Zapisz listę odtwarzania",
"ButtonScan": "Zeskanuj", "ButtonScan": "Zeskanuj",
"ButtonScanLibrary": "Skanuj bibliotekę", "ButtonScanLibrary": "Skanuj bibliotekę",
"ButtonScrollLeft": "Przewiń w lewo",
"ButtonScrollRight": "Przewiń w prawo",
"ButtonSearch": "Szukaj", "ButtonSearch": "Szukaj",
"ButtonSelectFolderPath": "Wybierz ścieżkę folderu", "ButtonSelectFolderPath": "Wybierz ścieżkę folderu",
"ButtonSeries": "Seria", "ButtonSeries": "Seria",
@@ -155,13 +160,14 @@
"HeaderMapDetails": "Szczegóły mapowania", "HeaderMapDetails": "Szczegóły mapowania",
"HeaderMatch": "Dopasuj", "HeaderMatch": "Dopasuj",
"HeaderMetadataOrderOfPrecedence": "Kolejność metadanych", "HeaderMetadataOrderOfPrecedence": "Kolejność metadanych",
"HeaderMetadataToEmbed": "Osadź metadane", "HeaderMetadataToEmbed": "Metadane do osadzenia",
"HeaderNewAccount": "Nowe konto", "HeaderNewAccount": "Nowe konto",
"HeaderNewLibrary": "Nowa biblioteka", "HeaderNewLibrary": "Nowa biblioteka",
"HeaderNotificationCreate": "Utwórz powiadomienie", "HeaderNotificationCreate": "Utwórz powiadomienie",
"HeaderNotificationUpdate": "Zaktualizuj powiadomienie", "HeaderNotificationUpdate": "Zaktualizuj powiadomienie",
"HeaderNotifications": "Powiadomienia", "HeaderNotifications": "Powiadomienia",
"HeaderOpenIDConnectAuthentication": "Uwierzytelnianie OpenID Connect", "HeaderOpenIDConnectAuthentication": "Uwierzytelnianie OpenID Connect",
"HeaderOpenListeningSessions": "Otwarte sesje słuchania",
"HeaderOpenRSSFeed": "Utwórz kanał RSS", "HeaderOpenRSSFeed": "Utwórz kanał RSS",
"HeaderOtherFiles": "Inne pliki", "HeaderOtherFiles": "Inne pliki",
"HeaderPasswordAuthentication": "Uwierzytelnianie hasłem", "HeaderPasswordAuthentication": "Uwierzytelnianie hasłem",
@@ -188,6 +194,7 @@
"HeaderSettingsExperimental": "Funkcje eksperymentalne", "HeaderSettingsExperimental": "Funkcje eksperymentalne",
"HeaderSettingsGeneral": "Ogólne", "HeaderSettingsGeneral": "Ogólne",
"HeaderSettingsScanner": "Skanowanie", "HeaderSettingsScanner": "Skanowanie",
"HeaderSettingsWebClient": "Klient webowy",
"HeaderSleepTimer": "Wyłącznik czasowy", "HeaderSleepTimer": "Wyłącznik czasowy",
"HeaderStatsLargestItems": "Największe pozycje", "HeaderStatsLargestItems": "Największe pozycje",
"HeaderStatsLongestItems": "Najdłuższe pozycje (godziny)", "HeaderStatsLongestItems": "Najdłuższe pozycje (godziny)",
@@ -438,7 +445,7 @@
"LabelNotificationsMaxQueueSize": "Maksymalny rozmiar kolejki dla powiadomień", "LabelNotificationsMaxQueueSize": "Maksymalny rozmiar kolejki dla powiadomień",
"LabelNotificationsMaxQueueSizeHelp": "Zdarzenia są ograniczone do 1 na sekundę. Zdarzenia będą ignorowane jeśli kolejka ma maksymalny rozmiar. Zapobiega to spamowaniu powiadomieniami.", "LabelNotificationsMaxQueueSizeHelp": "Zdarzenia są ograniczone do 1 na sekundę. Zdarzenia będą ignorowane jeśli kolejka ma maksymalny rozmiar. Zapobiega to spamowaniu powiadomieniami.",
"LabelNumberOfBooks": "Liczba książek", "LabelNumberOfBooks": "Liczba książek",
"LabelNumberOfEpisodes": "# odcinków", "LabelNumberOfEpisodes": "# Odcinków",
"LabelOpenRSSFeed": "Otwórz kanał RSS", "LabelOpenRSSFeed": "Otwórz kanał RSS",
"LabelOverwrite": "Nadpisz", "LabelOverwrite": "Nadpisz",
"LabelPassword": "Hasło", "LabelPassword": "Hasło",
@@ -501,11 +508,6 @@
"LabelSettingsBookshelfViewHelp": "Widok półki z książkami", "LabelSettingsBookshelfViewHelp": "Widok półki z książkami",
"LabelSettingsChromecastSupport": "Wsparcie Chromecast", "LabelSettingsChromecastSupport": "Wsparcie Chromecast",
"LabelSettingsDateFormat": "Format daty", "LabelSettingsDateFormat": "Format daty",
"LabelSettingsDisableWatcher": "Wyłącz monitorowanie",
"LabelSettingsDisableWatcherForLibrary": "Wyłącz monitorowanie folderów dla biblioteki",
"LabelSettingsDisableWatcherHelp": "Wyłącz automatyczne dodawanie/aktualizowanie elementów po wykryciu zmian w plikach. *Wymaga restartu serwera",
"LabelSettingsEnableWatcher": "Włącz monitorowanie",
"LabelSettingsEnableWatcherForLibrary": "Włącz monitorowanie folderów dla biblioteki",
"LabelSettingsEnableWatcherHelp": "Włącza automatyczne dodawanie/aktualizację pozycji gdy wykryte zostaną zmiany w plikach. Wymaga restartu serwera", "LabelSettingsEnableWatcherHelp": "Włącza automatyczne dodawanie/aktualizację pozycji gdy wykryte zostaną zmiany w plikach. Wymaga restartu serwera",
"LabelSettingsEpubsAllowScriptedContent": "Zezwalanie na skrypty w plikach epub", "LabelSettingsEpubsAllowScriptedContent": "Zezwalanie na skrypty w plikach epub",
"LabelSettingsEpubsAllowScriptedContentHelp": "Zezwala plikom epub na wykonywanie skryptów. Zaleca się mieć to ustawienie wyłączone, chyba że ma się zaufanie do źródła plików epub.", "LabelSettingsEpubsAllowScriptedContentHelp": "Zezwala plikom epub na wykonywanie skryptów. Zaleca się mieć to ustawienie wyłączone, chyba że ma się zaufanie do źródła plików epub.",
-5
View File
@@ -454,11 +454,6 @@
"LabelSettingsBookshelfViewHelp": "Aparência esqueomorfa com prateleiras de madeira", "LabelSettingsBookshelfViewHelp": "Aparência esqueomorfa com prateleiras de madeira",
"LabelSettingsChromecastSupport": "Suporte ao Chromecast", "LabelSettingsChromecastSupport": "Suporte ao Chromecast",
"LabelSettingsDateFormat": "Formato de data", "LabelSettingsDateFormat": "Formato de data",
"LabelSettingsDisableWatcher": "Desativar Monitoramento",
"LabelSettingsDisableWatcherForLibrary": "Desativa o monitoramento de pastas para a biblioteca",
"LabelSettingsDisableWatcherHelp": "Desativa o acréscimo/atualização de itens quando forem detectadas mudanças no arquivo. *Requer reiniciar o servidor",
"LabelSettingsEnableWatcher": "Ativar Monitoramento",
"LabelSettingsEnableWatcherForLibrary": "Ativa o monitoramento de pastas para a biblioteca",
"LabelSettingsEnableWatcherHelp": "Ativa o acréscimo/atualização de itens quando forem detectadas mudanças no arquivo. *Requer reiniciar o servidor", "LabelSettingsEnableWatcherHelp": "Ativa o acréscimo/atualização de itens quando forem detectadas mudanças no arquivo. *Requer reiniciar o servidor",
"LabelSettingsEpubsAllowScriptedContent": "Permitir scripts em epubs", "LabelSettingsEpubsAllowScriptedContent": "Permitir scripts em epubs",
"LabelSettingsEpubsAllowScriptedContentHelp": "Permitir que arquivos epub executem scripts. É recomendado manter essa configuração desativada, a não ser que confie na fonte dos arquivos epub.", "LabelSettingsEpubsAllowScriptedContentHelp": "Permitir que arquivos epub executem scripts. É recomendado manter essa configuração desativada, a não ser que confie na fonte dos arquivos epub.",
+1
View File
@@ -0,0 +1 @@
{}
+5 -6
View File
@@ -219,6 +219,7 @@
"LabelAccountTypeAdmin": "Администратор", "LabelAccountTypeAdmin": "Администратор",
"LabelAccountTypeGuest": "Гость", "LabelAccountTypeGuest": "Гость",
"LabelAccountTypeUser": "Пользователь", "LabelAccountTypeUser": "Пользователь",
"LabelActivities": "Мероприятия",
"LabelActivity": "Активность", "LabelActivity": "Активность",
"LabelAddToCollection": "Добавить в коллекцию", "LabelAddToCollection": "Добавить в коллекцию",
"LabelAddToCollectionBatch": "Добавить {0} книг в коллекцию", "LabelAddToCollectionBatch": "Добавить {0} книг в коллекцию",
@@ -283,7 +284,8 @@
"LabelContinueSeries": "Продолжить серию", "LabelContinueSeries": "Продолжить серию",
"LabelCover": "Обложка", "LabelCover": "Обложка",
"LabelCoverImageURL": "URL изображения обложки", "LabelCoverImageURL": "URL изображения обложки",
"LabelCreatedAt": "Создано", "LabelCoverProvider": "Провайдер обложек",
"LabelCreatedAt": "Создан",
"LabelCronExpression": "Выражение Cron", "LabelCronExpression": "Выражение Cron",
"LabelCurrent": "Текущий", "LabelCurrent": "Текущий",
"LabelCurrently": "Текущее:", "LabelCurrently": "Текущее:",
@@ -391,6 +393,7 @@
"LabelIntervalEvery6Hours": "Каждые 6 часов", "LabelIntervalEvery6Hours": "Каждые 6 часов",
"LabelIntervalEveryDay": "Каждый день", "LabelIntervalEveryDay": "Каждый день",
"LabelIntervalEveryHour": "Каждый час", "LabelIntervalEveryHour": "Каждый час",
"LabelIntervalEveryMinute": "Каждую минуту",
"LabelInvert": "Инвертировать", "LabelInvert": "Инвертировать",
"LabelItem": "Элемент", "LabelItem": "Элемент",
"LabelJumpBackwardAmount": "Прыжок назад на величину", "LabelJumpBackwardAmount": "Прыжок назад на величину",
@@ -555,11 +558,6 @@
"LabelSettingsBookshelfViewHelp": "Конструкция с деревянными полками", "LabelSettingsBookshelfViewHelp": "Конструкция с деревянными полками",
"LabelSettingsChromecastSupport": "Поддержка Chromecast", "LabelSettingsChromecastSupport": "Поддержка Chromecast",
"LabelSettingsDateFormat": "Формат даты", "LabelSettingsDateFormat": "Формат даты",
"LabelSettingsDisableWatcher": "Отключить отслеживание",
"LabelSettingsDisableWatcherForLibrary": "Отключить отслеживание для библиотеки",
"LabelSettingsDisableWatcherHelp": "Отключает автоматическое добавление/обновление элементов, когда обнаружено изменение файлов. *Требуется перезапуск сервера",
"LabelSettingsEnableWatcher": "Включить отслеживание",
"LabelSettingsEnableWatcherForLibrary": "Включить отслеживание за папками библиотеки",
"LabelSettingsEnableWatcherHelp": "Включает автоматическое добавление/обновление элементов при обнаружении изменений файлов. *Требуется перезапуск сервера", "LabelSettingsEnableWatcherHelp": "Включает автоматическое добавление/обновление элементов при обнаружении изменений файлов. *Требуется перезапуск сервера",
"LabelSettingsEpubsAllowScriptedContent": "Разрешение содержимого epub с скриптами", "LabelSettingsEpubsAllowScriptedContent": "Разрешение содержимого epub с скриптами",
"LabelSettingsEpubsAllowScriptedContentHelp": "Разрешить файлам epub выполнять скрипты. Рекомендуется отключать этот параметр, если вы не доверяете источнику файлов epub.", "LabelSettingsEpubsAllowScriptedContentHelp": "Разрешить файлам epub выполнять скрипты. Рекомендуется отключать этот параметр, если вы не доверяете источнику файлов epub.",
@@ -845,6 +843,7 @@
"MessageRestoreBackupConfirm": "Вы уверены, что хотите восстановить резервную копию, созданную", "MessageRestoreBackupConfirm": "Вы уверены, что хотите восстановить резервную копию, созданную",
"MessageRestoreBackupWarning": "Восстановление резервной копии перезапишет всю базу данных, расположенную в /config, и обложки изображений в /metadata/items и /metadata/authors.<br/><br/>Бэкапы не изменяют файлы в папках библиотеки. Если вы включили параметры сервера для хранения обложек и метаданных в папках библиотеки, то они не резервируются и не перезаписываются.<br/><br/>Все клиенты, использующие ваш сервер, будут автоматически обновлены.", "MessageRestoreBackupWarning": "Восстановление резервной копии перезапишет всю базу данных, расположенную в /config, и обложки изображений в /metadata/items и /metadata/authors.<br/><br/>Бэкапы не изменяют файлы в папках библиотеки. Если вы включили параметры сервера для хранения обложек и метаданных в папках библиотеки, то они не резервируются и не перезаписываются.<br/><br/>Все клиенты, использующие ваш сервер, будут автоматически обновлены.",
"MessageScheduleLibraryScanNote": "Большинству пользователей рекомендуется отключить эту функцию и включить функцию просмотра папок. Программа просмотра папок автоматически обнаружит изменения в папках вашей библиотеки. Программа просмотра папок работает не для каждой файловой системы (например, NFS), поэтому вместо этого можно использовать запланированные проверки библиотеки.", "MessageScheduleLibraryScanNote": "Большинству пользователей рекомендуется отключить эту функцию и включить функцию просмотра папок. Программа просмотра папок автоматически обнаружит изменения в папках вашей библиотеки. Программа просмотра папок работает не для каждой файловой системы (например, NFS), поэтому вместо этого можно использовать запланированные проверки библиотеки.",
"MessageScheduleRunEveryWeekdayAtTime": "Запуск каждые {0} по {1}",
"MessageSearchResultsFor": "Результаты поиска для", "MessageSearchResultsFor": "Результаты поиска для",
"MessageSelected": "{0} выбрано", "MessageSelected": "{0} выбрано",
"MessageServerCouldNotBeReached": "Не удалось связаться с сервером", "MessageServerCouldNotBeReached": "Не удалось связаться с сервером",
+93
View File
@@ -0,0 +1,93 @@
{
"ButtonAdd": "Pridať",
"ButtonAddChapters": "Pridať kapitoly",
"ButtonAddDevice": "Pridať zariadenie",
"ButtonAddLibrary": "Pridať knižnicu",
"ButtonAddPodcasts": "Pridať podcasty",
"ButtonAddUser": "Pridať užívateľa",
"ButtonAddYourFirstLibrary": "Pridajte vašu prvú knižnicu",
"ButtonApply": "Použiť",
"ButtonApplyChapters": "Použiť kapitoly",
"ButtonAuthors": "Autori",
"ButtonBack": "Späť",
"ButtonBatchEditPopulateFromExisting": "Vytvoriť z existujúcej",
"ButtonBatchEditPopulateMapDetails": "Vyplniť detaily na mape",
"ButtonBrowseForFolder": "Prehľadávať adresáre",
"ButtonCancel": "Zrušiť",
"ButtonCancelEncode": "Zrušiť kódovanie",
"ButtonChangeRootPassword": "Zmeniť Root heslo",
"ButtonCheckAndDownloadNewEpisodes": "Skontrolovať a stiahnuť nové epizódy",
"ButtonChooseAFolder": "Vyberte adresár",
"ButtonChooseFiles": "Vyberte súbory",
"ButtonClearFilter": "Zrušiť filter",
"ButtonCloseFeed": "Zatvoriť zdroj",
"ButtonCloseSession": "Ukončiť otvorené pripojenie",
"ButtonCollections": "Zbierky",
"ButtonConfigureScanner": "Nastaviť skener",
"ButtonCreate": "Vytvoriť",
"ButtonCreateBackup": "Vytvoriť zálohu",
"ButtonDelete": "Zmazať",
"ButtonDownloadQueue": "Poradie",
"ButtonEdit": "Upraviť",
"ButtonEditChapters": "Upraviť kapitoly",
"ButtonEditPodcast": "Upraviť podcast",
"ButtonEnable": "Povoliť",
"ButtonForceReScan": "Vynútiť preskenovanie",
"ButtonFullPath": "Zobraziť cestu",
"ButtonHide": "Skryť",
"ButtonHome": "Domov",
"ButtonIssues": "Problémy",
"ButtonJumpBackward": "Posun späť",
"ButtonJumpForward": "Posun vpred",
"ButtonLatest": "Najnovšie",
"ButtonLibrary": "Knižnica",
"ButtonLogout": "Odhlásenie",
"ButtonLookup": "Vyhľadať",
"ButtonManageTracks": "Spravovať stopy",
"ButtonMapChapterTitles": "Mapovať názvy kapitol",
"ButtonMatchAllAuthors": "Vyhľadať všetkých autorov",
"ButtonMatchBooks": "Vyhľadať knihy",
"ButtonNevermind": "Nevadí",
"ButtonNext": "Ďalšie",
"ButtonNextChapter": "Ďalšia kapitola",
"ButtonNextItemInQueue": "Ďalšia položka v poradí",
"ButtonOk": "OK",
"ButtonOpenFeed": "Otvoriť zdroj",
"ButtonOpenManager": "Otvoriť správcu",
"ButtonPause": "Zastaviť",
"ButtonPlay": "Prehrať",
"ButtonPlayAll": "Prehrať všetko",
"ButtonPlaying": "Prehráva sa",
"ButtonPlaylists": "Playlisty",
"ButtonPrevious": "Predchádzajúci",
"ButtonPreviousChapter": "Predchádzajúca kapitola",
"ButtonProbeAudioFile": "Preskúmaj zvukový súbor",
"ButtonPurgeAllCache": "Vymaž celú medzipamäť",
"ButtonPurgeItemsCache": "Vymaž medzipamäť položiek",
"ButtonQueueAddItem": "Pridať do poradia",
"ButtonQueueRemoveItem": "Vymazať z poradia",
"ButtonQuickEmbed": "Rýchle vloženie",
"ButtonQuickEmbedMetadata": "Rýchle vloženie metadát",
"ButtonQuickMatch": "Rýchle vyhľadanie",
"ButtonReScan": "Preskenovať",
"ButtonRead": "Načítať",
"ButtonReadLess": "Načítať menej",
"ButtonReadMore": "Načítať viac",
"ButtonRefresh": "Obnoviť",
"ButtonRemove": "Odstrániť",
"ButtonRemoveAll": "Odstrániť všetko",
"ButtonRemoveAllLibraryItems": "Odstrániť všetky položky knižnice",
"ButtonRemoveFromContinueListening": "Odstrániť z nedokončených podcastov",
"ButtonRemoveFromContinueReading": "Odtrániť z nedokončených audiokníh",
"ButtonRemoveSeriesFromContinueSeries": "Odstrániť z nedokončených sérií",
"ButtonReset": "Resetovať",
"ButtonResetToDefault": "Resetovať do predvolené",
"ButtonRestore": "Obnoviť zo zálohy",
"ButtonSave": "Uložiť",
"ButtonSaveAndClose": "Uložiť a zavrieť",
"ButtonSaveTracklist": "Uložiť zoznam",
"ButtonScan": "Skenovať",
"ButtonScanLibrary": "Skenovať knižnicu",
"HeaderMatch": "Spárovať",
"LabelBackupsNumberToKeepHelp": "Týmto spôsobom odstránite vždy iba jednu zálohu. V prípade, ak chcete odtrániť viacero záloh, mali by ste ich odstrániť manuálne."
}
+6 -5
View File
@@ -219,6 +219,7 @@
"LabelAccountTypeAdmin": "Administrator", "LabelAccountTypeAdmin": "Administrator",
"LabelAccountTypeGuest": "Gost", "LabelAccountTypeGuest": "Gost",
"LabelAccountTypeUser": "Uporabnik", "LabelAccountTypeUser": "Uporabnik",
"LabelActivities": "Aktivnosti",
"LabelActivity": "Aktivnost", "LabelActivity": "Aktivnost",
"LabelAddToCollection": "Dodaj v zbirko", "LabelAddToCollection": "Dodaj v zbirko",
"LabelAddToCollectionBatch": "Dodaj {0} knjig v zbirko", "LabelAddToCollectionBatch": "Dodaj {0} knjig v zbirko",
@@ -283,6 +284,7 @@
"LabelContinueSeries": "Nadaljuj s serijo", "LabelContinueSeries": "Nadaljuj s serijo",
"LabelCover": "Naslovnica", "LabelCover": "Naslovnica",
"LabelCoverImageURL": "URL naslovne slike", "LabelCoverImageURL": "URL naslovne slike",
"LabelCoverProvider": "Ponudnik naslovnic",
"LabelCreatedAt": "Ustvarjeno ob", "LabelCreatedAt": "Ustvarjeno ob",
"LabelCronExpression": "Cron izraz", "LabelCronExpression": "Cron izraz",
"LabelCurrent": "Trenutno", "LabelCurrent": "Trenutno",
@@ -391,6 +393,7 @@
"LabelIntervalEvery6Hours": "Vsakih 6 ur", "LabelIntervalEvery6Hours": "Vsakih 6 ur",
"LabelIntervalEveryDay": "Vsak dan", "LabelIntervalEveryDay": "Vsak dan",
"LabelIntervalEveryHour": "Vsako uro", "LabelIntervalEveryHour": "Vsako uro",
"LabelIntervalEveryMinute": "Vsako minuto",
"LabelInvert": "Obrni izbor", "LabelInvert": "Obrni izbor",
"LabelItem": "Element", "LabelItem": "Element",
"LabelJumpBackwardAmount": "Količina skoka nazaj", "LabelJumpBackwardAmount": "Količina skoka nazaj",
@@ -555,11 +558,8 @@
"LabelSettingsBookshelfViewHelp": "Skeuomorfna oblika z lesenimi policami", "LabelSettingsBookshelfViewHelp": "Skeuomorfna oblika z lesenimi policami",
"LabelSettingsChromecastSupport": "Podpora za Chromecast", "LabelSettingsChromecastSupport": "Podpora za Chromecast",
"LabelSettingsDateFormat": "Oblika datuma", "LabelSettingsDateFormat": "Oblika datuma",
"LabelSettingsDisableWatcher": "Onemogoči spremljanje datotečnega sistema", "LabelSettingsEnableWatcher": "Samodejno preglej knjižnice za spremembe",
"LabelSettingsDisableWatcherForLibrary": "Onemogoči spremljanje map za knjižnico", "LabelSettingsEnableWatcherForLibrary": "Samodejno preglej knjižnico za spremembe",
"LabelSettingsDisableWatcherHelp": "Onemogoči samodejno dodajanje/posodabljanje elementov, ko so zaznane spremembe datoteke. *Potreben je ponovni zagon strežnika",
"LabelSettingsEnableWatcher": "Omogoči spremljanje sprememb",
"LabelSettingsEnableWatcherForLibrary": "Omogoči spremljanje sprememb v mapi knjižnice",
"LabelSettingsEnableWatcherHelp": "Omogoča samodejno dodajanje/posodabljanje elementov, ko so zaznane spremembe datoteke. *Potreben je ponovni zagon strežnika", "LabelSettingsEnableWatcherHelp": "Omogoča samodejno dodajanje/posodabljanje elementov, ko so zaznane spremembe datoteke. *Potreben je ponovni zagon strežnika",
"LabelSettingsEpubsAllowScriptedContent": "Dovoli skriptirano vsebino v epubih", "LabelSettingsEpubsAllowScriptedContent": "Dovoli skriptirano vsebino v epubih",
"LabelSettingsEpubsAllowScriptedContentHelp": "Dovoli datotekam epub izvajanje skript. Priporočljivo je, da to nastavitev pustite onemogočeno, razen če zaupate viru datotek epub.", "LabelSettingsEpubsAllowScriptedContentHelp": "Dovoli datotekam epub izvajanje skript. Priporočljivo je, da to nastavitev pustite onemogočeno, razen če zaupate viru datotek epub.",
@@ -845,6 +845,7 @@
"MessageRestoreBackupConfirm": "Ali ste prepričani, da želite obnoviti varnostno kopijo, ustvarjeno ob", "MessageRestoreBackupConfirm": "Ali ste prepričani, da želite obnoviti varnostno kopijo, ustvarjeno ob",
"MessageRestoreBackupWarning": "Obnovitev varnostne kopije bo prepisala celotno zbirko podatkov, ki se nahaja v /config, in zajema slike v /metadata/items in /metadata/authors.<br /><br />Varnostne kopije ne spreminjajo nobenih datotek v mapah vaše knjižnice. Če ste omogočili nastavitve strežnika za shranjevanje naslovnic in metapodatkov v mapah vaše knjižnice, potem ti niso varnostno kopirani ali prepisani.<br /><br />Vsi odjemalci, ki uporabljajo vaš strežnik, bodo samodejno osveženi.", "MessageRestoreBackupWarning": "Obnovitev varnostne kopije bo prepisala celotno zbirko podatkov, ki se nahaja v /config, in zajema slike v /metadata/items in /metadata/authors.<br /><br />Varnostne kopije ne spreminjajo nobenih datotek v mapah vaše knjižnice. Če ste omogočili nastavitve strežnika za shranjevanje naslovnic in metapodatkov v mapah vaše knjižnice, potem ti niso varnostno kopirani ali prepisani.<br /><br />Vsi odjemalci, ki uporabljajo vaš strežnik, bodo samodejno osveženi.",
"MessageScheduleLibraryScanNote": "Za večino uporabnikov je priporočljivo, da to funkcijo pustite onemogočeno in ohranite nastavitev pregledovalnika map omogočeno. Pregledovalnik map bo samodejno zaznal spremembe v mapah vaše knjižnice. Pregledovalnik map ne deluje za vse datotečne sisteme (na primer NFS), zato lahko namesto tega uporabite načrtovane preglede knjižnic.", "MessageScheduleLibraryScanNote": "Za večino uporabnikov je priporočljivo, da to funkcijo pustite onemogočeno in ohranite nastavitev pregledovalnika map omogočeno. Pregledovalnik map bo samodejno zaznal spremembe v mapah vaše knjižnice. Pregledovalnik map ne deluje za vse datotečne sisteme (na primer NFS), zato lahko namesto tega uporabite načrtovane preglede knjižnic.",
"MessageScheduleRunEveryWeekdayAtTime": "Zaženi vsakih {0} ob {1}",
"MessageSearchResultsFor": "Rezultati iskanja za", "MessageSearchResultsFor": "Rezultati iskanja za",
"MessageSelected": "{0} izbrano", "MessageSelected": "{0} izbrano",
"MessageServerCouldNotBeReached": "Strežnika ni bilo mogoče doseči", "MessageServerCouldNotBeReached": "Strežnika ni bilo mogoče doseči",
+94 -43
View File
@@ -16,7 +16,7 @@
"ButtonCancel": "Avbryt", "ButtonCancel": "Avbryt",
"ButtonCancelEncode": "Avbryt omkodning", "ButtonCancelEncode": "Avbryt omkodning",
"ButtonChangeRootPassword": "Ändra lösenordet för root", "ButtonChangeRootPassword": "Ändra lösenordet för root",
"ButtonCheckAndDownloadNewEpisodes": "Sök & Ladda ner nya avsnitt", "ButtonCheckAndDownloadNewEpisodes": "Sök & Hämta nya avsnitt",
"ButtonChooseAFolder": "Välj en mapp", "ButtonChooseAFolder": "Välj en mapp",
"ButtonChooseFiles": "Välj filer", "ButtonChooseFiles": "Välj filer",
"ButtonClearFilter": "Rensa filter", "ButtonClearFilter": "Rensa filter",
@@ -36,7 +36,7 @@
"ButtonFullPath": "Fullständig sökväg", "ButtonFullPath": "Fullständig sökväg",
"ButtonHide": "Dölj", "ButtonHide": "Dölj",
"ButtonHome": "Hem", "ButtonHome": "Hem",
"ButtonIssues": "Problem", "ButtonIssues": "Objekt med problem",
"ButtonJumpBackward": "Hoppa bakåt", "ButtonJumpBackward": "Hoppa bakåt",
"ButtonJumpForward": "Hoppa framåt", "ButtonJumpForward": "Hoppa framåt",
"ButtonLatest": "Senaste", "ButtonLatest": "Senaste",
@@ -66,17 +66,19 @@
"ButtonPurgeItemsCache": "Rensa cache för föremål", "ButtonPurgeItemsCache": "Rensa cache för föremål",
"ButtonQueueAddItem": "Lägg till i kön", "ButtonQueueAddItem": "Lägg till i kön",
"ButtonQueueRemoveItem": "Ta bort från kön", "ButtonQueueRemoveItem": "Ta bort från kön",
"ButtonQuickEmbed": "Infoga metadata",
"ButtonQuickEmbedMetadata": "Infoga metadata",
"ButtonQuickMatch": "Snabbmatchning", "ButtonQuickMatch": "Snabbmatchning",
"ButtonReScan": "Ny skanning", "ButtonReScan": "Ny skanning",
"ButtonRead": "Läs", "ButtonRead": "Läs",
"ButtonReadLess": "Visa mindre", "ButtonReadLess": "Läs mindre",
"ButtonReadMore": "Visa mer", "ButtonReadMore": "Läs mer",
"ButtonRefresh": "Uppdatera", "ButtonRefresh": "Uppdatera",
"ButtonRemove": "Ta bort", "ButtonRemove": "Ta bort",
"ButtonRemoveAll": "Ta bort alla", "ButtonRemoveAll": "Ta bort alla",
"ButtonRemoveAllLibraryItems": "Ta bort alla objekt i biblioteket", "ButtonRemoveAllLibraryItems": "Ta bort alla objekt i biblioteket",
"ButtonRemoveFromContinueListening": "Radera från 'Fortsätt lyssna'", "ButtonRemoveFromContinueListening": "Radera från 'Fortsätt att lyssna'",
"ButtonRemoveFromContinueReading": "Radera från 'Fortsätt läsa'", "ButtonRemoveFromContinueReading": "Radera från 'Fortsätt att läsa'",
"ButtonRemoveSeriesFromContinueSeries": "Radera från 'Fortsätt med serien'", "ButtonRemoveSeriesFromContinueSeries": "Radera från 'Fortsätt med serien'",
"ButtonReset": "Tillbaka", "ButtonReset": "Tillbaka",
"ButtonResetToDefault": "Återställ till standard", "ButtonResetToDefault": "Återställ till standard",
@@ -86,6 +88,8 @@
"ButtonSaveTracklist": "Spara spårlista", "ButtonSaveTracklist": "Spara spårlista",
"ButtonScan": "Skanna", "ButtonScan": "Skanna",
"ButtonScanLibrary": "Skanna bibliotek", "ButtonScanLibrary": "Skanna bibliotek",
"ButtonScrollLeft": "Scroll vänster",
"ButtonScrollRight": "Scrolla höger",
"ButtonSearch": "Sök", "ButtonSearch": "Sök",
"ButtonSelectFolderPath": "Välj mappens sökväg", "ButtonSelectFolderPath": "Välj mappens sökväg",
"ButtonSeries": "Serier", "ButtonSeries": "Serier",
@@ -94,7 +98,7 @@
"ButtonShiftTimes": "Förskjut tider", "ButtonShiftTimes": "Förskjut tider",
"ButtonShow": "Visa", "ButtonShow": "Visa",
"ButtonStartM4BEncode": "Starta M4B-omkodning", "ButtonStartM4BEncode": "Starta M4B-omkodning",
"ButtonStartMetadataEmbed": "Starta inbäddning av metadata", "ButtonStartMetadataEmbed": "Infoga metadata",
"ButtonStats": "Statistik", "ButtonStats": "Statistik",
"ButtonSubmit": "Spara", "ButtonSubmit": "Spara",
"ButtonTest": "Testa", "ButtonTest": "Testa",
@@ -114,7 +118,7 @@
"HeaderAdvanced": "Avancerad", "HeaderAdvanced": "Avancerad",
"HeaderAppriseNotificationSettings": "Inställningar av meddelanden med Apprise", "HeaderAppriseNotificationSettings": "Inställningar av meddelanden med Apprise",
"HeaderAudioTracks": "Ljudspår", "HeaderAudioTracks": "Ljudspår",
"HeaderAudiobookTools": "Hantering av ljudboksfil", "HeaderAudiobookTools": "Hantering av ljudboksfiler",
"HeaderAuthentication": "Autentisering", "HeaderAuthentication": "Autentisering",
"HeaderBackups": "Säkerhetskopior", "HeaderBackups": "Säkerhetskopior",
"HeaderChangePassword": "Ändra lösenord", "HeaderChangePassword": "Ändra lösenord",
@@ -124,11 +128,12 @@
"HeaderCollectionItems": "Böcker i samlingen", "HeaderCollectionItems": "Böcker i samlingen",
"HeaderCover": "Omslag", "HeaderCover": "Omslag",
"HeaderCurrentDownloads": "Aktuella nedladdningar", "HeaderCurrentDownloads": "Aktuella nedladdningar",
"HeaderCustomMessageOnLogin": "Meddelande att visa på sidan för inloggning",
"HeaderCustomMetadataProviders": "Egen källa för metadata", "HeaderCustomMetadataProviders": "Egen källa för metadata",
"HeaderDetails": "Detaljer", "HeaderDetails": "Detaljer",
"HeaderDownloadQueue": "Nedladdningskö", "HeaderDownloadQueue": "Nedladdningskö",
"HeaderEbookFiles": "E-boksfiler", "HeaderEbookFiles": "E-boksfiler",
"HeaderEmail": "E-postadress", "HeaderEmail": "E-post",
"HeaderEmailSettings": "Inställningar för e-post", "HeaderEmailSettings": "Inställningar för e-post",
"HeaderEpisodes": "Avsnitt", "HeaderEpisodes": "Avsnitt",
"HeaderEreaderDevices": "Enheter för att läsa e-böcker", "HeaderEreaderDevices": "Enheter för att läsa e-böcker",
@@ -146,7 +151,7 @@
"HeaderListeningSessions": "Lyssningstillfällen", "HeaderListeningSessions": "Lyssningstillfällen",
"HeaderListeningStats": "Lyssningsstatistik", "HeaderListeningStats": "Lyssningsstatistik",
"HeaderLogin": "Logga in", "HeaderLogin": "Logga in",
"HeaderLogs": "Loggar", "HeaderLogs": "Loggning",
"HeaderManageGenres": "Hantera kategorier", "HeaderManageGenres": "Hantera kategorier",
"HeaderManageTags": "Hantera taggar", "HeaderManageTags": "Hantera taggar",
"HeaderMapDetails": "Gemensam information för samtliga objekt", "HeaderMapDetails": "Gemensam information för samtliga objekt",
@@ -156,7 +161,9 @@
"HeaderNewAccount": "Nytt konto", "HeaderNewAccount": "Nytt konto",
"HeaderNewLibrary": "Nytt bibliotek", "HeaderNewLibrary": "Nytt bibliotek",
"HeaderNotificationCreate": "Addera ett meddelande", "HeaderNotificationCreate": "Addera ett meddelande",
"HeaderNotificationUpdate": "Uppdateringsnotis",
"HeaderNotifications": "Meddelanden", "HeaderNotifications": "Meddelanden",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Autentisering",
"HeaderOpenRSSFeed": "Öppna RSS-flöde", "HeaderOpenRSSFeed": "Öppna RSS-flöde",
"HeaderOtherFiles": "Andra filer", "HeaderOtherFiles": "Andra filer",
"HeaderPasswordAuthentication": "Lösenordsautentisering", "HeaderPasswordAuthentication": "Lösenordsautentisering",
@@ -201,11 +208,13 @@
"HeaderYearReview": "Sammanställning av {0}", "HeaderYearReview": "Sammanställning av {0}",
"HeaderYourStats": "Din statistik", "HeaderYourStats": "Din statistik",
"LabelAbridged": "Förkortad version", "LabelAbridged": "Förkortad version",
"LabelAbridgedUnchecked": "Oavkortad (okontrollerad)",
"LabelAccessibleBy": "Tillgänglig för", "LabelAccessibleBy": "Tillgänglig för",
"LabelAccountType": "Kontotyp", "LabelAccountType": "Kontotyp",
"LabelAccountTypeAdmin": "Administratör", "LabelAccountTypeAdmin": "Administratör",
"LabelAccountTypeGuest": "Gäst", "LabelAccountTypeGuest": "Gäst",
"LabelAccountTypeUser": "Användare", "LabelAccountTypeUser": "Användare",
"LabelActivities": "Aktiviteter",
"LabelActivity": "Aktivitet", "LabelActivity": "Aktivitet",
"LabelAddToCollection": "Lägg till i en samling", "LabelAddToCollection": "Lägg till i en samling",
"LabelAddToCollectionBatch": "Lägg till {0} böcker i samlingen", "LabelAddToCollectionBatch": "Lägg till {0} böcker i samlingen",
@@ -221,7 +230,7 @@
"LabelAlreadyInYourLibrary": "Finns redan i samlingen", "LabelAlreadyInYourLibrary": "Finns redan i samlingen",
"LabelApiToken": "API-token", "LabelApiToken": "API-token",
"LabelAppend": "Lägg till", "LabelAppend": "Lägg till",
"LabelAudioBitrate": "Bitrate för ljud (t.ex. 128k)", "LabelAudioBitrate": "Bitrate (t.ex. 128k)",
"LabelAudioChannels": "Ljudkanaler (1 eller 2)", "LabelAudioChannels": "Ljudkanaler (1 eller 2)",
"LabelAudioCodec": "Codec för ljud", "LabelAudioCodec": "Codec för ljud",
"LabelAuthor": "Författare", "LabelAuthor": "Författare",
@@ -231,6 +240,9 @@
"LabelAutoDownloadEpisodes": "Automatisk nedladdning av avsnitt", "LabelAutoDownloadEpisodes": "Automatisk nedladdning av avsnitt",
"LabelAutoFetchMetadata": "Automatisk nedladdning av metadata", "LabelAutoFetchMetadata": "Automatisk nedladdning av metadata",
"LabelAutoFetchMetadataHelp": "Hämtar metadata för titel, författare och serier. Kompletterande metadata kan manuellt adderas efter uppladdningen.", "LabelAutoFetchMetadataHelp": "Hämtar metadata för titel, författare och serier. Kompletterande metadata kan manuellt adderas efter uppladdningen.",
"LabelAutoLaunch": "Automatisk start",
"LabelAutoLaunchDescription": "Omdirigera till auth-leverantören automatiskt när du navigerar till inloggningssidan (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
"LabelAutoRegisterDescription": "Skapa automatiskt nya användare efter inloggning", "LabelAutoRegisterDescription": "Skapa automatiskt nya användare efter inloggning",
"LabelBackToUser": "Tillbaka till användaren", "LabelBackToUser": "Tillbaka till användaren",
"LabelBackupAudioFiles": "Säkerhetskopiera ljudfiler", "LabelBackupAudioFiles": "Säkerhetskopiera ljudfiler",
@@ -242,7 +254,7 @@
"LabelBackupsNumberToKeep": "Antal säkerhetskopior att behålla", "LabelBackupsNumberToKeep": "Antal säkerhetskopior att behålla",
"LabelBackupsNumberToKeepHelp": "Endast en gammal säkerhetskopia tas bort åt gången, så om du redan har fler säkerhetskopior än det angivna värdet bör du ta bort dem manuellt.", "LabelBackupsNumberToKeepHelp": "Endast en gammal säkerhetskopia tas bort åt gången, så om du redan har fler säkerhetskopior än det angivna värdet bör du ta bort dem manuellt.",
"LabelBitrate": "Bitfrekvens", "LabelBitrate": "Bitfrekvens",
"LabelBonus": "Bonus", "LabelBonus": "Bonusavsnitt",
"LabelBooks": "Böcker", "LabelBooks": "Böcker",
"LabelButtonText": "Knapptext", "LabelButtonText": "Knapptext",
"LabelByAuthor": "av {0}", "LabelByAuthor": "av {0}",
@@ -266,6 +278,7 @@
"LabelContinueSeries": "Fortsätt med serien", "LabelContinueSeries": "Fortsätt med serien",
"LabelCover": "Omslag", "LabelCover": "Omslag",
"LabelCoverImageURL": "URL till omslagsbild", "LabelCoverImageURL": "URL till omslagsbild",
"LabelCoverProvider": "Källa för omslag",
"LabelCreatedAt": "Skapad", "LabelCreatedAt": "Skapad",
"LabelCronExpression": "Schemaläggning med hjälp av Cron (Cron Expression)", "LabelCronExpression": "Schemaläggning med hjälp av Cron (Cron Expression)",
"LabelCurrent": "Nuvarande", "LabelCurrent": "Nuvarande",
@@ -288,11 +301,13 @@
"LabelDownloadable": "Nedladdningsbar", "LabelDownloadable": "Nedladdningsbar",
"LabelDuration": "Varaktighet", "LabelDuration": "Varaktighet",
"LabelDurationComparisonExactMatch": "(exakt matchning)", "LabelDurationComparisonExactMatch": "(exakt matchning)",
"LabelDurationComparisonLonger": "({0} längre)",
"LabelDurationComparisonShorter": "({0} kortare)",
"LabelDurationFound": "Varaktighet hittad:", "LabelDurationFound": "Varaktighet hittad:",
"LabelEbook": "E-bok", "LabelEbook": "E-bok",
"LabelEbooks": "E-böcker", "LabelEbooks": "E-böcker",
"LabelEdit": "Redigera", "LabelEdit": "Redigera",
"LabelEmail": "E-postadress", "LabelEmail": "E-post",
"LabelEmailSettingsFromAddress": "Från e-postadress", "LabelEmailSettingsFromAddress": "Från e-postadress",
"LabelEmailSettingsRejectUnauthorized": "Avvisa icke-autentiserade certifikat", "LabelEmailSettingsRejectUnauthorized": "Avvisa icke-autentiserade certifikat",
"LabelEmailSettingsRejectUnauthorizedHelp": "Inaktivering av SSL-certifikatsvalidering kan exponera din anslutning för säkerhetsrisker, såsom man-in-the-middle-attacker. Inaktivera bara denna inställning om du förstår implikationerna och litar på den epostserver du ansluter till.", "LabelEmailSettingsRejectUnauthorizedHelp": "Inaktivering av SSL-certifikatsvalidering kan exponera din anslutning för säkerhetsrisker, såsom man-in-the-middle-attacker. Inaktivera bara denna inställning om du förstår implikationerna och litar på den epostserver du ansluter till.",
@@ -301,20 +316,23 @@
"LabelEmailSettingsTestAddress": "E-postadress för test", "LabelEmailSettingsTestAddress": "E-postadress för test",
"LabelEmbeddedCover": "Infogat omslag", "LabelEmbeddedCover": "Infogat omslag",
"LabelEnable": "Aktivera", "LabelEnable": "Aktivera",
"LabelEncodingBackupLocation": "En säkerhetskopia av dina orginalljudfiler kommer att placeras i katalogen:", "LabelEncodingBackupLocation": "En säkerhetskopia av ljudfilerna kommer att placeras i katalogen:",
"LabelEncodingClearItemCache": "Kom ihåg att regelbundet radera cachen för föremål. Du hittar funktionen längst ner på sidan 'Inställningar'.", "LabelEncodingChaptersNotEmbedded": "Information om kapitel kommer inte att inkluderas i ljudböcker med flera ljudspår.",
"LabelEncodingClearItemCache": "Kom ihåg att regelbundet rensa cachen för föremål. Du hittar funktionen längst ner på sidan 'Inställningar'.",
"LabelEncodingFinishedM4B": "Den färdiga M4B-filen kommer att placeras i katalogen:", "LabelEncodingFinishedM4B": "Den färdiga M4B-filen kommer att placeras i katalogen:",
"LabelEncodingInfoEmbedded": "Metadata kommer att adderas i ljudfilerna i mappen med ljudboken.", "LabelEncodingInfoEmbedded": "Metadata kommer att adderas i ljudfilerna i mappen med ljudboken.",
"LabelEncodingStartedNavigation": "När du startad omkodningen kan du lämna denna sida. Omkodningen fortsätter i bakgrunden.", "LabelEncodingStartedNavigation": "När du startat uppgiften kan du lämna denna sida. Arbetet fortsätter i bakgrunden.",
"LabelEncodingTimeWarning": "Avkodningen kan ta upp till 30 minuter eller ännu längre för riktigt stora filer.", "LabelEncodingTimeWarning": "Omkodningen kan ta upp till 30 minuter eller ännu längre för riktigt stora filer.",
"LabelEncodingWarningAdvancedSettings": "VARNING: Ändra inte inställningarna om du inte är bekant med inställningarna för omkodning med 'ffmpeg'.", "LabelEncodingWarningAdvancedSettings": "VARNING: Ändra inte inställningarna om du inte är bekant med inställningarna för omkodning med 'ffmpeg'.",
"LabelEncodingWatcherDisabled": "Om funktionen 'Watcher' är avstängd behöver du göra en ny skanning av ljudboken efteråt.", "LabelEncodingWatcherDisabled": "Om funktionen 'Watcher' är avstängd behöver du göra en ny skanning av ljudboken efteråt.",
"LabelEnd": "Slut", "LabelEnd": "Slut",
"LabelEndOfChapter": "Slut av kapitel", "LabelEndOfChapter": "Slut av kapitel",
"LabelEpisode": "Avsnitt", "LabelEpisode": "Avsnitt",
"LabelEpisodeNotLinkedToRssFeed": "Avsnittet är inte knutet till ett RSS-flöde",
"LabelEpisodeNumber": "Avsnitt #{0}", "LabelEpisodeNumber": "Avsnitt #{0}",
"LabelEpisodeTitle": "Titel på avsnittet", "LabelEpisodeTitle": "Titel på avsnittet",
"LabelEpisodeType": "Typ av avsnitt", "LabelEpisodeType": "Typ av avsnitt",
"LabelEpisodeUrlFromRssFeed": "URL-adress till avsnittet i RSS-flödet",
"LabelEpisodes": "Avsnitt", "LabelEpisodes": "Avsnitt",
"LabelEpisodic": "Uppdelad i avsnitt", "LabelEpisodic": "Uppdelad i avsnitt",
"LabelExample": "Exempel", "LabelExample": "Exempel",
@@ -327,6 +345,7 @@
"LabelFetchingMetadata": "Hämtar metadata", "LabelFetchingMetadata": "Hämtar metadata",
"LabelFile": "Fil", "LabelFile": "Fil",
"LabelFileBirthtime": "Tidpunkt, fil skapad", "LabelFileBirthtime": "Tidpunkt, fil skapad",
"LabelFileBornDate": "Skapad {0}",
"LabelFileModified": "Tidpunkt, fil ändrad", "LabelFileModified": "Tidpunkt, fil ändrad",
"LabelFileModifiedDate": "Ändrad {0}", "LabelFileModifiedDate": "Ändrad {0}",
"LabelFilename": "Filnamn", "LabelFilename": "Filnamn",
@@ -341,6 +360,7 @@
"LabelFontItalic": "Kursiv", "LabelFontItalic": "Kursiv",
"LabelFontScale": "Skala på typsnitt", "LabelFontScale": "Skala på typsnitt",
"LabelFontStrikethrough": "Genomstruken", "LabelFontStrikethrough": "Genomstruken",
"LabelFull": "Komplett",
"LabelGenre": "Kategori", "LabelGenre": "Kategori",
"LabelGenres": "Kategorier", "LabelGenres": "Kategorier",
"LabelHardDeleteFile": "Hård radering av fil", "LabelHardDeleteFile": "Hård radering av fil",
@@ -355,7 +375,7 @@
"LabelImageURLFromTheWeb": "Skriv URL-adressen till bilden på webben", "LabelImageURLFromTheWeb": "Skriv URL-adressen till bilden på webben",
"LabelInProgress": "Pågående", "LabelInProgress": "Pågående",
"LabelIncludeInTracklist": "Inkludera i spårlista", "LabelIncludeInTracklist": "Inkludera i spårlista",
"LabelIncomplete": "Ofullständig", "LabelIncomplete": "Ofullständigt",
"LabelInterval": "Intervall", "LabelInterval": "Intervall",
"LabelIntervalCustomDailyWeekly": "Anpassad daglig/veckovis", "LabelIntervalCustomDailyWeekly": "Anpassad daglig/veckovis",
"LabelIntervalEvery12Hours": "Var 12:e timme", "LabelIntervalEvery12Hours": "Var 12:e timme",
@@ -365,6 +385,7 @@
"LabelIntervalEvery6Hours": "Var 6:e timme", "LabelIntervalEvery6Hours": "Var 6:e timme",
"LabelIntervalEveryDay": "Varje dag", "LabelIntervalEveryDay": "Varje dag",
"LabelIntervalEveryHour": "Varje timme", "LabelIntervalEveryHour": "Varje timme",
"LabelIntervalEveryMinute": "Varje minut",
"LabelInvert": "Invertera", "LabelInvert": "Invertera",
"LabelItem": "Objekt", "LabelItem": "Objekt",
"LabelJumpBackwardAmount": "Inställning för \"hopp bakåt\"", "LabelJumpBackwardAmount": "Inställning för \"hopp bakåt\"",
@@ -383,6 +404,7 @@
"LabelLess": "Mindre", "LabelLess": "Mindre",
"LabelLibrariesAccessibleToUser": "Bibliotek användaren har tillgång till", "LabelLibrariesAccessibleToUser": "Bibliotek användaren har tillgång till",
"LabelLibrary": "Bibliotek", "LabelLibrary": "Bibliotek",
"LabelLibraryFilterSublistEmpty": "Ingen {0}",
"LabelLibraryItem": "Objekt", "LabelLibraryItem": "Objekt",
"LabelLibraryName": "Biblioteksnamn", "LabelLibraryName": "Biblioteksnamn",
"LabelLimit": "Begränsning", "LabelLimit": "Begränsning",
@@ -393,6 +415,8 @@
"LabelLogLevelWarn": "Varningar", "LabelLogLevelWarn": "Varningar",
"LabelLookForNewEpisodesAfterDate": "Sök efter nya avsnitt efter detta datum", "LabelLookForNewEpisodesAfterDate": "Sök efter nya avsnitt efter detta datum",
"LabelLowestPriority": "Lägst prioritet", "LabelLowestPriority": "Lägst prioritet",
"LabelMatchExistingUsersBy": "Matcha befintliga användare med",
"LabelMatchExistingUsersByDescription": "Används för att koppla existerande användare. När kopplingen sker kommer användaren att matchas med ett unikt ID från SSO-leverantören.",
"LabelMaxEpisodesToDownload": "Maximalt antal avsnitt att ladda ner (0 = obegränsat).", "LabelMaxEpisodesToDownload": "Maximalt antal avsnitt att ladda ner (0 = obegränsat).",
"LabelMaxEpisodesToDownloadPerCheck": "Maximalt antal nya avsnitt att ladda ner per tillfälle", "LabelMaxEpisodesToDownloadPerCheck": "Maximalt antal nya avsnitt att ladda ner per tillfälle",
"LabelMaxEpisodesToKeep": "Maximalt antal avsnitt att behålla", "LabelMaxEpisodesToKeep": "Maximalt antal avsnitt att behålla",
@@ -405,7 +429,7 @@
"LabelMetadataProvider": "Källa för metadata", "LabelMetadataProvider": "Källa för metadata",
"LabelMinute": "Minut", "LabelMinute": "Minut",
"LabelMinutes": "Minuter", "LabelMinutes": "Minuter",
"LabelMissing": "Saknar", "LabelMissing": "Saknad",
"LabelMissingEbook": "Saknar e-bok", "LabelMissingEbook": "Saknar e-bok",
"LabelMissingSupplementaryEbook": "Saknar kompletterande e-bok", "LabelMissingSupplementaryEbook": "Saknar kompletterande e-bok",
"LabelMore": "Mer", "LabelMore": "Mer",
@@ -416,7 +440,7 @@
"LabelNew": "Nytt", "LabelNew": "Nytt",
"LabelNewPassword": "Nytt lösenord", "LabelNewPassword": "Nytt lösenord",
"LabelNewestAuthors": "Senaste författarna", "LabelNewestAuthors": "Senaste författarna",
"LabelNewestEpisodes": "Senast adderade avsnitt", "LabelNewestEpisodes": "Senaste avsnitten",
"LabelNextBackupDate": "Nästa tillfälle för säkerhetskopiering", "LabelNextBackupDate": "Nästa tillfälle för säkerhetskopiering",
"LabelNextScheduledRun": "Nästa schemalagda körning", "LabelNextScheduledRun": "Nästa schemalagda körning",
"LabelNoCustomMetadataProviders": "Ingen egen källa för metadata", "LabelNoCustomMetadataProviders": "Ingen egen källa för metadata",
@@ -434,7 +458,7 @@
"LabelNotificationsMaxQueueSize": "Max köstorlek för aviseringsevenemang", "LabelNotificationsMaxQueueSize": "Max köstorlek för aviseringsevenemang",
"LabelNotificationsMaxQueueSizeHelp": "Evenemang är begränsade till att utlösa ett per sekund. Evenemang kommer att ignoreras om kön är full. Detta förhindrar aviseringsspam.", "LabelNotificationsMaxQueueSizeHelp": "Evenemang är begränsade till att utlösa ett per sekund. Evenemang kommer att ignoreras om kön är full. Detta förhindrar aviseringsspam.",
"LabelNumberOfBooks": "Antal böcker", "LabelNumberOfBooks": "Antal böcker",
"LabelNumberOfEpisodes": "Antal avsnitt", "LabelNumberOfEpisodes": "# av Avsnitt",
"LabelOpenRSSFeed": "Öppna RSS-flöde", "LabelOpenRSSFeed": "Öppna RSS-flöde",
"LabelOverwrite": "Skriv över", "LabelOverwrite": "Skriv över",
"LabelPaginationPageXOfY": "Sida {0} av {1}", "LabelPaginationPageXOfY": "Sida {0} av {1}",
@@ -459,22 +483,26 @@
"LabelPodcasts": "Podcasts", "LabelPodcasts": "Podcasts",
"LabelPort": "Port", "LabelPort": "Port",
"LabelPrefixesToIgnore": "Prefix att ignorera (skiftlägesokänsligt)", "LabelPrefixesToIgnore": "Prefix att ignorera (skiftlägesokänsligt)",
"LabelPreventIndexing": "Förhindra att ditt flöde indexeras av iTunes och Google-podcastsökmotorer", "LabelPreventIndexing": "Förhindra att ditt flöde indexeras av sökmotorer från iTunes och Google",
"LabelPrimaryEbook": "Primär e-bok", "LabelPrimaryEbook": "Primär e-bok",
"LabelProgress": "Framsteg", "LabelProgress": "Framsteg",
"LabelProvider": "Källa", "LabelProvider": "Källa",
"LabelPubDate": "Publiceringsdatum", "LabelPubDate": "Publiceringsdatum",
"LabelPublishYear": "Publiceringsår", "LabelPublishYear": "Publiceringsår",
"LabelPublishedDate": "Publicerad {0}",
"LabelPublishedDecade": "Årtionde för publicering", "LabelPublishedDecade": "Årtionde för publicering",
"LabelPublishedDecades": "Årtionde för publicering",
"LabelPublisher": "Utgivare", "LabelPublisher": "Utgivare",
"LabelPublishers": "Utgivare",
"LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post", "LabelRSSFeedCustomOwnerEmail": "Anpassad ägarens e-post",
"LabelRSSFeedCustomOwnerName": "Anpassat ägarnamn", "LabelRSSFeedCustomOwnerName": "Anpassat ägarnamn",
"LabelRSSFeedOpen": "Öppna RSS-flöde", "LabelRSSFeedOpen": "Öppna RSS-flöde",
"LabelRSSFeedPreventIndexing": "Förhindra indexering", "LabelRSSFeedPreventIndexing": "Förhindra indexering",
"LabelRSSFeedSlug": "RSS-flödesslag", "LabelRSSFeedSlug": "RSS-flödesslag",
"LabelRSSFeedURL": "RSS-flöde URL", "LabelRSSFeedURL": "URL-adress för RSS-flödet",
"LabelRandomly": "Slumpartat", "LabelRandomly": "Slumpartat",
"LabelRead": "Läst", "LabelReAddSeriesToContinueListening": "Addera serien på nytt till 'Fortsätt att lyssna'",
"LabelRead": "Läs",
"LabelReadAgain": "Läs igen", "LabelReadAgain": "Läs igen",
"LabelReadEbookWithoutProgress": "Läs e-bok utan att behålla framsteg", "LabelReadEbookWithoutProgress": "Läs e-bok utan att behålla framsteg",
"LabelRecentSeries": "Senaste serierna", "LabelRecentSeries": "Senaste serierna",
@@ -499,7 +527,8 @@
"LabelSelectEpisodesShowing": "Välj {0} avsnitt som visas", "LabelSelectEpisodesShowing": "Välj {0} avsnitt som visas",
"LabelSelectUsers": "Välj användare", "LabelSelectUsers": "Välj användare",
"LabelSendEbookToDevice": "Skicka e-bok till...", "LabelSendEbookToDevice": "Skicka e-bok till...",
"LabelSequence": "Sekvensnummer", "LabelSequence": "Ordningsnummer",
"LabelSerial": "Seriell",
"LabelSeries": "Serier", "LabelSeries": "Serier",
"LabelSeriesName": "Serienamn", "LabelSeriesName": "Serienamn",
"LabelSeriesProgress": "Status för serier", "LabelSeriesProgress": "Status för serier",
@@ -513,18 +542,13 @@
"LabelSettingsBookshelfViewHelp": "Bakgrund med ett utseende liknande en bokhylla i trä", "LabelSettingsBookshelfViewHelp": "Bakgrund med ett utseende liknande en bokhylla i trä",
"LabelSettingsChromecastSupport": "Stöd för Chromecast", "LabelSettingsChromecastSupport": "Stöd för Chromecast",
"LabelSettingsDateFormat": "Datumformat", "LabelSettingsDateFormat": "Datumformat",
"LabelSettingsDisableWatcher": "Inaktivera Watcher",
"LabelSettingsDisableWatcherForLibrary": "Inaktivera bevakning med Watcher för biblioteket",
"LabelSettingsDisableWatcherHelp": "Inaktiverar automatik att addera/uppdatera<br> objekt när ändringar av filer genomförs.<br>OBS: Kräver en omstart av servern",
"LabelSettingsEnableWatcher": "Aktivera Watcher",
"LabelSettingsEnableWatcherForLibrary": "Aktivera bevakning med Watcher för biblioteket",
"LabelSettingsEnableWatcherHelp": "Aktiverar automatik att addera/uppdatera<br> objekt när ändringar av filer genomförs.<br>OBS: Kräver en omstart av servern", "LabelSettingsEnableWatcherHelp": "Aktiverar automatik att addera/uppdatera<br> objekt när ändringar av filer genomförs.<br>OBS: Kräver en omstart av servern",
"LabelSettingsEpubsAllowScriptedContent": "Tillåt e-böcker i epubs-format som innehåller script", "LabelSettingsEpubsAllowScriptedContent": "Tillåt e-böcker i epubs-format som innehåller script",
"LabelSettingsEpubsAllowScriptedContentHelp": "Tillåt att epub-filer får använda script.<br>Det rekommenderas att denna inställning är<br>avstängd när du inte litar på källan för epub-filerna.", "LabelSettingsEpubsAllowScriptedContentHelp": "Tillåt att epub-filer får använda script.<br>Det rekommenderas att denna inställning är<br>avstängd när du inte litar på källan för epub-filerna.",
"LabelSettingsExperimentalFeatures": "Experimentella funktioner", "LabelSettingsExperimentalFeatures": "Experimentella funktioner",
"LabelSettingsExperimentalFeaturesHelp": "Funktioner under utveckling som behöver din feedback och hjälp med testning. Klicka för att öppna diskussionen på GitHub.", "LabelSettingsExperimentalFeaturesHelp": "Funktioner under utveckling som behöver din feedback och hjälp med testning. Klicka för att öppna diskussionen på GitHub.",
"LabelSettingsFindCovers": "Hitta ett omslag", "LabelSettingsFindCovers": "Hitta ett omslag",
"LabelSettingsFindCoversHelp": "Om din bok INTE har ett omslag inbäddat i filen eller en fil med omslaget i mappen kommer skannern att försöka hitta ett omslag.<br>OBS: Detta kommer att förlänga inläsningstiden", "LabelSettingsFindCoversHelp": "Om din bok INTE har ett omslag inkluderat i filen eller en fil med omslaget i mappen kommer skannern att försöka hitta ett omslag.<br>OBS: Detta kommer att förlänga inläsningstiden",
"LabelSettingsHideSingleBookSeries": "Dölj serier som endast innehåller en bok", "LabelSettingsHideSingleBookSeries": "Dölj serier som endast innehåller en bok",
"LabelSettingsHideSingleBookSeriesHelp": "Serier som endast har en bok kommer att<br>döljas från sidan 'Serier' och hyllorna på startsidan.", "LabelSettingsHideSingleBookSeriesHelp": "Serier som endast har en bok kommer att<br>döljas från sidan 'Serier' och hyllorna på startsidan.",
"LabelSettingsHomePageBookshelfView": "Använd vy liknande en bokhylla på startsidan", "LabelSettingsHomePageBookshelfView": "Använd vy liknande en bokhylla på startsidan",
@@ -550,6 +574,8 @@
"LabelSettingsStoreMetadataWithItemHelp": "Som standard lagras metadatafiler i mappen '/metadata/items'. Genom att aktivera detta alternativ kommer metadatafilerna att lagra i din biblioteksmapp", "LabelSettingsStoreMetadataWithItemHelp": "Som standard lagras metadatafiler i mappen '/metadata/items'. Genom att aktivera detta alternativ kommer metadatafilerna att lagra i din biblioteksmapp",
"LabelSettingsTimeFormat": "Tidsformat", "LabelSettingsTimeFormat": "Tidsformat",
"LabelShare": "Dela", "LabelShare": "Dela",
"LabelShareDownloadableHelp": "Tillåt att användare som fått en delad länk att ladda ner ett komprimerat objekt från biblioteket.",
"LabelShareURL": "Dela URL-länk",
"LabelShowAll": "Visa alla", "LabelShowAll": "Visa alla",
"LabelShowSeconds": "Visa sekunder", "LabelShowSeconds": "Visa sekunder",
"LabelShowSubtitles": "Visa underrubriker", "LabelShowSubtitles": "Visa underrubriker",
@@ -586,6 +612,7 @@
"LabelTextEditorBulletedList": "Punktlista", "LabelTextEditorBulletedList": "Punktlista",
"LabelTextEditorLink": "Länk", "LabelTextEditorLink": "Länk",
"LabelTextEditorNumberedList": "Numrerad lista", "LabelTextEditorNumberedList": "Numrerad lista",
"LabelTextEditorUnlink": "Radera länk",
"LabelTheme": "Utseende", "LabelTheme": "Utseende",
"LabelThemeDark": "Mörkt", "LabelThemeDark": "Mörkt",
"LabelThemeLight": "Ljust", "LabelThemeLight": "Ljust",
@@ -601,11 +628,12 @@
"LabelTimeToShift": "Tid att skifta i sekunder", "LabelTimeToShift": "Tid att skifta i sekunder",
"LabelTitle": "Titel", "LabelTitle": "Titel",
"LabelToolsEmbedMetadata": "Infoga metadata", "LabelToolsEmbedMetadata": "Infoga metadata",
"LabelToolsEmbedMetadataDescription": "Bädda in metadata i ljudfiler, inklusive omslagsbild och kapitel.", "LabelToolsEmbedMetadataDescription": "Infoga metadata i ljudfiler, inklusive omslagsbild och kapitel.",
"LabelToolsM4bEncoder": "Omkodning av M4B-fil",
"LabelToolsMakeM4b": "Skapa ljudboksfil i M4B-format", "LabelToolsMakeM4b": "Skapa ljudboksfil i M4B-format",
"LabelToolsMakeM4bDescription": "Skapa en ljudboksfil i M4B-format med inbäddad metadata, omslagsbild och kapitel.", "LabelToolsMakeM4bDescription": "Skapa en ljudboksfil i M4B-format som inkluderar metadata, omslagsbild och kapitel.",
"LabelToolsSplitM4b": "Dela upp M4B-fil i MP3-filer", "LabelToolsSplitM4b": "Dela upp M4B-fil i MP3-filer",
"LabelToolsSplitM4bDescription": "Skapa MP3-filer från en M4B-fil uppdelad i kapitel med inbäddad metadata, omslagsbild och kapitel.", "LabelToolsSplitM4bDescription": "Skapa MP3-filer från en M4B-fil uppdelad i kapitel som inkluderar metadata, omslagsbild och kapitel.",
"LabelTotalDuration": "Total varaktighet", "LabelTotalDuration": "Total varaktighet",
"LabelTotalTimeListened": "Total tid lyssnad", "LabelTotalTimeListened": "Total tid lyssnad",
"LabelTrackFromFilename": "Spår från filnamn", "LabelTrackFromFilename": "Spår från filnamn",
@@ -654,7 +682,7 @@
"MessageAddToPlayerQueue": "Lägg till i spellistan", "MessageAddToPlayerQueue": "Lägg till i spellistan",
"MessageAppriseDescription": "För att använda den här funktionen behöver du ha en instans av <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> igång eller en API som hanterar dessa begäranden. <br />Apprise API-urlen bör vara hela URL-sökvägen för att skicka meddelandet, t.ex., om din API-instans är tillgänglig på <code>http://192.168.1.1:8337</code>, bör du ange <code>http://192.168.1.1:8337/notify</code>.", "MessageAppriseDescription": "För att använda den här funktionen behöver du ha en instans av <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> igång eller en API som hanterar dessa begäranden. <br />Apprise API-urlen bör vara hela URL-sökvägen för att skicka meddelandet, t.ex., om din API-instans är tillgänglig på <code>http://192.168.1.1:8337</code>, bör du ange <code>http://192.168.1.1:8337/notify</code>.",
"MessageBackupsDescription": "Säkerhetskopior inkluderar användare, användarnas framsteg, biblioteksobjekt,<br>serverinställningar och bilder lagrade i <code>/metadata/items</code> & <code>/metadata/authors</code>.<br>De inkluderar <strong>INTE</strong> några filer lagrade i dina biblioteksmappar.", "MessageBackupsDescription": "Säkerhetskopior inkluderar användare, användarnas framsteg, biblioteksobjekt,<br>serverinställningar och bilder lagrade i <code>/metadata/items</code> & <code>/metadata/authors</code>.<br>De inkluderar <strong>INTE</strong> några filer lagrade i dina biblioteksmappar.",
"MessageBackupsLocationEditNote": "OBS: När du ändrar plats för säkerhetskopiorna så flyttas INTE gamla säkerhetskopior dit.", "MessageBackupsLocationEditNote": "OBS: När du ändrar plats för säkerhetskopiorna så flyttas INTE gamla säkerhetskopior dit",
"MessageBackupsLocationNoEditNote": "OBS: Platsen där säkerhetskopiorna lagras bestäms av en central inställning och kan inte ändras här.", "MessageBackupsLocationNoEditNote": "OBS: Platsen där säkerhetskopiorna lagras bestäms av en central inställning och kan inte ändras här.",
"MessageBackupsLocationPathEmpty": "Uppgiften om platsen för lagring av säkerhetskopior kan inte lämnas tom", "MessageBackupsLocationPathEmpty": "Uppgiften om platsen för lagring av säkerhetskopior kan inte lämnas tom",
"MessageBatchEditPopulateMapDetailsAllHelp": "Adderar information från alla objekt nedan i de fält som aktiverats. Om fälten innehåller olika uppgifter kommer informationen att slås samman.", "MessageBatchEditPopulateMapDetailsAllHelp": "Adderar information från alla objekt nedan i de fält som aktiverats. Om fälten innehåller olika uppgifter kommer informationen att slås samman.",
@@ -692,7 +720,8 @@
"MessageConfirmMarkSeriesNotFinished": "Är du säker på att du vill markera alla böcker i denna serie som ej avslutade?", "MessageConfirmMarkSeriesNotFinished": "Är du säker på att du vill markera alla böcker i denna serie som ej avslutade?",
"MessageConfirmPurgeCache": "När du rensar cashen kommer katalogen <code>/metadata/cache</code> att raderas. <br /><br />Är du säker på att du vill radera katalogen?", "MessageConfirmPurgeCache": "När du rensar cashen kommer katalogen <code>/metadata/cache</code> att raderas. <br /><br />Är du säker på att du vill radera katalogen?",
"MessageConfirmPurgeItemsCache": "När du rensar cashen för föremål kommer katalogen <code>/metadata/cache/items</code> att raderas. <br /><br />Är du säker på att du vill radera katalogen?", "MessageConfirmPurgeItemsCache": "När du rensar cashen för föremål kommer katalogen <code>/metadata/cache/items</code> att raderas. <br /><br />Är du säker på att du vill radera katalogen?",
"MessageConfirmQuickEmbed": "VARNING! Quick embed kommer inte att säkerhetskopiera dina ljudfiler. Se till att du har en säkerhetskopia av dina ljudfiler. <br><br>Vill du fortsätta?", "MessageConfirmQuickEmbed": "VARNING! När du infogar metadata i dina ljudfiler kommer INGEN SÄKERHETSKOPIA av filerna att göras. Se därför till att först säkerhetskopiera ljudfilerna. <br><br>Vill du fortsätta?",
"MessageConfirmQuickMatchEpisodes": "Snabbmatchning av avsnitt kommer att ersätta befintlig information vid en träff. Endast omatchade avsnitt kommer att uppdateras. Vill du fortsätta?",
"MessageConfirmReScanLibraryItems": "Är du säker på att du vill göra en ny skanning för {0} objekt?", "MessageConfirmReScanLibraryItems": "Är du säker på att du vill göra en ny skanning för {0} objekt?",
"MessageConfirmRemoveAllChapters": "Är du säker på att du vill ta bort alla kapitel?", "MessageConfirmRemoveAllChapters": "Är du säker på att du vill ta bort alla kapitel?",
"MessageConfirmRemoveAuthor": "Är du säker på att du vill ta bort författaren \"{0}\"?", "MessageConfirmRemoveAuthor": "Är du säker på att du vill ta bort författaren \"{0}\"?",
@@ -705,7 +734,7 @@
"MessageConfirmRemovePlaylist": "Är du säker på att du vill ta bort din spellista \"{0}\"?", "MessageConfirmRemovePlaylist": "Är du säker på att du vill ta bort din spellista \"{0}\"?",
"MessageConfirmRenameGenre": "Är du säker på att du vill byta namn på kategorin \"{0}\" till \"{1}\" för alla objekt?", "MessageConfirmRenameGenre": "Är du säker på att du vill byta namn på kategorin \"{0}\" till \"{1}\" för alla objekt?",
"MessageConfirmRenameGenreMergeNote": "OBS: Den här kategorin finns redan, så de kommer att slås samman.", "MessageConfirmRenameGenreMergeNote": "OBS: Den här kategorin finns redan, så de kommer att slås samman.",
"MessageConfirmRenameGenreWarning": "Varning! En liknande kategori med annat skrivsätt finns redan \"{0}\".", "MessageConfirmRenameGenreWarning": "VARNING! En liknande kategori med annat skrivsätt finns redan \"{0}\".",
"MessageConfirmRenameTag": "Är du säker på att du vill byta namn på taggen \"{0}\" till \"{1}\" för alla objekt?", "MessageConfirmRenameTag": "Är du säker på att du vill byta namn på taggen \"{0}\" till \"{1}\" för alla objekt?",
"MessageConfirmRenameTagMergeNote": "OBS: Den här taggen finns redan, så de kommer att slås samman.", "MessageConfirmRenameTagMergeNote": "OBS: Den här taggen finns redan, så de kommer att slås samman.",
"MessageConfirmRenameTagWarning": "VARNING! En liknande tagg med annat skrivsätt finns redan \"{0}\".", "MessageConfirmRenameTagWarning": "VARNING! En liknande tagg med annat skrivsätt finns redan \"{0}\".",
@@ -727,7 +756,7 @@
"MessageJoinUsOn": "Anslut dig till oss på", "MessageJoinUsOn": "Anslut dig till oss på",
"MessageLoading": "Laddar...", "MessageLoading": "Laddar...",
"MessageLoadingFolders": "Laddar mappar...", "MessageLoadingFolders": "Laddar mappar...",
"MessageLogsDescription": "Filer med loggar sparas i mappen <code>/metadata/logs</code> som JSON-filer.<br>Filer med information om krascher sparas i <code>/metadata/logs/crash_logs.txt</code>.", "MessageLogsDescription": "Filer med loggningsinformation sparas i mappen <code>/metadata/logs</code> som JSON-filer.<br>Filer med information om krascher sparas i <code>/metadata/logs/crash_logs.txt</code>.",
"MessageM4BFailed": "M4B misslyckades!", "MessageM4BFailed": "M4B misslyckades!",
"MessageM4BFinished": "M4B klar!", "MessageM4BFinished": "M4B klar!",
"MessageMapChapterTitles": "Kartlägg kapitelrubriker till dina befintliga ljudbokskapitel utan att justera tidstämplar", "MessageMapChapterTitles": "Kartlägg kapitelrubriker till dina befintliga ljudbokskapitel utan att justera tidstämplar",
@@ -735,13 +764,13 @@
"MessageMarkAllEpisodesNotFinished": "Markera alla avsnitt som ej avslutade", "MessageMarkAllEpisodesNotFinished": "Markera alla avsnitt som ej avslutade",
"MessageMarkAsFinished": "Markera som avslutad", "MessageMarkAsFinished": "Markera som avslutad",
"MessageMarkAsNotFinished": "Markera som ej avslutad", "MessageMarkAsNotFinished": "Markera som ej avslutad",
"MessageMatchBooksDescription": "kommer att försöka matcha böcker i biblioteket med en bok från<br/>den valda källan och fylla i uppgifter som saknas och omslag.<br/>Inga befintliga uppgifter kommer att ersättas.", "MessageMatchBooksDescription": "kommer att försöka matcha böcker i biblioteket med en bok från den valda källan och fylla i uppgifter som saknas och omslag. Inga befintliga uppgifter kommer att ersättas.",
"MessageNoAudioTracks": "Inga ljudspår har hittats", "MessageNoAudioTracks": "Inga ljudspår har hittats",
"MessageNoAuthors": "Inga författare", "MessageNoAuthors": "Inga författare",
"MessageNoBackups": "Inga säkerhetskopior", "MessageNoBackups": "Inga säkerhetskopior",
"MessageNoBookmarks": "Inga bokmärken", "MessageNoBookmarks": "Inga bokmärken",
"MessageNoChapters": "Inga kapitel", "MessageNoChapters": "Inga kapitel",
"MessageNoCollections": "Inga samlingar", "MessageNoCollections": "Inga Samlingar",
"MessageNoCoversFound": "Inga omslag hittades", "MessageNoCoversFound": "Inga omslag hittades",
"MessageNoDescription": "Ingen beskrivning", "MessageNoDescription": "Ingen beskrivning",
"MessageNoDevices": "Inga enheter angivna", "MessageNoDevices": "Inga enheter angivna",
@@ -751,11 +780,11 @@
"MessageNoEpisodes": "Inga avsnitt", "MessageNoEpisodes": "Inga avsnitt",
"MessageNoFoldersAvailable": "Inga mappar tillgängliga", "MessageNoFoldersAvailable": "Inga mappar tillgängliga",
"MessageNoGenres": "Inga kategorier", "MessageNoGenres": "Inga kategorier",
"MessageNoIssues": "Inga problem", "MessageNoIssues": "Inga objekt med problem hittades",
"MessageNoItems": "Inga objekt", "MessageNoItems": "Inga objekt",
"MessageNoItemsFound": "Inga objekt hittades", "MessageNoItemsFound": "Inga objekt hittades",
"MessageNoListeningSessions": "Inga lyssningstillfällen", "MessageNoListeningSessions": "Inga lyssningstillfällen",
"MessageNoLogs": "Inga loggar", "MessageNoLogs": "Inga loggningsinformation finns",
"MessageNoMediaProgress": "Ingen medieförlopp", "MessageNoMediaProgress": "Ingen medieförlopp",
"MessageNoNotifications": "Inga aviseringar", "MessageNoNotifications": "Inga aviseringar",
"MessageNoPodcastsFound": "Inga podcasts hittade", "MessageNoPodcastsFound": "Inga podcasts hittade",
@@ -775,6 +804,8 @@
"MessagePleaseWait": "Vänta ett ögonblick...", "MessagePleaseWait": "Vänta ett ögonblick...",
"MessagePodcastHasNoRSSFeedForMatching": "Podcasten har ingen RSS-flödes-URL att använda för matchning", "MessagePodcastHasNoRSSFeedForMatching": "Podcasten har ingen RSS-flödes-URL att använda för matchning",
"MessagePodcastSearchField": "Skriv sökfrågan eller URL-adressen för RSS-flödet", "MessagePodcastSearchField": "Skriv sökfrågan eller URL-adressen för RSS-flödet",
"MessageQuickEmbedInProgress": "Infogande av metadata pågår",
"MessageQuickEmbedQueue": "Kö för infogaden av metadata ({0} objekt i kön)",
"MessageQuickMatchAllEpisodes": "Snabbmatchning av alla avsnitt", "MessageQuickMatchAllEpisodes": "Snabbmatchning av alla avsnitt",
"MessageQuickMatchDescription": "Adderar uppgifter som saknas samt en omslagsbild från<br>första träffen i resultatet vid sökningen från '{0}'.<br>Skriver inte över befintliga uppgifter om inte<br>inställningen 'Prioritera matchad metadata' är aktiverad.", "MessageQuickMatchDescription": "Adderar uppgifter som saknas samt en omslagsbild från<br>första träffen i resultatet vid sökningen från '{0}'.<br>Skriver inte över befintliga uppgifter om inte<br>inställningen 'Prioritera matchad metadata' är aktiverad.",
"MessageRemoveChapter": "Ta bort kapitel", "MessageRemoveChapter": "Ta bort kapitel",
@@ -786,23 +817,32 @@
"MessageRestoreBackupConfirm": "Är du säker på att du vill läsa in säkerhetskopian som skapades den", "MessageRestoreBackupConfirm": "Är du säker på att du vill läsa in säkerhetskopian som skapades den",
"MessageRestoreBackupWarning": "Att återställa en säkerhetskopia kommer att skriva över hela databasen som finns i /config och omslagsbilder i /metadata/items & /metadata/authors.<br /><br />Säkerhetskopior ändrar inte några filer i dina biblioteksmappar. Om du har aktiverat serverinställningar för att lagra omslagskonst och metadata i dina biblioteksmappar säkerhetskopieras eller skrivs de inte över.<br /><br />Alla klienter som använder din server kommer att uppdateras automatiskt.", "MessageRestoreBackupWarning": "Att återställa en säkerhetskopia kommer att skriva över hela databasen som finns i /config och omslagsbilder i /metadata/items & /metadata/authors.<br /><br />Säkerhetskopior ändrar inte några filer i dina biblioteksmappar. Om du har aktiverat serverinställningar för att lagra omslagskonst och metadata i dina biblioteksmappar säkerhetskopieras eller skrivs de inte över.<br /><br />Alla klienter som använder din server kommer att uppdateras automatiskt.",
"MessageScheduleLibraryScanNote": "För de flesta användare rekommenderas att denna funktion ej aktiveras. Istället bör funktionen 'Watcher' vara aktiverad. Watcher kommer då automatiskt identifiera förändringar i biblioteket. För vissa filsystem (som t.ex. NFS) fungerar inte Watcher. Då kan schemalagda skanningar av biblioteken användas istället.", "MessageScheduleLibraryScanNote": "För de flesta användare rekommenderas att denna funktion ej aktiveras. Istället bör funktionen 'Watcher' vara aktiverad. Watcher kommer då automatiskt identifiera förändringar i biblioteket. För vissa filsystem (som t.ex. NFS) fungerar inte Watcher. Då kan schemalagda skanningar av biblioteken användas istället.",
"MessageScheduleRunEveryWeekdayAtTime": "Startar varje {0} klockan {1}",
"MessageSearchResultsFor": "Sökresultat för", "MessageSearchResultsFor": "Sökresultat för",
"MessageSelected": "{0} valda", "MessageSelected": "{0} valda",
"MessageServerCouldNotBeReached": "Servern kunde inte nås", "MessageServerCouldNotBeReached": "Servern kunde inte nås",
"MessageSetChaptersFromTracksDescription": "Ställ in kapitel med varje ljudfil som ett kapitel och kapitelrubrik som ljudfilens namn", "MessageSetChaptersFromTracksDescription": "Ställ in kapitel med varje ljudfil som ett kapitel och kapitelrubrik som ljudfilens namn",
"MessageStartPlaybackAtTime": "Starta uppspelning av \"{0}\" vid tidpunkt {1}?", "MessageStartPlaybackAtTime": "Starta uppspelning av \"{0}\" vid tidpunkt {1}?",
"MessageTaskAudioFileNotWritable": "Det går inte att skriva till ljudfilen \"{0}\"",
"MessageTaskCanceledByUser": "Uppgiften avslutades av användaren", "MessageTaskCanceledByUser": "Uppgiften avslutades av användaren",
"MessageTaskDownloadingEpisodeDescription": "Laddar ner avsnitt \"{0}\"", "MessageTaskDownloadingEpisodeDescription": "Laddar ner avsnitt \"{0}\"",
"MessageTaskEmbeddingMetadata": "Infogar metadata", "MessageTaskEmbeddingMetadata": "Infogar metadata",
"MessageTaskEmbeddingMetadataDescription": "Infogar metadata i ljudboken \"{0}\"", "MessageTaskEmbeddingMetadataDescription": "Infogar metadata i ljudboken \"{0}\"",
"MessageTaskEncodingM4bDescription": "Omkodning av ljudbok \"{0}\" till en M4B-fil", "MessageTaskEncodingM4bDescription": "Omkodning av ljudbok \"{0}\" till en M4B-fil",
"MessageTaskFailed": "Misslyckades", "MessageTaskFailed": "Misslyckades",
"MessageTaskFailedToBackupAudioFile": "Misslyckades med att göra backup på ljudfil \"{0}\"",
"MessageTaskFailedToCreateCacheDirectory": "Misslyckades med att skapa bibliotek för cachen", "MessageTaskFailedToCreateCacheDirectory": "Misslyckades med att skapa bibliotek för cachen",
"MessageTaskFailedToEmbedMetadataInFile": "Misslyckades med att infoga metadata i \"{0}\"", "MessageTaskFailedToEmbedMetadataInFile": "Misslyckades med att infoga metadata i \"{0}\"",
"MessageTaskFailedToMergeAudioFiles": "Misslyckades med att sammanfoga ljudfilerna", "MessageTaskFailedToMergeAudioFiles": "Misslyckades med att sammanfoga ljudfilerna",
"MessageTaskFailedToMoveM4bFile": "Misslyckades med att flytta M4B-filen", "MessageTaskFailedToMoveM4bFile": "Misslyckades med att flytta M4B-filen",
"MessageTaskFailedToWriteMetadataFile": "Misslyckades med att skapa filen med metadata", "MessageTaskFailedToWriteMetadataFile": "Misslyckades med att skapa filen med metadata",
"MessageTaskMatchingBooksInLibrary": "Matchar böcker i biblioteket \"{0}\"", "MessageTaskMatchingBooksInLibrary": "Matchar böcker i biblioteket \"{0}\"",
"MessageTaskNoFilesToScan": "Inga filer finns tillgängliga för skanning",
"MessageTaskOpmlImportDescription": "Skapar podcasts från {0} RSS-flöden",
"MessageTaskOpmlImportFeedDescription": "Importerar RSS-flödet \"{0}\"",
"MessageTaskOpmlImportFeedPodcastDescription": "Skapar podcast \"{0}\"",
"MessageTaskOpmlImportFeedPodcastExists": "En podcast finns redan med den adressen",
"MessageTaskOpmlImportFeedPodcastFailed": "Misslyckades med att skapa podcast",
"MessageTaskOpmlImportFinished": "Adderade {0} podcasts", "MessageTaskOpmlImportFinished": "Adderade {0} podcasts",
"MessageTaskOpmlParseFailed": "Misslyckades att tolka OPML-filen", "MessageTaskOpmlParseFailed": "Misslyckades att tolka OPML-filen",
"MessageTaskOpmlParseFastFail": "Felaktig OPML-fil. Ingen <opml> tag eller <outline> tag finns i filen", "MessageTaskOpmlParseFastFail": "Felaktig OPML-fil. Ingen <opml> tag eller <outline> tag finns i filen",
@@ -811,10 +851,12 @@
"MessageTaskScanItemsMissing": "{0} saknades", "MessageTaskScanItemsMissing": "{0} saknades",
"MessageTaskScanItemsUpdated": "{0} uppdaterades", "MessageTaskScanItemsUpdated": "{0} uppdaterades",
"MessageTaskScanNoChangesNeeded": "Inget adderades eller uppdaterades", "MessageTaskScanNoChangesNeeded": "Inget adderades eller uppdaterades",
"MessageTaskScanningFileChanges": "Söker efter ändrade filer i \"{0}\"",
"MessageTaskScanningLibrary": "Biblioteket \"{0}\" har skannats", "MessageTaskScanningLibrary": "Biblioteket \"{0}\" har skannats",
"MessageTaskTargetDirectoryNotWritable": "Det är inte tillåtet att skriva i den angivna katalogen",
"MessageThinking": "Tänker...", "MessageThinking": "Tänker...",
"MessageUploaderItemFailed": "Misslyckades med att ladda upp", "MessageUploaderItemFailed": "Misslyckades med att ladda upp",
"MessageUploaderItemSuccess": "Uppladdning lyckades!", "MessageUploaderItemSuccess": "har blivit uppladdad!",
"MessageUploading": "Laddar upp...", "MessageUploading": "Laddar upp...",
"MessageValidCronExpression": "Giltigt cron-uttryck", "MessageValidCronExpression": "Giltigt cron-uttryck",
"MessageWatcherIsDisabledGlobally": "Watcher är inaktiverad centralt under rubriken 'Inställningar'", "MessageWatcherIsDisabledGlobally": "Watcher är inaktiverad centralt under rubriken 'Inställningar'",
@@ -829,6 +871,9 @@
"NoteUploaderFoldersWithMediaFiles": "Mappar som innehåller mediefiler hanteras som separata objekt i biblioteket.", "NoteUploaderFoldersWithMediaFiles": "Mappar som innehåller mediefiler hanteras som separata objekt i biblioteket.",
"NoteUploaderOnlyAudioFiles": "Om du bara laddar upp ljudfiler kommer varje ljudfil att hanteras som en separat ljudbok.", "NoteUploaderOnlyAudioFiles": "Om du bara laddar upp ljudfiler kommer varje ljudfil att hanteras som en separat ljudbok.",
"NoteUploaderUnsupportedFiles": "Oaccepterade filer ignoreras. När du väljer eller släpper en mapp ignoreras andra filer som inte finns i ett objektmapp.", "NoteUploaderUnsupportedFiles": "Oaccepterade filer ignoreras. När du väljer eller släpper en mapp ignoreras andra filer som inte finns i ett objektmapp.",
"NotificationOnBackupCompletedDescription": "Aktiveras när en backup är genomförd",
"NotificationOnBackupFailedDescription": "Aktiveras när en backup misslyckas",
"NotificationOnEpisodeDownloadedDescription": "Aktiveras när avsnitt i en podcast automatiskt har hämtats",
"PlaceholderNewCollection": "Nytt namn på samlingen", "PlaceholderNewCollection": "Nytt namn på samlingen",
"PlaceholderNewFolderPath": "Nytt sökväg till mappen", "PlaceholderNewFolderPath": "Nytt sökväg till mappen",
"PlaceholderNewPlaylist": "Nytt namn på spellistan", "PlaceholderNewPlaylist": "Nytt namn på spellistan",
@@ -870,6 +915,7 @@
"ToastBackupRestoreFailed": "Det gick inte att återställa säkerhetskopian", "ToastBackupRestoreFailed": "Det gick inte att återställa säkerhetskopian",
"ToastBackupUploadFailed": "Det gick inte att ladda upp säkerhetskopian", "ToastBackupUploadFailed": "Det gick inte att ladda upp säkerhetskopian",
"ToastBackupUploadSuccess": "Säkerhetskopian uppladdad", "ToastBackupUploadSuccess": "Säkerhetskopian uppladdad",
"ToastBatchQuickMatchStarted": "Snabbmatchning av {0} böcker har påbörjats!",
"ToastBatchUpdateFailed": "Batchuppdateringen misslyckades", "ToastBatchUpdateFailed": "Batchuppdateringen misslyckades",
"ToastBatchUpdateSuccess": "Batchuppdateringen lyckades", "ToastBatchUpdateSuccess": "Batchuppdateringen lyckades",
"ToastBookmarkCreateFailed": "Det gick inte att skapa bokmärket", "ToastBookmarkCreateFailed": "Det gick inte att skapa bokmärket",
@@ -888,9 +934,12 @@
"ToastDateTimeInvalidOrIncomplete": "Datum och klockslag är felaktigt eller ej komplett", "ToastDateTimeInvalidOrIncomplete": "Datum och klockslag är felaktigt eller ej komplett",
"ToastDeleteFileFailed": "Misslyckades att radera filen", "ToastDeleteFileFailed": "Misslyckades att radera filen",
"ToastDeleteFileSuccess": "Filen har raderats", "ToastDeleteFileSuccess": "Filen har raderats",
"ToastDeviceAddFailed": "Misslyckades med att addera enheten",
"ToastDeviceNameAlreadyExists": "En enhet för att läsa e-böcker med det namnet finns redan",
"ToastDeviceTestEmailFailed": "Misslyckades med att skicka ett testmail", "ToastDeviceTestEmailFailed": "Misslyckades med att skicka ett testmail",
"ToastDeviceTestEmailSuccess": "Ett testmail har skickats", "ToastDeviceTestEmailSuccess": "Ett testmail har skickats",
"ToastEmailSettingsUpdateSuccess": "Inställningarna av e-post har uppdaterats", "ToastEmailSettingsUpdateSuccess": "Inställningarna av e-post har uppdaterats",
"ToastEncodeCancelFailed": "Misslyckades med att avbryta omkodningen",
"ToastEncodeCancelSucces": "Omkodningen avbruten", "ToastEncodeCancelSucces": "Omkodningen avbruten",
"ToastEpisodeDownloadQueueClearFailed": "Misslyckades med att tömma kön", "ToastEpisodeDownloadQueueClearFailed": "Misslyckades med att tömma kön",
"ToastEpisodeDownloadQueueClearSuccess": "Kö för nedladdning av avsnitt har tömts", "ToastEpisodeDownloadQueueClearSuccess": "Kö för nedladdning av avsnitt har tömts",
@@ -931,6 +980,7 @@
"ToastNewUserTagError": "Minst en tagg måste läggas till", "ToastNewUserTagError": "Minst en tagg måste läggas till",
"ToastNewUserUsernameError": "Ange ett användarnamn", "ToastNewUserUsernameError": "Ange ett användarnamn",
"ToastNoNewEpisodesFound": "Inga nya avsnitt kunde hittas", "ToastNoNewEpisodesFound": "Inga nya avsnitt kunde hittas",
"ToastNoRSSFeed": "Denna podcast har ingen RSS-flöde",
"ToastNoUpdatesNecessary": "Inga uppdateringar var nödvändiga", "ToastNoUpdatesNecessary": "Inga uppdateringar var nödvändiga",
"ToastNotificationCreateFailed": "Misslyckades med att skapa meddelandet", "ToastNotificationCreateFailed": "Misslyckades med att skapa meddelandet",
"ToastNotificationDeleteFailed": "Misslyckades med att radera meddelandet", "ToastNotificationDeleteFailed": "Misslyckades med att radera meddelandet",
@@ -942,6 +992,7 @@
"ToastPodcastCreateFailed": "Misslyckades med att skapa podcasten", "ToastPodcastCreateFailed": "Misslyckades med att skapa podcasten",
"ToastPodcastCreateSuccess": "Podcasten skapades framgångsrikt", "ToastPodcastCreateSuccess": "Podcasten skapades framgångsrikt",
"ToastPodcastNoEpisodesInFeed": "Inga avsnitt finns i RSS-flödet", "ToastPodcastNoEpisodesInFeed": "Inga avsnitt finns i RSS-flödet",
"ToastPodcastNoRssFeed": "Denna podcast har ingen RSS-flöde",
"ToastProviderCreatedFailed": "Misslyckades med att addera en källa", "ToastProviderCreatedFailed": "Misslyckades med att addera en källa",
"ToastProviderCreatedSuccess": "En ny källa har adderats", "ToastProviderCreatedSuccess": "En ny källa har adderats",
"ToastProviderNameAndUrlRequired": "Ett namn och en URL-adress krävs", "ToastProviderNameAndUrlRequired": "Ett namn och en URL-adress krävs",
+209 -1
View File
@@ -1 +1,209 @@
{} {
"ButtonAdd": "Ekle",
"ButtonAddChapters": "Bölüm Ekle",
"ButtonAddDevice": "Cihaz Ekle",
"ButtonAddLibrary": "Kütüphane Ekle",
"ButtonAddPodcasts": "Podcast Ekle",
"ButtonAddUser": "Kullanıcı Ekle",
"ButtonAddYourFirstLibrary": "İlk kütüphaneni ekle",
"ButtonApply": "Uygula",
"ButtonApplyChapters": "Bölümleri Uygula",
"ButtonAuthors": "Yazarlar",
"ButtonBack": "Geri",
"ButtonBatchEditPopulateFromExisting": "Mevcut olandan çoğalt",
"ButtonBatchEditPopulateMapDetails": "Harita detaylarını çoğalt",
"ButtonBrowseForFolder": "Klasör için göz at",
"ButtonCancel": "İptal",
"ButtonCancelEncode": "Kodlamayı Durdur",
"ButtonChangeRootPassword": "Root Şifresini Değiştir",
"ButtonCheckAndDownloadNewEpisodes": "Yeni Bölümleri Kontrol Et & İndir",
"ButtonChooseAFolder": "Klasör seç",
"ButtonChooseFiles": "Dosya seç",
"ButtonClearFilter": "Filtreyi Temizle",
"ButtonCloseFeed": "Akışı Kapat",
"ButtonCloseSession": "Acık Oturumu Kapat",
"ButtonCollections": "Koleksiyonlar",
"ButtonConfigureScanner": "Tarayıcı Ayarları",
"ButtonCreate": "Oluştur",
"ButtonCreateBackup": "Yedek Oluştur",
"ButtonDelete": "Sil",
"ButtonDownloadQueue": "Sıra",
"ButtonEdit": "Düzenle",
"ButtonEditChapters": "Bölümleri Düzenle",
"ButtonEditPodcast": "Podcast Düzenle",
"ButtonEnable": "Etkinleştir",
"ButtonFireAndFail": "Gönder ve hata al",
"ButtonFireOnTest": "onTest Gönder",
"ButtonForceReScan": "Zorla Yeniden Tara",
"ButtonFullPath": "Tam Dosya Yolu",
"ButtonHide": "Gizle",
"ButtonHome": "Ana sayfa",
"ButtonIssues": "Sorunlar",
"ButtonJumpBackward": "Geri Sar",
"ButtonJumpForward": "İleri Sar",
"ButtonLatest": "En yeni",
"ButtonLibrary": "Kütüphane",
"ButtonLogout": "Çıkış Yap",
"ButtonLookup": "Sorgula",
"ButtonManageTracks": "Parçaları Yönet",
"ButtonMapChapterTitles": "Bölüm Başlıklarını Haritalandır",
"ButtonNevermind": "Vazgeç",
"ButtonNext": "Sonraki",
"ButtonNextChapter": "Sonraki Bölüm",
"ButtonNextItemInQueue": "Sıradaki Sonraki Öğe",
"ButtonOk": "Tamam",
"ButtonOpenFeed": "Akışı Aç",
"ButtonOpenManager": "Yöneticiyi Aç",
"ButtonPause": "Durdur",
"ButtonPlay": "Oynat",
"ButtonPlayAll": "Hepsini Oynat",
"ButtonPlaying": "Oynatılıyor",
"ButtonPlaylists": "Oynatma listeleri",
"ButtonPrevious": "Önceki",
"ButtonPreviousChapter": "Önceki Bölüm",
"ButtonProbeAudioFile": "Ses Dosyasını Yokla",
"ButtonPurgeAllCache": "Bütün Önbelleği Temizle",
"ButtonPurgeItemsCache": "Öğenin Önbelleğini Temizle",
"ButtonQueueAddItem": "Sıraya ekle",
"ButtonQueueRemoveItem": "Sıradan çıkar",
"ButtonReScan": "Yeniden Tara",
"ButtonRead": "Oku",
"ButtonReadLess": "Daha az göster",
"ButtonReadMore": "Daha fazla göster",
"ButtonRefresh": "Yenile",
"ButtonRemove": "Kaldır",
"ButtonRemoveAll": "Hepsini Sil",
"ButtonRemoveAllLibraryItems": "Bütün Kütüphane Öğelerini Sil",
"ButtonSave": "Kaydet",
"ButtonSearch": "Ara",
"ButtonSeries": "Dizi",
"ButtonSubmit": "Gönder",
"ButtonYes": "Evet",
"HeaderAccount": "Hesap",
"HeaderAdvanced": "Gelişmiş",
"HeaderAudioTracks": "Ses Kanalları",
"HeaderChapters": "Bölümler",
"HeaderCollection": "Koleksiyon",
"HeaderCollectionItems": "Koleksiyon Öğeleri",
"HeaderDetails": "Detaylar",
"HeaderEbookFiles": "Ebook Dosyaları",
"HeaderEpisodes": "Bölümler",
"HeaderEreaderSettings": "Ereader Ayarları",
"HeaderLatestEpisodes": "En son bölümler",
"HeaderLibraries": "Kütüphaneler",
"HeaderOpenRSSFeed": "RSS Akışını Aç",
"HeaderPlaylist": "Oynatma listesi",
"HeaderPlaylistItems": "Oynatma Listesi Öğeleri",
"HeaderRSSFeedGeneral": "RSS Detayları",
"HeaderRSSFeedIsOpen": "RSS Akışı Açık",
"HeaderSettings": "Ayarlar",
"HeaderSleepTimer": "Uyku Zamanlayıcısı",
"HeaderStatsMinutesListeningChart": "Dinlenilen Dakika (son 7 gün)",
"HeaderStatsRecentSessions": "Geçmiş Oturumlar",
"HeaderTableOfContents": "İçindekiler",
"HeaderYourStats": "İstatistiklerin",
"LabelAddToPlaylist": "Oynatma Listesine Ekle",
"LabelAddedAt": "Eklenme Zamanı",
"LabelAddedDate": "Eklendi {0}",
"LabelAll": "Hepsi",
"LabelAuthor": "Yazar",
"LabelAuthorFirstLast": "Yazar (İlk Son)",
"LabelAuthorLastFirst": "Yazar (Son, İlk)",
"LabelAuthors": "Yazarlar",
"LabelAutoDownloadEpisodes": "Bölümleri Otomatik Olarak İndir",
"LabelBooks": "Kitaplar",
"LabelChapters": "Bölümler",
"LabelClosePlayer": "Oynatıcıyı kapat",
"LabelCollapseSeries": "Seriyi Daralt",
"LabelComplete": "Tamamlandı",
"LabelContinueListening": "Dinlemeye Devam Et",
"LabelContinueReading": "Okumaya Devam Et",
"LabelContinueSeries": "Seriye Devam Et",
"LabelDescription": "Açıklama",
"LabelDiscover": "Keşfet",
"LabelDownload": "İndir",
"LabelDuration": "Süre",
"LabelEbook": "Ekitap",
"LabelEbooks": "Ekitaplar",
"LabelEnable": "Etkinleştir",
"LabelEnd": "Son",
"LabelEndOfChapter": "Bölüm Sonu",
"LabelEpisode": "Bölüm",
"LabelFeedURL": "Akış URLsi",
"LabelFile": "Dosya",
"LabelFileBirthtime": "Dosya Oluşum Zamanı",
"LabelFileModified": "Dosya Düzenlendi",
"LabelFilename": "Dosya İsmi",
"LabelFinished": "Tamamlandı",
"LabelFolder": "Klasör",
"LabelFontBoldness": "Font Kalınlığı",
"LabelFontScale": "Font büyüklüğü",
"LabelGenre": "Tür",
"LabelGenres": "Türler",
"LabelHasEbook": "Ekitabı var",
"LabelHasSupplementaryEbook": "İlave ekitabı var",
"LabelHost": "Sunucu",
"LabelInProgress": "İlerleme Halinde",
"LabelIncomplete": "Tamamlanmamış",
"LabelLanguage": "Dil",
"LabelLayout": "Düzen",
"LabelLayoutSinglePage": "Tek sayfa",
"LabelLineSpacing": "Satır aralığı",
"LabelListenAgain": "Tekrar Dinle",
"LabelMediaType": "Medya Türü",
"LabelMissing": "Kayıp",
"LabelMore": "Daha fazla",
"LabelMoreInfo": "Daha fazla bilgi",
"LabelName": "İsim",
"LabelNarrator": "Anlatıcı",
"LabelNarrators": "Anlatıcılar",
"LabelNewestAuthors": "En Yeni Yazarlar",
"LabelNewestEpisodes": "En Yeni Bölümler",
"LabelNotFinished": "Tamamlanmadı",
"LabelNotStarted": "Başlanmadı",
"LabelNumberOfEpisodes": "Bölüm Sayısı",
"LabelPassword": "Şifre",
"LabelPath": "Yol",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcastler",
"LabelPreventIndexing": "Akışınızın iTunes ve Google podcast dizinleri tarafından dizinlenmesini önleyin",
"LabelProgress": "İlerleme",
"LabelPubDate": "Yay. Tarihi",
"LabelPublishYear": "Yayım Yılı",
"LabelPublishedDate": "Yayımlandı {0}",
"LabelRSSFeedCustomOwnerEmail": "Özelleştirilmiş sahip Emaili",
"LabelRSSFeedCustomOwnerName": "Özelleştirilmis sahip İsmi",
"LabelRSSFeedPreventIndexing": "Dizinlemeyi Önle",
"LabelRandomly": "Rastgele",
"LabelRead": "Oku",
"LabelReadAgain": "Tekrar Oku",
"LabelRecentlyAdded": "Yakınlarda Eklenmiş",
"LabelSeason": "Sezon",
"LabelSetEbookAsPrimary": "Birincil olarak ayarla",
"LabelSetEbookAsSupplementary": "Yedek olarak ayarla",
"LabelShowAll": "Hepsini Göster",
"LabelSize": "Boyut",
"LabelSleepTimer": "Uyku Zamanlayıcısı",
"LabelStart": "Başla",
"LabelStatsBestDay": "En İyi Gün",
"LabelStatsDailyAverage": "Günlük Ortalama",
"LabelStatsDays": "Günler",
"LabelStatsDaysListened": "Dinlenen Günler",
"LabelStatsInARow": "art arda",
"LabelStatsItemsFinished": "Bitirilen Öğeler",
"LabelStatsMinutes": "dakika",
"LabelStatsMinutesListening": "Dinlenen Dakika",
"LabelTag": "Etiket",
"LabelTags": "Etiketler",
"LabelTheme": "Tema",
"LabelThemeDark": "Koyu",
"LabelThemeLight": "Açık",
"LabelTimeRemaining": "{0} kalan",
"LabelTitle": "Başlık",
"LabelTracks": "Parçalar",
"LabelType": "Tür",
"LabelUnknown": "Bilinmeyen",
"LabelUser": "Kullanıcı",
"LabelUsername": "Kullanıcı Adı",
"LabelYourBookmarks": "Yer İşaretleriniz"
}
+7 -6
View File
@@ -219,6 +219,7 @@
"LabelAccountTypeAdmin": "Адміністратор", "LabelAccountTypeAdmin": "Адміністратор",
"LabelAccountTypeGuest": "Гість", "LabelAccountTypeGuest": "Гість",
"LabelAccountTypeUser": "Користувач", "LabelAccountTypeUser": "Користувач",
"LabelActivities": "Діяльність",
"LabelActivity": "Активність", "LabelActivity": "Активність",
"LabelAddToCollection": "Додати у добірку", "LabelAddToCollection": "Додати у добірку",
"LabelAddToCollectionBatch": "Додати книги до добірки: {0}", "LabelAddToCollectionBatch": "Додати книги до добірки: {0}",
@@ -283,6 +284,7 @@
"LabelContinueSeries": "Продовжити серії", "LabelContinueSeries": "Продовжити серії",
"LabelCover": "Обкладинка", "LabelCover": "Обкладинка",
"LabelCoverImageURL": "URL-адреса обкладинки", "LabelCoverImageURL": "URL-адреса обкладинки",
"LabelCoverProvider": "Постачальник покриття",
"LabelCreatedAt": "Дата створення", "LabelCreatedAt": "Дата створення",
"LabelCronExpression": "Команда cron", "LabelCronExpression": "Команда cron",
"LabelCurrent": "Поточне", "LabelCurrent": "Поточне",
@@ -391,6 +393,7 @@
"LabelIntervalEvery6Hours": "Кожні 6 годин", "LabelIntervalEvery6Hours": "Кожні 6 годин",
"LabelIntervalEveryDay": "Щодня", "LabelIntervalEveryDay": "Щодня",
"LabelIntervalEveryHour": "Щогодини", "LabelIntervalEveryHour": "Щогодини",
"LabelIntervalEveryMinute": "Кожну хвилину",
"LabelInvert": "Інвертувати", "LabelInvert": "Інвертувати",
"LabelItem": "Елемент", "LabelItem": "Елемент",
"LabelJumpBackwardAmount": "Час переходу назад", "LabelJumpBackwardAmount": "Час переходу назад",
@@ -555,11 +558,8 @@
"LabelSettingsBookshelfViewHelp": "Імітує вигляд дерев'яних полиць", "LabelSettingsBookshelfViewHelp": "Імітує вигляд дерев'яних полиць",
"LabelSettingsChromecastSupport": "Підтримка Chromecast", "LabelSettingsChromecastSupport": "Підтримка Chromecast",
"LabelSettingsDateFormat": "Формат дати", "LabelSettingsDateFormat": "Формат дати",
"LabelSettingsDisableWatcher": "Вимкнути спостерігача", "LabelSettingsEnableWatcher": "Автоматично сканувати бібліотеки на наявність змін",
"LabelSettingsDisableWatcherForLibrary": "Вимкнути спостерігання тек бібліотеки", "LabelSettingsEnableWatcherForLibrary": "Автоматично сканувати бібліотеку на наявність змін",
"LabelSettingsDisableWatcherHelp": "Вимикає автоматичне додавання/оновлення елементів, коли спостерігаються зміни файлів. *Потребує перезавантаження сервера",
"LabelSettingsEnableWatcher": "Увімкнути спостерігача",
"LabelSettingsEnableWatcherForLibrary": "Увімкнути спостерігання тек бібліотеки",
"LabelSettingsEnableWatcherHelp": "Вмикає автоматичне додавання/оновлення елементів, коли спостерігаються зміни файлів. *Потребує перезавантаження сервера", "LabelSettingsEnableWatcherHelp": "Вмикає автоматичне додавання/оновлення елементів, коли спостерігаються зміни файлів. *Потребує перезавантаження сервера",
"LabelSettingsEpubsAllowScriptedContent": "Дозволити JavaScript-вміст у epub", "LabelSettingsEpubsAllowScriptedContent": "Дозволити JavaScript-вміст у epub",
"LabelSettingsEpubsAllowScriptedContentHelp": "Дозволяти epub-файлам виконувати код. Вмикайте цей параметр лише якщо ви довіряєте джерелу epub-файлів.", "LabelSettingsEpubsAllowScriptedContentHelp": "Дозволяти epub-файлам виконувати код. Вмикайте цей параметр лише якщо ви довіряєте джерелу epub-файлів.",
@@ -577,7 +577,7 @@
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропускати попередні книги у Продовжити серії", "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Пропускати попередні книги у Продовжити серії",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Полиця Продовжити серії на головній сторінці показує найпершу непочату книгу з тих серій, у яких ви завершили хоча б одну книгу та не маєте книг у процесі. Якщо увімкнути це налаштування, то серії продовжуватимуться з останньої завершеної книги, а не з першої непочатої.", "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Полиця Продовжити серії на головній сторінці показує найпершу непочату книгу з тих серій, у яких ви завершили хоча б одну книгу та не маєте книг у процесі. Якщо увімкнути це налаштування, то серії продовжуватимуться з останньої завершеної книги, а не з першої непочатої.",
"LabelSettingsParseSubtitles": "Дістати підзаголовки", "LabelSettingsParseSubtitles": "Дістати підзаголовки",
"LabelSettingsParseSubtitlesHelp": "Дістати підзаголовки з назв тек аудіокниг.<br>Підзаголовок мусить йти після \" - \"<br>Наприклад, \"Назва книги - Це підзаголовок\" має підзаголовок \"Це підзаголовок\"", "LabelSettingsParseSubtitlesHelp": "Витягти субтитри з імен папок аудіокниг.<br>Підзаголовки мають бути розділені символом \" - \"<br>тобто. «Назва книги тут підзаголовок» має підзаголовок «Тут підзаголовок»",
"LabelSettingsPreferMatchedMetadata": "Надавати перевагу віднайденим метаданим", "LabelSettingsPreferMatchedMetadata": "Надавати перевагу віднайденим метаданим",
"LabelSettingsPreferMatchedMetadataHelp": "Подробиці буде перезаписано віднайденими даними Швидкого пошуку. Без цього Швидкий пошук заповнить лише подробиці, яких бракує.", "LabelSettingsPreferMatchedMetadataHelp": "Подробиці буде перезаписано віднайденими даними Швидкого пошуку. Без цього Швидкий пошук заповнить лише подробиці, яких бракує.",
"LabelSettingsSkipMatchingBooksWithASIN": "Не шукати книги, що мають ASIN", "LabelSettingsSkipMatchingBooksWithASIN": "Не шукати книги, що мають ASIN",
@@ -845,6 +845,7 @@
"MessageRestoreBackupConfirm": "Ви дійсно бажаєте відновити резервну копію від", "MessageRestoreBackupConfirm": "Ви дійсно бажаєте відновити резервну копію від",
"MessageRestoreBackupWarning": "Відновлення резервної копії перезапише всю базу даних, розташовану в /config, і зображення обкладинок в /metadata/items та /metadata/authors.<br /><br />Резервні копії не змінюють жодних файлів у теках бібліотеки. Якщо у налаштуваннях сервера увімкнено збереження обкладинок і метаданих у теках бібліотеки, вони не створюються під час резервного копіювання і не перезаписуються..<br /><br />Всі клієнти, що користуються вашим сервером, будуть автоматично оновлені.", "MessageRestoreBackupWarning": "Відновлення резервної копії перезапише всю базу даних, розташовану в /config, і зображення обкладинок в /metadata/items та /metadata/authors.<br /><br />Резервні копії не змінюють жодних файлів у теках бібліотеки. Якщо у налаштуваннях сервера увімкнено збереження обкладинок і метаданих у теках бібліотеки, вони не створюються під час резервного копіювання і не перезаписуються..<br /><br />Всі клієнти, що користуються вашим сервером, будуть автоматично оновлені.",
"MessageScheduleLibraryScanNote": "Для більшості користувачів рекомендується залишити цю функцію вимкненою та залишити параметр перегляду папок увімкненим. Засіб спостереження за папками автоматично виявить зміни в папках вашої бібліотеки. Засіб спостереження за папками не працює для кожної файлової системи (наприклад, NFS), тому замість нього можна використовувати сканування бібліотек за розкладом.", "MessageScheduleLibraryScanNote": "Для більшості користувачів рекомендується залишити цю функцію вимкненою та залишити параметр перегляду папок увімкненим. Засіб спостереження за папками автоматично виявить зміни в папках вашої бібліотеки. Засіб спостереження за папками не працює для кожної файлової системи (наприклад, NFS), тому замість нього можна використовувати сканування бібліотек за розкладом.",
"MessageScheduleRunEveryWeekdayAtTime": "Запуск кожні {0} о {1}",
"MessageSearchResultsFor": "Результати пошуку для", "MessageSearchResultsFor": "Результати пошуку для",
"MessageSelected": "Вибрано: {0}", "MessageSelected": "Вибрано: {0}",
"MessageServerCouldNotBeReached": "Не вдалося підключитися до сервера", "MessageServerCouldNotBeReached": "Не вдалося підключитися до сервера",
-5
View File
@@ -413,11 +413,6 @@
"LabelSettingsBookshelfViewHelp": "Thiết kế giả lập với kệ gỗ", "LabelSettingsBookshelfViewHelp": "Thiết kế giả lập với kệ gỗ",
"LabelSettingsChromecastSupport": "Hỗ trợ Chromecast", "LabelSettingsChromecastSupport": "Hỗ trợ Chromecast",
"LabelSettingsDateFormat": "Định dạng Ngày", "LabelSettingsDateFormat": "Định dạng Ngày",
"LabelSettingsDisableWatcher": "Tắt Watcher",
"LabelSettingsDisableWatcherForLibrary": "Tắt watcher thư mục cho thư viện",
"LabelSettingsDisableWatcherHelp": "Tắt chức năng tự động thêm/cập nhật các mục khi phát hiện thay đổi tập tin. *Yêu cầu khởi động lại máy chủ",
"LabelSettingsEnableWatcher": "Bật Watcher",
"LabelSettingsEnableWatcherForLibrary": "Bật watcher thư mục cho thư viện",
"LabelSettingsEnableWatcherHelp": "Bật chức năng tự động thêm/cập nhật các mục khi phát hiện thay đổi tập tin. *Yêu cầu khởi động lại máy chủ", "LabelSettingsEnableWatcherHelp": "Bật chức năng tự động thêm/cập nhật các mục khi phát hiện thay đổi tập tin. *Yêu cầu khởi động lại máy chủ",
"LabelSettingsExperimentalFeatures": "Tính năng thử nghiệm", "LabelSettingsExperimentalFeatures": "Tính năng thử nghiệm",
"LabelSettingsExperimentalFeaturesHelp": "Các tính năng đang phát triển có thể cần phản hồi của bạn và sự giúp đỡ trong thử nghiệm. Nhấp để mở thảo luận trên github.", "LabelSettingsExperimentalFeaturesHelp": "Các tính năng đang phát triển có thể cần phản hồi của bạn và sự giúp đỡ trong thử nghiệm. Nhấp để mở thảo luận trên github.",
+7 -6
View File
@@ -219,6 +219,7 @@
"LabelAccountTypeAdmin": "管理员", "LabelAccountTypeAdmin": "管理员",
"LabelAccountTypeGuest": "来宾", "LabelAccountTypeGuest": "来宾",
"LabelAccountTypeUser": "用户", "LabelAccountTypeUser": "用户",
"LabelActivities": "活动",
"LabelActivity": "活动", "LabelActivity": "活动",
"LabelAddToCollection": "添加到收藏", "LabelAddToCollection": "添加到收藏",
"LabelAddToCollectionBatch": "批量添加 {0} 个媒体到收藏", "LabelAddToCollectionBatch": "批量添加 {0} 个媒体到收藏",
@@ -251,7 +252,7 @@
"LabelBackToUser": "返回到用户", "LabelBackToUser": "返回到用户",
"LabelBackupAudioFiles": "备份音频文件", "LabelBackupAudioFiles": "备份音频文件",
"LabelBackupLocation": "备份位置", "LabelBackupLocation": "备份位置",
"LabelBackupsEnableAutomaticBackups": "启用自动备份", "LabelBackupsEnableAutomaticBackups": "自动备份",
"LabelBackupsEnableAutomaticBackupsHelp": "备份保存到 /metadata/backups", "LabelBackupsEnableAutomaticBackupsHelp": "备份保存到 /metadata/backups",
"LabelBackupsMaxBackupSize": "最大备份大小 (GB) (0 为无限制)", "LabelBackupsMaxBackupSize": "最大备份大小 (GB) (0 为无限制)",
"LabelBackupsMaxBackupSizeHelp": "为了防止错误配置, 如果备份超过配置的大小, 备份将失败.", "LabelBackupsMaxBackupSizeHelp": "为了防止错误配置, 如果备份超过配置的大小, 备份将失败.",
@@ -283,6 +284,7 @@
"LabelContinueSeries": "继续收听系列", "LabelContinueSeries": "继续收听系列",
"LabelCover": "封面", "LabelCover": "封面",
"LabelCoverImageURL": "封面图像 URL", "LabelCoverImageURL": "封面图像 URL",
"LabelCoverProvider": "封面提供者",
"LabelCreatedAt": "创建时间", "LabelCreatedAt": "创建时间",
"LabelCronExpression": "计划任务表达式", "LabelCronExpression": "计划任务表达式",
"LabelCurrent": "当前", "LabelCurrent": "当前",
@@ -391,6 +393,7 @@
"LabelIntervalEvery6Hours": "每 6 小时", "LabelIntervalEvery6Hours": "每 6 小时",
"LabelIntervalEveryDay": "每天", "LabelIntervalEveryDay": "每天",
"LabelIntervalEveryHour": "每小时", "LabelIntervalEveryHour": "每小时",
"LabelIntervalEveryMinute": "每分钟",
"LabelInvert": "倒转", "LabelInvert": "倒转",
"LabelItem": "项目", "LabelItem": "项目",
"LabelJumpBackwardAmount": "向后跳转时间", "LabelJumpBackwardAmount": "向后跳转时间",
@@ -555,11 +558,8 @@
"LabelSettingsBookshelfViewHelp": "带有木架子的拟物化设计", "LabelSettingsBookshelfViewHelp": "带有木架子的拟物化设计",
"LabelSettingsChromecastSupport": "Chromecast 支持", "LabelSettingsChromecastSupport": "Chromecast 支持",
"LabelSettingsDateFormat": "日期格式", "LabelSettingsDateFormat": "日期格式",
"LabelSettingsDisableWatcher": "禁用监视程序", "LabelSettingsEnableWatcher": "自动扫描库以查找更改",
"LabelSettingsDisableWatcherForLibrary": "禁用媒体库的文件夹监视程序", "LabelSettingsEnableWatcherForLibrary": "自动扫描库以查找更改",
"LabelSettingsDisableWatcherHelp": "检测到文件更改时禁用自动添加和更新项目. *需要重启服务器",
"LabelSettingsEnableWatcher": "启用监视程序",
"LabelSettingsEnableWatcherForLibrary": "为库启用文件夹监视程序",
"LabelSettingsEnableWatcherHelp": "当检测到文件更改时, 启用项目的自动添加/更新. *需要重新启动服务器", "LabelSettingsEnableWatcherHelp": "当检测到文件更改时, 启用项目的自动添加/更新. *需要重新启动服务器",
"LabelSettingsEpubsAllowScriptedContent": "允许 epubs 中包含脚本内容", "LabelSettingsEpubsAllowScriptedContent": "允许 epubs 中包含脚本内容",
"LabelSettingsEpubsAllowScriptedContentHelp": "允许 epub 文件执行脚本. 建议将此设置保持禁用, 除非你信任 epub 文件的来源.", "LabelSettingsEpubsAllowScriptedContentHelp": "允许 epub 文件执行脚本. 建议将此设置保持禁用, 除非你信任 epub 文件的来源.",
@@ -845,6 +845,7 @@
"MessageRestoreBackupConfirm": "你确定要恢复创建的这个备份", "MessageRestoreBackupConfirm": "你确定要恢复创建的这个备份",
"MessageRestoreBackupWarning": "恢复备份将覆盖位于 /config 的整个数据库并覆盖 /metadata/items & /metadata/authors 中的图像.<br /><br />备份不会修改媒体库文件夹中的任何文件. 如果你已启用服务器设置将封面和元数据存储在库文件夹中,则不会备份或覆盖这些内容.<br /><br />将自动刷新使用服务器的所有客户端.", "MessageRestoreBackupWarning": "恢复备份将覆盖位于 /config 的整个数据库并覆盖 /metadata/items & /metadata/authors 中的图像.<br /><br />备份不会修改媒体库文件夹中的任何文件. 如果你已启用服务器设置将封面和元数据存储在库文件夹中,则不会备份或覆盖这些内容.<br /><br />将自动刷新使用服务器的所有客户端.",
"MessageScheduleLibraryScanNote": "对于大多数用户, 建议禁用此功能并保持文件夹监视程序设置启用. 文件夹监视程序将自动检测库文件夹中的更改. 文件夹监视程序不适用于每个文件系统 (如 NFS), 因此可以使用计划库扫描.", "MessageScheduleLibraryScanNote": "对于大多数用户, 建议禁用此功能并保持文件夹监视程序设置启用. 文件夹监视程序将自动检测库文件夹中的更改. 文件夹监视程序不适用于每个文件系统 (如 NFS), 因此可以使用计划库扫描.",
"MessageScheduleRunEveryWeekdayAtTime": "每隔 {0} 在 {1} 运行一次",
"MessageSearchResultsFor": "搜索结果", "MessageSearchResultsFor": "搜索结果",
"MessageSelected": "{0} 已选择", "MessageSelected": "{0} 已选择",
"MessageServerCouldNotBeReached": "无法访问服务器", "MessageServerCouldNotBeReached": "无法访问服务器",
-5
View File
@@ -458,11 +458,6 @@
"LabelSettingsBookshelfViewHelp": "帶有木架子的擬物化設計", "LabelSettingsBookshelfViewHelp": "帶有木架子的擬物化設計",
"LabelSettingsChromecastSupport": "Chromecast 支援", "LabelSettingsChromecastSupport": "Chromecast 支援",
"LabelSettingsDateFormat": "日期格式", "LabelSettingsDateFormat": "日期格式",
"LabelSettingsDisableWatcher": "禁用監視程序",
"LabelSettingsDisableWatcherForLibrary": "禁用媒體庫的資料夾監視程序",
"LabelSettingsDisableWatcherHelp": "檢測到檔案更改時禁用自動新增和更新項目. *需要重啟伺服器",
"LabelSettingsEnableWatcher": "啟用監視程序",
"LabelSettingsEnableWatcherForLibrary": "為庫啟用資料夾監視程序",
"LabelSettingsEnableWatcherHelp": "當檢測到檔案更改時, 啟用項目的自動新增/更新. *需要重新啟動伺服器", "LabelSettingsEnableWatcherHelp": "當檢測到檔案更改時, 啟用項目的自動新增/更新. *需要重新啟動伺服器",
"LabelSettingsExperimentalFeatures": "實驗功能", "LabelSettingsExperimentalFeatures": "實驗功能",
"LabelSettingsExperimentalFeaturesHelp": "開發中的功能需要你的反饋並幫助測試. 點擊打開 github 討論.", "LabelSettingsExperimentalFeaturesHelp": "開發中的功能需要你的反饋並幫助測試. 點擊打開 github 討論.",
+404 -136
View File
@@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.19.2", "version": "2.20.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.19.2", "version": "2.20.0",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"axios": "^0.27.2", "axios": "^0.27.2",
@@ -25,7 +25,7 @@
"semver": "^7.6.3", "semver": "^7.6.3",
"sequelize": "^6.35.2", "sequelize": "^6.35.2",
"socket.io": "^4.5.4", "socket.io": "^4.5.4",
"sqlite3": "^5.1.6", "sqlite3": "^5.1.7",
"ssrf-req-filter": "^1.1.0", "ssrf-req-filter": "^1.1.0",
"xml2js": "^0.5.0" "xml2js": "^0.5.0"
}, },
@@ -587,39 +587,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz",
"integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==",
"dependencies": {
"detect-libc": "^2.0.0",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.7",
"nopt": "^5.0.0",
"npmlog": "^5.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tar": "^6.1.11"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@mapbox/node-pre-gyp/node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@npmcli/fs": { "node_modules/@npmcli/fs": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz",
@@ -741,7 +708,8 @@
"node_modules/abbrev": { "node_modules/abbrev": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"devOptional": true
}, },
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
@@ -759,6 +727,7 @@
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"optional": true,
"dependencies": { "dependencies": {
"debug": "4" "debug": "4"
}, },
@@ -770,6 +739,7 @@
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"optional": true,
"dependencies": { "dependencies": {
"ms": "2.1.2" "ms": "2.1.2"
}, },
@@ -785,7 +755,8 @@
"node_modules/agent-base/node_modules/ms": { "node_modules/agent-base/node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"optional": true
}, },
"node_modules/agentkeepalive": { "node_modules/agentkeepalive": {
"version": "4.3.0", "version": "4.3.0",
@@ -850,6 +821,7 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"devOptional": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -897,7 +869,8 @@
"node_modules/aproba": { "node_modules/aproba": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
"optional": true
}, },
"node_modules/archy": { "node_modules/archy": {
"version": "1.0.0", "version": "1.0.0",
@@ -905,18 +878,6 @@
"integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==",
"dev": true "dev": true
}, },
"node_modules/are-we-there-yet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/argparse": { "node_modules/argparse": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
@@ -957,7 +918,28 @@
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"devOptional": true
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
}, },
"node_modules/base64id": { "node_modules/base64id": {
"version": "2.0.0", "version": "2.0.0",
@@ -976,6 +958,26 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"license": "MIT",
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.1", "version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
@@ -1003,6 +1005,7 @@
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"devOptional": true,
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@@ -1058,6 +1061,30 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/buffer-equal-constant-time": { "node_modules/buffer-equal-constant-time": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -1326,6 +1353,7 @@
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"optional": true,
"bin": { "bin": {
"color-support": "bin.js" "color-support": "bin.js"
} }
@@ -1350,12 +1378,14 @@
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"devOptional": true
}, },
"node_modules/console-control-strings": { "node_modules/console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"optional": true
}, },
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
@@ -1461,6 +1491,21 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-eql": { "node_modules/deep-eql": {
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz",
@@ -1473,6 +1518,15 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/default-require-extensions": { "node_modules/default-require-extensions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz",
@@ -1499,7 +1553,8 @@
"node_modules/delegates": { "node_modules/delegates": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"optional": true
}, },
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
@@ -1519,9 +1574,10 @@
} }
}, },
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.0.1", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
"integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
"license": "Apache-2.0",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -1613,7 +1669,8 @@
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"devOptional": true
}, },
"node_modules/encodeurl": { "node_modules/encodeurl": {
"version": "1.0.2", "version": "1.0.2",
@@ -1644,6 +1701,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/engine.io": { "node_modules/engine.io": {
"version": "6.5.4", "version": "6.5.4",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz",
@@ -1777,6 +1843,15 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"license": "(MIT OR WTFPL)",
"engines": {
"node": ">=6"
}
},
"node_modules/express": { "node_modules/express": {
"version": "4.18.2", "version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
@@ -1844,6 +1919,12 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"license": "MIT"
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -1993,6 +2074,12 @@
} }
] ]
}, },
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT"
},
"node_modules/fs-minipass": { "node_modules/fs-minipass": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
@@ -2007,7 +2094,8 @@
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"devOptional": true
}, },
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
@@ -2017,25 +2105,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
"dependencies": {
"aproba": "^1.0.3 || ^2.0.0",
"color-support": "^1.1.2",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.1",
"object-assign": "^4.1.1",
"signal-exit": "^3.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wide-align": "^1.1.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/gensync": { "node_modules/gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -2085,10 +2154,17 @@
"node": ">=8.0.0" "node": ">=8.0.0"
} }
}, },
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"license": "MIT"
},
"node_modules/glob": { "node_modules/glob": {
"version": "7.2.3", "version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"devOptional": true,
"dependencies": { "dependencies": {
"fs.realpath": "^1.0.0", "fs.realpath": "^1.0.0",
"inflight": "^1.0.4", "inflight": "^1.0.4",
@@ -2164,7 +2240,8 @@
"node_modules/has-unicode": { "node_modules/has-unicode": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"optional": true
}, },
"node_modules/hasha": { "node_modules/hasha": {
"version": "5.2.2", "version": "5.2.2",
@@ -2277,6 +2354,7 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"optional": true,
"dependencies": { "dependencies": {
"agent-base": "6", "agent-base": "6",
"debug": "4" "debug": "4"
@@ -2289,6 +2367,7 @@
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"optional": true,
"dependencies": { "dependencies": {
"ms": "2.1.2" "ms": "2.1.2"
}, },
@@ -2304,7 +2383,8 @@
"node_modules/https-proxy-agent/node_modules/ms": { "node_modules/https-proxy-agent/node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"optional": true
}, },
"node_modules/humanize-ms": { "node_modules/humanize-ms": {
"version": "1.2.1", "version": "1.2.1",
@@ -2326,6 +2406,26 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/ignore-by-default": { "node_modules/ignore-by-default": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
@@ -2368,6 +2468,7 @@
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"devOptional": true,
"dependencies": { "dependencies": {
"once": "^1.3.0", "once": "^1.3.0",
"wrappy": "1" "wrappy": "1"
@@ -2378,6 +2479,12 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
}, },
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC"
},
"node_modules/ip": { "node_modules/ip": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
@@ -2417,6 +2524,7 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"devOptional": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -2885,6 +2993,7 @@
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"dev": true,
"dependencies": { "dependencies": {
"semver": "^6.0.0" "semver": "^6.0.0"
}, },
@@ -2899,6 +3008,7 @@
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
@@ -2993,10 +3103,23 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"devOptional": true,
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
@@ -3004,6 +3127,15 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": { "node_modules/minipass": {
"version": "3.3.6", "version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
@@ -3103,6 +3235,12 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT"
},
"node_modules/mocha": { "node_modules/mocha": {
"version": "10.2.0", "version": "10.2.0",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz",
@@ -3399,6 +3537,12 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
} }
}, },
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"license": "MIT"
},
"node_modules/negotiator": { "node_modules/negotiator": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -3438,30 +3582,24 @@
"isarray": "0.0.1" "isarray": "0.0.1"
} }
}, },
"node_modules/node-addon-api": { "node_modules/node-abi": {
"version": "4.3.0", "version": "3.74.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz",
"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==",
}, "license": "MIT",
"node_modules/node-fetch": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
"integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==",
"dependencies": { "dependencies": {
"whatwg-url": "^5.0.0" "semver": "^7.3.5"
}, },
"engines": { "engines": {
"node": "4.x || >=6.0.0" "node": ">=10"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
} }
}, },
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT"
},
"node_modules/node-gyp": { "node_modules/node-gyp": {
"version": "8.4.1", "version": "8.4.1",
"resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz",
@@ -3658,17 +3796,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
"dependencies": {
"are-we-there-yet": "^2.0.0",
"console-control-strings": "^1.1.0",
"gauge": "^3.0.0",
"set-blocking": "^2.0.0"
}
},
"node_modules/nyc": { "node_modules/nyc": {
"version": "15.1.0", "version": "15.1.0",
"resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz",
@@ -3962,6 +4089,7 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"devOptional": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -4029,6 +4157,32 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/process-on-spawn": { "node_modules/process-on-spawn": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz",
@@ -4078,6 +4232,16 @@
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
"dev": true "dev": true
}, },
"node_modules/pump": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/qs": { "node_modules/qs": {
"version": "6.11.0", "version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
@@ -4131,6 +4295,30 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/rc/node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/readable-stream": { "node_modules/readable-stream": {
"version": "3.6.2", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
@@ -4210,6 +4398,7 @@
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"devOptional": true,
"dependencies": { "dependencies": {
"glob": "^7.1.3" "glob": "^7.1.3"
}, },
@@ -4404,7 +4593,8 @@
"node_modules/set-blocking": { "node_modules/set-blocking": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"devOptional": true
}, },
"node_modules/setprototypeof": { "node_modules/setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
@@ -4448,7 +4638,53 @@
"node_modules/signal-exit": { "node_modules/signal-exit": {
"version": "3.0.7", "version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"devOptional": true
},
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
}, },
"node_modules/simple-update-notifier": { "node_modules/simple-update-notifier": {
"version": "1.1.0", "version": "1.1.0",
@@ -4701,13 +4937,15 @@
"dev": true "dev": true
}, },
"node_modules/sqlite3": { "node_modules/sqlite3": {
"version": "5.1.6", "version": "5.1.7",
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.6.tgz", "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz",
"integrity": "sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw==", "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"@mapbox/node-pre-gyp": "^1.0.0", "bindings": "^1.5.0",
"node-addon-api": "^4.2.0", "node-addon-api": "^7.0.0",
"prebuild-install": "^7.1.1",
"tar": "^6.1.11" "tar": "^6.1.11"
}, },
"optionalDependencies": { "optionalDependencies": {
@@ -4770,6 +5008,7 @@
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"devOptional": true,
"dependencies": { "dependencies": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0", "is-fullwidth-code-point": "^3.0.0",
@@ -4783,6 +5022,7 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"devOptional": true,
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"
}, },
@@ -4839,6 +5079,40 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/tar-fs": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz",
"integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-fs/node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"license": "ISC"
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"license": "MIT",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tar/node_modules/minipass": { "node_modules/tar/node_modules/minipass": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
@@ -4907,10 +5181,17 @@
"nodetouch": "bin/nodetouch.js" "nodetouch": "bin/nodetouch.js"
} }
}, },
"node_modules/tr46": { "node_modules/tunnel-agent": {
"version": "0.0.3", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
}, },
"node_modules/type-detect": { "node_modules/type-detect": {
"version": "4.0.8", "version": "4.0.8",
@@ -5061,20 +5342,6 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -5100,6 +5367,7 @@
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"optional": true,
"dependencies": { "dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4" "string-width": "^1.0.2 || 2 || 3 || 4"
} }
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.19.2", "version": "2.20.0",
"buildNumber": 1, "buildNumber": 1,
"description": "Self-hosted audiobook and podcast server", "description": "Self-hosted audiobook and podcast server",
"main": "index.js", "main": "index.js",
@@ -52,7 +52,7 @@
"semver": "^7.6.3", "semver": "^7.6.3",
"sequelize": "^6.35.2", "sequelize": "^6.35.2",
"socket.io": "^4.5.4", "socket.io": "^4.5.4",
"sqlite3": "^5.1.6", "sqlite3": "^5.1.7",
"ssrf-req-filter": "^1.1.0", "ssrf-req-filter": "^1.1.0",
"xml2js": "^0.5.0" "xml2js": "^0.5.0"
}, },
+112
View File
@@ -190,7 +190,13 @@ class Database {
await this.buildModels(force) await this.buildModels(force)
Logger.info(`[Database] Db initialized with models:`, Object.keys(this.sequelize.models).join(', ')) Logger.info(`[Database] Db initialized with models:`, Object.keys(this.sequelize.models).join(', '))
await this.addTriggers()
await this.loadData() await this.loadData()
Logger.info(`[Database] running ANALYZE`)
await this.sequelize.query('ANALYZE')
Logger.info(`[Database] ANALYZE completed`)
} }
/** /**
@@ -767,6 +773,112 @@ class Database {
return textQuery return textQuery
} }
/**
* This is used to create necessary triggers for new databases.
* It adds triggers to update libraryItems.title[IgnorePrefix] when (books|podcasts).title[IgnorePrefix] is updated
*/
async addTriggers() {
await this.addTriggerIfNotExists('books', 'title', 'id', 'libraryItems', 'title', 'mediaId')
await this.addTriggerIfNotExists('books', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
await this.addTriggerIfNotExists('podcasts', 'title', 'id', 'libraryItems', 'title', 'mediaId')
await this.addTriggerIfNotExists('podcasts', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
await this.addAuthorNamesTriggersIfNotExist()
}
async addTriggerIfNotExists(sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) {
const action = `update_${targetTable}_${targetColumn}`
const fromSource = sourceTable === 'books' ? '' : `_from_${sourceTable}_${sourceColumn}`
const triggerName = this.convertToSnakeCase(`${action}${fromSource}`)
const [[{ count }]] = await this.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='${triggerName}'`)
if (count > 0) return // Trigger already exists
Logger.info(`[Database] Adding trigger ${triggerName}`)
await this.sequelize.query(`
CREATE TRIGGER ${triggerName}
AFTER UPDATE OF ${sourceColumn} ON ${sourceTable}
FOR EACH ROW
BEGIN
UPDATE ${targetTable}
SET ${targetColumn} = NEW.${sourceColumn}
WHERE ${targetTable}.${targetIdColumn} = NEW.${sourceIdColumn};
END;
`)
}
async addAuthorNamesTriggersIfNotExist() {
const libraryItems = 'libraryItems'
const bookAuthors = 'bookAuthors'
const authors = 'authors'
const columns = [
{ name: 'authorNamesFirstLast', source: `${authors}.name`, spec: { type: Sequelize.STRING, allowNull: true } },
{ name: 'authorNamesLastFirst', source: `${authors}.lastFirst`, spec: { type: Sequelize.STRING, allowNull: true } }
]
const authorsSort = `${bookAuthors}.createdAt ASC`
const columnNames = columns.map((column) => column.name).join(', ')
const columnSourcesExpression = columns.map((column) => `GROUP_CONCAT(${column.source}, ', ' ORDER BY ${authorsSort})`).join(', ')
const authorsJoin = `${authors} JOIN ${bookAuthors} ON ${authors}.id = ${bookAuthors}.authorId`
const addBookAuthorsTriggerIfNotExists = async (action) => {
const modifiedRecord = action === 'delete' ? 'OLD' : 'NEW'
const triggerName = this.convertToSnakeCase(`update_${libraryItems}_authorNames_on_${bookAuthors}_${action}`)
const authorNamesSubQuery = `
SELECT ${columnSourcesExpression}
FROM ${authorsJoin}
WHERE ${bookAuthors}.bookId = ${modifiedRecord}.bookId
`
const [[{ count }]] = await this.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='${triggerName}'`)
if (count > 0) return // Trigger already exists
Logger.info(`[Database] Adding trigger ${triggerName}`)
await this.sequelize.query(`
CREATE TRIGGER ${triggerName}
AFTER ${action} ON ${bookAuthors}
FOR EACH ROW
BEGIN
UPDATE ${libraryItems}
SET (${columnNames}) = (${authorNamesSubQuery})
WHERE mediaId = ${modifiedRecord}.bookId;
END;
`)
}
const addAuthorsUpdateTriggerIfNotExists = async () => {
const triggerName = this.convertToSnakeCase(`update_${libraryItems}_authorNames_on_authors_update`)
const authorNamesSubQuery = `
SELECT ${columnSourcesExpression}
FROM ${authorsJoin}
WHERE ${bookAuthors}.bookId = ${libraryItems}.mediaId
`
const [[{ count }]] = await this.sequelize.query(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='trigger' AND name='${triggerName}'`)
if (count > 0) return // Trigger already exists
Logger.info(`[Database] Adding trigger ${triggerName}`)
await this.sequelize.query(`
CREATE TRIGGER ${triggerName}
AFTER UPDATE OF name ON ${authors}
FOR EACH ROW
BEGIN
UPDATE ${libraryItems}
SET (${columnNames}) = (${authorNamesSubQuery})
WHERE mediaId IN (SELECT bookId FROM ${bookAuthors} WHERE authorId = NEW.id);
END;
`)
}
await addBookAuthorsTriggerIfNotExists('insert')
await addBookAuthorsTriggerIfNotExists('delete')
await addAuthorsUpdateTriggerIfNotExists()
}
convertToSnakeCase(str) {
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
}
TextSearchQuery = class { TextSearchQuery = class {
constructor(sequelize, supportsUnaccent, query) { constructor(sequelize, supportsUnaccent, query) {
this.sequelize = sequelize this.sequelize = sequelize
+1 -6
View File
@@ -21,12 +21,7 @@ class Logger {
} }
get levelString() { get levelString() {
for (const key in LogLevel) { return this.getLogLevelString(this.logLevel)
if (LogLevel[key] === this.logLevel) {
return key
}
}
return 'UNKNOWN'
} }
/** /**
+5 -10
View File
@@ -5,7 +5,7 @@ const Logger = require('./Logger')
const Task = require('./objects/Task') const Task = require('./objects/Task')
const TaskManager = require('./managers/TaskManager') const TaskManager = require('./managers/TaskManager')
const { filePathToPOSIX, isSameOrSubPath, getFileMTimeMs } = require('./utils/fileUtils') const { filePathToPOSIX, isSameOrSubPath, getFileMTimeMs, shouldIgnoreFile } = require('./utils/fileUtils')
/** /**
* @typedef PendingFileUpdate * @typedef PendingFileUpdate
@@ -286,15 +286,10 @@ class FolderWatcher extends EventEmitter {
const relPath = path.replace(folderPath, '') const relPath = path.replace(folderPath, '')
if (Path.extname(relPath).toLowerCase() === '.part') { // Check for ignored extensions or directories, such as dotfiles and hidden directories
Logger.debug(`[Watcher] Ignoring .part file "${relPath}"`) const shouldIgnore = shouldIgnoreFile(relPath)
return false if (shouldIgnore) {
} Logger.debug(`[Watcher] Ignoring ${shouldIgnore} - "${relPath}"`)
// Ignore files/folders starting with "."
const hasDotPath = relPath.split('/').find((p) => p.startsWith('.'))
if (hasDotPath) {
Logger.debug(`[Watcher] Ignoring dot path "${relPath}" | Piece "${hasDotPath}"`)
return false return false
} }
+15
View File
@@ -254,6 +254,11 @@ class LibraryController {
* @param {Response} res * @param {Response} res
*/ */
async update(req, res) { async update(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to update library`)
return res.sendStatus(403)
}
// Validation // Validation
const updatePayload = {} const updatePayload = {}
const keysToCheck = ['name', 'provider', 'mediaType', 'icon'] const keysToCheck = ['name', 'provider', 'mediaType', 'icon']
@@ -519,6 +524,11 @@ class LibraryController {
* @param {Response} res * @param {Response} res
*/ */
async delete(req, res) { async delete(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to delete library`)
return res.sendStatus(403)
}
// Remove library watcher // Remove library watcher
Watcher.removeLibrary(req.library) Watcher.removeLibrary(req.library)
@@ -639,6 +649,11 @@ class LibraryController {
* @param {Response} res * @param {Response} res
*/ */
async removeLibraryItemsWithIssues(req, res) { async removeLibraryItemsWithIssues(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to delete library items missing or invalid`)
return res.sendStatus(403)
}
const libraryItemsWithIssues = await Database.libraryItemModel.findAll({ const libraryItemsWithIssues = await Database.libraryItemModel.findAll({
where: { where: {
libraryId: req.library.id, libraryId: req.library.id,
+7 -1
View File
@@ -107,7 +107,9 @@ class PodcastController {
libraryFiles: [], libraryFiles: [],
extraData: {}, extraData: {},
libraryId: library.id, libraryId: library.id,
libraryFolderId: folder.id libraryFolderId: folder.id,
title: podcast.title,
titleIgnorePrefix: podcast.titleIgnorePrefix
}, },
{ transaction } { transaction }
) )
@@ -498,6 +500,10 @@ class PodcastController {
req.libraryItem.changed('libraryFiles', true) req.libraryItem.changed('libraryFiles', true)
await req.libraryItem.save() await req.libraryItem.save()
// update number of episodes
req.libraryItem.media.numEpisodes = req.libraryItem.media.podcastEpisodes.length
await req.libraryItem.media.save()
SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded()) SocketAuthority.emitter('item_updated', req.libraryItem.toOldJSONExpanded())
res.json(req.libraryItem.toOldJSON()) res.json(req.libraryItem.toOldJSON())
} }
+15 -1
View File
@@ -130,7 +130,21 @@ class MigrationManager {
async initUmzug(umzugStorage = new SequelizeStorage({ sequelize: this.sequelize })) { async initUmzug(umzugStorage = new SequelizeStorage({ sequelize: this.sequelize })) {
// This check is for dependency injection in tests // This check is for dependency injection in tests
const files = (await fs.readdir(this.migrationsDir)).filter((file) => !file.startsWith('.')).map((file) => path.join(this.migrationsDir, file)) const files = (await fs.readdir(this.migrationsDir))
.filter((file) => {
// Only include .js files and exclude dot files
return !file.startsWith('.') && path.extname(file).toLowerCase() === '.js'
})
.map((file) => path.join(this.migrationsDir, file))
// Validate migration names
for (const file of files) {
const migrationName = path.basename(file, path.extname(file))
const migrationVersion = this.extractVersionFromTag(migrationName)
if (!migrationVersion) {
throw new Error(`Invalid migration file: "${migrationName}". Unable to extract version from filename.`)
}
}
const parent = new Umzug({ const parent = new Umzug({
migrations: { migrations: {
+17 -1
View File
@@ -72,6 +72,15 @@ class PodcastManager {
*/ */
async startPodcastEpisodeDownload(podcastEpisodeDownload) { async startPodcastEpisodeDownload(podcastEpisodeDownload) {
if (this.currentDownload) { if (this.currentDownload) {
// Prevent downloading episodes from the same URL for the same library item.
// Allow downloading for different library items in case of the same podcast existing in multiple libraries (e.g. different folders)
if (this.downloadQueue.some((d) => d.url === podcastEpisodeDownload.url && d.libraryItem.id === podcastEpisodeDownload.libraryItem.id)) {
Logger.warn(`[PodcastManager] Episode already in queue: "${this.currentDownload.episodeTitle}"`)
return
} else if (this.currentDownload.url === podcastEpisodeDownload.url && this.currentDownload.libraryItem.id === podcastEpisodeDownload.libraryItem.id) {
Logger.warn(`[PodcastManager] Episode download already in progress for "${podcastEpisodeDownload.episodeTitle}"`)
return
}
this.downloadQueue.push(podcastEpisodeDownload) this.downloadQueue.push(podcastEpisodeDownload)
SocketAuthority.emitter('episode_download_queued', podcastEpisodeDownload.toJSONForClient()) SocketAuthority.emitter('episode_download_queued', podcastEpisodeDownload.toJSONForClient())
return return
@@ -232,6 +241,11 @@ class PodcastManager {
await libraryItem.save() await libraryItem.save()
if (libraryItem.media.numEpisodes !== libraryItem.media.podcastEpisodes.length) {
libraryItem.media.numEpisodes = libraryItem.media.podcastEpisodes.length
await libraryItem.media.save()
}
SocketAuthority.emitter('item_updated', libraryItem.toOldJSONExpanded()) SocketAuthority.emitter('item_updated', libraryItem.toOldJSONExpanded())
const podcastEpisodeExpanded = podcastEpisode.toOldJSONExpanded(libraryItem.id) const podcastEpisodeExpanded = podcastEpisode.toOldJSONExpanded(libraryItem.id)
podcastEpisodeExpanded.libraryItem = libraryItem.toOldJSONExpanded() podcastEpisodeExpanded.libraryItem = libraryItem.toOldJSONExpanded()
@@ -622,7 +636,9 @@ class PodcastManager {
libraryFiles: [], libraryFiles: [],
extraData: {}, extraData: {},
libraryId: folder.libraryId, libraryId: folder.libraryId,
libraryFolderId: folder.id libraryFolderId: folder.id,
title: podcast.title,
titleIgnorePrefix: podcast.titleIgnorePrefix
}, },
{ transaction } { transaction }
) )
+2
View File
@@ -14,3 +14,5 @@ Please add a record of every database migration that you create to this file. Th
| v2.17.6 | v2.17.6-share-add-isdownloadable | Adds the isDownloadable column to the mediaItemShares table | | v2.17.6 | v2.17.6-share-add-isdownloadable | Adds the isDownloadable column to the mediaItemShares table |
| v2.17.7 | v2.17.7-add-indices | Adds indices to the libraryItems and books tables to reduce query times | | v2.17.7 | v2.17.7-add-indices | Adds indices to the libraryItems and books tables to reduce query times |
| v2.19.1 | v2.19.1-copy-title-to-library-items | Copies title and titleIgnorePrefix to the libraryItems table, creates update triggers and indices | | v2.19.1 | v2.19.1-copy-title-to-library-items | Copies title and titleIgnorePrefix to the libraryItems table, creates update triggers and indices |
| v2.19.4 | v2.19.4-improve-podcast-queries | Adds numEpisodes to podcasts, adds podcastId to mediaProgresses, copies podcast title to libraryItems |
| v2.20.0 | v2.20.0-improve-author-sort-queries | Adds AuthorNames(FirstLast\|LastFirst) to libraryItems to improve author sort queries |
@@ -0,0 +1,219 @@
const util = require('util')
/**
* @typedef MigrationContext
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
* @property {import('../Logger')} logger - a Logger object.
*
* @typedef MigrationOptions
* @property {MigrationContext} context - an object containing the migration context.
*/
const migrationVersion = '2.19.4'
const migrationName = `${migrationVersion}-improve-podcast-queries`
const loggerPrefix = `[${migrationVersion} migration]`
/**
* This upward migration adds a numEpisodes column to the podcasts table and populates it.
* It also adds a podcastId column to the mediaProgresses table and populates it.
* It also copies the title and titleIgnorePrefix columns from the podcasts table to the libraryItems table,
* and adds triggers to update them when the corresponding columns in the podcasts table are updated.
*
* @param {MigrationOptions} options - an object containing the migration context.
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
*/
async function up({ context: { queryInterface, logger } }) {
// Upwards migration script
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
// Add numEpisodes column to podcasts table
await addColumn(queryInterface, logger, 'podcasts', 'numEpisodes', { type: queryInterface.sequelize.Sequelize.INTEGER, allowNull: false, defaultValue: 0 })
// Populate numEpisodes column with the number of episodes for each podcast
await populateNumEpisodes(queryInterface, logger)
// Add podcastId column to mediaProgresses table
await addColumn(queryInterface, logger, 'mediaProgresses', 'podcastId', { type: queryInterface.sequelize.Sequelize.UUID, allowNull: true })
// Populate podcastId column with the podcastId for each mediaProgress
await populatePodcastId(queryInterface, logger)
// Copy title and titleIgnorePrefix columns from podcasts to libraryItems
await copyColumn(queryInterface, logger, 'podcasts', 'title', 'id', 'libraryItems', 'title', 'mediaId')
await copyColumn(queryInterface, logger, 'podcasts', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
// Add triggers to update title and titleIgnorePrefix in libraryItems
await addTrigger(queryInterface, logger, 'podcasts', 'title', 'id', 'libraryItems', 'title', 'mediaId')
await addTrigger(queryInterface, logger, 'podcasts', 'titleIgnorePrefix', 'id', 'libraryItems', 'titleIgnorePrefix', 'mediaId')
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
}
/**
* This downward migration removes the triggers on the podcasts table,
* the numEpisodes column from the podcasts table, and the podcastId column from the mediaProgresses table.
*
* @param {MigrationOptions} options - an object containing the migration context.
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
*/
async function down({ context: { queryInterface, logger } }) {
// Downward migration script
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
// Remove triggers from libraryItems
await removeTrigger(queryInterface, logger, 'podcasts', 'title', 'libraryItems', 'title')
await removeTrigger(queryInterface, logger, 'podcasts', 'titleIgnorePrefix', 'libraryItems', 'titleIgnorePrefix')
// Remove numEpisodes column from podcasts table
await removeColumn(queryInterface, logger, 'podcasts', 'numEpisodes')
// Remove podcastId column from mediaProgresses table
await removeColumn(queryInterface, logger, 'mediaProgresses', 'podcastId')
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
}
async function populateNumEpisodes(queryInterface, logger) {
logger.info(`${loggerPrefix} populating numEpisodes column in podcasts table`)
await queryInterface.sequelize.query(`
UPDATE podcasts
SET numEpisodes = (SELECT COUNT(*) FROM podcastEpisodes WHERE podcastEpisodes.podcastId = podcasts.id)
`)
logger.info(`${loggerPrefix} populated numEpisodes column in podcasts table`)
}
async function populatePodcastId(queryInterface, logger) {
logger.info(`${loggerPrefix} populating podcastId column in mediaProgresses table`)
// bulk update podcastId to the podcastId of the podcastEpisode if the mediaItemType is podcastEpisode
await queryInterface.sequelize.query(`
UPDATE mediaProgresses
SET podcastId = (SELECT podcastId FROM podcastEpisodes WHERE podcastEpisodes.id = mediaProgresses.mediaItemId)
WHERE mediaItemType = 'podcastEpisode'
`)
logger.info(`${loggerPrefix} populated podcastId column in mediaProgresses table`)
}
/**
* Utility function to add a column to a table. If the column already exists, it logs a message and continues.
*
* @param {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
* @param {import('../Logger')} logger - a Logger object.
* @param {string} table - the name of the table to add the column to.
* @param {string} column - the name of the column to add.
* @param {Object} options - the options for the column.
*/
async function addColumn(queryInterface, logger, table, column, options) {
logger.info(`${loggerPrefix} adding column "${column}" to table "${table}"`)
const tableDescription = await queryInterface.describeTable(table)
if (!tableDescription[column]) {
await queryInterface.addColumn(table, column, options)
logger.info(`${loggerPrefix} added column "${column}" to table "${table}"`)
} else {
logger.info(`${loggerPrefix} column "${column}" already exists in table "${table}"`)
}
}
/**
* Utility function to remove a column from a table. If the column does not exist, it logs a message and continues.
*
* @param {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
* @param {import('../Logger')} logger - a Logger object.
* @param {string} table - the name of the table to remove the column from.
* @param {string} column - the name of the column to remove.
*/
async function removeColumn(queryInterface, logger, table, column) {
logger.info(`${loggerPrefix} removing column "${column}" from table "${table}"`)
const tableDescription = await queryInterface.describeTable(table)
if (tableDescription[column]) {
await queryInterface.sequelize.query(`ALTER TABLE ${table} DROP COLUMN ${column}`)
logger.info(`${loggerPrefix} removed column "${column}" from table "${table}"`)
} else {
logger.info(`${loggerPrefix} column "${column}" does not exist in table "${table}"`)
}
}
/**
* Utility function to add a trigger to update a column in a target table when a column in a source table is updated.
* If the trigger already exists, it drops it and creates a new one.
* sourceIdColumn and targetIdColumn are used to match the source and target rows.
*
* @param {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
* @param {import('../Logger')} logger - a Logger object.
* @param {string} sourceTable - the name of the source table.
* @param {string} sourceColumn - the name of the column to update.
* @param {string} sourceIdColumn - the name of the id column of the source table.
* @param {string} targetTable - the name of the target table.
* @param {string} targetColumn - the name of the column to update.
* @param {string} targetIdColumn - the name of the id column of the target table.
*/
async function addTrigger(queryInterface, logger, sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) {
logger.info(`${loggerPrefix} adding trigger to update ${targetTable}.${targetColumn} when ${sourceTable}.${sourceColumn} is updated`)
const triggerName = convertToSnakeCase(`update_${targetTable}_${targetColumn}_from_${sourceTable}_${sourceColumn}`)
await queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
await queryInterface.sequelize.query(`
CREATE TRIGGER ${triggerName}
AFTER UPDATE OF ${sourceColumn} ON ${sourceTable}
FOR EACH ROW
BEGIN
UPDATE ${targetTable}
SET ${targetColumn} = NEW.${sourceColumn}
WHERE ${targetTable}.${targetIdColumn} = NEW.${sourceIdColumn};
END;
`)
logger.info(`${loggerPrefix} added trigger to update ${targetTable}.${targetColumn} when ${sourceTable}.${sourceColumn} is updated`)
}
/**
* Utility function to remove an update trigger from a table.
*
* @param {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
* @param {import('../Logger')} logger - a Logger object.
* @param {string} sourceTable - the name of the source table.
* @param {string} sourceColumn - the name of the column to update.
* @param {string} targetTable - the name of the target table.
* @param {string} targetColumn - the name of the column to update.
*/
async function removeTrigger(queryInterface, logger, sourceTable, sourceColumn, targetTable, targetColumn) {
logger.info(`${loggerPrefix} removing trigger to update ${targetTable}.${targetColumn}`)
const triggerName = convertToSnakeCase(`update_${targetTable}_${targetColumn}_from_${sourceTable}_${sourceColumn}`)
await queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
logger.info(`${loggerPrefix} removed trigger to update ${targetTable}.${targetColumn}`)
}
/**
* Utility function to copy a column from a source table to a target table.
* sourceIdColumn and targetIdColumn are used to match the source and target rows.
*
* @param {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
* @param {import('../Logger')} logger - a Logger object.
* @param {string} sourceTable - the name of the source table.
* @param {string} sourceColumn - the name of the column to copy.
* @param {string} sourceIdColumn - the name of the id column of the source table.
* @param {string} targetTable - the name of the target table.
* @param {string} targetColumn - the name of the column to copy to.
* @param {string} targetIdColumn - the name of the id column of the target table.
*/
async function copyColumn(queryInterface, logger, sourceTable, sourceColumn, sourceIdColumn, targetTable, targetColumn, targetIdColumn) {
logger.info(`${loggerPrefix} copying column "${sourceColumn}" from table "${sourceTable}" to table "${targetTable}"`)
await queryInterface.sequelize.query(`
UPDATE ${targetTable}
SET ${targetColumn} = ${sourceTable}.${sourceColumn}
FROM ${sourceTable}
WHERE ${targetTable}.${targetIdColumn} = ${sourceTable}.${sourceIdColumn}
`)
logger.info(`${loggerPrefix} copied column "${sourceColumn}" from table "${sourceTable}" to table "${targetTable}"`)
}
/**
* Utility function to convert a string to snake case, e.g. "titleIgnorePrefix" -> "title_ignore_prefix"
*
* @param {string} str - the string to convert to snake case.
* @returns {string} - the string in snake case.
*/
function convertToSnakeCase(str) {
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
}
module.exports = { up, down }
@@ -0,0 +1,272 @@
const util = require('util')
const { Sequelize } = require('sequelize')
/**
* @typedef MigrationContext
* @property {import('sequelize').QueryInterface} queryInterface - a suquelize QueryInterface object.
* @property {import('../Logger')} logger - a Logger object.
*
* @typedef MigrationOptions
* @property {MigrationContext} context - an object containing the migration context.
*/
const migrationVersion = '2.20.0'
const migrationName = `${migrationVersion}-improve-author-sort-queries`
const loggerPrefix = `[${migrationVersion} migration]`
// Migration constants
const libraryItems = 'libraryItems'
const bookAuthors = 'bookAuthors'
const authors = 'authors'
const podcastEpisodes = 'podcastEpisodes'
const columns = [
{ name: 'authorNamesFirstLast', source: `${authors}.name`, spec: { type: Sequelize.STRING, allowNull: true } },
{ name: 'authorNamesLastFirst', source: `${authors}.lastFirst`, spec: { type: Sequelize.STRING, allowNull: true } }
]
const authorsSort = `${bookAuthors}.createdAt ASC`
const columnNames = columns.map((column) => column.name).join(', ')
const columnSourcesExpression = columns.map((column) => `GROUP_CONCAT(${column.source}, ', ' ORDER BY ${authorsSort})`).join(', ')
const authorsJoin = `${authors} JOIN ${bookAuthors} ON ${authors}.id = ${bookAuthors}.authorId`
/**
* This upward migration adds an authorNames column to the libraryItems table and populates it.
* It also creates triggers to update the authorNames column when the corresponding bookAuthors and authors records are updated.
* It also creates an index on the authorNames column.
*
* It also adds an index on publishedAt to the podcastEpisodes table.
*
* @param {MigrationOptions} options - an object containing the migration context.
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
*/
async function up({ context: { queryInterface, logger } }) {
const helper = new MigrationHelper(queryInterface, logger)
// Upwards migration script
logger.info(`${loggerPrefix} UPGRADE BEGIN: ${migrationName}`)
// Add authorNames columns to libraryItems table
await helper.addColumns()
// Populate authorNames columns with the author names for each libraryItem
await helper.populateColumnsFromSource()
// Create triggers to update the authorNames column when the corresponding bookAuthors and authors records are updated
await helper.addTriggers()
// Create indexes on the authorNames columns
await helper.addIndexes()
// Add index on publishedAt to the podcastEpisodes table
await helper.addIndex(podcastEpisodes, ['publishedAt'])
logger.info(`${loggerPrefix} UPGRADE END: ${migrationName}`)
}
/**
* This downward migration removes the authorNames column from the libraryItems table,
* the triggers on the bookAuthors and authors tables, and the index on the authorNames column.
*
* It also removes the index on publishedAt from the podcastEpisodes table.
*
* @param {MigrationOptions} options - an object containing the migration context.
* @returns {Promise<void>} - A promise that resolves when the migration is complete.
*/
async function down({ context: { queryInterface, logger } }) {
// Downward migration script
logger.info(`${loggerPrefix} DOWNGRADE BEGIN: ${migrationName}`)
const helper = new MigrationHelper(queryInterface, logger)
// Remove triggers to update authorNames columns
await helper.removeTriggers()
// Remove index on publishedAt from the podcastEpisodes table
await helper.removeIndex(podcastEpisodes, ['publishedAt'])
// Remove indexes on the authorNames columns
await helper.removeIndexes()
// Remove authorNames columns from libraryItems table
await helper.removeColumns()
logger.info(`${loggerPrefix} DOWNGRADE END: ${migrationName}`)
}
class MigrationHelper {
constructor(queryInterface, logger) {
this.queryInterface = queryInterface
this.logger = logger
}
async addColumn(table, column, options) {
this.logger.info(`${loggerPrefix} adding column "${column}" to table "${table}"`)
const tableDescription = await this.queryInterface.describeTable(table)
if (!tableDescription[column]) {
await this.queryInterface.addColumn(table, column, options)
this.logger.info(`${loggerPrefix} added column "${column}" to table "${table}"`)
} else {
this.logger.info(`${loggerPrefix} column "${column}" already exists in table "${table}"`)
}
}
async addColumns() {
this.logger.info(`${loggerPrefix} adding ${columnNames} columns to ${libraryItems} table`)
for (const column of columns) {
await this.addColumn(libraryItems, column.name, column.spec)
}
this.logger.info(`${loggerPrefix} added ${columnNames} columns to ${libraryItems} table`)
}
async removeColumn(table, column) {
this.logger.info(`${loggerPrefix} removing column "${column}" from table "${table}"`)
const tableDescription = await this.queryInterface.describeTable(table)
if (tableDescription[column]) {
await this.queryInterface.sequelize.query(`ALTER TABLE ${table} DROP COLUMN ${column}`)
this.logger.info(`${loggerPrefix} removed column "${column}" from table "${table}"`)
} else {
this.logger.info(`${loggerPrefix} column "${column}" does not exist in table "${table}"`)
}
}
async removeColumns() {
this.logger.info(`${loggerPrefix} removing ${columnNames} columns from ${libraryItems} table`)
for (const column of columns) {
await this.removeColumn(libraryItems, column.name)
}
this.logger.info(`${loggerPrefix} removed ${columnNames} columns from ${libraryItems} table`)
}
async populateColumnsFromSource() {
this.logger.info(`${loggerPrefix} populating ${columnNames} columns in ${libraryItems} table`)
const authorNamesSubQuery = `
SELECT ${columnSourcesExpression}
FROM ${authorsJoin}
WHERE ${bookAuthors}.bookId = ${libraryItems}.mediaId
`
await this.queryInterface.sequelize.query(`
UPDATE ${libraryItems}
SET (${columnNames}) = (${authorNamesSubQuery})
WHERE mediaType = 'book';
`)
this.logger.info(`${loggerPrefix} populated ${columnNames} columns in ${libraryItems} table`)
}
async addBookAuthorsTrigger(action) {
this.logger.info(`${loggerPrefix} adding trigger to update ${libraryItems} ${columnNames} on ${bookAuthors} ${action}`)
const modifiedRecord = action === 'delete' ? 'OLD' : 'NEW'
const triggerName = convertToSnakeCase(`update_${libraryItems}_authorNames_on_${bookAuthors}_${action}`)
const authorNamesSubQuery = `
SELECT ${columnSourcesExpression}
FROM ${authorsJoin}
WHERE ${bookAuthors}.bookId = ${modifiedRecord}.bookId
`
await this.queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
await this.queryInterface.sequelize.query(`
CREATE TRIGGER ${triggerName}
AFTER ${action} ON ${bookAuthors}
FOR EACH ROW
BEGIN
UPDATE ${libraryItems}
SET (${columnNames}) = (${authorNamesSubQuery})
WHERE mediaId = ${modifiedRecord}.bookId;
END;
`)
this.logger.info(`${loggerPrefix} added trigger to update ${libraryItems} ${columnNames} on ${bookAuthors} ${action}`)
}
async addAuthorsUpdateTrigger() {
this.logger.info(`${loggerPrefix} adding trigger to update ${libraryItems} ${columnNames} on ${authors} update`)
const triggerName = convertToSnakeCase(`update_${libraryItems}_authorNames_on_authors_update`)
const authorNamesSubQuery = `
SELECT ${columnSourcesExpression}
FROM ${authorsJoin}
WHERE ${bookAuthors}.bookId = ${libraryItems}.mediaId
`
await this.queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
await this.queryInterface.sequelize.query(`
CREATE TRIGGER ${triggerName}
AFTER UPDATE OF name ON ${authors}
FOR EACH ROW
BEGIN
UPDATE ${libraryItems}
SET (${columnNames}) = (${authorNamesSubQuery})
WHERE mediaId IN (SELECT bookId FROM ${bookAuthors} WHERE authorId = NEW.id);
END;
`)
this.logger.info(`${loggerPrefix} added trigger to update ${libraryItems} ${columnNames} on ${authors} update`)
}
async addTriggers() {
await this.addBookAuthorsTrigger('insert')
await this.addBookAuthorsTrigger('delete')
await this.addAuthorsUpdateTrigger()
}
async removeBookAuthorsTrigger(action) {
this.logger.info(`${loggerPrefix} removing trigger to update ${libraryItems} ${columnNames} on ${bookAuthors} ${action}`)
const triggerName = convertToSnakeCase(`update_${libraryItems}_authorNames_on_${bookAuthors}_${action}`)
await this.queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
this.logger.info(`${loggerPrefix} removed trigger to update ${libraryItems} ${columnNames} on ${bookAuthors} ${action}`)
}
async removeAuthorsUpdateTrigger() {
this.logger.info(`${loggerPrefix} removing trigger to update ${libraryItems} ${columnNames} on ${authors} update`)
const triggerName = convertToSnakeCase(`update_${libraryItems}_authorNames_on_authors_update`)
await this.queryInterface.sequelize.query(`DROP TRIGGER IF EXISTS ${triggerName}`)
this.logger.info(`${loggerPrefix} removed trigger to update ${libraryItems} ${columnNames} on ${authors} update`)
}
async removeTriggers() {
await this.removeBookAuthorsTrigger('insert')
await this.removeBookAuthorsTrigger('delete')
await this.removeAuthorsUpdateTrigger()
}
async addIndex(tableName, columns) {
const columnString = columns.map((column) => util.inspect(column)).join(', ')
const indexName = convertToSnakeCase(`${tableName}_${columns.map((column) => (typeof column === 'string' ? column : column.name)).join('_')}`)
try {
this.logger.info(`${loggerPrefix} adding index on [${columnString}] to table ${tableName}. index name: ${indexName}"`)
await this.queryInterface.addIndex(tableName, columns)
this.logger.info(`${loggerPrefix} added index on [${columnString}] to table ${tableName}. index name: ${indexName}"`)
} catch (error) {
if (error.name === 'SequelizeDatabaseError' && error.message.includes('already exists')) {
this.logger.info(`${loggerPrefix} index [${columnString}] for table "${tableName}" already exists`)
} else {
throw error
}
}
}
async addIndexes() {
for (const column of columns) {
await this.addIndex(libraryItems, ['libraryId', 'mediaType', { name: column.name, collate: 'NOCASE' }])
}
}
async removeIndex(tableName, columns) {
this.logger.info(`${loggerPrefix} removing index [${columns.join(', ')}] from table "${tableName}"`)
await this.queryInterface.removeIndex(tableName, columns)
this.logger.info(`${loggerPrefix} removed index [${columns.join(', ')}] from table "${tableName}"`)
}
async removeIndexes() {
for (const column of columns) {
await this.removeIndex(libraryItems, ['libraryId', 'mediaType', column.name])
}
}
}
/**
* Utility function to convert a string to snake case, e.g. "titleIgnorePrefix" -> "title_ignore_prefix"
*
* @param {string} str - the string to convert to snake case.
* @returns {string} - the string in snake case.
*/
function convertToSnakeCase(str) {
return str.replace(/([A-Z])/g, '_$1').toLowerCase()
}
module.exports = { up, down }
+4
View File
@@ -374,6 +374,10 @@ class Book extends Model {
if (payload.metadata) { if (payload.metadata) {
const metadataStringKeys = ['title', 'subtitle', 'publishedYear', 'publishedDate', 'publisher', 'description', 'isbn', 'asin', 'language'] const metadataStringKeys = ['title', 'subtitle', 'publishedYear', 'publishedDate', 'publisher', 'description', 'isbn', 'asin', 'language']
metadataStringKeys.forEach((key) => { metadataStringKeys.forEach((key) => {
if (typeof payload.metadata[key] == 'number') {
payload.metadata[key] = String(payload.metadata[key])
}
if ((typeof payload.metadata[key] === 'string' || payload.metadata[key] === null) && this[key] !== payload.metadata[key]) { if ((typeof payload.metadata[key] === 'string' || payload.metadata[key] === null) && this[key] !== payload.metadata[key]) {
this[key] = payload.metadata[key] || null this[key] = payload.metadata[key] || null
+14 -2
View File
@@ -77,6 +77,10 @@ class LibraryItem extends Model {
this.title // Only used for sorting this.title // Only used for sorting
/** @type {string} */ /** @type {string} */
this.titleIgnorePrefix // Only used for sorting this.titleIgnorePrefix // Only used for sorting
/** @type {string} */
this.authorNamesFirstLast // Only used for sorting
/** @type {string} */
this.authorNamesLastFirst // Only used for sorting
} }
/** /**
@@ -103,7 +107,7 @@ class LibraryItem extends Model {
{ {
model: this.sequelize.models.series, model: this.sequelize.models.series,
through: { through: {
attributes: ['sequence', 'createdAt'] attributes: ['id', 'sequence', 'createdAt']
} }
} }
] ]
@@ -683,7 +687,9 @@ class LibraryItem extends Model {
libraryFiles: DataTypes.JSON, libraryFiles: DataTypes.JSON,
extraData: DataTypes.JSON, extraData: DataTypes.JSON,
title: DataTypes.STRING, title: DataTypes.STRING,
titleIgnorePrefix: DataTypes.STRING titleIgnorePrefix: DataTypes.STRING,
authorNamesFirstLast: DataTypes.STRING,
authorNamesLastFirst: DataTypes.STRING
}, },
{ {
sequelize, sequelize,
@@ -710,6 +716,12 @@ class LibraryItem extends Model {
{ {
fields: ['libraryId', 'mediaType', { name: 'titleIgnorePrefix', collate: 'NOCASE' }] fields: ['libraryId', 'mediaType', { name: 'titleIgnorePrefix', collate: 'NOCASE' }]
}, },
{
fields: ['libraryId', 'mediaType', { name: 'authorNamesFirstLast', collate: 'NOCASE' }]
},
{
fields: ['libraryId', 'mediaType', { name: 'authorNamesLastFirst', collate: 'NOCASE' }]
},
{ {
fields: ['libraryId', 'mediaId', 'mediaType'] fields: ['libraryId', 'mediaId', 'mediaType']
}, },
+15 -2
View File
@@ -34,6 +34,8 @@ class MediaProgress extends Model {
this.updatedAt this.updatedAt
/** @type {Date} */ /** @type {Date} */
this.createdAt this.createdAt
/** @type {UUIDV4} */
this.podcastId
} }
static removeById(mediaProgressId) { static removeById(mediaProgressId) {
@@ -69,7 +71,8 @@ class MediaProgress extends Model {
ebookLocation: DataTypes.STRING, ebookLocation: DataTypes.STRING,
ebookProgress: DataTypes.FLOAT, ebookProgress: DataTypes.FLOAT,
finishedAt: DataTypes.DATE, finishedAt: DataTypes.DATE,
extraData: DataTypes.JSON extraData: DataTypes.JSON,
podcastId: DataTypes.UUID
}, },
{ {
sequelize, sequelize,
@@ -123,6 +126,16 @@ class MediaProgress extends Model {
} }
}) })
// make sure to call the afterDestroy hook for each instance
MediaProgress.addHook('beforeBulkDestroy', (options) => {
options.individualHooks = true
})
// update the potentially cached user after destroying the media progress
MediaProgress.addHook('afterDestroy', (instance) => {
user.mediaProgressRemoved(instance)
})
user.hasMany(MediaProgress, { user.hasMany(MediaProgress, {
onDelete: 'CASCADE' onDelete: 'CASCADE'
}) })
@@ -174,7 +187,7 @@ class MediaProgress extends Model {
if (!this.extraData) this.extraData = {} if (!this.extraData) this.extraData = {}
if (progressPayload.isFinished !== undefined) { if (progressPayload.isFinished !== undefined) {
if (progressPayload.isFinished && !this.isFinished) { if (progressPayload.isFinished && !this.isFinished) {
this.finishedAt = Date.now() this.finishedAt = progressPayload.finishedAt || Date.now()
this.extraData.progress = 1 this.extraData.progress = 1
this.changed('extraData', true) this.changed('extraData', true)
delete progressPayload.finishedAt delete progressPayload.finishedAt
+13 -1
View File
@@ -1,6 +1,7 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils') const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils')
const Logger = require('../Logger') const Logger = require('../Logger')
const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters')
/** /**
* @typedef PodcastExpandedProperties * @typedef PodcastExpandedProperties
@@ -61,6 +62,8 @@ class Podcast extends Model {
this.createdAt this.createdAt
/** @type {Date} */ /** @type {Date} */
this.updatedAt this.updatedAt
/** @type {number} */
this.numEpisodes
/** @type {import('./PodcastEpisode')[]} */ /** @type {import('./PodcastEpisode')[]} */
this.podcastEpisodes this.podcastEpisodes
@@ -138,13 +141,22 @@ class Podcast extends Model {
maxNewEpisodesToDownload: DataTypes.INTEGER, maxNewEpisodesToDownload: DataTypes.INTEGER,
coverPath: DataTypes.STRING, coverPath: DataTypes.STRING,
tags: DataTypes.JSON, tags: DataTypes.JSON,
genres: DataTypes.JSON genres: DataTypes.JSON,
numEpisodes: DataTypes.INTEGER
}, },
{ {
sequelize, sequelize,
modelName: 'podcast' modelName: 'podcast'
} }
) )
Podcast.addHook('afterDestroy', async (instance) => {
libraryItemsPodcastFilters.clearCountCache('podcast', 'afterDestroy')
})
Podcast.addHook('afterCreate', async (instance) => {
libraryItemsPodcastFilters.clearCountCache('podcast', 'afterCreate')
})
} }
get hasMediaFiles() { get hasMediaFiles() {
+13 -1
View File
@@ -1,5 +1,5 @@
const { DataTypes, Model } = require('sequelize') const { DataTypes, Model } = require('sequelize')
const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters')
/** /**
* @typedef ChapterObject * @typedef ChapterObject
* @property {number} id * @property {number} id
@@ -122,6 +122,10 @@ class PodcastEpisode extends Model {
{ {
name: 'podcastEpisode_createdAt_podcastId', name: 'podcastEpisode_createdAt_podcastId',
fields: ['createdAt', 'podcastId'] fields: ['createdAt', 'podcastId']
},
{
name: 'podcast_episodes_published_at',
fields: ['publishedAt']
} }
] ]
} }
@@ -132,6 +136,14 @@ class PodcastEpisode extends Model {
onDelete: 'CASCADE' onDelete: 'CASCADE'
}) })
PodcastEpisode.belongsTo(podcast) PodcastEpisode.belongsTo(podcast)
PodcastEpisode.addHook('afterDestroy', async (instance) => {
libraryItemsPodcastFilters.clearCountCache('podcastEpisode', 'afterDestroy')
})
PodcastEpisode.addHook('afterCreate', async (instance) => {
libraryItemsPodcastFilters.clearCountCache('podcastEpisode', 'afterCreate')
})
} }
get size() { get size() {
+13 -1
View File
@@ -404,6 +404,14 @@ class User extends Model {
return count > 0 return count > 0
} }
static mediaProgressRemoved(mediaProgress) {
const cachedUser = userCache.getById(mediaProgress.userId)
if (cachedUser) {
Logger.debug(`[User] mediaProgressRemoved: ${mediaProgress.id} from user ${cachedUser.id}`)
cachedUser.mediaProgresses = cachedUser.mediaProgresses.filter((mp) => mp.id !== mediaProgress.id)
}
}
/** /**
* Initialize model * Initialize model
* @param {import('../Database').sequelize} sequelize * @param {import('../Database').sequelize} sequelize
@@ -626,6 +634,7 @@ class User extends Model {
/** @type {import('./MediaProgress')|null} */ /** @type {import('./MediaProgress')|null} */
let mediaProgress = null let mediaProgress = null
let mediaItemId = null let mediaItemId = null
let podcastId = null
if (progressPayload.episodeId) { if (progressPayload.episodeId) {
const podcastEpisode = await this.sequelize.models.podcastEpisode.findByPk(progressPayload.episodeId, { const podcastEpisode = await this.sequelize.models.podcastEpisode.findByPk(progressPayload.episodeId, {
attributes: ['id', 'podcastId'], attributes: ['id', 'podcastId'],
@@ -654,6 +663,7 @@ class User extends Model {
} }
mediaItemId = podcastEpisode.id mediaItemId = podcastEpisode.id
mediaProgress = podcastEpisode.mediaProgresses?.[0] mediaProgress = podcastEpisode.mediaProgresses?.[0]
podcastId = podcastEpisode.podcastId
} else { } else {
const libraryItem = await this.sequelize.models.libraryItem.findByPk(progressPayload.libraryItemId, { const libraryItem = await this.sequelize.models.libraryItem.findByPk(progressPayload.libraryItemId, {
attributes: ['id', 'mediaId', 'mediaType'], attributes: ['id', 'mediaId', 'mediaType'],
@@ -686,6 +696,7 @@ class User extends Model {
const newMediaProgressPayload = { const newMediaProgressPayload = {
userId: this.id, userId: this.id,
mediaItemId, mediaItemId,
podcastId,
mediaItemType: progressPayload.episodeId ? 'podcastEpisode' : 'book', mediaItemType: progressPayload.episodeId ? 'podcastEpisode' : 'book',
duration: isNullOrNaN(progressPayload.duration) ? 0 : Number(progressPayload.duration), duration: isNullOrNaN(progressPayload.duration) ? 0 : Number(progressPayload.duration),
currentTime: isNullOrNaN(progressPayload.currentTime) ? 0 : Number(progressPayload.currentTime), currentTime: isNullOrNaN(progressPayload.currentTime) ? 0 : Number(progressPayload.currentTime),
@@ -694,13 +705,14 @@ class User extends Model {
ebookLocation: progressPayload.ebookLocation || null, ebookLocation: progressPayload.ebookLocation || null,
ebookProgress: isNullOrNaN(progressPayload.ebookProgress) ? 0 : Number(progressPayload.ebookProgress), ebookProgress: isNullOrNaN(progressPayload.ebookProgress) ? 0 : Number(progressPayload.ebookProgress),
finishedAt: progressPayload.finishedAt || null, finishedAt: progressPayload.finishedAt || null,
createdAt: progressPayload.createdAt || new Date(),
extraData: { extraData: {
libraryItemId: progressPayload.libraryItemId, libraryItemId: progressPayload.libraryItemId,
progress: isNullOrNaN(progressPayload.progress) ? 0 : Number(progressPayload.progress) progress: isNullOrNaN(progressPayload.progress) ? 0 : Number(progressPayload.progress)
} }
} }
if (newMediaProgressPayload.isFinished) { if (newMediaProgressPayload.isFinished) {
newMediaProgressPayload.finishedAt = new Date() newMediaProgressPayload.finishedAt = newMediaProgressPayload.finishedAt || new Date()
newMediaProgressPayload.extraData.progress = 1 newMediaProgressPayload.extraData.progress = 1
} else { } else {
newMediaProgressPayload.finishedAt = null newMediaProgressPayload.finishedAt = null
+2 -1
View File
@@ -43,7 +43,8 @@ class PodcastEpisodeDownload {
season: this.rssPodcastEpisode?.season ?? null, season: this.rssPodcastEpisode?.season ?? null,
episode: this.rssPodcastEpisode?.episode ?? null, episode: this.rssPodcastEpisode?.episode ?? null,
episodeType: this.rssPodcastEpisode?.episodeType ?? 'full', episodeType: this.rssPodcastEpisode?.episodeType ?? 'full',
publishedAt: this.rssPodcastEpisode?.publishedAt ?? null publishedAt: this.rssPodcastEpisode?.publishedAt ?? null,
guid: this.rssPodcastEpisode?.guid ?? null
} }
} }
+53 -18
View File
@@ -41,6 +41,9 @@ class CustomProviderAdapter {
} }
const queryString = new URLSearchParams(queryObj).toString() const queryString = new URLSearchParams(queryObj).toString()
const url = `${provider.url}/search?${queryString}`
Logger.debug(`[CustomMetadataProvider] Search url: ${url}`)
// Setup headers // Setup headers
const axiosOptions = { const axiosOptions = {
timeout timeout
@@ -52,7 +55,7 @@ class CustomProviderAdapter {
} }
const matches = await axios const matches = await axios
.get(`${provider.url}/search?${queryString}`, axiosOptions) .get(url, axiosOptions)
.then((res) => { .then((res) => {
if (!res?.data || !Array.isArray(res.data.matches)) return null if (!res?.data || !Array.isArray(res.data.matches)) return null
return res.data.matches return res.data.matches
@@ -66,25 +69,57 @@ class CustomProviderAdapter {
throw new Error('Custom provider returned malformed response') throw new Error('Custom provider returned malformed response')
} }
const toStringOrUndefined = (value) => {
if (typeof value === 'string' || typeof value === 'number') return String(value)
if (Array.isArray(value) && value.every((v) => typeof v === 'string' || typeof v === 'number')) return value.join(',')
return undefined
}
const validateSeriesArray = (series) => {
if (!Array.isArray(series) || !series.length) return undefined
return series
.map((s) => {
if (!s?.series || typeof s.series !== 'string') return undefined
const _series = {
series: s.series
}
if (s.sequence && (typeof s.sequence === 'string' || typeof s.sequence === 'number')) {
_series.sequence = String(s.sequence)
}
return _series
})
.filter((s) => s !== undefined)
}
// re-map keys to throw out // re-map keys to throw out
return matches.map(({ title, subtitle, author, narrator, publisher, publishedYear, description, cover, isbn, asin, genres, tags, series, language, duration }) => { return matches.map((match) => {
return { const { title, subtitle, author, narrator, publisher, publishedYear, description, cover, isbn, asin, genres, tags, series, language, duration } = match
title,
subtitle, const payload = {
author, title: toStringOrUndefined(title),
narrator, subtitle: toStringOrUndefined(subtitle),
publisher, author: toStringOrUndefined(author),
publishedYear, narrator: toStringOrUndefined(narrator),
description: htmlSanitizer.sanitize(description), publisher: toStringOrUndefined(publisher),
cover, publishedYear: toStringOrUndefined(publishedYear),
isbn, description: description && typeof description === 'string' ? htmlSanitizer.sanitize(description) : undefined,
asin, cover: toStringOrUndefined(cover),
genres, isbn: toStringOrUndefined(isbn),
tags: tags?.join(',') || null, asin: toStringOrUndefined(asin),
series: series?.length ? series : null, genres: Array.isArray(genres) && genres.every((g) => typeof g === 'string') ? genres : undefined,
language, tags: toStringOrUndefined(tags),
duration series: validateSeriesArray(series),
language: toStringOrUndefined(language),
duration: !isNaN(duration) && duration !== null ? Number(duration) : undefined
} }
// Remove undefined values
for (const key in payload) {
if (payload[key] === undefined) {
delete payload[key]
}
}
return payload
}) })
} }
} }
+2 -2
View File
@@ -66,10 +66,10 @@ class OpenLibrary {
} }
parsePublishYear(doc, worksData) { parsePublishYear(doc, worksData) {
if (doc.first_publish_year && !isNaN(doc.first_publish_year)) return doc.first_publish_year if (doc.first_publish_year && !isNaN(doc.first_publish_year)) return String(doc.first_publish_year)
if (worksData.first_publish_date) { if (worksData.first_publish_date) {
var year = worksData.first_publish_date.split('-')[0] var year = worksData.first_publish_date.split('-')[0]
if (!isNaN(year)) return year if (!isNaN(year)) return String(year)
} }
return null return null
} }
+82 -57
View File
@@ -392,21 +392,51 @@ class ApiRouter {
async checkRemoveEmptySeries(seriesIds) { async checkRemoveEmptySeries(seriesIds) {
if (!seriesIds?.length) return if (!seriesIds?.length) return
const series = await Database.seriesModel.findAll({ const transaction = await Database.sequelize.transaction()
where: { try {
id: seriesIds const seriesToRemove = (
}, await Database.seriesModel.findAll({
attributes: ['id', 'name', 'libraryId'], where: [
include: { {
model: Database.bookModel, id: seriesIds
attributes: ['id'] },
} sequelize.where(sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 0)
}) ],
attributes: ['id', 'name', 'libraryId'],
include: {
model: Database.bookModel,
attributes: ['id'],
required: false // Ensure it includes series even if no books exist
},
transaction
})
).map((s) => ({ id: s.id, name: s.name, libraryId: s.libraryId }))
for (const s of series) { if (seriesToRemove.length) {
if (!s.books.length) { await Database.seriesModel.destroy({
await this.removeEmptySeries(s) where: {
id: seriesToRemove.map((s) => s.id)
},
transaction
})
} }
await transaction.commit()
seriesToRemove.forEach(({ id, name, libraryId }) => {
Logger.info(`[ApiRouter] Series "${name}" is now empty. Removing series`)
// Remove series from library filter data
Database.removeSeriesFromFilterData(libraryId, id)
SocketAuthority.emitter('series_removed', { id: id, libraryId: libraryId })
})
// Close rss feeds - remove from db and emit socket event
if (seriesToRemove.length) {
await RssFeedManager.closeFeedsForEntityIds(seriesToRemove.map((s) => s.id))
}
} catch (error) {
await transaction.rollback()
Logger.error(`[ApiRouter] Error removing empty series: ${error.message}`)
} }
} }
@@ -420,61 +450,56 @@ class ApiRouter {
async checkRemoveAuthorsWithNoBooks(authorIds) { async checkRemoveAuthorsWithNoBooks(authorIds) {
if (!authorIds?.length) return if (!authorIds?.length) return
const bookAuthorsToRemove = ( const transaction = await Database.sequelize.transaction()
await Database.authorModel.findAll({ try {
where: [ // Select authors with locking to prevent concurrent updates
{ const bookAuthorsToRemove = (
id: authorIds, await Database.authorModel.findAll({
asin: { where: [
[sequelize.Op.or]: [null, ''] {
id: authorIds,
asin: {
[sequelize.Op.or]: [null, '']
},
description: {
[sequelize.Op.or]: [null, '']
},
imagePath: {
[sequelize.Op.or]: [null, '']
}
}, },
description: { sequelize.where(sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 0)
[sequelize.Op.or]: [null, ''] ],
}, attributes: ['id', 'name', 'libraryId'],
imagePath: { raw: true,
[sequelize.Op.or]: [null, ''] transaction
} })
}, ).map((au) => ({ id: au.id, name: au.name, libraryId: au.libraryId }))
sequelize.where(sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 0)
],
attributes: ['id', 'name', 'libraryId'],
raw: true
})
).map((au) => ({ id: au.id, name: au.name, libraryId: au.libraryId }))
if (bookAuthorsToRemove.length) { if (bookAuthorsToRemove.length) {
await Database.authorModel.destroy({ await Database.authorModel.destroy({
where: { where: {
id: bookAuthorsToRemove.map((au) => au.id) id: bookAuthorsToRemove.map((au) => au.id)
} },
}) transaction
})
}
await transaction.commit()
// Remove all book authors after completing remove from database
bookAuthorsToRemove.forEach(({ id, name, libraryId }) => { bookAuthorsToRemove.forEach(({ id, name, libraryId }) => {
Database.removeAuthorFromFilterData(libraryId, id) Database.removeAuthorFromFilterData(libraryId, id)
// TODO: Clients were expecting full author in payload but its unnecessary // TODO: Clients were expecting full author in payload but its unnecessary
SocketAuthority.emitter('author_removed', { id, libraryId }) SocketAuthority.emitter('author_removed', { id, libraryId })
Logger.info(`[ApiRouter] Removed author "${name}" with no books`) Logger.info(`[ApiRouter] Removed author "${name}" with no books`)
}) })
} catch (error) {
await transaction.rollback()
Logger.error(`[ApiRouter] Error removing authors: ${error.message}`)
} }
} }
/**
* Remove an empty series & close an open RSS feed
* @param {import('../models/Series')} series
*/
async removeEmptySeries(series) {
await RssFeedManager.closeFeedForEntityId(series.id)
Logger.info(`[ApiRouter] Series "${series.name}" is now empty. Removing series`)
// Remove series from library filter data
Database.removeSeriesFromFilterData(series.libraryId, series.id)
SocketAuthority.emitter('series_removed', {
id: series.id,
libraryId: series.libraryId
})
await series.destroy()
}
async getUserListeningSessionsHelper(userId) { async getUserListeningSessionsHelper(userId) {
const userSessions = await Database.getPlaybackSessions({ userId }) const userSessions = await Database.getPlaybackSessions({ userId })
return userSessions.sort((a, b) => b.updatedAt - a.updatedAt) return userSessions.sort((a, b) => b.updatedAt - a.updatedAt)
+2
View File
@@ -523,6 +523,8 @@ class BookScanner {
libraryItemObj.extraData = {} libraryItemObj.extraData = {}
libraryItemObj.title = bookMetadata.title libraryItemObj.title = bookMetadata.title
libraryItemObj.titleIgnorePrefix = getTitleIgnorePrefix(bookMetadata.title) libraryItemObj.titleIgnorePrefix = getTitleIgnorePrefix(bookMetadata.title)
libraryItemObj.authorNamesFirstLast = bookMetadata.authors.join(', ')
libraryItemObj.authorNamesLastFirst = bookMetadata.authors.map((author) => Database.authorModel.getLastFirst(author)).join(', ')
// Set isSupplementary flag on ebook library files // Set isSupplementary flag on ebook library files
for (const libraryFile of libraryItemObj.libraryFiles) { for (const libraryFile of libraryItemObj.libraryFiles) {
+1 -11
View File
@@ -4,7 +4,6 @@ const fs = require('../libs/fsExtra')
const date = require('../libs/dateAndTime') const date = require('../libs/dateAndTime')
const Logger = require('../Logger') const Logger = require('../Logger')
const { LogLevel } = require('../utils/constants')
const { secondsToTimestamp, elapsedPretty } = require('../utils/index') const { secondsToTimestamp, elapsedPretty } = require('../utils/index')
class LibraryScan { class LibraryScan {
@@ -109,20 +108,11 @@ class LibraryScan {
this.elapsed = this.finishedAt - this.startedAt this.elapsed = this.finishedAt - this.startedAt
} }
getLogLevelString(level) {
for (const key in LogLevel) {
if (LogLevel[key] === level) {
return key
}
}
return 'UNKNOWN'
}
addLog(level, ...args) { addLog(level, ...args) {
const logObj = { const logObj = {
timestamp: this.timestamp, timestamp: this.timestamp,
message: args.join(' '), message: args.join(' '),
levelName: this.getLogLevelString(level), levelName: Logger.getLogLevelString(level),
level level
} }
+5 -3
View File
@@ -2,7 +2,7 @@ const { parseOpfMetadataXML } = require('../utils/parsers/parseOpfMetadata')
const { readTextFile } = require('../utils/fileUtils') const { readTextFile } = require('../utils/fileUtils')
class OpfFileScanner { class OpfFileScanner {
constructor() { } constructor() {}
/** /**
* Parse metadata from .opf file found in library scan and update bookMetadata * Parse metadata from .opf file found in library scan and update bookMetadata
@@ -15,11 +15,13 @@ class OpfFileScanner {
const opfMetadata = xmlText ? await parseOpfMetadataXML(xmlText) : null const opfMetadata = xmlText ? await parseOpfMetadataXML(xmlText) : null
if (opfMetadata) { if (opfMetadata) {
for (const key in opfMetadata) { for (const key in opfMetadata) {
if (key === 'tags') { // Add tags only if tags are empty if (key === 'tags') {
// Add tags only if tags are empty
if (opfMetadata.tags.length) { if (opfMetadata.tags.length) {
bookMetadata.tags = opfMetadata.tags bookMetadata.tags = opfMetadata.tags
} }
} else if (key === 'genres') { // Add genres only if genres are empty } else if (key === 'genres') {
// Add genres only if genres are empty
if (opfMetadata.genres.length) { if (opfMetadata.genres.length) {
bookMetadata.genres = opfMetadata.genres bookMetadata.genres = opfMetadata.genres
} }
+74 -57
View File
@@ -1,4 +1,4 @@
const uuidv4 = require("uuid").v4 const uuidv4 = require('uuid').v4
const Path = require('path') const Path = require('path')
const { LogLevel } = require('../utils/constants') const { LogLevel } = require('../utils/constants')
const { getTitleIgnorePrefix } = require('../utils/index') const { getTitleIgnorePrefix } = require('../utils/index')
@@ -8,9 +8,9 @@ const { filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtil
const AudioFile = require('../objects/files/AudioFile') const AudioFile = require('../objects/files/AudioFile')
const CoverManager = require('../managers/CoverManager') const CoverManager = require('../managers/CoverManager')
const LibraryFile = require('../objects/files/LibraryFile') const LibraryFile = require('../objects/files/LibraryFile')
const fsExtra = require("../libs/fsExtra") const fsExtra = require('../libs/fsExtra')
const PodcastEpisode = require("../models/PodcastEpisode") const PodcastEpisode = require('../models/PodcastEpisode')
const AbsMetadataFileScanner = require("./AbsMetadataFileScanner") const AbsMetadataFileScanner = require('./AbsMetadataFileScanner')
/** /**
* Metadata for podcasts pulled from files * Metadata for podcasts pulled from files
@@ -32,7 +32,7 @@ const AbsMetadataFileScanner = require("./AbsMetadataFileScanner")
*/ */
class PodcastScanner { class PodcastScanner {
constructor() { } constructor() {}
/** /**
* @param {import('../models/LibraryItem')} existingLibraryItem * @param {import('../models/LibraryItem')} existingLibraryItem
@@ -59,28 +59,34 @@ class PodcastScanner {
if (libraryItemData.hasAudioFileChanges || libraryItemData.audioLibraryFiles.length !== existingPodcastEpisodes.length) { if (libraryItemData.hasAudioFileChanges || libraryItemData.audioLibraryFiles.length !== existingPodcastEpisodes.length) {
// Filter out and destroy episodes that were removed // Filter out and destroy episodes that were removed
existingPodcastEpisodes = await Promise.all(existingPodcastEpisodes.filter(async ep => { existingPodcastEpisodes = await Promise.all(
if (libraryItemData.checkAudioFileRemoved(ep.audioFile)) { existingPodcastEpisodes.filter(async (ep) => {
libraryScan.addLog(LogLevel.INFO, `Podcast episode "${ep.title}" audio file was removed`) if (libraryItemData.checkAudioFileRemoved(ep.audioFile)) {
// TODO: Should clean up other data linked to this episode libraryScan.addLog(LogLevel.INFO, `Podcast episode "${ep.title}" audio file was removed`)
await ep.destroy() // TODO: Should clean up other data linked to this episode
return false await ep.destroy()
} return false
return true }
})) return true
})
)
// Update audio files that were modified // Update audio files that were modified
if (libraryItemData.audioLibraryFilesModified.length) { if (libraryItemData.audioLibraryFilesModified.length) {
let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, libraryItemData.audioLibraryFilesModified.map(lf => lf.new)) let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(
existingLibraryItem.mediaType,
libraryItemData,
libraryItemData.audioLibraryFilesModified.map((lf) => lf.new)
)
for (const podcastEpisode of existingPodcastEpisodes) { for (const podcastEpisode of existingPodcastEpisodes) {
let matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.metadata.path === podcastEpisode.audioFile.metadata.path) let matchedScannedAudioFile = scannedAudioFiles.find((saf) => saf.metadata.path === podcastEpisode.audioFile.metadata.path)
if (!matchedScannedAudioFile) { if (!matchedScannedAudioFile) {
matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.ino === podcastEpisode.audioFile.ino) matchedScannedAudioFile = scannedAudioFiles.find((saf) => saf.ino === podcastEpisode.audioFile.ino)
} }
if (matchedScannedAudioFile) { if (matchedScannedAudioFile) {
scannedAudioFiles = scannedAudioFiles.filter(saf => saf !== matchedScannedAudioFile) scannedAudioFiles = scannedAudioFiles.filter((saf) => saf !== matchedScannedAudioFile)
const audioFile = new AudioFile(podcastEpisode.audioFile) const audioFile = new AudioFile(podcastEpisode.audioFile)
audioFile.updateFromScan(matchedScannedAudioFile) audioFile.updateFromScan(matchedScannedAudioFile)
podcastEpisode.audioFile = audioFile.toJSON() podcastEpisode.audioFile = audioFile.toJSON()
@@ -131,15 +137,20 @@ class PodcastScanner {
let hasMediaChanges = false let hasMediaChanges = false
if (existingPodcastEpisodes.length !== media.numEpisodes) {
media.numEpisodes = existingPodcastEpisodes.length
hasMediaChanges = true
}
// Check if cover was removed // Check if cover was removed
if (media.coverPath && libraryItemData.imageLibraryFilesRemoved.some(lf => lf.metadata.path === media.coverPath)) { if (media.coverPath && libraryItemData.imageLibraryFilesRemoved.some((lf) => lf.metadata.path === media.coverPath)) {
media.coverPath = null media.coverPath = null
hasMediaChanges = true hasMediaChanges = true
} }
// Update cover if it was modified // Update cover if it was modified
if (media.coverPath && libraryItemData.imageLibraryFilesModified.length) { if (media.coverPath && libraryItemData.imageLibraryFilesModified.length) {
let coverMatch = libraryItemData.imageLibraryFilesModified.find(iFile => iFile.old.metadata.path === media.coverPath) let coverMatch = libraryItemData.imageLibraryFilesModified.find((iFile) => iFile.old.metadata.path === media.coverPath)
if (coverMatch) { if (coverMatch) {
const coverPath = coverMatch.new.metadata.path const coverPath = coverMatch.new.metadata.path
if (coverPath !== media.coverPath) { if (coverPath !== media.coverPath) {
@@ -154,7 +165,7 @@ class PodcastScanner {
// Check if cover is not set and image files were found // Check if cover is not set and image files were found
if (!media.coverPath && libraryItemData.imageLibraryFiles.length) { if (!media.coverPath && libraryItemData.imageLibraryFiles.length) {
// Prefer using a cover image with the name "cover" otherwise use the first image // Prefer using a cover image with the name "cover" otherwise use the first image
const coverMatch = libraryItemData.imageLibraryFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path)) const coverMatch = libraryItemData.imageLibraryFiles.find((iFile) => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
media.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path media.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path
hasMediaChanges = true hasMediaChanges = true
} }
@@ -167,7 +178,7 @@ class PodcastScanner {
if (key === 'genres') { if (key === 'genres') {
const existingGenres = media.genres || [] const existingGenres = media.genres || []
if (podcastMetadata.genres.some(g => !existingGenres.includes(g)) || existingGenres.some(g => !podcastMetadata.genres.includes(g))) { if (podcastMetadata.genres.some((g) => !existingGenres.includes(g)) || existingGenres.some((g) => !podcastMetadata.genres.includes(g))) {
libraryScan.addLog(LogLevel.DEBUG, `Updating podcast genres "${existingGenres.join(',')}" => "${podcastMetadata.genres.join(',')}" for podcast "${podcastMetadata.title}"`) libraryScan.addLog(LogLevel.DEBUG, `Updating podcast genres "${existingGenres.join(',')}" => "${podcastMetadata.genres.join(',')}" for podcast "${podcastMetadata.title}"`)
media.genres = podcastMetadata.genres media.genres = podcastMetadata.genres
media.changed('genres', true) media.changed('genres', true)
@@ -175,7 +186,7 @@ class PodcastScanner {
} }
} else if (key === 'tags') { } else if (key === 'tags') {
const existingTags = media.tags || [] const existingTags = media.tags || []
if (podcastMetadata.tags.some(t => !existingTags.includes(t)) || existingTags.some(t => !podcastMetadata.tags.includes(t))) { if (podcastMetadata.tags.some((t) => !existingTags.includes(t)) || existingTags.some((t) => !podcastMetadata.tags.includes(t))) {
libraryScan.addLog(LogLevel.DEBUG, `Updating podcast tags "${existingTags.join(',')}" => "${podcastMetadata.tags.join(',')}" for podcast "${podcastMetadata.title}"`) libraryScan.addLog(LogLevel.DEBUG, `Updating podcast tags "${existingTags.join(',')}" => "${podcastMetadata.tags.join(',')}" for podcast "${podcastMetadata.title}"`)
media.tags = podcastMetadata.tags media.tags = podcastMetadata.tags
media.changed('tags', true) media.changed('tags', true)
@@ -190,7 +201,7 @@ class PodcastScanner {
// If no cover then extract cover from audio file if available // If no cover then extract cover from audio file if available
if (!media.coverPath && existingPodcastEpisodes.length) { if (!media.coverPath && existingPodcastEpisodes.length) {
const audioFiles = existingPodcastEpisodes.map(ep => ep.audioFile) const audioFiles = existingPodcastEpisodes.map((ep) => ep.audioFile)
const extractedCoverPath = await CoverManager.saveEmbeddedCoverArt(audioFiles, existingLibraryItem.id, existingLibraryItem.path) const extractedCoverPath = await CoverManager.saveEmbeddedCoverArt(audioFiles, existingLibraryItem.id, existingLibraryItem.path)
if (extractedCoverPath) { if (extractedCoverPath) {
libraryScan.addLog(LogLevel.DEBUG, `Updating podcast "${podcastMetadata.title}" extracted embedded cover art from audio file to path "${extractedCoverPath}"`) libraryScan.addLog(LogLevel.DEBUG, `Updating podcast "${podcastMetadata.title}" extracted embedded cover art from audio file to path "${extractedCoverPath}"`)
@@ -267,7 +278,7 @@ class PodcastScanner {
// Set cover image from library file // Set cover image from library file
if (libraryItemData.imageLibraryFiles.length) { if (libraryItemData.imageLibraryFiles.length) {
// Prefer using a cover image with the name "cover" otherwise use the first image // Prefer using a cover image with the name "cover" otherwise use the first image
const coverMatch = libraryItemData.imageLibraryFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path)) const coverMatch = libraryItemData.imageLibraryFiles.find((iFile) => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
podcastMetadata.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path podcastMetadata.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path
} }
@@ -283,7 +294,8 @@ class PodcastScanner {
lastEpisodeCheck: 0, lastEpisodeCheck: 0,
maxEpisodesToKeep: 0, maxEpisodesToKeep: 0,
maxNewEpisodesToDownload: 3, maxNewEpisodesToDownload: 3,
podcastEpisodes: newPodcastEpisodes podcastEpisodes: newPodcastEpisodes,
numEpisodes: newPodcastEpisodes.length
} }
const libraryItemObj = libraryItemData.libraryItemObject const libraryItemObj = libraryItemData.libraryItemObject
@@ -291,6 +303,8 @@ class PodcastScanner {
libraryItemObj.isMissing = false libraryItemObj.isMissing = false
libraryItemObj.isInvalid = false libraryItemObj.isInvalid = false
libraryItemObj.extraData = {} libraryItemObj.extraData = {}
libraryItemObj.title = podcastObject.title
libraryItemObj.titleIgnorePrefix = getTitleIgnorePrefix(podcastObject.title)
// If cover was not found in folder then check embedded covers in audio files // If cover was not found in folder then check embedded covers in audio files
if (!podcastObject.coverPath && scannedAudioFiles.length) { if (!podcastObject.coverPath && scannedAudioFiles.length) {
@@ -399,41 +413,44 @@ class PodcastScanner {
explicit: !!libraryItem.media.explicit, explicit: !!libraryItem.media.explicit,
podcastType: libraryItem.media.podcastType podcastType: libraryItem.media.podcastType
} }
return fsExtra.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2)).then(async () => { return fsExtra
// Add metadata.json to libraryFiles array if it is new .writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2))
let metadataLibraryFile = libraryItem.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath)) .then(async () => {
if (storeMetadataWithItem) { // Add metadata.json to libraryFiles array if it is new
if (!metadataLibraryFile) { let metadataLibraryFile = libraryItem.libraryFiles.find((lf) => lf.metadata.path === filePathToPOSIX(metadataFilePath))
const newLibraryFile = new LibraryFile() if (storeMetadataWithItem) {
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`) if (!metadataLibraryFile) {
metadataLibraryFile = newLibraryFile.toJSON() const newLibraryFile = new LibraryFile()
libraryItem.libraryFiles.push(metadataLibraryFile) await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
} else { metadataLibraryFile = newLibraryFile.toJSON()
const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath) libraryItem.libraryFiles.push(metadataLibraryFile)
if (fileTimestamps) { } else {
metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs if (fileTimestamps) {
metadataLibraryFile.metadata.size = fileTimestamps.size metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
metadataLibraryFile.ino = fileTimestamps.ino metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs
metadataLibraryFile.metadata.size = fileTimestamps.size
metadataLibraryFile.ino = fileTimestamps.ino
}
}
const libraryItemDirTimestamps = await getFileTimestampsWithIno(libraryItem.path)
if (libraryItemDirTimestamps) {
libraryItem.mtime = libraryItemDirTimestamps.mtimeMs
libraryItem.ctime = libraryItemDirTimestamps.ctimeMs
let size = 0
libraryItem.libraryFiles.forEach((lf) => (size += !isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0))
libraryItem.size = size
} }
} }
const libraryItemDirTimestamps = await getFileTimestampsWithIno(libraryItem.path)
if (libraryItemDirTimestamps) {
libraryItem.mtime = libraryItemDirTimestamps.mtimeMs
libraryItem.ctime = libraryItemDirTimestamps.ctimeMs
let size = 0
libraryItem.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0))
libraryItem.size = size
}
}
libraryScan.addLog(LogLevel.DEBUG, `Success saving abmetadata to "${metadataFilePath}"`) libraryScan.addLog(LogLevel.DEBUG, `Success saving abmetadata to "${metadataFilePath}"`)
return metadataLibraryFile return metadataLibraryFile
}).catch((error) => { })
libraryScan.addLog(LogLevel.ERROR, `Failed to save json file at "${metadataFilePath}"`, error) .catch((error) => {
return null libraryScan.addLog(LogLevel.ERROR, `Failed to save json file at "${metadataFilePath}"`, error)
}) return null
})
} }
} }
module.exports = new PodcastScanner() module.exports = new PodcastScanner()
+3 -13
View File
@@ -1,6 +1,5 @@
const uuidv4 = require("uuid").v4 const uuidv4 = require('uuid').v4
const Logger = require('../Logger') const Logger = require('../Logger')
const { LogLevel } = require('../utils/constants')
class ScanLogger { class ScanLogger {
constructor() { constructor() {
@@ -44,20 +43,11 @@ class ScanLogger {
this.elapsed = this.finishedAt - this.startedAt this.elapsed = this.finishedAt - this.startedAt
} }
getLogLevelString(level) {
for (const key in LogLevel) {
if (LogLevel[key] === level) {
return key
}
}
return 'UNKNOWN'
}
addLog(level, ...args) { addLog(level, ...args) {
const logObj = { const logObj = {
timestamp: (new Date()).toISOString(), timestamp: new Date().toISOString(),
message: args.join(' '), message: args.join(' '),
levelName: this.getLogLevelString(level), levelName: Logger.getLogLevelString(level),
level level
} }
-6
View File
@@ -48,13 +48,7 @@ class Scanner {
let updatePayload = {} let updatePayload = {}
let hasUpdated = false let hasUpdated = false
let existingAuthors = [] // Used for checking if authors or series are now empty
let existingSeries = []
if (libraryItem.isBook) { if (libraryItem.isBook) {
existingAuthors = libraryItem.media.authors.map((a) => a.id)
existingSeries = libraryItem.media.series.map((s) => s.id)
const searchISBN = options.isbn || libraryItem.media.isbn const searchISBN = options.isbn || libraryItem.media.isbn
const searchASIN = options.asin || libraryItem.media.asin const searchASIN = options.asin || libraryItem.media.asin
+39 -10
View File
@@ -131,6 +131,40 @@ async function readTextFile(path) {
} }
module.exports.readTextFile = readTextFile module.exports.readTextFile = readTextFile
/**
* Check if file or directory should be ignored. Returns a string of the reason to ignore, or null if not ignored
*
* @param {string} path
* @returns {string}
*/
module.exports.shouldIgnoreFile = (path) => {
// Check if directory or file name starts with "."
if (Path.basename(path).startsWith('.')) {
return 'dotfile'
}
if (path.split('/').find((p) => p.startsWith('.'))) {
return 'dotpath'
}
// If these strings exist anywhere in the filename or directory name, ignore. Vendor specific hidden directories
const includeAnywhereIgnore = ['@eaDir']
const filteredInclude = includeAnywhereIgnore.filter((str) => path.includes(str))
if (filteredInclude.length) {
return `${filteredInclude[0]} directory`
}
const extensionIgnores = ['.part', '.tmp', '.crdownload', '.download', '.bak', '.old', '.temp', '.tempfile', '.tempfile~']
// Check extension
if (extensionIgnores.includes(Path.extname(path).toLowerCase())) {
// Return the extension that is ignored
return `${Path.extname(path)} file`
}
// Should not ignore this file or directory
return null
}
/** /**
* @typedef FilePathItem * @typedef FilePathItem
* @property {string} name - file name e.g. "audiofile.m4b" * @property {string} name - file name e.g. "audiofile.m4b"
@@ -147,7 +181,7 @@ module.exports.readTextFile = readTextFile
* @param {string} [relPathToReplace] * @param {string} [relPathToReplace]
* @returns {FilePathItem[]} * @returns {FilePathItem[]}
*/ */
async function recurseFiles(path, relPathToReplace = null) { module.exports.recurseFiles = async (path, relPathToReplace = null) => {
path = filePathToPOSIX(path) path = filePathToPOSIX(path)
if (!path.endsWith('/')) path = path + '/' if (!path.endsWith('/')) path = path + '/'
@@ -197,14 +231,10 @@ async function recurseFiles(path, relPathToReplace = null) {
return false return false
} }
if (item.extension === '.part') { // Check for ignored extensions or directories
Logger.debug(`[fileUtils] Ignoring .part file "${relpath}"`) const shouldIgnore = this.shouldIgnoreFile(relpath)
return false if (shouldIgnore) {
} Logger.debug(`[fileUtils] Ignoring ${shouldIgnore} - "${relpath}"`)
// Ignore any file if a directory or the filename starts with "."
if (relpath.split('/').find((p) => p.startsWith('.'))) {
Logger.debug(`[fileUtils] Ignoring path has . "${relpath}"`)
return false return false
} }
@@ -235,7 +265,6 @@ async function recurseFiles(path, relPathToReplace = null) {
return list return list
} }
module.exports.recurseFiles = recurseFiles
/** /**
* *
+5 -1
View File
@@ -107,7 +107,8 @@ async function parse(ebookFile) {
// Attempt to find filepath to cover image: // Attempt to find filepath to cover image:
// Metadata may include <meta name="cover" content="id"/> where content is the id of the cover image in the manifest // Metadata may include <meta name="cover" content="id"/> where content is the id of the cover image in the manifest
// Otherwise the first image in the manifest is used as the cover image // Otherwise find image in the manifest with cover-image property set
// As a fallback the first image in the manifest is used as the cover image
let packageMetadata = packageJson.package?.metadata let packageMetadata = packageJson.package?.metadata
if (Array.isArray(packageMetadata)) { if (Array.isArray(packageMetadata)) {
packageMetadata = packageMetadata[0] packageMetadata = packageMetadata[0]
@@ -118,6 +119,9 @@ async function parse(ebookFile) {
if (metaCoverId) { if (metaCoverId) {
manifestFirstImage = packageJson.package?.manifest?.[0]?.item?.find((item) => item.$?.id === metaCoverId) manifestFirstImage = packageJson.package?.manifest?.[0]?.item?.find((item) => item.$?.id === metaCoverId)
} }
if (!manifestFirstImage) {
manifestFirstImage = packageJson.package?.manifest?.[0]?.item?.find((item) => item.$?.['properties']?.split(' ')?.includes('cover-image'))
}
if (!manifestFirstImage) { if (!manifestFirstImage) {
manifestFirstImage = packageJson.package?.manifest?.[0]?.item?.find((item) => item.$?.['media-type']?.startsWith('image/')) manifestFirstImage = packageJson.package?.manifest?.[0]?.item?.find((item) => item.$?.['media-type']?.startsWith('image/'))
} }
+25 -4
View File
@@ -22,11 +22,22 @@ function parseCreators(metadata) {
Object.keys(c['$']) Object.keys(c['$'])
.find((key) => key.startsWith('xmlns:')) .find((key) => key.startsWith('xmlns:'))
?.split(':')[1] || 'opf' ?.split(':')[1] || 'opf'
return { const creator = {
value: c['_'], value: c['_'],
role: c['$'][`${namespace}:role`] || null, role: c['$'][`${namespace}:role`] || null,
fileAs: c['$'][`${namespace}:file-as`] || null fileAs: c['$'][`${namespace}:file-as`] || null
} }
const id = c['$']['id']
if (id && metadata.meta.refines?.some((r) => r.refines === `#${id}`)) {
const creatorMeta = metadata.meta.refines.filter((r) => r.refines === `#${id}`)
if (creatorMeta) {
creator.role = creatorMeta.find((r) => r.property === 'role')?.value || creator.role || null
creator.fileAs = creatorMeta.find((r) => r.property === 'file-as')?.value || creator.fileAs || null
}
}
return creator
}) })
} }
@@ -187,7 +198,6 @@ module.exports.parseOpfMetadataJson = (json) => {
const prefix = packageKey.split(':').shift() const prefix = packageKey.split(':').shift()
let metadata = prefix ? json[packageKey][`${prefix}:metadata`] || json[packageKey].metadata : json[packageKey].metadata let metadata = prefix ? json[packageKey][`${prefix}:metadata`] || json[packageKey].metadata : json[packageKey].metadata
if (!metadata) return null if (!metadata) return null
if (Array.isArray(metadata)) { if (Array.isArray(metadata)) {
if (!metadata.length) return null if (!metadata.length) return null
metadata = metadata[0] metadata = metadata[0]
@@ -198,12 +208,22 @@ module.exports.parseOpfMetadataJson = (json) => {
metadata.meta = {} metadata.meta = {}
if (metadataMeta?.length) { if (metadataMeta?.length) {
metadataMeta.forEach((meta) => { metadataMeta.forEach((meta) => {
if (meta && meta['$'] && meta['$'].name) { if (meta?.['$']?.name) {
metadata.meta[meta['$'].name] = [meta['$'].content || ''] metadata.meta[meta['$'].name] = [meta['$'].content || '']
} else if (meta?.['$']?.refines) {
// https://www.w3.org/TR/epub-33/#sec-meta-elem
if (!metadata.meta.refines) {
metadata.meta.refines = []
}
metadata.meta.refines.push({
value: meta._,
refines: meta['$'].refines,
property: meta['$'].property
})
} }
}) })
} }
const creators = parseCreators(metadata) const creators = parseCreators(metadata)
const authors = (fetchCreators(creators, 'aut') || []).map((au) => au?.trim()).filter((au) => au) const authors = (fetchCreators(creators, 'aut') || []).map((au) => au?.trim()).filter((au) => au)
const narrators = (fetchNarrators(creators, metadata) || []).map((nrt) => nrt?.trim()).filter((nrt) => nrt) const narrators = (fetchNarrators(creators, metadata) || []).map((nrt) => nrt?.trim()).filter((nrt) => nrt)
@@ -227,5 +247,6 @@ module.exports.parseOpfMetadataJson = (json) => {
module.exports.parseOpfMetadataXML = async (xml) => { module.exports.parseOpfMetadataXML = async (xml) => {
const json = await xmlToJSON(xml) const json = await xmlToJSON(xml)
if (!json) return null if (!json) return null
return this.parseOpfMetadataJson(json) return this.parseOpfMetadataJson(json)
} }
+11 -3
View File
@@ -145,15 +145,15 @@ function extractEpisodeData(item) {
if (item.enclosure?.[0]?.['$']?.url) { if (item.enclosure?.[0]?.['$']?.url) {
enclosure = item.enclosure[0]['$'] enclosure = item.enclosure[0]['$']
} else if(item['media:content']?.find(c => c?.['$']?.url && (c?.['$']?.type ?? "").startsWith("audio"))) { } else if (item['media:content']?.find((c) => c?.['$']?.url && (c?.['$']?.type ?? '').startsWith('audio'))) {
enclosure = item['media:content'].find(c => (c['$']?.type ?? "").startsWith("audio"))['$'] enclosure = item['media:content'].find((c) => (c['$']?.type ?? '').startsWith('audio'))['$']
} else { } else {
Logger.error(`[podcastUtils] Invalid podcast episode data`) Logger.error(`[podcastUtils] Invalid podcast episode data`)
return null return null
} }
const episode = { const episode = {
enclosure: enclosure, enclosure: enclosure
} }
episode.enclosure.url = episode.enclosure.url.trim() episode.enclosure.url = episode.enclosure.url.trim()
@@ -343,6 +343,14 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => {
return payload.podcast return payload.podcast
}) })
.catch((error) => { .catch((error) => {
// Check for failures due to redirecting from http to https. If original url was http, upgrade to https and try again
if (error.code === 'ERR_FR_REDIRECTION_FAILURE' && error.cause.code === 'ERR_INVALID_PROTOCOL') {
if (feedUrl.startsWith('http://') && error.request._options.protocol === 'https:') {
Logger.info('Redirection from http to https detected. Upgrading Request', error.request._options.href)
feedUrl = feedUrl.replace('http://', 'https://')
return this.getPodcastFeed(feedUrl, excludeEpisodeMetadata)
}
}
Logger.error('[podcastUtils] getPodcastFeed Error', error) Logger.error('[podcastUtils] getPodcastFeed Error', error)
return null return null
}) })
+6 -3
View File
@@ -4,6 +4,7 @@ const Database = require('../../Database')
const libraryItemsBookFilters = require('./libraryItemsBookFilters') const libraryItemsBookFilters = require('./libraryItemsBookFilters')
const libraryItemsPodcastFilters = require('./libraryItemsPodcastFilters') const libraryItemsPodcastFilters = require('./libraryItemsPodcastFilters')
const { createNewSortInstance } = require('../../libs/fastSort') const { createNewSortInstance } = require('../../libs/fastSort')
const { profile } = require('../../utils/profiler')
const naturalSort = createNewSortInstance({ const naturalSort = createNewSortInstance({
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
}) })
@@ -474,7 +475,8 @@ module.exports = {
// Check how many podcasts are in library to determine if we need to load all of the data // Check how many podcasts are in library to determine if we need to load all of the data
// This is done to handle the edge case of podcasts having been deleted and not having // This is done to handle the edge case of podcasts having been deleted and not having
// an updatedAt timestamp to trigger a reload of the filter data // an updatedAt timestamp to trigger a reload of the filter data
const podcastCountFromDatabase = await Database.podcastModel.count({ const podcastModelCount = process.env.QUERY_PROFILING ? profile(Database.podcastModel.count.bind(Database.podcastModel)) : Database.podcastModel.count.bind(Database.podcastModel)
const podcastCountFromDatabase = await podcastModelCount({
include: { include: {
model: Database.libraryItemModel, model: Database.libraryItemModel,
attributes: [], attributes: [],
@@ -489,7 +491,7 @@ module.exports = {
// data was loaded. If so, we can skip loading all of the data. // data was loaded. If so, we can skip loading all of the data.
// Because many items could change, just check the count of items instead // Because many items could change, just check the count of items instead
// of actually loading the data twice // of actually loading the data twice
const changedPodcasts = await Database.podcastModel.count({ const changedPodcasts = await podcastModelCount({
include: { include: {
model: Database.libraryItemModel, model: Database.libraryItemModel,
attributes: [], attributes: [],
@@ -520,7 +522,8 @@ module.exports = {
} }
// Something has changed in the podcasts table, so reload all of the filter data for library // Something has changed in the podcasts table, so reload all of the filter data for library
const podcasts = await Database.podcastModel.findAll({ const findAll = process.env.QUERY_PROFILING ? profile(Database.podcastModel.findAll.bind(Database.podcastModel)) : Database.podcastModel.findAll.bind(Database.podcastModel)
const podcasts = await findAll({
include: { include: {
model: Database.libraryItemModel, model: Database.libraryItemModel,
attributes: [], attributes: [],
+28 -35
View File
@@ -264,9 +264,9 @@ module.exports = {
} else if (sortBy === 'media.metadata.publishedYear') { } else if (sortBy === 'media.metadata.publishedYear') {
return [[Sequelize.literal(`CAST(\`book\`.\`publishedYear\` AS INTEGER)`), dir]] return [[Sequelize.literal(`CAST(\`book\`.\`publishedYear\` AS INTEGER)`), dir]]
} else if (sortBy === 'media.metadata.authorNameLF') { } else if (sortBy === 'media.metadata.authorNameLF') {
return [[Sequelize.literal('author_name COLLATE NOCASE'), dir]] return [[Sequelize.literal('`libraryItem`.`authorNamesLastFirst` COLLATE NOCASE'), dir]]
} else if (sortBy === 'media.metadata.authorName') { } else if (sortBy === 'media.metadata.authorName') {
return [[Sequelize.literal('author_name COLLATE NOCASE'), dir]] return [[Sequelize.literal('`libraryItem`.`authorNamesFirstLast` COLLATE NOCASE'), dir]]
} else if (sortBy === 'media.metadata.title') { } else if (sortBy === 'media.metadata.title') {
if (collapseseries) { if (collapseseries) {
return [[Sequelize.literal('display_title COLLATE NOCASE'), dir]] return [[Sequelize.literal('display_title COLLATE NOCASE'), dir]]
@@ -344,22 +344,28 @@ module.exports = {
countCache.clear() countCache.clear()
}, },
async findAndCountAll(findOptions, limit, offset) { async findAndCountAll(findOptions, limit, offset, useCountCache) {
const findOptionsKey = stringifySequelizeQuery(findOptions) const model = Database.bookModel
Logger.debug(`[LibraryItemsBookFilters] findOptionsKey: ${findOptionsKey}`) if (useCountCache) {
const countCacheKey = stringifySequelizeQuery(findOptions)
Logger.debug(`[LibraryItemsBookFilters] countCacheKey: ${countCacheKey}`)
if (!countCache.has(countCacheKey)) {
const count = await model.count(findOptions)
countCache.set(countCacheKey, count)
}
findOptions.limit = limit || null
findOptions.offset = offset
const rows = await model.findAll(findOptions)
return { rows, count: countCache.get(countCacheKey) }
}
findOptions.limit = limit || null findOptions.limit = limit || null
findOptions.offset = offset findOptions.offset = offset
if (countCache.has(findOptionsKey)) { return await model.findAndCountAll(findOptions)
const rows = await Database.bookModel.findAll(findOptions)
return { rows, count: countCache.get(findOptionsKey) }
} else {
const result = await Database.bookModel.findAndCountAll(findOptions)
countCache.set(findOptionsKey, result.count)
return result
}
}, },
/** /**
@@ -391,18 +397,7 @@ module.exports = {
const includeRSSFeed = include.includes('rssfeed') const includeRSSFeed = include.includes('rssfeed')
const includeMediaItemShare = !!user?.isAdminOrUp && include.includes('share') const includeMediaItemShare = !!user?.isAdminOrUp && include.includes('share')
// For sorting by author name an additional attribute must be added
// with author names concatenated
let bookAttributes = null let bookAttributes = null
if (sortBy === 'media.metadata.authorNameLF') {
bookAttributes = {
include: [[Sequelize.literal(`(SELECT group_concat(lastFirst, ", ") FROM (SELECT a.lastFirst FROM authors AS a, bookAuthors as ba WHERE ba.authorId = a.id AND ba.bookId = book.id ORDER BY ba.createdAt ASC))`), 'author_name']]
}
} else if (sortBy === 'media.metadata.authorName') {
bookAttributes = {
include: [[Sequelize.literal(`(SELECT group_concat(name, ", ") FROM (SELECT a.name FROM authors AS a, bookAuthors as ba WHERE ba.authorId = a.id AND ba.bookId = book.id ORDER BY ba.createdAt ASC))`), 'author_name']]
}
}
const libraryItemWhere = { const libraryItemWhere = {
libraryId libraryId
@@ -434,19 +429,17 @@ module.exports = {
const libraryItemIncludes = [] const libraryItemIncludes = []
const bookIncludes = [] const bookIncludes = []
if (includeRSSFeed) {
if (filterGroup === 'feed-open' || includeRSSFeed) {
const rssFeedRequired = filterGroup === 'feed-open'
libraryItemIncludes.push({ libraryItemIncludes.push({
model: Database.feedModel, model: Database.feedModel,
required: filterGroup === 'feed-open', required: rssFeedRequired,
separate: true separate: !rssFeedRequired
}) })
} }
if (filterGroup === 'feed-open' && !includeRSSFeed) {
libraryItemIncludes.push({ if (filterGroup === 'share-open') {
model: Database.feedModel,
required: true
})
} else if (filterGroup === 'share-open') {
bookIncludes.push({ bookIncludes.push({
model: Database.mediaItemShareModel, model: Database.mediaItemShareModel,
required: true required: true
@@ -608,7 +601,7 @@ module.exports = {
} }
const findAndCountAll = process.env.QUERY_PROFILING ? profile(this.findAndCountAll) : this.findAndCountAll const findAndCountAll = process.env.QUERY_PROFILING ? profile(this.findAndCountAll) : this.findAndCountAll
const { rows: books, count } = await findAndCountAll(findOptions, limit, offset) const { rows: books, count } = await findAndCountAll(findOptions, limit, offset, !filterGroup)
const libraryItems = books.map((bookExpanded) => { const libraryItems = books.map((bookExpanded) => {
const libraryItem = bookExpanded.libraryItem const libraryItem = bookExpanded.libraryItem
@@ -1,6 +1,10 @@
const Sequelize = require('sequelize') const Sequelize = require('sequelize')
const Database = require('../../Database') const Database = require('../../Database')
const Logger = require('../../Logger') const Logger = require('../../Logger')
const { profile } = require('../../utils/profiler')
const stringifySequelizeQuery = require('../stringifySequelizeQuery')
const countCache = new Map()
module.exports = { module.exports = {
/** /**
@@ -84,9 +88,9 @@ module.exports = {
return [[Sequelize.literal(`\`podcast\`.\`author\` COLLATE NOCASE ${nullDir}`)]] return [[Sequelize.literal(`\`podcast\`.\`author\` COLLATE NOCASE ${nullDir}`)]]
} else if (sortBy === 'media.metadata.title') { } else if (sortBy === 'media.metadata.title') {
if (global.ServerSettings.sortingIgnorePrefix) { if (global.ServerSettings.sortingIgnorePrefix) {
return [[Sequelize.literal('`podcast`.`titleIgnorePrefix` COLLATE NOCASE'), dir]] return [[Sequelize.literal('`libraryItem`.`titleIgnorePrefix` COLLATE NOCASE'), dir]]
} else { } else {
return [[Sequelize.literal('`podcast`.`title` COLLATE NOCASE'), dir]] return [[Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir]]
} }
} else if (sortBy === 'media.numTracks') { } else if (sortBy === 'media.numTracks') {
return [['numEpisodes', dir]] return [['numEpisodes', dir]]
@@ -96,6 +100,34 @@ module.exports = {
return [] return []
}, },
clearCountCache(model, hook) {
Logger.debug(`[LibraryItemsPodcastFilters] ${model}.${hook}: Clearing count cache`)
countCache.clear()
},
async findAndCountAll(findOptions, model, limit, offset, useCountCache) {
if (useCountCache) {
const countCacheKey = stringifySequelizeQuery(findOptions)
Logger.debug(`[LibraryItemsPodcastFilters] countCacheKey: ${countCacheKey}`)
if (!countCache.has(countCacheKey)) {
const count = await model.count(findOptions)
countCache.set(countCacheKey, count)
}
findOptions.limit = limit || null
findOptions.offset = offset
const rows = await model.findAll(findOptions)
return { rows, count: countCache.get(countCacheKey) }
}
findOptions.limit = limit || null
findOptions.offset = offset
return await model.findAndCountAll(findOptions)
},
/** /**
* Get library items for podcast media type using filter and sort * Get library items for podcast media type using filter and sort
* @param {string} libraryId * @param {string} libraryId
@@ -120,7 +152,8 @@ module.exports = {
if (includeRSSFeed) { if (includeRSSFeed) {
libraryItemIncludes.push({ libraryItemIncludes.push({
model: Database.feedModel, model: Database.feedModel,
required: filterGroup === 'feed-open' required: filterGroup === 'feed-open',
separate: true
}) })
} }
if (filterGroup === 'issues') { if (filterGroup === 'issues') {
@@ -139,9 +172,6 @@ module.exports = {
} }
const podcastIncludes = [] const podcastIncludes = []
if (includeNumEpisodesIncomplete) {
podcastIncludes.push([Sequelize.literal(`(SELECT count(*) FROM podcastEpisodes pe LEFT OUTER JOIN mediaProgresses mp ON mp.mediaItemId = pe.id AND mp.userId = :userId WHERE pe.podcastId = podcast.id AND (mp.isFinished = 0 OR mp.isFinished IS NULL))`), 'numEpisodesIncomplete'])
}
let { mediaWhere, replacements } = this.getMediaGroupQuery(filterGroup, filterValue) let { mediaWhere, replacements } = this.getMediaGroupQuery(filterGroup, filterValue)
replacements.userId = user.id replacements.userId = user.id
@@ -153,12 +183,12 @@ module.exports = {
replacements = { ...replacements, ...userPermissionPodcastWhere.replacements } replacements = { ...replacements, ...userPermissionPodcastWhere.replacements }
podcastWhere.push(...userPermissionPodcastWhere.podcastWhere) podcastWhere.push(...userPermissionPodcastWhere.podcastWhere)
const { rows: podcasts, count } = await Database.podcastModel.findAndCountAll({ const findOptions = {
where: podcastWhere, where: podcastWhere,
replacements, replacements,
distinct: true, distinct: true,
attributes: { attributes: {
include: [[Sequelize.literal(`(SELECT count(*) FROM podcastEpisodes pe WHERE pe.podcastId = podcast.id)`), 'numEpisodes'], ...podcastIncludes] include: [...podcastIncludes]
}, },
include: [ include: [
{ {
@@ -169,10 +199,12 @@ module.exports = {
} }
], ],
order: this.getOrder(sortBy, sortDesc), order: this.getOrder(sortBy, sortDesc),
subQuery: false, subQuery: false
limit: limit || null, }
offset
}) const findAndCountAll = process.env.QUERY_PROFILING ? profile(this.findAndCountAll) : this.findAndCountAll
const { rows: podcasts, count } = await findAndCountAll(findOptions, Database.podcastModel, limit, offset, !filterGroup)
const libraryItems = podcasts.map((podcastExpanded) => { const libraryItems = podcasts.map((podcastExpanded) => {
const libraryItem = podcastExpanded.libraryItem const libraryItem = podcastExpanded.libraryItem
@@ -183,11 +215,15 @@ module.exports = {
if (libraryItem.feeds?.length) { if (libraryItem.feeds?.length) {
libraryItem.rssFeed = libraryItem.feeds[0] libraryItem.rssFeed = libraryItem.feeds[0]
} }
if (podcast.dataValues.numEpisodesIncomplete) {
libraryItem.numEpisodesIncomplete = podcast.dataValues.numEpisodesIncomplete if (includeNumEpisodesIncomplete) {
} const numEpisodesComplete = user.mediaProgresses.reduce((acc, mp) => {
if (podcast.dataValues.numEpisodes) { if (mp.podcastId === podcast.id && mp.isFinished) {
podcast.numEpisodes = podcast.dataValues.numEpisodes acc += 1
}
return acc
}, 0)
libraryItem.numEpisodesIncomplete = podcast.numEpisodes - numEpisodesComplete
} }
libraryItem.media = podcast libraryItem.media = podcast
@@ -268,28 +304,31 @@ module.exports = {
const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(user) const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(user)
const { rows: podcastEpisodes, count } = await Database.podcastEpisodeModel.findAndCountAll({ const findOptions = {
where: podcastEpisodeWhere, where: podcastEpisodeWhere,
replacements: userPermissionPodcastWhere.replacements, replacements: userPermissionPodcastWhere.replacements,
include: [ include: [
{ {
model: Database.podcastModel, model: Database.podcastModel,
required: true,
where: userPermissionPodcastWhere.podcastWhere, where: userPermissionPodcastWhere.podcastWhere,
include: [ include: [
{ {
model: Database.libraryItemModel, model: Database.libraryItemModel,
required: true,
where: libraryItemWhere where: libraryItemWhere
} }
] ]
}, },
...podcastEpisodeIncludes ...podcastEpisodeIncludes
], ],
distinct: true,
subQuery: false, subQuery: false,
order: podcastEpisodeOrder, order: podcastEpisodeOrder
limit, }
offset
}) const findAndCountAll = process.env.QUERY_PROFILING ? profile(this.findAndCountAll) : this.findAndCountAll
const { rows: podcastEpisodes, count } = await findAndCountAll(findOptions, Database.podcastEpisodeModel, limit, offset, !filterGroup)
const libraryItems = podcastEpisodes.map((ep) => { const libraryItems = podcastEpisodes.map((ep) => {
const libraryItem = ep.podcast.libraryItem const libraryItem = ep.podcast.libraryItem
@@ -426,7 +465,7 @@ module.exports = {
async getRecentEpisodes(user, library, limit, offset) { async getRecentEpisodes(user, library, limit, offset) {
const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(user) const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(user)
const episodes = await Database.podcastEpisodeModel.findAll({ const findOptions = {
where: { where: {
'$mediaProgresses.isFinished$': { '$mediaProgresses.isFinished$': {
[Sequelize.Op.or]: [null, false] [Sequelize.Op.or]: [null, false]
@@ -457,7 +496,11 @@ module.exports = {
subQuery: false, subQuery: false,
limit, limit,
offset offset
}) }
const findtAll = process.env.QUERY_PROFILING ? profile(Database.podcastEpisodeModel.findAll.bind(Database.podcastEpisodeModel)) : Database.podcastEpisodeModel.findAll.bind(Database.podcastEpisodeModel)
const episodes = await findtAll(findOptions)
const episodeResults = episodes.map((ep) => { const episodeResults = episodes.map((ep) => {
ep.podcast.podcastEpisodes = [] // Not needed ep.podcast.podcastEpisodes = [] // Not needed

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