Compare commits

...

54 Commits

Author SHA1 Message Date
advplyr
a89a24e48e
Merge pull request #4598 from advplyr/episode_meta_tagging
Update podcast episode downloads to always attempt embedding meta tags
2025-08-17 10:20:54 -04:00
advplyr
a968aca304 Update podcast episode downloads to always attempt embedding meta tags regardless of format 2025-08-17 09:05:29 -05:00
advplyr
fd4932cdbb Add additional debug logs for OIDC login 2025-08-15 17:23:20 -05:00
advplyr
dcaca43817
Merge pull request #4384 from josh-vin/feat/ChaptersEnhancments
Enhancement: Improves chapter editing and adds bulk import
2025-08-14 17:38:56 -04:00
advplyr
0eed4e82f9 Fix bulk add chapter icon button tooltip 2025-08-14 16:35:28 -05:00
advplyr
2ed2328401 Remove negative chapter end check & tooltip 2025-08-14 16:18:33 -05:00
advplyr
8b260c8bc6 Update bulk chapter modal styles, decreased text and button sizes 2025-08-14 16:16:34 -05:00
advplyr
7dcb9b98a0 Chapter lookup modal add back button to clear lookup results 2025-08-14 16:03:32 -05:00
advplyr
311ac7104e
Merge pull request #4590 from advplyr/fix_authorize_race_condition
Fix authorize race condition by not updating the user on token refresh
2025-08-13 09:36:17 -04:00
advplyr
2c45b28d48 Fix authorize race condition by not updating the user on token refresh #4567 2025-08-13 08:31:01 -05:00
advplyr
b53613f82c
Merge pull request #4552 from Toby222/master
Replace some SVG icons with material-symbols
2025-08-12 18:55:50 -04:00
advplyr
751371abb8 Update ReadIcon svg with material-symbols 2025-08-12 17:46:01 -05:00
advplyr
6365c02875 Update explicit material symbols icon to fill 2025-08-12 17:40:48 -05:00
advplyr
fb3834156b Version bump v2.28.0 2025-08-10 17:42:32 -05:00
advplyr
c03f3f722d
Merge pull request #4559 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-08-10 18:31:40 -04:00
FiendFEARing
a06f48ca29
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1138 of 1138 strings)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-08-10 22:26:27 +00:00
advplyr
4018be6330 Fix oidc auto-register not cleaning up new user on errors #4563 2025-08-10 17:26:15 -05:00
advplyr
99a3867ce9 Update callback url check
Co-authored-by: Denis Arnst <git@sapd.eu>
2025-08-10 17:08:25 -05:00
advplyr
2116f60133
Merge pull request #4565 from advplyr/redirect_transcode_requests
Fix server crash when transcode requests are made to the direct play endpoint
2025-08-07 18:31:45 -04:00
advplyr
794f0ef42a Fix server crash when transcode requests are made to the direct play endpoint #4555 2025-08-07 17:21:05 -05:00
Josh Vincent
3e423839a1 Fixes UI for Bulk Chapter adder, and changes logic around locking 2025-08-04 18:33:06 -06:00
Josh Vincent
2773c8c4a9 Merge remote-tracking branch 'josh-vin/master' into feat/ChaptersEnhancments 2025-08-04 18:32:28 -06:00
advplyr
e510174f12
Merge pull request #4557 from Vito0912/cors
Allow a whitelist of CORS origins
2025-08-04 19:02:30 -04:00
advplyr
08c9e8d47d Fix i18n string order 2025-08-04 17:56:56 -05:00
advplyr
1908ec3df5 Remove commented out experimental features setting 2025-08-04 17:54:59 -05:00
advplyr
df3878d4ca Add Security section to settings with allowed cors origin setting, increase width of setting inputs 2025-08-04 17:54:29 -05:00
Vito0912
1097de6f1f
now updates the input field 2025-08-04 19:17:46 +02:00
Vito0912
e408070b19
better heading 2025-08-03 14:02:33 +02:00
Vito0912
af67c2e86f
locale 2025-08-03 13:57:44 +02:00
Vito0912
6a52d2a968
CORS 2025-08-03 13:52:58 +02:00
Tobias Berger
5ef632a7eb
Replace some SVG icons with material-symbols 2025-08-01 09:20:34 +02:00
Josh Vincent
77d7a50b99 Merge remote-tracking branch 'josh-vin/master' into feat/ChaptersEnhancments 2025-07-30 16:51:12 -06:00
Josh Vincent
9da0be6d36 Allow clicking on elapsedTime to adjust chapter start 2025-06-08 13:18:41 -06:00
Josh Vincent
c41bdb951c Moves the lock button and fixes padding on bulk add feature.
Moves the lock button the right of the Title text box.

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

Adds a warning toast when attempting to modify locked chapters.

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

Implements time increment buttons to precisely adjust chapter start times.

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

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

Updates UI tooltips and icons for improved clarity and user experience.
2025-06-06 11:22:38 -06:00
37 changed files with 882 additions and 310 deletions

View File

@ -3,24 +3,18 @@
<div class="flex md:hidden h-10 items-center">
<nuxt-link :to="`/library/${currentLibraryId}`" class="grow h-full flex justify-center items-center" :class="isHomePage ? 'bg-primary/80' : 'bg-primary/40'">
<p v-if="isHomePage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonHome }}</p>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
<span v-else class="material-symbols text-lg">home</span>
</nuxt-link>
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="grow h-full flex justify-center items-center" :class="isLibraryPage ? 'bg-primary/80' : 'bg-primary/40'">
<p v-if="isLibraryPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonLibrary }}</p>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
<span v-else class="material-symbols text-lg">import_contacts</span>
</nuxt-link>
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="grow h-full flex justify-center items-center" :class="isPodcastLatestPage ? 'bg-primary/80' : 'bg-primary/40'">
<p class="text-sm">{{ $strings.ButtonLatest }}</p>
</nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="grow h-full flex justify-center items-center" :class="isSeriesPage ? 'bg-primary/80' : 'bg-primary/40'">
<p v-if="isSeriesPage" class="text-sm">{{ $strings.ButtonSeries }}</p>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
</svg>
<span v-else class="material-symbols text-lg">view_column</span>
</nuxt-link>
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="grow h-full flex justify-center items-center" :class="isPlaylistsPage ? 'bg-primary/80' : 'bg-primary/40'">
<p v-if="isPlaylistsPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonPlaylists }}</p>
@ -32,12 +26,7 @@
</nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary/80' : 'bg-primary/40'">
<p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
<svg v-else class="w-5 h-5" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z"
/>
</svg>
<span v-else class="material-symbols text-lg">groups</span>
</nuxt-link>
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary/80' : 'bg-primary/40'">
<p class="text-sm">{{ $strings.ButtonAdd }}</p>

View File

@ -5,9 +5,7 @@
<div id="siderail-buttons-container" role="navigation" aria-label="Library Navigation" :class="{ 'player-open': streamLibraryItem }" class="w-full overflow-y-auto overflow-x-hidden">
<nuxt-link :to="`/library/${currentLibraryId}`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="homePage ? 'bg-primary/80' : 'bg-bg/60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
<span class="material-symbols text-2xl">home</span>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonHome }}</p>
@ -23,9 +21,7 @@
</nuxt-link>
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="showLibrary ? 'bg-primary/80' : 'bg-bg/60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
<span class="material-symbols text-2xl">import_contacts</span>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLibrary }}</p>
@ -33,9 +29,7 @@
</nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isSeriesPage ? 'bg-primary/80' : 'bg-bg/60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
</svg>
<span class="material-symbols text-2xl">view_column</span>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSeries }}</p>
@ -59,12 +53,7 @@
</nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary/80' : 'bg-bg/60'">
<svg class="w-6 h-6" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z"
/>
</svg>
<span class="material-symbols text-2xl">groups</span>
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAuthors }}</p>

View File

@ -1,9 +1,7 @@
<template>
<div class="flex flex-wrap justify-center mt-6">
<div class="flex p-2">
<svg class="h-14 w-14" viewBox="0 0 24 24">
<path fill="currentColor" d="M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z" />
</svg>
<span class="material-symbols text-5xl py-1">newsstand</span>
<div class="px-1">
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalItems) }}</p>
<p class="text-xs md:text-sm text-white/80">{{ $strings.LabelStatsItemsInLibrary }}</p>
@ -19,9 +17,7 @@
</div>
<div v-if="isBookLibrary" class="flex p-2">
<svg class="h-14 w-14" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" />
</svg>
<span class="material-symbols text-5xl py-1">person</span>
<div class="px-1">
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalAuthors) }}</p>
<p class="text-xs md:text-sm text-white/80">{{ $strings.LabelStatsAuthors }}</p>

View File

@ -1,12 +1,8 @@
<template>
<button :aria-label="isRead ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" :class="borderless ? '' : 'bg-primary border border-gray-600'" @click="clickBtn">
<div class="w-5 h-5 text-white relative">
<svg v-if="isRead" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-7 19.6l-7-4.66V3h14v12.93l-7 4.67zm-2.01-7.42l-2.58-2.59L6 12l4 4 8-8-1.42-1.42z" />
</svg>
<div class="w-5 h-5 relative">
<span v-if="isRead" class="material-symbols fill text-xl text-success">beenhere</span>
<span v-else class="material-symbols text-xl text-white">beenhere</span>
</div>
</button>
</template>

View File

@ -1,40 +1,6 @@
<template>
<ui-tooltip :text="$strings.LabelExplicit" direction="top">
<svg xmlns="http://www.w3.org/2000/svg" width="12px" height="12px" viewBox="0 0 512 512" class="ml-1">
<path
fill="white"
d="M 89.00,40.12
C 89.00,40.12 127.00,40.12 127.00,40.12
127.00,40.12 198.00,40.12 198.00,40.12
198.00,40.12 416.00,40.12 416.00,40.12
446.58,40.05 472.95,66.42 473.00,97.00
473.00,97.00 473.00,303.00 473.00,303.00
473.00,303.00 473.00,418.00 473.00,418.00
472.65,447.55 445.06,472.95 416.00,473.00
416.00,473.00 210.00,473.00 210.00,473.00
210.00,473.00 95.00,473.00 95.00,473.00
65.45,472.65 40.05,445.06 40.00,416.00
40.00,416.00 40.00,136.00 40.00,136.00
40.00,136.00 40.00,109.00 40.00,109.00
40.00,109.00 40.00,96.00 40.00,96.00
40.07,81.58 46.89,67.14 57.01,57.01
61.17,52.86 64.86,50.13 70.00,47.31
77.25,43.33 81.02,42.18 89.00,40.12 Z
M 337.00,121.00
C 337.00,121.00 175.00,121.00 175.00,121.00
175.00,121.00 175.00,392.00 175.00,392.00
175.00,392.00 337.00,392.00 337.00,392.00
337.00,392.00 337.00,349.00 337.00,349.00
337.00,349.00 226.00,349.00 226.00,349.00
226.00,349.00 226.00,274.00 226.00,274.00
226.00,274.00 332.00,274.00 332.00,274.00
332.00,274.00 332.00,232.00 332.00,232.00
332.00,232.00 226.00,232.00 226.00,232.00
226.00,232.00 226.00,164.00 226.00,164.00
226.00,164.00 337.00,164.00 337.00,164.00
337.00,164.00 337.00,121.00 337.00,121.00 Z"
/>
</svg>
<span class="material-symbols fill text-sm ml-1 !block">explicit</span>
</ui-tooltip>
</template>

View File

@ -199,7 +199,7 @@ export default {
}
} else {
console.error('User has no more accessible libraries')
this.$store.commit('libraries/setCurrentLibrary', null)
this.$store.commit('libraries/setCurrentLibrary', { id: null })
}
}
},

View File

@ -1,12 +1,12 @@
{
"name": "audiobookshelf-client",
"version": "2.27.0",
"version": "2.28.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf-client",
"version": "2.27.0",
"version": "2.28.0",
"license": "ISC",
"dependencies": {
"@nuxtjs/axios": "^5.13.6",

View File

@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "2.27.0",
"version": "2.28.0",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast client",
"main": "index.js",

View File

@ -53,34 +53,69 @@
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
<div class="w-8 min-w-8 md:w-12 md:min-w-12"></div>
<div class="w-24 min-w-24 md:w-32 md:min-w-32 px-2">{{ $strings.LabelStart }}</div>
<div class="grow px-2">{{ $strings.LabelTitle }}</div>
<div class="w-38 min-w-38 md:w-40 md:min-w-40 px-1 pl-8">{{ $strings.LabelStart }}</div>
<div class="grow px-1 min-w-54">{{ $strings.LabelTitle }}</div>
<div class="w-7 min-w-7 px-1 flex items-center justify-center">
<ui-tooltip :text="allChaptersLocked ? $strings.TooltipUnlockAllChapters : $strings.TooltipLockAllChapters" direction="bottom">
<button class="w-7 h-7 rounded-full flex items-center justify-center cursor-pointer transition-colors duration-150" :class="allChaptersLocked ? 'text-orange-400 hover:text-orange-300' : 'text-gray-300 hover:text-white'" @click="toggleAllChaptersLock">
<span class="material-symbols text-xl">{{ allChaptersLocked ? 'lock' : 'lock_open' }}</span>
</button>
</ui-tooltip>
</div>
<div class="w-32"></div>
</div>
<template v-for="chapter in newChapters">
<div :key="chapter.id" class="flex py-1">
<div v-for="chapter in newChapters" :key="chapter.id" class="flex py-1">
<div class="w-8 min-w-8 md:w-12 md:min-w-12">#{{ chapter.id + 1 }}</div>
<div class="w-24 min-w-24 md:w-32 md:min-w-32 px-1">
<div class="w-38 min-w-38 md:w-40 md:min-w-40 px-1">
<div class="flex items-center gap-1">
<ui-tooltip :text="$strings.TooltipSubtractOneSecond" direction="bottom">
<button
class="w-6 h-6 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150 flex-shrink-0"
:class="{ 'opacity-50 cursor-not-allowed': chapter.id === 0 && chapter.start - timeIncrementAmount < 0 }"
@click="incrementChapterTime(chapter, -timeIncrementAmount)"
:disabled="chapter.id === 0 && chapter.start - timeIncrementAmount < 0"
>
<span class="material-symbols text-sm">remove</span>
</button>
</ui-tooltip>
<div class="flex-1 min-w-0">
<ui-text-input v-if="showSecondInputs" v-model="chapter.start" type="number" class="text-xs" @change="checkChapters" />
<ui-time-picker v-else class="text-xs" v-model="chapter.start" :show-three-digit-hour="mediaDuration >= 360000" @change="checkChapters" />
</div>
<ui-tooltip :text="$strings.TooltipAddOneSecond" direction="bottom">
<button class="w-6 h-6 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150 flex-shrink-0" :class="{ 'opacity-50 cursor-not-allowed': chapter.start + timeIncrementAmount >= mediaDuration }" @click="incrementChapterTime(chapter, timeIncrementAmount)" :disabled="chapter.start + timeIncrementAmount >= mediaDuration">
<span class="material-symbols text-sm">add</span>
</button>
</ui-tooltip>
</div>
</div>
<div class="grow px-1">
<ui-text-input v-model="chapter.title" @change="checkChapters" class="text-xs min-w-52" />
</div>
<div class="w-7 min-w-7 px-1 py-1">
<div class="flex items-center justify-center">
<ui-tooltip :text="lockedChapters.has(chapter.id) ? $strings.TooltipUnlockChapter : $strings.TooltipLockChapter" direction="bottom">
<button class="w-7 h-7 rounded-full flex items-center justify-center transform hover:scale-110 duration-150 flex-shrink-0" :class="lockedChapters.has(chapter.id) ? 'text-orange-400 hover:text-orange-300' : 'text-gray-300 hover:text-white'" @click="toggleChapterLock(chapter, $event)">
<span class="material-symbols text-base">{{ lockedChapters.has(chapter.id) ? 'lock' : 'lock_open' }}</span>
</button>
</ui-tooltip>
</div>
</div>
<div class="w-32 min-w-32 px-2 py-1">
<div class="flex items-center">
<ui-tooltip :text="$strings.MessageRemoveChapter" direction="bottom">
<button v-if="newChapters.length > 1" class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-error transform hover:scale-110 duration-150" @click="removeChapter(chapter)">
<span class="material-symbols text-base">remove</span>
<span class="material-symbols text-base">delete</span>
</button>
</ui-tooltip>
<ui-tooltip :text="$strings.MessageInsertChapterBelow" direction="bottom">
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-success transform hover:scale-110 duration-150" @click="addChapter(chapter)">
<span class="material-symbols text-lg">add</span>
<span class="material-symbols text-lg">add_row_below</span>
</button>
</ui-tooltip>
<ui-tooltip :text="selectedChapterId === chapter.id && isPlayingChapter ? $strings.MessagePauseChapter : $strings.MessagePlayChapter" direction="bottom">
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150" @click="playChapter(chapter)">
<widgets-loading-spinner v-if="selectedChapterId === chapter.id && isLoadingChapter" />
@ -88,7 +123,9 @@
<span v-else class="material-symbols text-base">play_arrow</span>
</button>
</ui-tooltip>
<ui-tooltip v-if="selectedChapterId === chapter.id && (isPlayingChapter || isLoadingChapter)" :text="$strings.TooltipAdjustChapterStart" direction="bottom">
<div class="ml-2 text-xs text-gray-300 font-mono min-w-10 cursor-pointer hover:text-white transition-colors duration-150" @click="adjustChapterStartTime(chapter)">{{ elapsedTime }}s</div>
</ui-tooltip>
<ui-tooltip v-if="chapter.error" :text="chapter.error" direction="left">
<button class="w-7 h-7 rounded-full flex items-center justify-center text-error">
<span class="material-symbols text-lg">error_outline</span>
@ -97,7 +134,20 @@
</div>
</div>
</div>
</template>
<div class="flex items-center mt-4 mb-2">
<div class="w-8 min-w-8 md:w-12 md:min-w-12"></div>
<div class="w-38 min-w-38 md:w-40 md:min-w-40 px-1"></div>
<div class="flex items-center gap-2 grow px-1">
<ui-text-input v-model="bulkChapterInput" :placeholder="$strings.PlaceholderBulkChapterInput" class="text-xs grow min-w-52" @keyup.enter="handleBulkChapterAdd" />
</div>
<div class="w-39 min-w-39 px-1 py-1">
<ui-tooltip :text="$strings.TooltipAddChapters" direction="bottom" class="inline-block align-middle">
<button class="w-5 h-5 rounded-full flex items-center justify-center text-gray-300 hover:text-success transform hover:scale-110 duration-150 flex-shrink-0" :aria-label="$strings.TooltipAddChapters" :class="{ 'opacity-50 cursor-not-allowed': !bulkChapterInput.trim() }" :disabled="!bulkChapterInput.trim()" @click="handleBulkChapterAdd">
<span class="material-symbols text-lg">add</span>
</button>
</ui-tooltip>
</div>
</div>
</div>
<div class="w-full max-w-xl py-4 px-2">
@ -114,26 +164,23 @@
<div class="w-20">{{ $strings.LabelDuration }}</div>
<div class="w-20 hidden md:block text-center">{{ $strings.HeaderChapters }}</div>
</div>
<template v-for="track in audioTracks">
<div :key="track.ino" class="flex items-center py-2" :class="currentTrackIndex === track.index && isPlayingChapter ? 'bg-success/10' : ''">
<div v-for="track in audioTracks" :key="track.ino" class="flex items-center py-2" :class="currentTrackIndex === track.index && isPlayingChapter ? 'bg-success/10' : ''">
<div class="grow max-w-[calc(100%-80px)] pr-2">
<p class="text-xs truncate max-w-sm">{{ track.metadata.filename }}</p>
</div>
<div class="w-20" style="min-width: 80px">
<p class="text-xs font-mono text-gray-200">{{ $secondsToTimestamp(Math.round(track.duration), false, true) }}</p>
</div>
<div class="w-20 hidden md:flex justify-center" style="min-width: 80px">
<span v-if="(track.chapters || []).length" class="material-symbols text-success text-sm">check</span>
<div class="w-20 hidden md:flex justify-center" style="min-width: 80px"><span v-if="(track.chapters || []).length" class="material-symbols text-success text-sm">check</span></div>
</div>
</div>
</template>
</div>
</div>
<div v-if="saving" class="w-full h-full absolute top-0 left-0 bottom-0 right-0 z-30 bg-black/25 flex items-center justify-center">
<ui-loading-indicator />
</div>
<!-- audible chapter lookup modal -->
<modals-modal v-model="showFindChaptersModal" name="edit-book" :width="500" :processing="findingChapters">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
@ -159,12 +206,16 @@
</div>
</div>
<div v-else class="w-full p-4">
<div class="flex justify-between mb-4">
<div class="flex mb-4">
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-white flex-shrink-0" :aria-label="$strings.ButtonBack" @click="resetChapterLookupData">
<span class="material-symbols text-lg">arrow_back</span>
</button>
<p>
{{ $strings.LabelDurationFound }} <span class="font-semibold">{{ $secondsToTimestamp(chapterData.runtimeLengthSec) }}</span
><br />
{{ $strings.LabelDurationFound }} <span class="font-semibold">{{ $secondsToTimestamp(chapterData.runtimeLengthSec) }}</span>
<br />
<span class="font-semibold" :class="{ 'text-warning': chapters.length !== chapterData.chapters.length }">{{ chapterData.chapters.length }}</span> {{ $strings.LabelChaptersFound }}
</p>
<div class="grow" />
<p>
{{ $strings.LabelYourAudiobookDuration }}: <span class="font-semibold">{{ $secondsToTimestamp(mediaDurationRounded) }}</span
><br />
@ -198,17 +249,49 @@
<p class="pl-2">{{ $strings.MessageChapterStartIsAfter }}</p>
</div>
</div>
<div class="flex items-center pt-2">
<ui-btn small color="bg-primary" class="mr-1" @click="applyChapterNamesOnly">{{ $strings.ButtonMapChapterTitles }}</ui-btn>
<div class="flex items-center pt-2 justify-between">
<div class="flex items-center gap-2">
<ui-btn small color="bg-primary" @click="applyChapterNamesOnly">{{ $strings.ButtonMapChapterTitles }}</ui-btn>
<ui-tooltip :text="$strings.MessageMapChapterTitles" direction="top" class="flex items-center">
<span class="material-symbols text-xl text-gray-200">info</span>
</ui-tooltip>
<div class="grow" />
</div>
<ui-btn small color="bg-success" @click="applyChapterData">{{ $strings.ButtonApplyChapters }}</ui-btn>
</div>
</div>
</div>
</modals-modal>
<!-- create bulk chapters modal -->
<modals-modal v-model="showBulkChapterModal" name="bulk-chapters" :width="400">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
<p class="text-3xl text-white truncate pointer-events-none">{{ $strings.HeaderBulkChapterModal }}</p>
</div>
</template>
<div class="w-full h-full max-h-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative p-6">
<div class="flex flex-col space-y-8">
<p class="text-base">{{ $strings.MessageBulkChapterPattern }}</p>
<div v-if="detectedPattern" class="text-sm text-gray-400 bg-gray-800 p-2 rounded">
<strong>{{ $strings.LabelDetectedPattern }}</strong> "{{ detectedPattern.before }}{{ formatNumberWithPadding(detectedPattern.startingNumber, detectedPattern) }}{{ detectedPattern.after }}"
<br />
<strong>{{ $strings.LabelNextChapters }}</strong>
"{{ detectedPattern.before }}{{ formatNumberWithPadding(detectedPattern.startingNumber + 1, detectedPattern) }}{{ detectedPattern.after }}", "{{ detectedPattern.before }}{{ formatNumberWithPadding(detectedPattern.startingNumber + 2, detectedPattern) }}{{ detectedPattern.after }}", etc.
</div>
<div class="flex px-1 items-center">
<label class="text-base font-medium">{{ $strings.LabelNumberOfChapters }}</label>
<div class="grow" />
<ui-text-input v-model="bulkChapterCount" type="number" min="1" max="50" class="w-14" :style="{ height: `2em` }" @keyup.enter="addBulkChapters" />
</div>
<div class="flex px-1 items-center">
<ui-btn small @click="showBulkChapterModal = false">{{ $strings.ButtonCancel }}</ui-btn>
<div class="grow" />
<ui-btn small color="bg-success" @click="addBulkChapters">{{ $strings.ButtonAddChapters }}</ui-btn>
</div>
</div>
</div>
</modals-modal>
</div>
</template>
@ -265,7 +348,17 @@ export default {
removeBranding: false,
showSecondInputs: false,
audibleRegions: ['US', 'CA', 'UK', 'AU', 'FR', 'DE', 'JP', 'IT', 'IN', 'ES'],
hasChanges: false
hasChanges: false,
timeIncrementAmount: 1,
elapsedTime: 0,
playStartTime: null,
elapsedTimeInterval: null,
lockedChapters: new Set(),
lastSelectedLockIndex: null,
bulkChapterInput: '',
showBulkChapterModal: false,
bulkChapterCount: 1,
detectedPattern: null
}
},
computed: {
@ -304,9 +397,18 @@ export default {
},
selectedChapterId() {
return this.selectedChapter ? this.selectedChapter.id : null
},
allChaptersLocked() {
return this.newChapters.length > 0 && this.newChapters.every((chapter) => this.lockedChapters.has(chapter.id))
}
},
methods: {
formatNumberWithPadding(number, pattern) {
if (!pattern || !pattern.hasLeadingZeros || !pattern.originalPadding) {
return number.toString()
}
return number.toString().padStart(pattern.originalPadding, '0')
},
setChaptersFromTracks() {
let currentStartTime = 0
let index = 0
@ -321,7 +423,7 @@ export default {
currentStartTime += track.duration
}
this.newChapters = chapters
this.lockedChapters = new Set()
this.checkChapters()
},
toggleRemoveBranding() {
@ -334,19 +436,22 @@ export default {
const amount = Number(this.shiftAmount)
const lastChapter = this.newChapters[this.newChapters.length - 1]
if (lastChapter.start + amount > this.mediaDurationRounded) {
this.$toast.error(this.$strings.ToastChaptersInvalidShiftAmountLast)
return
}
// Check if any unlocked chapters would be affected negatively
const unlockedChapters = this.newChapters.filter((chap) => !this.lockedChapters.has(chap.id))
if (this.newChapters[1].start + amount <= 0) {
this.$toast.error(this.$strings.ToastChaptersInvalidShiftAmountStart)
if (unlockedChapters.length === 0) {
this.$toast.warning(this.$strings.ToastChaptersAllLocked)
return
}
for (let i = 0; i < this.newChapters.length; i++) {
const chap = this.newChapters[i]
// Skip locked chapters
if (this.lockedChapters.has(chap.id)) {
continue
}
chap.end = Math.min(chap.end + amount, this.mediaDuration)
if (i > 0) {
chap.start = Math.max(0, chap.start + amount)
@ -354,6 +459,83 @@ export default {
}
this.checkChapters()
},
incrementChapterTime(chapter, amount) {
if (chapter.id === 0 && chapter.start + amount < 0) {
return
}
if (chapter.start + amount >= this.mediaDuration) {
return
}
chapter.start = Math.max(0, chapter.start + amount)
this.checkChapters()
},
adjustChapterStartTime(chapter) {
const newStartTime = chapter.start + this.elapsedTime
chapter.start = newStartTime
this.checkChapters()
this.$toast.success(this.$strings.ToastChapterStartTimeAdjusted.replace('{0}', this.elapsedTime))
this.destroyAudioEl()
},
startElapsedTimeTracking() {
this.elapsedTime = 0
this.playStartTime = Date.now()
this.elapsedTimeInterval = setInterval(() => {
this.elapsedTime = Math.floor((Date.now() - this.playStartTime) / 1000)
}, 100)
},
stopElapsedTimeTracking() {
if (this.elapsedTimeInterval) {
clearInterval(this.elapsedTimeInterval)
this.elapsedTimeInterval = null
}
this.elapsedTime = 0
this.playStartTime = null
},
toggleChapterLock(chapter, event) {
const chapterId = chapter.id
if (event.shiftKey && this.lastSelectedLockIndex !== null) {
const startIndex = Math.min(this.lastSelectedLockIndex, chapterId)
const endIndex = Math.max(this.lastSelectedLockIndex, chapterId)
const shouldLock = !this.lockedChapters.has(chapterId)
for (let i = startIndex; i <= endIndex; i++) {
if (shouldLock) {
this.lockedChapters.add(i)
} else {
this.lockedChapters.delete(i)
}
}
} else {
if (this.lockedChapters.has(chapterId)) {
this.lockedChapters.delete(chapterId)
} else {
this.lockedChapters.add(chapterId)
}
}
this.lastSelectedLockIndex = chapterId
this.lockedChapters = new Set(this.lockedChapters)
},
lockAllChapters() {
this.newChapters.forEach((chapter) => {
this.lockedChapters.add(chapter.id)
})
this.lockedChapters = new Set(this.lockedChapters)
},
unlockAllChapters() {
this.lockedChapters.clear()
this.lockedChapters = new Set(this.lockedChapters)
},
toggleAllChaptersLock() {
if (this.allChaptersLocked) {
this.unlockAllChapters()
} else {
this.lockAllChapters()
}
},
editItem() {
this.$store.commit('showEditModal', this.libraryItem)
},
@ -368,6 +550,10 @@ export default {
this.checkChapters()
},
removeChapter(chapter) {
if (this.lockedChapters.has(chapter.id)) {
this.$toast.warning(this.$strings.ToastChapterLocked)
return
}
this.newChapters = this.newChapters.filter((ch) => ch.id !== chapter.id)
this.checkChapters()
},
@ -451,6 +637,7 @@ export default {
console.log('Audio playing')
this.isLoadingChapter = false
this.isPlayingChapter = true
this.startElapsedTimeTracking()
})
audioEl.addEventListener('ended', () => {
console.log('Audio ended')
@ -473,6 +660,10 @@ export default {
this.selectedChapter = null
this.isPlayingChapter = false
this.isLoadingChapter = false
this.stopElapsedTimeTracking()
},
resetChapterLookupData() {
this.chapterData = null
},
saveChapters() {
this.checkChapters()
@ -523,7 +714,7 @@ export default {
},
applyChapterNamesOnly() {
this.newChapters.forEach((chapter, index) => {
if (this.chapterData.chapters[index]) {
if (this.chapterData.chapters[index] && !this.lockedChapters.has(chapter.id)) {
chapter.title = this.chapterData.chapters[index].title
}
})
@ -535,7 +726,7 @@ export default {
},
applyChapterData() {
let index = 0
this.newChapters = this.chapterData.chapters
const audibleChapters = this.chapterData.chapters
.filter((chap) => chap.startOffsetSec < this.mediaDuration)
.map((chap) => {
return {
@ -545,6 +736,21 @@ export default {
title: chap.title
}
})
const merged = []
let audibleIdx = 0
for (let i = 0; i < Math.max(this.newChapters.length, audibleChapters.length); i++) {
const isLocked = this.lockedChapters.has(i)
if (isLocked && this.newChapters[i]) {
merged.push({ ...this.newChapters[i], id: i })
} else if (audibleChapters[audibleIdx]) {
merged.push({ ...audibleChapters[audibleIdx], id: i })
audibleIdx++
} else if (this.newChapters[i]) {
merged.push({ ...this.newChapters[i], id: i })
}
}
this.newChapters = merged
this.showFindChaptersModal = false
this.chapterData = null
@ -643,6 +849,7 @@ export default {
}
]
}
this.lockedChapters = new Set()
this.checkChapters()
},
removeAllChaptersClick() {
@ -684,6 +891,91 @@ export default {
this.saving = false
})
},
handleBulkChapterAdd() {
const input = this.bulkChapterInput.trim()
if (!input) return
const numberMatch = input.match(/(\d+)/)
if (numberMatch) {
// Extract the base pattern and number, preserving zero-padding
const originalNumberString = numberMatch[1]
const foundNumber = parseInt(originalNumberString)
const numberIndex = numberMatch.index
const beforeNumber = input.substring(0, numberIndex)
const afterNumber = input.substring(numberIndex + originalNumberString.length)
this.detectedPattern = {
before: beforeNumber,
after: afterNumber,
startingNumber: foundNumber,
originalPadding: originalNumberString.length,
hasLeadingZeros: originalNumberString.length > 1 && originalNumberString.startsWith('0')
}
this.bulkChapterCount = 1
this.showBulkChapterModal = true
} else {
this.addSingleChapterFromInput(input)
}
},
addSingleChapterFromInput(title) {
// Find the last chapter to determine where to add the new one
const lastChapter = this.newChapters[this.newChapters.length - 1]
const newStart = lastChapter ? lastChapter.end : 0
const newEnd = Math.min(newStart + 300, this.mediaDuration)
const newChapter = {
id: this.newChapters.length,
start: newStart,
end: newEnd,
title: title
}
this.newChapters.push(newChapter)
this.bulkChapterInput = ''
this.checkChapters()
},
addBulkChapters() {
const count = parseInt(this.bulkChapterCount)
if (!count || count < 1 || count > 150) {
this.$toast.error(this.$strings.ToastBulkChapterInvalidCount)
return
}
const { before, after, startingNumber, originalPadding, hasLeadingZeros } = this.detectedPattern
const lastChapter = this.newChapters[this.newChapters.length - 1]
const baseStart = lastChapter ? lastChapter.start + 1 : 0
// Add multiple chapters with the detected pattern
for (let i = 0; i < count; i++) {
const chapterNumber = startingNumber + i
let formattedNumber = chapterNumber.toString()
// Apply zero-padding if the original had leading zeros
if (hasLeadingZeros && originalPadding > 1) {
formattedNumber = chapterNumber.toString().padStart(originalPadding, '0')
}
const newStart = baseStart + i
const newEnd = Math.min(newStart + i + i, this.mediaDuration)
const newChapter = {
id: this.newChapters.length,
start: newStart,
end: newEnd,
title: `${before}${formattedNumber}${after}`
}
this.newChapters.push(newChapter)
}
this.bulkChapterInput = ''
this.showBulkChapterModal = false
this.detectedPattern = null
this.checkChapters()
},
libraryItemUpdated(libraryItem) {
if (libraryItem.id === this.libraryItem.id) {
if (!!libraryItem.media.metadata.asin && this.mediaMetadata.asin !== libraryItem.media.metadata.asin) {

View File

@ -131,35 +131,26 @@
</div>
<div class="grow py-2">
<ui-dropdown :label="$strings.LabelSettingsDateFormat" v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-52" @input="(val) => updateSettingsKey('dateFormat', val)" />
<ui-dropdown :label="$strings.LabelSettingsDateFormat" v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-72" @input="(val) => updateSettingsKey('dateFormat', val)" />
<p class="text-xs ml-1 text-white/60">{{ $strings.LabelExample }}: {{ dateExample }}</p>
</div>
<div class="grow py-2">
<ui-dropdown :label="$strings.LabelSettingsTimeFormat" v-model="newServerSettings.timeFormat" :items="timeFormats" small class="max-w-52" @input="(val) => updateSettingsKey('timeFormat', val)" />
<ui-dropdown :label="$strings.LabelSettingsTimeFormat" v-model="newServerSettings.timeFormat" :items="timeFormats" small class="max-w-72" @input="(val) => updateSettingsKey('timeFormat', val)" />
<p class="text-xs ml-1 text-white/60">{{ $strings.LabelExample }}: {{ timeExample }}</p>
</div>
<div class="py-2">
<ui-dropdown :label="$strings.LabelLanguageDefaultServer" ref="langDropdown" v-model="newServerSettings.language" :items="$languageCodeOptions" small class="max-w-52" @input="updateServerLanguage" />
<ui-dropdown :label="$strings.LabelLanguageDefaultServer" ref="langDropdown" v-model="newServerSettings.language" :items="$languageCodeOptions" small class="max-w-72" @input="updateServerLanguage" />
</div>
<!-- old experimental features -->
<!-- <div class="pt-4">
<h2 class="font-semibold">{{ $strings.HeaderSettingsExperimental }}</h2>
<div class="pt-4">
<h2 class="font-semibold">{{ $strings.HeaderSettingsSecurity }}</h2>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch labeledBy="settings-experimental-features" v-model="showExperimentalFeatures" />
<ui-tooltip :text="$strings.LabelSettingsExperimentalFeaturesHelp">
<p class="pl-4">
<span id="settings-experimental-features">{{ $strings.LabelSettingsExperimentalFeatures }}</span>
<a :aria-label="$strings.LabelSettingsExperimentalFeaturesHelp" href="https://github.com/advplyr/audiobookshelf/discussions/75" target="_blank">
<span class="material-symbols icon-text">info</span>
</a>
</p>
</ui-tooltip>
</div> -->
<div class="py-2">
<ui-multi-select v-model="newServerSettings.allowedOrigins" :items="newServerSettings.allowedOrigins" :label="$strings.LabelCorsAllowed" class="max-w-72" @input="updateCorsOrigins" />
</div>
</div>
</div>
</app-settings-content>
@ -323,6 +314,27 @@ export default {
updateServerLanguage(val) {
this.updateSettingsKey('language', val)
},
updateCorsOrigins(val) {
const validOrigins = []
const invalidOrigins = []
val.forEach((origin) => {
const trimmedOrigin = origin.trim().toLowerCase()
try {
new URL(trimmedOrigin)
validOrigins.push(trimmedOrigin)
} catch {
invalidOrigins.push(trimmedOrigin)
}
})
if (invalidOrigins.length > 0) {
this.$toast.error(this.$strings.ToastInvalidUrls)
}
this.newServerSettings.allowedOrigins = validOrigins
this.updateSettingsKey('allowedOrigins', validOrigins)
},
updateSettingsKey(key, val) {
if (key === 'scannerDisableWatcher') {
this.newServerSettings.scannerDisableWatcher = val
@ -352,6 +364,7 @@ export default {
initServerSettings() {
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
this.newServerSettings.sortingPrefixes = [...(this.newServerSettings.sortingPrefixes || [])]
this.newServerSettings.allowedOrigins = [...(this.newServerSettings.allowedOrigins || [])]
this.scannerEnableWatcher = !this.newServerSettings.scannerDisableWatcher
this.homepageUseBookshelfView = this.newServerSettings.homeBookshelfView != this.$constants.BookshelfView.DETAIL

View File

@ -189,7 +189,7 @@ export default {
require('@/plugins/chromecast.js').default(this)
}
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
this.$store.commit('libraries/setCurrentLibrary', { id: userDefaultLibraryId })
this.$store.commit('user/setUser', user)
// Access token only returned from login, not authorize
if (user.accessToken) {

View File

@ -133,7 +133,7 @@ export const actions = {
commit('setNumUserPlaylists', numUserPlaylists)
commit('scanners/setCustomMetadataProviders', customMetadataProviders, { root: true })
commit('setCurrentLibrary', libraryId)
commit('setCurrentLibrary', { id: libraryId })
return data
})
.catch((error) => {
@ -182,8 +182,8 @@ export const mutations = {
setLibraryIssues(state, val) {
state.issues = val
},
setCurrentLibrary(state, val) {
state.currentLibraryId = val
setCurrentLibrary(state, { id }) {
state.currentLibraryId = id
},
set(state, libraries) {
state.libraries = libraries

View File

@ -152,7 +152,6 @@ export const actions = {
.$post('/auth/refresh')
.then(async (response) => {
const newAccessToken = response.user.accessToken
commit('setUser', response.user)
commit('setAccessToken', newAccessToken)
// Emit event used to re-authenticate socket in default.vue since $root is not available here
if (this.$eventBus) {

View File

@ -240,7 +240,7 @@
"LabelAllUsersExcludingGuests": "Усе карыстальнікі, акрамя гасцей",
"LabelAllUsersIncludingGuests": "Усе карыстальнікі, уключаючы гасцей",
"LabelAlreadyInYourLibrary": "Ужо ў вашай бібліятэцы",
"LabelApiKeyCreated": "API-ключ «{0}» паспяхова створаны.",
"LabelApiKeyCreated": "API-ключ \"{0}\" паспяхова створаны.",
"LabelApiKeyCreatedDescription": "Пераканайцеся, што вы скапіявалі API-ключ зараз, бо паўторна яго ўбачыць не атрымаецца.",
"LabelApiKeyUser": "Дзейнічаць ад імя карыстальніка",
"LabelApiKeyUserDescription": "Гэты API-ключ будзе мець тыя ж правы, што і карыстальнік, ад імя якога ён дзейнічае. У журналах гэта будзе выглядаць так, быццам запыт робіць сам карыстальнік.",
@ -309,7 +309,7 @@
"LabelDevice": "Прылада",
"LabelDeviceInfo": "Інфармацыя пра прыладу",
"LabelDeviceIsAvailableTo": "Прылада даступная для...",
"LabelDirectory": "Дырэкторыя",
"LabelDirectory": "Каталог",
"LabelDiscFromFilename": "Дыск з імя файла",
"LabelDiscFromMetadata": "Дыск па метададзеных",
"LabelDiscover": "Знайсці",
@ -327,7 +327,7 @@
"LabelEmail": "Электронная пошта",
"LabelEmailSettingsFromAddress": "Адрас адпраўніка",
"LabelEmailSettingsRejectUnauthorized": "Адхіляць неаўтарызаваныя сертыфікаты",
"LabelEmailSettingsRejectUnauthorizedHelp": "Адключэнне праверкі SSL-сертыфіката можа зрабіць ваша злучэнне ўразлівым перад пагрозамі бяспекі, такімі як атакі «чалавек пасярэдзіне». Адключайце гэтую опцыю толькі калі цалкам разумееце наступствы і ўпэўнены ў надзейнасці паштовага сервера.",
"LabelEmailSettingsRejectUnauthorizedHelp": "Адключэнне праверкі SSL-сертыфіката можа зрабіць ваша злучэнне ўразлівым перад пагрозамі бяспекі, такімі як атакі \"чалавек пасярэдзіне\". Адключайце гэтую опцыю толькі калі цалкам разумееце наступствы і ўпэўнены ў надзейнасці паштовага сервера.",
"LabelEmailSettingsSecure": "Бяспечныя",
"LabelEmailSettingsSecureHelp": "Калі ўключана, злучэнне будзе выкарыстоўваць TLS пры падключэнні да сервера. Калі выключана, TLS будзе выкарыстоўвацца толькі ў выпадку падтрымкі пашырэння STARTTLS на серверы. У большасці выпадкаў усталюйце значэнне true пры падключэнні да порта 465. Для партоў 587 або 25 не ўключайце яго. (інфармацыя з nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Тэставы адрас",
@ -338,6 +338,7 @@
"LabelEncodingClearItemCache": "Пераканайцеся, што перыядычна ачышчаеце кэш элементаў.",
"LabelEncodingFinishedM4B": "Гатовы файл M4B будзе змешчаны ў вашу тэчку з аўдыякнігамі па адрасе:",
"LabelEncodingInfoEmbedded": "Метададзеныя будуць убудаваны ў аўдыядарожкі ўнутры вашай тэчкі з аўдыякнігамі.",
"LabelEncodingStartedNavigation": "Пасля запуску задачы вы можаце перайсці на іншую старонку.",
"LabelEncodingTimeWarning": "Кадаванне можа заняць да 30 хвілін.",
"LabelEnd": "Канец",
"LabelEndOfChapter": "Канец раздзела",
@ -495,7 +496,7 @@
"LabelSettingsParseSubtitlesHelp": "Выдзяляць падзагаловак з назваў тэчак аўдыякніг.<br>Падзагаловак павінен быць аддзелены знакам \" - \".<br>Напрыклад, \"Назва кнігі - Падзагаловак тут\" мае падзагаловак \"Падзагаловак тут\"",
"LabelSettingsPreferMatchedMetadata": "Аддаваць перавагу супадаючым метададзеным",
"LabelSettingsPreferMatchedMetadataHelp": "Супадаючыя дадзеныя будуць замяняць дэталі элемента пры выкарыстанні функцыі Хуткі пошук. Па змаўчанні Хуткі пошук запаўняе толькі адсутныя дэталі.",
"LabelSettingsStoreCoversWithItemHelp": "Па змаўчанні вокладкі захоўваюцца ў /metadata/items, уключэнне гэтай опцыі забяспечыць захоўванне вокладак у тэчцы элемента вашай бібліятэкі. Захоўвацца будзе толькі адзін файл з назвай «cover»",
"LabelSettingsStoreCoversWithItemHelp": "Па змаўчанні вокладкі захоўваюцца ў /metadata/items, уключэнне гэтай опцыі забяспечыць захоўванне вокладак у тэчцы элемента вашай бібліятэкі. Захоўвацца будзе толькі адзін файл з назвай \"cover\"",
"LabelSettingsStoreMetadataWithItem": "Захоўваць метададзеныя разам з элементам",
"LabelSettingsStoreMetadataWithItemHelp": "Па змаўчанні метададзеныя захоўваюцца ў /metadata/items. Уключэнне гэтай опцыі забяспечыць захоўванне файлаў метададзеных у тэчках элементаў вашай бібліятэкі",
"LabelSettingsTimeFormat": "Фармат часу",
@ -527,7 +528,7 @@
"LabelTags": "Меткі",
"LabelTagsAccessibleToUser": "Меткі, даступныя карыстальніку",
"LabelTagsNotAccessibleToUser": "Меткі, недаступныя карыстальніку",
"LabelTasks": "Выконваюцца задачы",
"LabelTasks": "Запушчаныя задачы",
"LabelTextEditorBulletedList": "Маркіраваны спіс",
"LabelTextEditorLink": "Спасылка",
"LabelTextEditorNumberedList": "Нумараваны спіс",
@ -607,7 +608,7 @@
"MessageChapterErrorStartGteDuration": "Няправільны час пачатку: ён павінен быць меншым за працягласць аўдыякнігі",
"MessageChapterErrorStartLtPrev": "Няправільны час пачатку: ён павінен быць большым або роўным часу пачатку папярэдняга раздзела",
"MessageConfirmCloseFeed": "Вы ўпэўнены, што жадаеце закрыць гэтую стужку?",
"MessageConfirmDeleteMetadataProvider": "Ці ўпэўненыя вы, што жадаеце выдаліць карыстацкага пастаўшчыка метададзеных «{0}»?",
"MessageConfirmDeleteMetadataProvider": "Ці ўпэўненыя вы, што жадаеце выдаліць карыстацкага пастаўшчыка метададзеных \"{0}\"?",
"MessageConfirmEmbedMetadataInAudioFiles": "Ці ўпэўненыя вы, што жадаеце ўбудаваць метададзеныя ў {0} аўдыёфайлаў?",
"MessageConfirmPurgeCache": "Ачышчэнне кэша выдаліць увесь каталог па адрасе <code>/metadata/cache</code>. <br /><br /> Ці сапраўды вы жадаеце выдаліць каталог кэша?",
"MessageConfirmPurgeItemsCache": "Ачышчэнне кэша элементаў выдаліць увесь каталог па адрасе <code>/metadata/cache/items</code>. <br /> Вы ўпэўнены?",
@ -636,6 +637,7 @@
"MessageNoMediaProgress": "Няма прагрэсу медыя",
"MessageNoPodcastFeed": "Няправільны падкаст: Няма стужкі",
"MessageNoPodcastsFound": "Падкасты не знойдзены",
"MessageNoTasksRunning": "Няма запушчаных задач",
"MessageNoUpdatesWereNecessary": "Абнаўленні не патрабаваліся",
"MessageNoUserPlaylists": "У вас няма спісаў прайгравання",
"MessageNoUserPlaylistsHelp": "Спісы прайгравання прыватныя. Толькі карыстальнік, які іх стварыў, можа іх бачыць.",
@ -643,17 +645,28 @@
"MessagePlaylistCreateFromCollection": "Стварыць спіс прайгравання з калекцыі",
"MessagePodcastHasNoRSSFeedForMatching": "У падкаста няма URL RSS-стужкі для супадзення",
"MessagePodcastSearchField": "Увядзіце пошукавы запыт або URL RSS-стужкі",
"MessageQuickMatchDescription": "Запоўніць пустыя дэталі элемента і вокладку першым вынікам супадзення з «{0}». Не замяняе дэталі, калі опцыя «Аддаваць перавагу супадаючым метададзеным» на серверы не ўключана.",
"MessageQuickMatchDescription": "Запоўніць пустыя дэталі элемента і вокладку першым вынікам супадзення з '{0}'. Не замяняе дэталі, калі опцыя «Аддаваць перавагу супадаючым метададзеным» на серверы не ўключана.",
"MessageReportBugsAndContribute": "Паведамляйце пра памылкі, прапануйце новыя функцыі і ўдзельнічайце на",
"MessageRestoreBackupWarning": "Аднаўленне рэзервовай копіі перазапіша ўсю базу даных, размешчаную ў /config, а таксама вокладкі ў /metadata/items і /metadata/authors. <br /><br /> Рэзервовыя копіі не змяняюць файлы ў вашых тэчках бібліятэкі. Калі вы ўключылі наладкі сервера для захоўвання воклак і метададзеных у тэчках бібліятэкі, гэтыя файлы не будуць захаваныя ў рэзервовых копіях і не зменяцца. <br /><br /> Усе кліенты, якія карыстаюцца вашым серверам, будуць аўтаматычна абноўлены.",
"MessageScheduleRunEveryWeekdayAtTime": "Выконваць кожныя {0} у {1}",
"MessageStartPlaybackAtTime": "Пачаць прайграванне для \"{0}\" з {1}?",
"MessageTaskAudioFileNotWritable": "Аўдыёфайл \"{0}\" недаступны для запісу",
"MessageTaskCanceledByUser": "Задача скасавана карыстальнікам",
"MessageTaskDownloadingEpisodeDescription": "Спампоўка эпізоду \"{0}\"",
"MessageTaskEmbeddingMetadata": "Убудаванне метададзеных",
"MessageTaskEmbeddingMetadataDescription": "Убудаванне метададзеных у аўдыёкнігу «{0}»",
"MessageTaskFailedToEmbedMetadataInFile": "Не ўдалося ўбудаваць метададзеныя ў файл «{0}»",
"MessageTaskEmbeddingMetadataDescription": "Убудаванне метададзеных у аўдыёкнігу \"{0}\"",
"MessageTaskEncodingM4b": "Кадаванне M4B",
"MessageTaskEncodingM4bDescription": "Кадаванне аўдыякнігі \"{0}\" у адзін файл m4b",
"MessageTaskFailed": "Не ўдалося",
"MessageTaskFailedToBackupAudioFile": "Не ўдалося зрабіць рэзервовую копію аўдыёфайла \"{0}\"",
"MessageTaskFailedToCreateCacheDirectory": "Не ўдалося стварыць каталог кэша",
"MessageTaskFailedToEmbedMetadataInFile": "Не ўдалося ўбудаваць метададзеныя ў файл \"{0}\"",
"MessageTaskFailedToMergeAudioFiles": "Не ўдалося аб’яднаць аўдыёфайлы",
"MessageTaskFailedToMoveM4bFile": "Не ўдалося перамясціць файл m4b",
"MessageTaskFailedToWriteMetadataFile": "Не ўдалося захаваць файл метададзеных",
"MessageTaskMatchingBooksInLibrary": "Пошук супадзенняў кніг у бібліятэцы \"{0}\"",
"MessageTaskNoFilesToScan": "Няма файлаў для сканавання",
"MessageTaskOpmlImport": "Імпарт OPML",
"MessageTaskOpmlImportDescription": "Стварэнне падкастаў з {0} RSS-стужак",
"MessageTaskOpmlImportFeed": "Імпарт стужкі з OPML",
"MessageTaskOpmlImportFeedDescription": "Імпартаванне RSS-стужкі \"{0}\"",
@ -662,6 +675,7 @@
"MessageTaskOpmlImportFeedPodcastExists": "Падкаст ужо існуе па гэтым шляху",
"MessageTaskOpmlImportFeedPodcastFailed": "Не ўдалося стварыць падкаст",
"MessageTaskOpmlParseNoneFound": "У OPML-файле не знойдзена стужак",
"MessageTaskTargetDirectoryNotWritable": "Мэтавы каталог недаступны для запісу",
"NoteChapterEditorTimes": "Заўвага: Час пачатку першага раздзела павінен заставацца 0:00, а час пачатку апошняга раздзела не можа перавышаць працягласць гэтай аўдыякнігі.",
"NoteRSSFeedPodcastAppsHttps": "Папярэджанне: большасць праграм для падкастаў патрабуюць, каб URL RSS-стужкі выкарыстоўваў HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Папярэджанне: адзін ці больш вашых эпізодаў не маюць даты публікацыі. Некаторыя праграмы для падкастаў патрабуюць гэтага.",
@ -718,6 +732,7 @@
"ToastSendEbookToDeviceFailed": "Не ўдалося адправіць электронную кнігу на прыладу",
"ToastSendEbookToDeviceSuccess": "Электронная кніга адпраўлена на прыладу \"{0}\"",
"ToastSleepTimerDone": "Таймер сну скончыўся... Хр-р-р",
"ToastUploaderItemExistsInSubdirectoryError": "Элемент \"{0}\" выкарыстоўвае падкаталог шляху загрузкі.",
"ToastUserPasswordMustChange": "Новы пароль не можа супадаць са старым",
"ToastUserRootRequireName": "Неабходна ўвесці імя карыстальніка адміністратара"
}

View File

@ -199,6 +199,7 @@
"HeaderSettingsExperimental": "Experimentelle Funktionen",
"HeaderSettingsGeneral": "Allgemein",
"HeaderSettingsScanner": "Scanner",
"HeaderSettingsSecurity": "Sicherheit",
"HeaderSettingsWebClient": "Web-Client",
"HeaderSleepTimer": "Sleep-Timer",
"HeaderStatsLargestItems": "Größte Medien",
@ -293,6 +294,7 @@
"LabelContinueListening": "Weiterhören",
"LabelContinueReading": "Weiterlesen",
"LabelContinueSeries": "Serien fortsetzen",
"LabelCorsAllowed": "Erlaubte CORS Quellen",
"LabelCover": "Titelbild",
"LabelCoverImageURL": "URL des Titelbildes",
"LabelCoverProvider": "Titelbildanbieter",
@ -418,6 +420,7 @@
"LabelLanguages": "Sprachen",
"LabelLastBookAdded": "Zuletzt hinzugefügtes Buch",
"LabelLastBookUpdated": "Zuletzt aktualisiertes Buch",
"LabelLastProgressDate": "Letzter Fortschritt: {0}",
"LabelLastSeen": "Zuletzt gesehen",
"LabelLastTime": "Letztes Mal",
"LabelLastUpdate": "Letzte Aktualisierung",
@ -430,6 +433,7 @@
"LabelLibraryFilterSublistEmpty": "Keine {0}",
"LabelLibraryItem": "Bibliothekseintrag",
"LabelLibraryName": "Bibliotheksname",
"LabelLibrarySortByProgress": "Fortschritt aktualisiert",
"LabelLimit": "Begrenzung",
"LabelLineSpacing": "Zeilenabstand",
"LabelListenAgain": "Erneut anhören",
@ -529,7 +533,7 @@
"LabelPublishers": "Herausgeber",
"LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail",
"LabelRSSFeedCustomOwnerName": "Benutzerdefinierter Name des Eigentümers",
"LabelRSSFeedOpen": "RSS Feed offen",
"LabelRSSFeedOpen": "RSS-Feed offen",
"LabelRSSFeedPreventIndexing": "Indizierung verhindern",
"LabelRSSFeedSlug": "RSS-Feed-Schlagwort",
"LabelRSSFeedURL": "RSS-Feed-URL",
@ -803,6 +807,7 @@
"MessageFeedURLWillBe": "Feed-URL wird {0} sein",
"MessageFetching": "Wird abgerufen …",
"MessageForceReScanDescription": "Durchsucht alle Dateien erneut, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu durchsucht.",
"MessageHeatmapNoListeningSessions": "Keine Hörsitzungen am {0}",
"MessageImportantNotice": "Wichtiger Hinweis!",
"MessageInsertChapterBelow": "Kapitel unten einfügen",
"MessageInvalidAsin": "Ungültige ASIN",
@ -1030,6 +1035,7 @@
"ToastInvalidImageUrl": "Ungültiger Bild URL",
"ToastInvalidMaxEpisodesToDownload": "Ungültige Max. Anzahl an Episoden zum Herunterladen",
"ToastInvalidUrl": "Ungültiger URL",
"ToastInvalidUrls": "Eine oder mehrere URLs sind in einem falschen Format",
"ToastItemCoverUpdateSuccess": "Titelbild aktualisiert",
"ToastItemDeletedFailed": "Fehler beim löschen des Artikels",
"ToastItemDeletedSuccess": "Artikel gelöscht",

View File

@ -127,6 +127,7 @@
"HeaderAudiobookTools": "Audiobook File Management Tools",
"HeaderAuthentication": "Authentication",
"HeaderBackups": "Backups",
"HeaderBulkChapterModal": "Add Multiple Chapters",
"HeaderChangePassword": "Change Password",
"HeaderChapters": "Chapters",
"HeaderChooseAFolder": "Choose a Folder",
@ -199,6 +200,7 @@
"HeaderSettingsExperimental": "Experimental Features",
"HeaderSettingsGeneral": "General",
"HeaderSettingsScanner": "Scanner",
"HeaderSettingsSecurity": "Security",
"HeaderSettingsWebClient": "Web Client",
"HeaderSleepTimer": "Sleep Timer",
"HeaderStatsLargestItems": "Largest Items",
@ -293,6 +295,7 @@
"LabelContinueListening": "Continue Listening",
"LabelContinueReading": "Continue Reading",
"LabelContinueSeries": "Continue Series",
"LabelCorsAllowed": "Allowed CORS Origins",
"LabelCover": "Cover",
"LabelCoverImageURL": "Cover Image URL",
"LabelCoverProvider": "Cover Provider",
@ -306,6 +309,7 @@
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
"LabelDescription": "Description",
"LabelDeselectAll": "Deselect All",
"LabelDetectedPattern": "Detected pattern:",
"LabelDevice": "Device",
"LabelDeviceInfo": "Device Info",
"LabelDeviceIsAvailableTo": "Device is available to...",
@ -470,6 +474,7 @@
"LabelNewestAuthors": "Newest Authors",
"LabelNewestEpisodes": "Newest Episodes",
"LabelNextBackupDate": "Next backup date",
"LabelNextChapters": "Next chapters will be:",
"LabelNextScheduledRun": "Next scheduled run",
"LabelNoApiKeys": "No API keys",
"LabelNoCustomMetadataProviders": "No custom metadata providers",
@ -487,6 +492,7 @@
"LabelNotificationsMaxQueueSize": "Max queue size for notification events",
"LabelNotificationsMaxQueueSizeHelp": "Events are limited to firing 1 per second. Events will be ignored if the queue is at max size. This prevents notification spamming.",
"LabelNumberOfBooks": "Number of Books",
"LabelNumberOfChapters": "Number of chapters:",
"LabelNumberOfEpisodes": "# of Episodes",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
@ -743,6 +749,7 @@
"MessageBookshelfNoResultsForFilter": "No results for filter \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "No results for query",
"MessageBookshelfNoSeries": "You have no series",
"MessageBulkChapterPattern": "How many chapters would you like to add with this numbering pattern?",
"MessageChapterEndIsAfter": "Chapter end is after the end of your audiobook",
"MessageChapterErrorFirstNotZero": "First chapter must start at 0",
"MessageChapterErrorStartGteDuration": "Invalid start time must be less than audiobook duration",
@ -946,6 +953,7 @@
"NotificationOnRSSFeedDisabledDescription": "Triggered when automatic episode downloads are disabled due to too many failed attempts",
"NotificationOnRSSFeedFailedDescription": "Triggered when the RSS feed request fails for an automatic episode download",
"NotificationOnTestDescription": "Event for testing the notification system",
"PlaceholderBulkChapterInput": "Enter chapter title or use numbering (e.g., 'Episode 1', 'Chapter 10', '1.')",
"PlaceholderNewCollection": "New collection name",
"PlaceholderNewFolderPath": "New folder path",
"PlaceholderNewPlaylist": "New playlist name",
@ -999,8 +1007,12 @@
"ToastBookmarkCreateFailed": "Failed to create bookmark",
"ToastBookmarkCreateSuccess": "Bookmark added",
"ToastBookmarkRemoveSuccess": "Bookmark removed",
"ToastBulkChapterInvalidCount": "Enter a number between 1 and 150",
"ToastCachePurgeFailed": "Failed to purge cache",
"ToastCachePurgeSuccess": "Cache purged successfully",
"ToastChapterLocked": "Chapter is locked.",
"ToastChapterStartTimeAdjusted": "Chapter start time adjusted by {0} seconds",
"ToastChaptersAllLocked": "All chapters are locked. Unlock some chapters to shift their times.",
"ToastChaptersHaveErrors": "Chapters have errors",
"ToastChaptersInvalidShiftAmountLast": "Invalid shift amount. The last chapter start time would extend beyond the duration of this audiobook.",
"ToastChaptersInvalidShiftAmountStart": "Invalid shift amount. The first chapter would have zero or negative length and would be overwritten by the second chapter. Increase the start duration of second chapter.",
@ -1034,6 +1046,7 @@
"ToastInvalidImageUrl": "Invalid image URL",
"ToastInvalidMaxEpisodesToDownload": "Invalid max episodes to download",
"ToastInvalidUrl": "Invalid URL",
"ToastInvalidUrls": "One or more URLs are invalid",
"ToastItemCoverUpdateSuccess": "Item cover updated",
"ToastItemDeletedFailed": "Failed to delete item",
"ToastItemDeletedSuccess": "Deleted item",
@ -1133,5 +1146,13 @@
"ToastUserPasswordChangeSuccess": "Password changed successfully",
"ToastUserPasswordMismatch": "Passwords do not match",
"ToastUserPasswordMustChange": "New password cannot match old password",
"ToastUserRootRequireName": "Must enter a root username"
"ToastUserRootRequireName": "Must enter a root username",
"TooltipAddChapters": "Add chapter(s)",
"TooltipAddOneSecond": "Add 1 second",
"TooltipAdjustChapterStart": "Click to adjust start time",
"TooltipLockAllChapters": "Lock all chapters",
"TooltipLockChapter": "Lock chapter (Shift+click for range)",
"TooltipSubtractOneSecond": "Subtract 1 second",
"TooltipUnlockAllChapters": "Unlock all chapters",
"TooltipUnlockChapter": "Unlock chapter (Shift+click for range)"
}

View File

@ -124,6 +124,7 @@
"HeaderAudiobookTools": "Herramientas de Gestión de Archivos de Audiolibro",
"HeaderAuthentication": "Autenticación",
"HeaderBackups": "Respaldos",
"HeaderBulkChapterModal": "Añadir Múltiples Capítulos",
"HeaderChangePassword": "Cambiar contraseña",
"HeaderChapters": "Capítulos",
"HeaderChooseAFolder": "Escoger una Carpeta",
@ -297,6 +298,7 @@
"LabelDeleteFromFileSystemCheckbox": "Eliminar del sistema de archivos (desmarque para quitar de la base de datos solamente)",
"LabelDescription": "Descripción",
"LabelDeselectAll": "Deseleccionar Todos",
"LabelDetectedPattern": "Patrón detectado:",
"LabelDevice": "Dispositivo",
"LabelDeviceInfo": "Información del dispositivo",
"LabelDeviceIsAvailableTo": "El dispositivo está disponible para...",
@ -454,6 +456,7 @@
"LabelNewestAuthors": "Autores más nuevos",
"LabelNewestEpisodes": "Episodios más nuevos",
"LabelNextBackupDate": "Fecha del siguiente respaldo",
"LabelNextChapters": "Los próximos capítulos serán:",
"LabelNextScheduledRun": "Próxima ejecución programada",
"LabelNoCustomMetadataProviders": "Sin proveedores de metadatos personalizados",
"LabelNoEpisodesSelected": "Ningún Episodio Seleccionado",
@ -470,6 +473,7 @@
"LabelNotificationsMaxQueueSize": "Tamaño máximo de la cola de notificaciones",
"LabelNotificationsMaxQueueSizeHelp": "Las notificaciones están limitadas a 1 por segundo. Las notificaciones serán ignoradas si llegan al numero máximo de cola para prevenir spam de eventos.",
"LabelNumberOfBooks": "Número de libros",
"LabelNumberOfChapters": "Número de capítulos:",
"LabelNumberOfEpisodes": "N.º de episodios",
"LabelOpenIDAdvancedPermsClaimDescription": "Nombre de la notificación de OpenID que contiene permisos avanzados para acciones de usuario dentro de la aplicación que se aplicarán a roles que no sean de administrador (<b>si están configurados</b>). Si el reclamo no aparece en la respuesta, se denegará el acceso a ABS. Si falta una sola opción, se tratará como <code>falsa</code>. Asegúrese de que la notificación del proveedor de identidades coincida con la estructura esperada:",
"LabelOpenIDClaims": "Deje las siguientes opciones vacías para desactivar la asignación avanzada de grupos y permisos, lo que asignaría de manera automática al grupo «Usuario».",
@ -722,6 +726,7 @@
"MessageBookshelfNoResultsForFilter": "El filtro «{0}: {1}» no produjo ningún resultado",
"MessageBookshelfNoResultsForQuery": "No hay resultados para la consulta",
"MessageBookshelfNoSeries": "No tiene ninguna serie",
"MessageBulkChapterPattern": "¿Cuántos capítulos desea añadir con este patrón de numeración?",
"MessageChapterEndIsAfter": "El final del capítulo es después del final de tu audiolibro",
"MessageChapterErrorFirstNotZero": "El primer capítulo debe iniciar en 0",
"MessageChapterErrorStartGteDuration": "El tiempo de inicio no es válido: debe ser inferior a la duración del audiolibro",
@ -919,6 +924,7 @@
"NotificationOnBackupFailedDescription": "Se activa cuando falla una copia de seguridad",
"NotificationOnEpisodeDownloadedDescription": "Se activa cuando se descarga automáticamente un episodio de un podcast",
"NotificationOnTestDescription": "Evento para probar el sistema de notificaciones",
"PlaceholderBulkChapterInput": "Ingrese título de capítulo o use numeración (ej. 'Episodio 1', 'Capítulo 10', '1.')",
"PlaceholderNewCollection": "Nuevo nombre de la colección",
"PlaceholderNewFolderPath": "Nueva ruta de carpeta",
"PlaceholderNewPlaylist": "Nuevo nombre de la lista de reproducción",
@ -972,8 +978,10 @@
"ToastBookmarkCreateFailed": "No se pudo crear el marcador",
"ToastBookmarkCreateSuccess": "Marcador añadido",
"ToastBookmarkRemoveSuccess": "Marcador eliminado",
"ToastBulkChapterInvalidCount": "Por favor ingrese un número válido entre 1 y 150",
"ToastCachePurgeFailed": "No se pudo purgar la antememoria",
"ToastCachePurgeSuccess": "Se purgó la antememoria correctamente",
"ToastChaptersAllLocked": "Todos los capítulos están bloqueados. Desbloquee algunos capítulos para cambiar sus tiempos.",
"ToastChaptersHaveErrors": "Los capítulos tienen errores",
"ToastChaptersInvalidShiftAmountLast": "Cantidad de desplazamiento no válida. La hora de inicio del último capítulo se extendería más allá de la duración de este audiolibro.",
"ToastChaptersInvalidShiftAmountStart": "Cantidad de desplazamiento no válida. El primer capítulo tendría una duración cero o negativa y lo sobrescribiría el segundo capítulo. Aumente la duración inicial del segundo capítulo.",
@ -1103,5 +1111,12 @@
"ToastUserPasswordChangeSuccess": "Contraseña modificada correctamente",
"ToastUserPasswordMismatch": "No coinciden las contraseñas",
"ToastUserPasswordMustChange": "La nueva contraseña no puede ser igual que la anterior",
"ToastUserRootRequireName": "Debe introducir un nombre de usuario administrativo"
"ToastUserRootRequireName": "Debe introducir un nombre de usuario administrativo",
"TooltipAddChapters": "Añadir capítulo(s)",
"TooltipAddOneSecond": "Añadir 1 segundo",
"TooltipLockAllChapters": "Bloquear todos los capítulos",
"TooltipLockChapter": "Bloquear capítulo (Mayús+clic para rango)",
"TooltipSubtractOneSecond": "Restar 1 segundo",
"TooltipUnlockAllChapters": "Desbloquear todos los capítulos",
"TooltipUnlockChapter": "Desbloquear capítulo (Mayús+clic para rango)"
}

View File

@ -11,7 +11,7 @@
"ButtonAuthors": "Autorid",
"ButtonBack": "Tagasi",
"ButtonBrowseForFolder": "Sirvi kausta",
"ButtonCancel": "Tühista",
"ButtonCancel": "Katkesta",
"ButtonCancelEncode": "Tühista kodeerimine",
"ButtonChangeRootPassword": "Muuda põhiparooli",
"ButtonCheckAndDownloadNewEpisodes": "Kontrolli ja laadi alla uued episoodid",
@ -20,9 +20,9 @@
"ButtonClearFilter": "Tühista filter",
"ButtonCloseFeed": "Sulge voog",
"ButtonCloseSession": "Sulge avatud sessioon",
"ButtonCollections": "Kogud",
"ButtonCollections": "Kollektsioonid",
"ButtonConfigureScanner": "Konfigureeri skanner",
"ButtonCreate": "Loo",
"ButtonCreate": "Loo uus",
"ButtonCreateBackup": "Loo varundus",
"ButtonDelete": "Kustuta",
"ButtonDownloadQueue": "Järjekord",
@ -37,7 +37,7 @@
"ButtonIssues": "Probleemid",
"ButtonJumpBackward": "Hüppa tagasi",
"ButtonJumpForward": "Hüppa edasi",
"ButtonLatest": "Uusim",
"ButtonLatest": "Viimased",
"ButtonLibrary": "Raamatukogu",
"ButtonLogout": "Logi välja",
"ButtonLookup": "Otsi",
@ -52,11 +52,11 @@
"ButtonOk": "Ok",
"ButtonOpenFeed": "Ava voog",
"ButtonOpenManager": "Ava haldur",
"ButtonPause": "Peata",
"ButtonPlay": "Mängi",
"ButtonPause": "Paus",
"ButtonPlay": "Play",
"ButtonPlayAll": "Mängi kõik",
"ButtonPlaying": "Mängib",
"ButtonPlaylists": "Esitusloendid",
"ButtonPlaylists": "Playlist",
"ButtonPrevious": "Eelmine",
"ButtonPreviousChapter": "Eelmine peatükk",
"ButtonPurgeAllCache": "Tühjenda kogu vahemälu",
@ -69,7 +69,7 @@
"ButtonReadLess": "Loe vähem",
"ButtonReadMore": "Loe rohkem",
"ButtonRefresh": "Värskenda",
"ButtonRemove": "Eemalda",
"ButtonRemove": "Kustuta",
"ButtonRemoveAll": "Eemalda kõik",
"ButtonRemoveAllLibraryItems": "Eemalda kõik raamatukogu esemed",
"ButtonRemoveFromContinueListening": "Eemalda jätkake kuulamisest",
@ -120,12 +120,12 @@
"HeaderCustomMetadataProviders": "Kohandatud metaandmete pakkujad",
"HeaderDetails": "Detailid",
"HeaderDownloadQueue": "Allalaadimise järjekord",
"HeaderEbookFiles": "E-raamatute failid",
"HeaderEbookFiles": "E-raamatu failid",
"HeaderEmail": "E-post",
"HeaderEmailSettings": "E-posti seaded",
"HeaderEpisodes": "Episoodid",
"HeaderEreaderDevices": "E-lugerite seadmed",
"HeaderEreaderSettings": "E-lugerite seadistused",
"HeaderEreaderSettings": "E-lugeja sätted",
"HeaderFiles": "Failid",
"HeaderFindChapters": "Leia peatükid",
"HeaderIgnoredFiles": "Ignoreeritud failid",
@ -155,8 +155,8 @@
"HeaderPasswordAuthentication": "Parooli autentimine",
"HeaderPermissions": "Õigused",
"HeaderPlayerQueue": "Mängija järjekord",
"HeaderPlaylist": "Mänguloend",
"HeaderPlaylistItems": "Mänguloendi esemed",
"HeaderPlaylist": "Playlist",
"HeaderPlaylistItems": "Playlisti esemed",
"HeaderPodcastsToAdd": "Lisatavad podcastid",
"HeaderPreviewCover": "Eelvaate kaas",
"HeaderRSSFeedGeneral": "RSS-i üksikasjad",
@ -174,7 +174,7 @@
"HeaderSettingsExperimental": "Katsetusfunktsioonid",
"HeaderSettingsGeneral": "Üldised",
"HeaderSettingsScanner": "Skanner",
"HeaderSleepTimer": "Uinaku taimer",
"HeaderSleepTimer": "Unetaimer",
"HeaderStatsLargestItems": "Suurimad esemed",
"HeaderStatsLongestItems": "Kõige pikemad esemed (tunnid)",
"HeaderStatsMinutesListeningChart": "Kuulamise minutid (viimased 7 päeva)",
@ -197,9 +197,10 @@
"LabelActivity": "Tegevus",
"LabelAddToCollection": "Lisa kogusse",
"LabelAddToCollectionBatch": "Lisa {0} raamatut kogusse",
"LabelAddToPlaylist": "Lisa mänguloendisse",
"LabelAddToPlaylist": "Lisa playlisti",
"LabelAddToPlaylistBatch": "Lisa {0} eset mänguloendisse",
"LabelAddedAt": "Lisatud",
"LabelAddedDate": "Lisatud {0}",
"LabelAdminUsersOnly": "Ainult administraatorid",
"LabelAll": "Kõik",
"LabelAllUsers": "Kõik kasutajad",
@ -208,10 +209,10 @@
"LabelAlreadyInYourLibrary": "Juba teie raamatukogus",
"LabelAppend": "Lisa",
"LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Eesnimi Perekonnanimi)",
"LabelAuthorLastFirst": "Autor (Perekonnanimi, Eesnimi)",
"LabelAuthorFirstLast": "Autor (eesnimi perekonnanimi)",
"LabelAuthorLastFirst": "Autor (perekonnanimi, eesnimi)",
"LabelAuthors": "Autorid",
"LabelAutoDownloadEpisodes": "Automaatne episoodide allalaadimine",
"LabelAutoDownloadEpisodes": "Episoodide automaatne allalaadimine",
"LabelAutoFetchMetadata": "Automaatne metaandmete hankimine",
"LabelAutoFetchMetadataHelp": "Toob tiitli, autori ja seeria metaandmed üleslaadimise hõlbustamiseks. Lisametaandmed võivad pärast üleslaadimist vajada vastavust.",
"LabelAutoLaunch": "Automaatne käivitamine",
@ -265,7 +266,7 @@
"LabelDiscover": "Avasta",
"LabelDownload": "Lae alla",
"LabelDownloadNEpisodes": "Lae alla {0} episoodi",
"LabelDuration": "Kestus",
"LabelDuration": "Kestvus",
"LabelDurationFound": "Leitud kestus:",
"LabelEbook": "E-raamat",
"LabelEbooks": "E-raamatud",
@ -278,6 +279,7 @@
"LabelEmbeddedCover": "Manustatud kaas",
"LabelEnable": "Luba",
"LabelEnd": "Lõpp",
"LabelEndOfChapter": "Peatükki lõpp",
"LabelEpisode": "Episood",
"LabelEpisodeTitle": "Episoodi pealkiri",
"LabelEpisodeType": "Episoodi tüüp",
@ -288,13 +290,14 @@
"LabelFile": "Fail",
"LabelFileBirthtime": "Faili sünniaeg",
"LabelFileModified": "Faili muudetud",
"LabelFilename": "Failinimi",
"LabelFilename": "Faili nimi",
"LabelFilterByUser": "Filtri alusel kasutaja järgi",
"LabelFindEpisodes": "Otsi episoodid",
"LabelFinished": "Lõpetatud",
"LabelFolder": "Kaust",
"LabelFolders": "Kataloogid",
"LabelFontBold": "Paks",
"LabelFontBoldness": "Fondi paksus",
"LabelFontFamily": "Fondi pere",
"LabelFontItalic": "Kaldkiri",
"LabelFontScale": "Fondi suurus",
@ -303,7 +306,7 @@
"LabelGenre": "Žanr",
"LabelGenres": "Žanrid",
"LabelHardDeleteFile": "Faili lõplik kustutamine",
"LabelHasEbook": "On e-raamat",
"LabelHasEbook": "E-raamat olemas",
"LabelHasSupplementaryEbook": "On täiendav e-raamat",
"LabelHighestPriority": "Kõrgeim prioriteet",
"LabelHour": "Tund",
@ -311,7 +314,7 @@
"LabelImageURLFromTheWeb": "Pildi URL veebist",
"LabelInProgress": "Pooleli",
"LabelIncludeInTracklist": "Kaasa jälgimisloendis",
"LabelIncomplete": "Puudulik",
"LabelIncomplete": "Lõpetamata",
"LabelInterval": "Intervall",
"LabelIntervalCustomDailyWeekly": "Kohandatud päevane/nädalane",
"LabelIntervalEvery12Hours": "Iga 12 tunni tagant",
@ -365,12 +368,12 @@
"LabelNarrators": "Jutustajad",
"LabelNew": "Uus",
"LabelNewPassword": "Uus parool",
"LabelNewestAuthors": "Uusimad autorid",
"LabelNewestEpisodes": "Uusimad episoodid",
"LabelNewestAuthors": "Uuemad autorid",
"LabelNewestEpisodes": "Uuemad episoodid",
"LabelNextBackupDate": "Järgmine varukoopia kuupäev",
"LabelNextScheduledRun": "Järgmine ajakava järgmine",
"LabelNoEpisodesSelected": "Episoodid pole valitud",
"LabelNotFinished": "Ei ole lõpetatud",
"LabelNotFinished": "Lõpetamata",
"LabelNotStarted": "Pole alustatud",
"LabelNotes": "Märkused",
"LabelNotificationAppriseURL": "Apprise URL-id",
@ -383,7 +386,7 @@
"LabelNotificationsMaxQueueSize": "Teavituste sündmuste maksimaalne järjekorra suurus",
"LabelNotificationsMaxQueueSizeHelp": "Sündmused on piiratud 1 sekundiga. Sündmusi ignoreeritakse, kui järjekord on maksimumsuuruses. See takistab teavituste rämpsposti.",
"LabelNumberOfBooks": "Raamatute arv",
"LabelNumberOfEpisodes": "Episoodide arv",
"LabelNumberOfEpisodes": "# episoode",
"LabelOpenRSSFeed": "Ava RSS voog",
"LabelOverwrite": "Kirjuta üle",
"LabelPassword": "Parool",
@ -398,16 +401,18 @@
"LabelPhotoPathURL": "Foto tee/URL",
"LabelPlayMethod": "Esitusmeetod",
"LabelPlaylists": "Mänguloendid",
"LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "Podcasti otsingu piirkond",
"LabelPodcastType": "Podcasti tüüp",
"LabelPodcasts": "Podcastid",
"LabelPrefixesToIgnore": "Eiramiseks eesliited (tõstutundetu)",
"LabelPreventIndexing": "Vältige oma voogu indekseerimist iTunes'i ja Google podcasti kataloogides",
"LabelPrimaryEbook": "Esmane e-raamat",
"LabelProgress": "Edenemine",
"LabelProgress": "Progress",
"LabelProvider": "Pakkuja",
"LabelPubDate": "Avaldamise kuupäev",
"LabelPublishYear": "Aasta avaldamine",
"LabelPubDate": "Publitseerimise kuupäev",
"LabelPublishYear": "Publitseerimise aasta",
"LabelPublishedDate": "Publitseeritud {0}",
"LabelPublisher": "Kirjastaja",
"LabelRSSFeedCustomOwnerEmail": "Kohandatud omaniku e-post",
"LabelRSSFeedCustomOwnerName": "Kohandatud omaniku nimi",
@ -415,7 +420,8 @@
"LabelRSSFeedPreventIndexing": "Vältige indekseerimist",
"LabelRSSFeedSlug": "RSS voog Slug",
"LabelRSSFeedURL": "RSS voog URL",
"LabelRead": "Lugenud",
"LabelRandomly": "Juhuslikus järjekorras",
"LabelRead": "Loetud läbi",
"LabelReadAgain": "Loe uuesti",
"LabelReadEbookWithoutProgress": "Lugege e-raamatut ilma edenemist säilitamata",
"LabelRecentSeries": "Hiljutised seeriad",
@ -469,9 +475,9 @@
"LabelSettingsStoreMetadataWithItem": "Salvesta metaandmed üksusega",
"LabelSettingsStoreMetadataWithItemHelp": "Vaikimisi salvestatakse metaandmed /metadata/items kausta. Selle seadistuse lubamine salvestab metaandmed teie raamatukogu üksuse kaustadesse",
"LabelSettingsTimeFormat": "Kellaaja vorming",
"LabelShowAll": "Näita kõiki",
"LabelShowAll": "Näita kõik",
"LabelSize": "Suurus",
"LabelSleepTimer": "Uinaku taimer",
"LabelSleepTimer": "Unetaimer",
"LabelStart": "Alusta",
"LabelStartTime": "Alustamise aeg",
"LabelStarted": "Alustatud",
@ -480,13 +486,13 @@
"LabelStatsAuthors": "Autorid",
"LabelStatsBestDay": "Parim päev",
"LabelStatsDailyAverage": "Päevane keskmine",
"LabelStatsDays": "Päevad",
"LabelStatsDays": "Päevi",
"LabelStatsDaysListened": "Kuulatud päevad",
"LabelStatsHours": "Tunnid",
"LabelStatsInARow": "järjest",
"LabelStatsItemsFinished": "Lõpetatud üksused",
"LabelStatsItemsInLibrary": "Üksused raamatukogus",
"LabelStatsMinutes": "minutit",
"LabelStatsMinutes": "minuteid",
"LabelStatsMinutesListening": "Kuulamise minutid",
"LabelStatsOverallDays": "Kokku päevad",
"LabelStatsOverallHours": "Kokku tunnid",
@ -502,7 +508,7 @@
"LabelTextEditorNumberedList": "Numberloend",
"LabelTextEditorUnlink": "Eemalda link",
"LabelTheme": "Teema",
"LabelThemeDark": "Tume",
"LabelThemeDark": "Pime",
"LabelThemeLight": "Hele",
"LabelTimeBase": "Aja alus",
"LabelTimeListened": "Kuulatud aeg",
@ -527,7 +533,7 @@
"LabelType": "Tüüp",
"LabelUnabridged": "Täismahus",
"LabelUndo": "Võta tagasi",
"LabelUnknown": "Tundmatu",
"LabelUnknown": "Teadmata",
"LabelUpdateCover": "Uuenda kaant",
"LabelUpdateCoverHelp": "Luba üle kirjutamine olemasolevate kaante jaoks valitud raamatutele, kui leitakse sobivus",
"LabelUpdateDetails": "Uuenda üksikasju",

View File

@ -127,6 +127,7 @@
"HeaderAudiobookTools": "Outils de gestion de fichiers de livres audio",
"HeaderAuthentication": "Authentification",
"HeaderBackups": "Sauvegardes",
"HeaderBulkChapterModal": "Ajouter Plusieurs Chapitres",
"HeaderChangePassword": "Modifier le mot de passe",
"HeaderChapters": "Chapitres",
"HeaderChooseAFolder": "Sélectionner un dossier",
@ -199,6 +200,7 @@
"HeaderSettingsExperimental": "Fonctionnalités expérimentales",
"HeaderSettingsGeneral": "Général",
"HeaderSettingsScanner": "Analyseur",
"HeaderSettingsSecurity": "Sécurité",
"HeaderSettingsWebClient": "Client Web",
"HeaderSleepTimer": "Minuterie",
"HeaderStatsLargestItems": "Éléments les plus grands",
@ -206,7 +208,7 @@
"HeaderStatsMinutesListeningChart": "Minutes découte (7 derniers jours)",
"HeaderStatsRecentSessions": "Sessions récentes",
"HeaderStatsTop10Authors": "Top 10 Auteurs",
"HeaderStatsTop5Genres": "Top 5 Genres",
"HeaderStatsTop5Genres": "Top 5 des genres",
"HeaderTableOfContents": "Table des matières",
"HeaderTools": "Outils",
"HeaderUpdateAccount": "Mettre à jour le compte",
@ -293,6 +295,7 @@
"LabelContinueListening": "Continuer l'écoute",
"LabelContinueReading": "Continuer la lecture",
"LabelContinueSeries": "Continuer les séries",
"LabelCorsAllowed": "Origines autorisées pour les requêtes CORS",
"LabelCover": "Couverture",
"LabelCoverImageURL": "URL vers limage de couverture",
"LabelCoverProvider": "Source des couvertures",
@ -306,6 +309,7 @@
"LabelDeleteFromFileSystemCheckbox": "Supprimer du système de fichiers (décocher pour ne supprimer que de la base de données)",
"LabelDescription": "Description",
"LabelDeselectAll": "Tout déselectionner",
"LabelDetectedPattern": "Motif détecté :",
"LabelDevice": "Appareil",
"LabelDeviceInfo": "Détail de lappareil",
"LabelDeviceIsAvailableTo": "Lappareil est disponible pour…",
@ -359,7 +363,7 @@
"LabelExpiresAt": "Expire à",
"LabelExpiresInSeconds": "Expire dans (secondes)",
"LabelExpiresNever": "Jamais",
"LabelExplicit": "Restriction",
"LabelExplicit": "Contenu explicite",
"LabelExplicitChecked": "Explicite (vérifié)",
"LabelExplicitUnchecked": "Non explicite (non vérifié)",
"LabelExportOPML": "Exporter OPML",
@ -418,6 +422,7 @@
"LabelLanguages": "Langues",
"LabelLastBookAdded": "Dernier livre ajouté",
"LabelLastBookUpdated": "Dernier livre mis à jour",
"LabelLastProgressDate": "Dernière position : {0}",
"LabelLastSeen": "Vu dernièrement",
"LabelLastTime": "Progression",
"LabelLastUpdate": "Dernière mise à jour",
@ -430,14 +435,16 @@
"LabelLibraryFilterSublistEmpty": "Aucun {0}",
"LabelLibraryItem": "Élément de bibliothèque",
"LabelLibraryName": "Nom de la bibliothèque",
"LabelLibrarySortByProgress": "Progression mise à jour",
"LabelLimit": "Limite",
"LabelLineSpacing": "Espacement des lignes",
"LabelListenAgain": "Écouter à nouveau",
"LabelLogLevelDebug": "Débogage",
"LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Warn",
"LabelLogLevelWarn": "Attention",
"LabelLookForNewEpisodesAfterDate": "Rechercher les nouveaux épisodes après cette date",
"LabelLowestPriority": "Priorité la plus basse",
"LabelMatchConfidence": "Confiance",
"LabelMatchExistingUsersBy": "Correspondance avec les utilisateurs existants",
"LabelMatchExistingUsersByDescription": "Utilisé pour connecter les utilisateurs existants. Une fois connectés, les utilisateurs seront associés à un identifiant unique provenant de votre fournisseur SSO",
"LabelMaxEpisodesToDownload": "Nombre maximum dépisodes à télécharger. 0 pour illimité.",
@ -467,6 +474,7 @@
"LabelNewestAuthors": "Auteurs récents",
"LabelNewestEpisodes": "Épisodes récents",
"LabelNextBackupDate": "Date de la prochaine sauvegarde",
"LabelNextChapters": "Les prochains chapitres seront :",
"LabelNextScheduledRun": "Prochain lancement prévu",
"LabelNoApiKeys": "Aucune clé API",
"LabelNoCustomMetadataProviders": "Aucun fournisseurs de métadonnées personnalisés",
@ -484,6 +492,7 @@
"LabelNotificationsMaxQueueSize": "Nombres de notifications maximum à mettre en attente",
"LabelNotificationsMaxQueueSizeHelp": "La limite de notification est de un évènement par seconde. Les notifications seront ignorées si la file dattente est à son maximum. Cela empêche un flot trop important.",
"LabelNumberOfBooks": "Nombre de livres",
"LabelNumberOfChapters": "Nombre de chapitres :",
"LabelNumberOfEpisodes": "Nombre d'épisodes",
"LabelOpenIDAdvancedPermsClaimDescription": "Nom de la demande OpenID qui contient des autorisations avancées pour les actions de lutilisateur dans lapplication, qui sappliqueront à des rôles autres que celui dadministrateur (<b>sil est configuré</b>). Si la demande est absente de la réponse, laccès à ABS sera refusé. Si une seule option est manquante, elle sera considérée comme <code>false</code>. Assurez-vous que la demande du fournisseur didentité correspond à la structure attendue :",
"LabelOpenIDClaims": "Laissez les options suivantes vides pour désactiver lattribution avancée de groupes et dautorisations, en attribuant alors automatiquement le groupe « Utilisateur ».",
@ -614,7 +623,7 @@
"LabelShareOpen": "Ouvrir le partage",
"LabelShareURL": "Partager lURL",
"LabelShowAll": "Tout afficher",
"LabelShowSeconds": "Afficher les seondes",
"LabelShowSeconds": "Afficher les secondes",
"LabelShowSubtitles": "Afficher les sous-titres",
"LabelSize": "Taille",
"LabelSleepTimer": "Minuterie de mise en veille",
@ -655,6 +664,7 @@
"LabelTheme": "Thème",
"LabelThemeDark": "Sombre",
"LabelThemeLight": "Clair",
"LabelThemeSepia": "Sépia",
"LabelTimeBase": "Base de temps",
"LabelTimeDurationXHours": "{0} heures",
"LabelTimeDurationXMinutes": "{0} minutes",
@ -739,6 +749,7 @@
"MessageBookshelfNoResultsForFilter": "Aucun résultat pour le filtre « {0} : {1} »",
"MessageBookshelfNoResultsForQuery": "Aucun résultat pour la requête",
"MessageBookshelfNoSeries": "Vous navez aucune série",
"MessageBulkChapterPattern": "Combien de chapitres souhaitez-vous ajouter avec ce motif de numérotation ?",
"MessageChapterEndIsAfter": "La fin du chapitre se situe après la fin de votre livre audio",
"MessageChapterErrorFirstNotZero": "Le premier capitre doit débuter à 0",
"MessageChapterErrorStartGteDuration": "Horodatage invalide car il doit débuter avant la fin du livre",
@ -801,6 +812,8 @@
"MessageFeedURLWillBe": "LURL du flux sera {0}",
"MessageFetching": "Récupération…",
"MessageForceReScanDescription": "analysera de nouveau tous les fichiers. Les étiquettes ID3 des fichiers audio, les fichiers OPF et les fichiers texte seront analysés comme sils étaient nouveaux.",
"MessageHeatmapListeningTimeTooltip": "<strong>{0} À lécoute</strong> sur {1}",
"MessageHeatmapNoListeningSessions": "Aucune session en cours sur {0}",
"MessageImportantNotice": "Information importante !",
"MessageInsertChapterBelow": "Insérer le chapitre ci-dessous",
"MessageInvalidAsin": "ASIN invalide",
@ -837,7 +850,7 @@
"MessageNoItems": "Aucun élément",
"MessageNoItemsFound": "Aucun élément trouvé",
"MessageNoListeningSessions": "Aucune session découte en cours",
"MessageNoLogs": "Aucun journaux",
"MessageNoLogs": "Aucun journal",
"MessageNoMediaProgress": "Aucun média en cours",
"MessageNoNotifications": "Aucune notification",
"MessageNoPodcastFeed": "Podcast invalide : pas de flux",
@ -940,11 +953,12 @@
"NotificationOnRSSFeedDisabledDescription": "Déclenché lorsque les téléchargements automatiques dépisodes sont désactivés en raison dun trop grand nombre de tentatives infructueuses",
"NotificationOnRSSFeedFailedDescription": "Déclenché lorsque la demande de flux RSS échoue pour un téléchargement automatique dépisode",
"NotificationOnTestDescription": "Événement pour tester le système de notification",
"PlaceholderBulkChapterInput": "Entrez le titre du chapitre ou utilisez la numérotation (ex. 'Épisode 1', 'Chapitre 10', '1.')",
"PlaceholderNewCollection": "Nom de la nouvelle collection",
"PlaceholderNewFolderPath": "Nouveau chemin de dossier",
"PlaceholderNewPlaylist": "Nouveau nom de liste de lecture",
"PlaceholderSearch": "Recherche…",
"PlaceholderSearchEpisode": "Recherche dépisode…",
"PlaceholderSearchEpisode": "Rechercher un épisode…",
"StatsAuthorsAdded": "auteurs ajoutés",
"StatsBooksAdded": "livres ajoutés",
"StatsBooksAdditional": "Les ajouts comprennent…",
@ -993,8 +1007,10 @@
"ToastBookmarkCreateFailed": "Échec de la création de signet",
"ToastBookmarkCreateSuccess": "Signet ajouté",
"ToastBookmarkRemoveSuccess": "Signet supprimé",
"ToastBulkChapterInvalidCount": "Veuillez entrer un nombre valide entre 1 et 150",
"ToastCachePurgeFailed": "Échec de la purge du cache",
"ToastCachePurgeSuccess": "Cache purgé avec succès",
"ToastChaptersAllLocked": "Tous les chapitres sont verrouillés. Déverrouillez certains chapitres pour décaler leurs temps.",
"ToastChaptersHaveErrors": "Les chapitres contiennent des erreurs",
"ToastChaptersInvalidShiftAmountLast": "Durée de décalage non valide. Lheure de début du dernier chapitre pourrait dépasser la durée de ce livre audio.",
"ToastChaptersInvalidShiftAmountStart": "Durée de décalage non valide. Le premier chapitre aurait une longueur nulle ou négative et serait écrasé par le second. Augmentez la durée de début du second chapitre.",
@ -1028,6 +1044,7 @@
"ToastInvalidImageUrl": "URL de l'image invalide",
"ToastInvalidMaxEpisodesToDownload": "Nombre maximum dépisodes à télécharger non valide",
"ToastInvalidUrl": "URL invalide",
"ToastInvalidUrls": "Une ou plusieurs URL sont invalides",
"ToastItemCoverUpdateSuccess": "Couverture mise à jour",
"ToastItemDeletedFailed": "La suppression de l'élément à échouée",
"ToastItemDeletedSuccess": "Élément supprimé",
@ -1127,5 +1144,12 @@
"ToastUserPasswordChangeSuccess": "Mot de passe modifié avec succès",
"ToastUserPasswordMismatch": "Les mots de passe ne correspondent pas",
"ToastUserPasswordMustChange": "Le nouveau mot de passe ne peut pas être identique à lancien",
"ToastUserRootRequireName": "Vous devez entrer un nom dutilisateur root"
"ToastUserRootRequireName": "Vous devez entrer un nom dutilisateur root",
"TooltipAddChapters": "Ajouter chapitre(s)",
"TooltipAddOneSecond": "Ajouter 1 seconde",
"TooltipLockAllChapters": "Verrouiller tous les chapitres",
"TooltipLockChapter": "Verrouiller le chapitre (Maj+clic pour plage)",
"TooltipSubtractOneSecond": "Soustraire 1 seconde",
"TooltipUnlockAllChapters": "Déverrouiller tous les chapitres",
"TooltipUnlockChapter": "Déverrouiller le chapitre (Maj+clic pour plage)"
}

View File

@ -1,12 +1,18 @@
{
"ButtonAdd": "जोड़ें",
"ButtonAddApiKey": "एपीआई कुंजी जोड़ें",
"ButtonAddChapters": "अध्याय जोड़ें",
"ButtonAddDevice": "उपकरण जोड़ें",
"ButtonAddLibrary": "संग्रह जोड़ें",
"ButtonAddPodcasts": "पॉडकास्ट जोड़ें",
"ButtonAddUser": "उपयोगकर्ता जोड़ें",
"ButtonAddYourFirstLibrary": "अपनी पहली पुस्तकालय जोड़ें",
"ButtonApply": "लागू करें",
"ButtonApplyChapters": "अध्यायों में परिवर्तन लागू करें",
"ButtonAuthors": "लेखक",
"ButtonBack": "पीछे",
"ButtonBatchEditPopulateFromExisting": "मौजूदा से आबाद करें",
"ButtonBatchEditPopulateMapDetails": "मानचित्र विवरण भरें",
"ButtonBrowseForFolder": "फ़ोल्डर खोजें",
"ButtonCancel": "रद्द करें",
"ButtonCancelEncode": "एनकोड रद्द करें",
@ -15,7 +21,9 @@
"ButtonChooseAFolder": "एक फ़ोल्डर चुनें",
"ButtonChooseFiles": "फ़ाइलें चुनें",
"ButtonClearFilter": "लागू फ़िल्टर साफ़ करें",
"ButtonClose": "बंद करें",
"ButtonCloseFeed": "फ़ीड बंद करें",
"ButtonCloseSession": "वर्तमान सत्र बंद करें",
"ButtonCollections": "संग्रह",
"ButtonConfigureScanner": "स्कैनर सेटिंग्स बदलें",
"ButtonCreate": "बनाएं",
@ -25,6 +33,7 @@
"ButtonEdit": "संपादित करें",
"ButtonEditChapters": "अध्याय संपादित करें",
"ButtonEditPodcast": "पॉडकास्ट संपादित करें",
"ButtonEnable": "सक्षम करें",
"ButtonForceReScan": "बलपूर्वक पुन: स्कैन करें",
"ButtonFullPath": "पूर्ण पथ",
"ButtonHide": "छुपाएं",

View File

@ -199,6 +199,7 @@
"HeaderSettingsExperimental": "Eksperimentalne značajke",
"HeaderSettingsGeneral": "Općenito",
"HeaderSettingsScanner": "Skener",
"HeaderSettingsSecurity": "Sigurnost",
"HeaderSettingsWebClient": "Web klijent",
"HeaderSleepTimer": "Timer za spavanje",
"HeaderStatsLargestItems": "Najveće stavke",
@ -293,6 +294,7 @@
"LabelContinueListening": "Nastavi slušati",
"LabelContinueReading": "Nastavi čitati",
"LabelContinueSeries": "Nastavi serijal",
"LabelCorsAllowed": "Dozvoljena CORS ishodišta",
"LabelCover": "Naslovnica",
"LabelCoverImageURL": "URL naslovnice",
"LabelCoverProvider": "Pružatelj naslovnica",
@ -418,6 +420,7 @@
"LabelLanguages": "Jezici",
"LabelLastBookAdded": "Zadnja dodana knjiga",
"LabelLastBookUpdated": "Zadnja ažurirana knjiga",
"LabelLastProgressDate": "Zadnji napredak: {0}",
"LabelLastSeen": "Zadnji puta viđen",
"LabelLastTime": "Zadnje doslušano vrijeme",
"LabelLastUpdate": "Zadnje ažuriranje",
@ -430,6 +433,7 @@
"LabelLibraryFilterSublistEmpty": "Br {0}",
"LabelLibraryItem": "Stavka knjižnice",
"LabelLibraryName": "Ime knjižnice",
"LabelLibrarySortByProgress": "Napredak ažuriran",
"LabelLimit": "Ograničenje",
"LabelLineSpacing": "Razmak između redaka",
"LabelListenAgain": "Ponovno poslušaj",
@ -438,6 +442,7 @@
"LabelLogLevelWarn": "Warn",
"LabelLookForNewEpisodesAfterDate": "Traži nove nastavke nakon ovog datuma",
"LabelLowestPriority": "Najniži prioritet",
"LabelMatchConfidence": "Pouzdanost",
"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",
"LabelMaxEpisodesToDownload": "Najveći broj nastavaka za preuzimanje. 0 za neograničeno.",
@ -655,6 +660,7 @@
"LabelTheme": "Tema",
"LabelThemeDark": "Tamna",
"LabelThemeLight": "Svijetla",
"LabelThemeSepia": "Sepija",
"LabelTimeBase": "Baza vremena",
"LabelTimeDurationXHours": "{0} sati",
"LabelTimeDurationXMinutes": "{0} minuta",
@ -801,6 +807,8 @@
"MessageFeedURLWillBe": "URL izvora bit će {0}",
"MessageFetching": "Dohvaćam...",
"MessageForceReScanDescription": "će ponovno skenirati sve datoteke kao nove datoteke. ID3 tagovi zvučnih datoteka, OPF datoteke i tekstualne datoteke skenirat će se kao da su nove.",
"MessageHeatmapListeningTimeTooltip": "<strong>{0} sluša</strong> na {1}",
"MessageHeatmapNoListeningSessions": "Nema sesija slušanja na {0}",
"MessageImportantNotice": "Važna obavijest!",
"MessageInsertChapterBelow": "Unesi poglavlje ispod",
"MessageInvalidAsin": "Nevažeći ASIN",
@ -1028,6 +1036,7 @@
"ToastInvalidImageUrl": "Neispravan URL slike",
"ToastInvalidMaxEpisodesToDownload": "Neispravan unos maksimalnog broja nastavaka",
"ToastInvalidUrl": "Neispravan URL",
"ToastInvalidUrls": "Jedan ili više URL-ova nisu ispravni",
"ToastItemCoverUpdateSuccess": "Naslovnica stavke ažurirana",
"ToastItemDeletedFailed": "Brisanje stavke nije uspjelo",
"ToastItemDeletedSuccess": "Stavka je izbrisana",

View File

@ -1,15 +1,65 @@
{
"ButtonAdd": "追加",
"ButtonAddApiKey": "APIキーの追加",
"ButtonAddChapters": "チャプターの追加",
"ButtonAddDevice": "端末の追加",
"ButtonAddLibrary": "ライブラリーの追加",
"ButtonAddPodcasts": "ポッドキャストの追加",
"ButtonAddUser": "ユーザーの追加",
"ButtonAddYourFirstLibrary": "最初のライブラリーを追加",
"ButtonApply": "確定",
"ButtonApplyChapters": "チャプターを確定する",
"ButtonAuthors": "作者",
"ButtonBack": "戻る",
"ButtonCancel": "キャンセル",
"ButtonChangeRootPassword": "Rootのパスワードを変更する",
"ButtonChooseAFolder": "フォルダーを選ぶ",
"ButtonChooseFiles": "ファイルを選ぶ",
"ButtonClearFilter": "絞り込みを解除",
"ButtonClose": "閉じる",
"ButtonCollections": "コレクション",
"ButtonCreate": "作成",
"ButtonCreateBackup": "バックアップを作成する",
"ButtonDelete": "削除",
"ButtonDownloadQueue": "次に再生",
"ButtonEdit": "編集",
"ButtonEditChapters": "チャプターの編集",
"ButtonEditPodcast": "ポッドキャストの編集",
"ButtonEnable": "オンにする",
"ButtonHide": "非表示",
"ButtonHome": "ホーム",
"ButtonJumpBackward": "巻き戻し",
"ButtonJumpForward": "早送り",
"ButtonLibrary": "ライブラリー",
"ButtonLogout": "ログアウト",
"ButtonOk": "はい",
"ButtonPlay": "プレイ",
"ButtonPlaying": "プレイ中",
"ButtonPrevious": "先",
"ButtonQueueAddItem": "次に再生する",
"ButtonQueueRemoveItem": "次に再生から削除",
"ButtonReScan": "再スキャン",
"ButtonRead": "野村",
"ButtonReadLess": "閉じる",
"ButtonReadMore": "もっと見る",
"ButtonRefresh": "再読み込み",
"ButtonRemove": "削除",
"ButtonRemoveAll": "全て削除",
"ButtonRemoveAllLibraryItems": "ライブラリーの項目を全て削除",
"ButtonReset": "元に戻す",
"ButtonResetToDefault": "デフォルトに戻す",
"ButtonRestore": "復元",
"ButtonSave": "保存",
"ButtonSaveAndClose": "保存して閉じる",
"ButtonScan": "スキャン",
"ButtonScanLibrary": "ライブラリーをスキャン",
"ButtonScrollLeft": "左にスクロール",
"ButtonScrollRight": "右にスクロール",
"ButtonSearch": "検索",
"ButtonYes": "はい",
"HeaderPlayerSettings": "プレーヤーの設定",
"LabelBooks": "ほん",
"LabelContinueListening": "続きから聞く",
"LabelLanguage": "言語",
"LabelLanguages": "言語",
"LabelName": "名",

View File

@ -1,5 +1,6 @@
{
"ButtonAdd": "Legg til",
"ButtonAddApiKey": "Legg til API-nøkkel",
"ButtonAddChapters": "Legg til kapittel",
"ButtonAddDevice": "Legg til enhet",
"ButtonAddLibrary": "Legg til bibliotek",
@ -10,6 +11,8 @@
"ButtonApplyChapters": "Bruk kapittel",
"ButtonAuthors": "Forfattere",
"ButtonBack": "Tilbake",
"ButtonBatchEditPopulateFromExisting": "Opprett fra eksisterende",
"ButtonBatchEditPopulateMapDetails": "Legg til detaljer",
"ButtonBrowseForFolder": "Bla gjennom mappe",
"ButtonCancel": "Avbryt",
"ButtonCancelEncode": "Avbryt konvertering",
@ -18,6 +21,7 @@
"ButtonChooseAFolder": "Velg mappe",
"ButtonChooseFiles": "Velg filer",
"ButtonClearFilter": "Fjern filter",
"ButtonClose": "Lukk",
"ButtonCloseFeed": "Lukk Feed",
"ButtonCloseSession": "Lukk åpen økt",
"ButtonCollections": "Samlinger",
@ -117,6 +121,7 @@
"HeaderAccount": "Konto",
"HeaderAddCustomMetadataProvider": "Legg til egendefinert metadata tilbyder",
"HeaderAdvanced": "Avansert",
"HeaderApiKeys": "API-nøkler",
"HeaderAppriseNotificationSettings": "Apprise varslingsinstillinger",
"HeaderAudioTracks": "Lydspor",
"HeaderAudiobookTools": "Lydbok Filbehandlingsverktøy",
@ -160,6 +165,7 @@
"HeaderMetadataOrderOfPrecedence": "Prioriteringsrekkefølge for metadata",
"HeaderMetadataToEmbed": "Metadata å bake inn",
"HeaderNewAccount": "Ny konto",
"HeaderNewApiKey": "Ny API-nøkkel",
"HeaderNewLibrary": "Ny bibliotek",
"HeaderNotificationCreate": "Opprett varsling",
"HeaderNotificationUpdate": "Oppdater varsling",
@ -193,6 +199,7 @@
"HeaderSettingsExperimental": "Eksperimentelle funksjoner",
"HeaderSettingsGeneral": "Generell",
"HeaderSettingsScanner": "Skanner",
"HeaderSettingsSecurity": "Sikkerhet",
"HeaderSettingsWebClient": "Webklient",
"HeaderSleepTimer": "Sove timer",
"HeaderStatsLargestItems": "Største enheter",
@ -204,6 +211,7 @@
"HeaderTableOfContents": "Innholdsfortegnelse",
"HeaderTools": "Verktøy",
"HeaderUpdateAccount": "Oppdater konto",
"HeaderUpdateApiKey": "Oppdater API-nøkkel",
"HeaderUpdateAuthor": "Oppdater forfatter",
"HeaderUpdateDetails": "Oppdater detaljer",
"HeaderUpdateLibrary": "Oppdater bibliotek",
@ -233,6 +241,10 @@
"LabelAllUsersExcludingGuests": "Alle brukere bortsett fra gjester",
"LabelAllUsersIncludingGuests": "Alle brukere inkludert gjester",
"LabelAlreadyInYourLibrary": "Allerede i biblioteket",
"LabelApiKeyCreated": "API-nøkkel \"{0}\" ble opprettet.",
"LabelApiKeyCreatedDescription": "Husk å kopiere API-nøkkelen nå siden du ikke kan se den igjen senere.",
"LabelApiKeyUser": "Handle på vegne av bruker",
"LabelApiKeyUserDescription": "Denne API-nøkkelen vil ha de samme tillatelsene som brukeren den handler på vegne av. I loggene vil dette se ut som om brukeren selv foretok forespørselen.",
"LabelApiToken": "API token",
"LabelAppend": "Legge til",
"LabelAudioBitrate": "Bitrate for lyd (f.eks. 128k)",
@ -252,7 +264,7 @@
"LabelBackToUser": "Tilbake til bruker",
"LabelBackupAudioFiles": "Sikkerhetskopier lydfiler",
"LabelBackupLocation": "Mappe for sikkerhetskopiering",
"LabelBackupsEnableAutomaticBackups": "Aktiver automatisk sikkerhetskopi",
"LabelBackupsEnableAutomaticBackups": "Automatiske sikkerhetskopier",
"LabelBackupsEnableAutomaticBackupsHelp": "Sikkerhetskopier lagret under /metadata/backups",
"LabelBackupsMaxBackupSize": "Maksimal størrelse for sikkerhetskopi (i GB) (0 for ubegrenset)",
"LabelBackupsMaxBackupSizeHelp": "For å forhindre feilkonfigurasjon, vil sikkerhetskopier mislykkes hvis de oveskride konfigurert størrelse.",
@ -282,6 +294,7 @@
"LabelContinueListening": "Fortsett lytting",
"LabelContinueReading": "Fortsett lesing",
"LabelContinueSeries": "Fortsett serier",
"LabelCorsAllowed": "Tillate CORS-opprinnelser",
"LabelCover": "Omslag",
"LabelCoverImageURL": "Omslagsbilde URL",
"LabelCoverProvider": "Tilbyder av omslagsbilde",
@ -344,6 +357,10 @@
"LabelExample": "Eksempel",
"LabelExpandSeries": "Vis serie",
"LabelExpandSubSeries": "Vis underserie",
"LabelExpired": "Utløpt",
"LabelExpiresAt": "Utløper",
"LabelExpiresInSeconds": "Utløper om (sekunder)",
"LabelExpiresNever": "Aldri",
"LabelExplicit": "Eksplisitt",
"LabelExplicitChecked": "Eksplisitt (avhuket)",
"LabelExplicitUnchecked": "Ikke eksplisitt (ikke avhuket)",
@ -373,7 +390,7 @@
"LabelGenres": "Sjangre",
"LabelHardDeleteFile": "Tving sletting av fil",
"LabelHasEbook": "Har e-bok",
"LabelHasSupplementaryEbook": "Har komplimentær e-bok",
"LabelHasSupplementaryEbook": "Har supplerende e-bok",
"LabelHideSubtitles": "Skjul undertitler",
"LabelHighestPriority": "Høyeste prioritet",
"LabelHost": "Tjener",
@ -403,10 +420,11 @@
"LabelLanguages": "Språk",
"LabelLastBookAdded": "Siste bok lagt til",
"LabelLastBookUpdated": "Siste bok oppdatert",
"LabelLastProgressDate": "Siste fremgang: {0}",
"LabelLastSeen": "Sist sett",
"LabelLastTime": "Siste tid",
"LabelLastUpdate": "Siste oppdatering",
"LabelLayout": "Oppsett",
"LabelLayout": "Utseende",
"LabelLayoutSinglePage": "Enkeltside",
"LabelLayoutSplitPage": "Del side",
"LabelLess": "Mindre",
@ -415,6 +433,7 @@
"LabelLibraryFilterSublistEmpty": "Ingen {0}",
"LabelLibraryItem": "Bibliotek enhet",
"LabelLibraryName": "Bibliotek navn",
"LabelLibrarySortByProgress": "Fremgang oppdatert",
"LabelLimit": "Begrensning",
"LabelLineSpacing": "Linjemellomrom",
"LabelListenAgain": "Lytt igjen",
@ -468,7 +487,7 @@
"LabelNotificationsMaxQueueSize": "Maksimalt antall varslinger i kø",
"LabelNotificationsMaxQueueSizeHelp": "Hendelser er begrenset til avfyre én gang per sekund. Hendelser blir ignorert om køen er full. Dette forhindrer overflod av varslinger.",
"LabelNumberOfBooks": "Antall bøker",
"LabelNumberOfEpisodes": "Antall episoder",
"LabelNumberOfEpisodes": "# episoder",
"LabelOpenIDAdvancedPermsClaimDescription": "Navnet på OpenID claim'et som inneholder avanserte tilganger for brukerhandlinger i applikasjonen som vil brukes for ikke-administratorroller (<b>hvis konfigurert</b>). Hvis claim'et mangler fra responsen, nektes tilgang til ABS. Hvis en enkelt opsjon mangler, blir behandlet som <code>false</code>. Påse at identitetstilbyderens claim stemmer overens med den forventede strukturen:",
"LabelOpenIDClaims": "La følge valg være tomme for å slå av avanserte gruppe og tillatelser. Gruppen \"Bruker\" vil da også automatisk legges til.",
"LabelOpenRSSFeed": "Åpne RSS Feed",
@ -510,11 +529,11 @@
"LabelPublishers": "Utgivere",
"LabelRSSFeedCustomOwnerEmail": "Tilpasset eier e-post",
"LabelRSSFeedCustomOwnerName": "Tilpasset eier Navn",
"LabelRSSFeedOpen": "RSS Feed åpne",
"LabelRSSFeedOpen": "RSS-strøm åpen",
"LabelRSSFeedPreventIndexing": "Forhindre indeksering",
"LabelRSSFeedSlug": "RSS-feed ID",
"LabelRSSFeedURL": "RSS-feed URL",
"LabelRandomly": "Tilfeldig",
"LabelRandomly": "Tilfeldighet",
"LabelReAddSeriesToContinueListening": "Legg til igjen til \"Fortsett å lytte\"",
"LabelRead": "Les",
"LabelReadAgain": "Les igjen",
@ -624,7 +643,7 @@
"LabelStatsWeekListening": "Uker lyttet",
"LabelSubtitle": "Undertittel",
"LabelSupportedFileTypes": "Støttede filtyper",
"LabelTag": "Tag",
"LabelTag": "Merke",
"LabelTags": "Tagger",
"LabelTagsAccessibleToUser": "Tagger tilgjengelig for bruker",
"LabelTagsNotAccessibleToUser": "Tagger ikke tilgjengelig for bruker",
@ -829,7 +848,7 @@
"MessagePlaylistCreateFromCollection": "Lag spilleliste fra samling",
"MessagePleaseWait": "Vennligst vent...",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast har ingen RSS feed url til bruk av sammenligning",
"MessagePodcastSearchField": "Skriv inn søkeord eller RSS-feed URL",
"MessagePodcastSearchField": "Skriv inn søkeord eller URL til en RSS-strøm",
"MessageQuickEmbedInProgress": "Hurtiginnbygging pågår",
"MessageQuickEmbedQueue": "Kø for hurtiginnbygging ({0} i kø)",
"MessageQuickMatchAllEpisodes": "Kjapp matching av alle episoder",

View File

@ -241,6 +241,7 @@
"LabelAllUsersIncludingGuests": "Wszyscy użytkownicy, łącznie z gośćmi",
"LabelAlreadyInYourLibrary": "Już istnieje w twojej bibliotece",
"LabelApiKeyCreated": "Klucz API \"{0}\" został pomyślnie utworzony.",
"LabelApiKeyCreatedDescription": "Pamiętaj o skopiowaniu klucza API, ponieważ nie będziesz już mógł go zobaczyć.",
"LabelApiToken": "API Token",
"LabelAppend": "Dołącz",
"LabelAudioBitrate": "Audio Bitrate (np. 128k)",
@ -312,6 +313,7 @@
"LabelDiscover": "Odkrywaj",
"LabelDownload": "Pobierz",
"LabelDownloadNEpisodes": "Ściąganie {0} odcinków",
"LabelDownloadable": "Do pobrania",
"LabelDuration": "Czas trwania",
"LabelDurationComparisonExactMatch": "(dokładne dopasowanie)",
"LabelDurationComparisonLonger": "({0} dłużej)",
@ -334,6 +336,9 @@
"LabelEncodingClearItemCache": "Pamiętaj o okresowym czyszczeniu pamięci podręcznej elementów.",
"LabelEncodingFinishedM4B": "Ukończony plik M4B zostanie umieszczony w folderze audiobooka pod adresem:",
"LabelEncodingInfoEmbedded": "Metadane zostaną osadzone w ścieżkach audio w folderze z audiobookiem.",
"LabelEncodingStartedNavigation": "Po uruchomieniu zadania możesz opuścić tę stronę.",
"LabelEncodingTimeWarning": "Konwersja może potrwać do 30 minut.",
"LabelEncodingWarningAdvancedSettings": "Ostrzeżenie: Nie aktualizuj tych ustawień, jeśli nie jesteś zaznajomiony ze sposobem działania ffmpeg i opcji konwersji.",
"LabelEncodingWatcherDisabled": "Jeśli monitorowanie folderów jest wyłączone, należy ponownie przeskanować audiobooka.",
"LabelEnd": "Zakończ",
"LabelEndOfChapter": "Koniec rozdziału",
@ -583,8 +588,9 @@
"LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Pozostały czas jest mniejszy niż (sekund)",
"LabelSettingsLibraryMarkAsFinishedWhen": "Oznacz element multimedialny jako ukończony, gdy",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Pomiń poprzednie książki przy kontynuacji serii",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Strona „Kontynuuj serię” wyświetla pierwszą nierozpoczętą książkę z serii, w której ukończono co najmniej jedną książkę i żadnej nie rozpoczęto. Włączając to ustawienie, będziesz kontynuować serię po przeczytaniu ostatniej książki, a nie od pierwszej nierozpoczętej książki z serii.",
"LabelSettingsParseSubtitles": "Przetwarzaj podtytuły",
"LabelSettingsParseSubtitlesHelp": "Opcja pozwala na pobranie podtytułu z nazwy folderu z audiobookiem. <br>Podtytuł musi być rozdzielony za pomocą separatora \" - \"<br>Przykład: \"Book Title - A Subtitle Here\" podtytuł \"A Subtitle Here\"",
"LabelSettingsParseSubtitlesHelp": "Opcja pozwala na pobranie podtytułu z nazwy folderu z audiobookiem. <br>Podtytuł musi być rozdzielony za pomocą separatora \" - \"<br>Przykład: \"Tytuł książki - Podtytuł\" podtytuł \"Podtytuł\"",
"LabelSettingsPreferMatchedMetadata": "Preferowanie dopasowanych metadanych",
"LabelSettingsPreferMatchedMetadataHelp": "Dopasowane dane będą miały pierwszeństwo nad szczegółami pozycji podczas używania Szybkiego dopasowania. Domyślnie Szybkie dopasowanie uzupełnia tylko brakujące szczegóły.",
"LabelSettingsSkipMatchingBooksWithASIN": "Pomiń dopasowanie książek, które już mają ASIN",
@ -696,46 +702,87 @@
"LabelYourPlaylists": "Twoje playlisty",
"LabelYourProgress": "Twój postęp",
"MessageAppriseDescription": "Aby użyć tej funkcji, konieczne jest posiadanie instancji <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> albo innego rozwiązania, które obsługuje schemat zapytań Apprise. <br />URL do interfejsu API powinno być całkowitą ścieżką, np., jeśli Twoje API do powiadomień jest dostępne pod adresem <code>http://192.168.1.1:8337</code> to wpisany tutaj URL powinien mieć postać: <code>http://192.168.1.1:8337/notify</code>.",
"MessageAuthenticationLegacyTokenWarning": "Starsze tokeny API zostaną w przyszłości usunięte. Zamiast nich należy używać <a href=\"/config/api-keys\">kluczy API</a>.",
"MessageAuthenticationSecurityMessage": "Uwierzytelnianie zostało ulepszone ze względów bezpieczeństwa. Wszyscy użytkownicy muszą się ponownie zalogować.",
"MessageBackupsDescription": "Kopie zapasowe obejmują użytkowników, postępy użytkowników, szczegóły pozycji biblioteki, ustawienia serwera i obrazy przechowywane w <code>/metadata/items</code> & <code>/metadata/authors</code>. Kopie zapasowe nie obejmują żadnych plików przechowywanych w folderach biblioteki.",
"MessageBackupsLocationEditNote": "Uwaga: Zmiana lokalizacji kopii zapasowej nie przenosi ani nie modyfikuje istniejących kopii zapasowych",
"MessageBackupsLocationNoEditNote": "Uwaga: Lokalizacja kopii zapasowej jest ustawiona poprzez zmienną środowiskową i nie może być tutaj zmieniona.",
"MessageBackupsLocationPathEmpty": "Ścieżka do kopii zapasowej nie może być pusta",
"MessageBatchEditPopulateMapDetailsAllHelp": "Wypełnij włączone pola danymi ze wszystkich elementów. Pola z wieloma wartościami zostaną scalone.",
"MessageBatchQuickMatchDescription": "Quick Match będzie próbował dodać brakujące okładki i metadane dla wybranych elementów. Włącz poniższe opcje, aby umożliwić Quick Match nadpisanie istniejących okładek i/lub metadanych.",
"MessageBookshelfNoCollections": "Nie posiadasz jeszcze żadnych kolekcji",
"MessageBookshelfNoCollectionsHelp": "Kolekcje są publiczne. Wszyscy użytkownicy mający dostęp do biblioteki mogą je zobaczyć.",
"MessageBookshelfNoRSSFeeds": "Nie posiadasz żadnych otwartych feedów RSS",
"MessageBookshelfNoResultsForFilter": "Nie znaleziono żadnych pozycji przy aktualnym filtrowaniu \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Brak wyników zapytania",
"MessageBookshelfNoSeries": "Nie masz jeszcze żadnych serii",
"MessageChapterEndIsAfter": "Koniec rozdziału następuje po zakończeniu audiobooka",
"MessageChapterErrorFirstNotZero": "Pierwszy rozdział musi rozpoczynać się na 0",
"MessageChapterErrorStartGteDuration": "Nieprawidłowy czas rozpoczęcia, musi być krótszy niż długość audiobooka",
"MessageChapterErrorStartLtPrev": "Nieprawidłowy czas rozpoczęcia, musi być większy lub taki sam, jak czas rozpoczęcia poprzedniego rozdziału.",
"MessageChapterStartIsAfter": "Początek rozdziału następuje po zakończeniu audiobooka",
"MessageChaptersNotFound": "Nie znaleziono rozdziałów",
"MessageCheckingCron": "Sprawdzanie cron...",
"MessageConfirmCloseFeed": "Czy na pewno chcesz zamknąć ten kanał?",
"MessageConfirmDeleteApiKey": "Czy na pewno chcesz usunąć klucz API \"{0}\"?",
"MessageConfirmDeleteBackup": "Czy na pewno chcesz usunąć kopię zapasową dla {0}?",
"MessageConfirmDeleteDevice": "Czy na pewno chcesz usunąć czytnik e-booków \"{0}\"?",
"MessageConfirmDeleteFile": "Ta operacja usunie plik z twojego dysku. Jesteś pewien?",
"MessageConfirmDeleteLibrary": "Czy na pewno chcesz trwale usunąć bibliotekę \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "Ta operacja usunie pozycję biblioteki z bazy danych i z dysku. Czy jesteś pewien?",
"MessageConfirmDeleteLibraryItems": "{0} element(ów) zostanie teraz usuniętych z bazy danych i systemu plików. Czy jesteś pewien?",
"MessageConfirmDeleteMetadataProvider": "Czy na pewno chcesz usunąć niestandardowego dostawcę metadanych: \"{0}\"?",
"MessageConfirmDeleteNotification": "Czy na pewno chcesz usunąć to powiadomienie?",
"MessageConfirmDeleteSession": "Czy na pewno chcesz usunąć tę sesję?",
"MessageConfirmEmbedMetadataInAudioFiles": "Czy na pewno chcesz osadzić metadane w {0} plikach audio?",
"MessageConfirmForceReScan": "Czy na pewno chcesz wymusić ponowne skanowanie?",
"MessageConfirmMarkAllEpisodesFinished": "Czy na pewno chcesz oznaczyć wszystkie odcinki jako ukończone?",
"MessageConfirmMarkAllEpisodesNotFinished": "Czy na pewno chcesz oznaczyć wszystkie odcinki jako nieukończone?",
"MessageConfirmMarkItemFinished": "Czy na pewno chcesz oznaczyć \"{0}\" jako zakończone?",
"MessageConfirmMarkItemNotFinished": "Czy na pewno chcesz oznaczyć \"{0}\" jako nieukończone?",
"MessageConfirmMarkSeriesFinished": "Czy na pewno chcesz oznaczyć wszystkie książki w tej serii jako ukończone?",
"MessageConfirmMarkSeriesNotFinished": "Czy na pewno chcesz oznaczyć wszystkie książki w tej serii jako nieukończone?",
"MessageConfirmNotificationTestTrigger": "Czy wywołać to powiadomienie za pomocą danych testowych?",
"MessageConfirmPurgeCache": "Wyczyszczenie pamięci podręcznej spowoduje usunięcie całego katalogu <code>/metadata/cache</code>. <br /><br />Czy na pewno chcesz usunąć katalog pamięci podręcznej?",
"MessageConfirmPurgeItemsCache": "Wyczyszczenie pamięci podręcznej elementów spowoduje usunięcie całego katalogu <code>/metadata/cache/items</code>.<br />Czy jesteś pewien?",
"MessageConfirmQuickEmbed": "Ostrzeżenie! Szybkie osadzanie nie utworzy kopii zapasowej plików audio. Upewnij się, że masz kopię zapasową plików audio. <br><br>Czy chcesz kontynuować?",
"MessageConfirmQuickMatchEpisodes": "Szybkie dopasowywanie odcinków spowoduje nadpisanie szczegółów w przypadku znalezienia dopasowania. Zaktualizowane zostaną tylko niedopasowane odcinki. Jesteś pewien?",
"MessageConfirmReScanLibraryItems": "Czy na pewno chcesz ponownie zeskanować {0} pozycji?",
"MessageConfirmRemoveAllChapters": "Czy na pewno chcesz usunąć wszystkie rozdziały?",
"MessageConfirmRemoveAuthor": "Czy na pewno chcesz usunąć autora \"{0}\"?",
"MessageConfirmRemoveCollection": "Czy na pewno chcesz usunąć kolekcję \"{0}\"?",
"MessageConfirmRemoveEpisode": "Czy na pewno chcesz usunąć odcinek \"{0}\"?",
"MessageConfirmRemoveEpisodeNote": "Uwaga: Plik audio nie zostanie usunięty, chyba że przełączysz opcję „Twarde usunięcie pliku”",
"MessageConfirmRemoveEpisodes": "Czy na pewno chcesz usunąć {0} odcinki?",
"MessageConfirmRemoveListeningSessions": "Czy na pewno chcesz usunąć {0} sesji słuchania?",
"MessageConfirmRemoveMetadataFiles": "Czy na pewno chcesz usunąć wszystkie metadane.{0} plików w folderach elementów biblioteki?",
"MessageConfirmRemoveNarrator": "Czy na pewno chcesz usunąć lektora \"{0}\"?",
"MessageConfirmRemovePlaylist": "Czy jesteś pewien, że chcesz usunąć twoją playlistę \"{0}\"?",
"MessageConfirmRenameGenre": "Czy na pewno chcesz zmienić nazwę gatunku \"{0}\" na \"{1}\" dla wszystkich elementów?",
"MessageConfirmRenameGenreMergeNote": "Uwaga: Ten gatunek już istnieje, więc zostaną połączone.",
"MessageConfirmRenameGenreWarning": "Uwaga! Podobny gatunek z inną wielkością liter już istnieje: \"{0}\".",
"MessageConfirmRenameTag": "Czy na pewno chcesz zmienić nazwę tagu \"{0}\" na \"{1}\" dla wszystkich elementów?",
"MessageConfirmRenameTagMergeNote": "Uwaga: Ten tag już istnieje, więc zostaną scalone.",
"MessageConfirmRenameTagWarning": "Uwaga! Podobny tag z inną wielkością liter już istnieje: \"{0}\".",
"MessageConfirmResetProgress": "Czy na pewno chcesz zresetować swój postęp?",
"MessageConfirmSendEbookToDevice": "Czy na pewno chcesz wysłać {0} e-booka \"{1}\" na urządzenie \"{2}\"?",
"MessageConfirmUnlinkOpenId": "Czy na pewno chcesz odłączyć tego użytkownika od OpenID?",
"MessageDaysListenedInTheLastYear": "{0} dni słuchania w ciągu ostatniego roku",
"MessageDownloadingEpisode": "Pobieranie odcinka",
"MessageDragFilesIntoTrackOrder": "przeciągnij pliki aby ustawić właściwą kolejność utworów",
"MessageEmbedFailed": "Niepowodzenie wstawiania!",
"MessageEmbedFinished": "Osadzanie zakończone!",
"MessageEmbedQueue": "W kolejce do osadzenia metadanych ({0} w kolejce)",
"MessageEpisodesQueuedForDownload": "{0} odcinki w kolejce do pobrania",
"MessageEreaderDevices": "Aby zagwarantować dostawę e-booków, konieczne może być dodanie powyższego adresu e-mail jako prawidłowego nadawcy dla każdego z urządzeń wymienionych poniżej.",
"MessageFeedURLWillBe": "URL kanału: {0}",
"MessageFetching": "Pobieranie...",
"MessageForceReScanDescription": "przeskanuje wszystkie pliki ponownie, jak przy świeżym skanowaniu. Tagi ID3 plików audio, pliki OPF i pliki tekstowe będą skanowane jak nowe.",
"MessageImportantNotice": "Ważna informacja!",
"MessageInsertChapterBelow": "Wstaw rozdział poniżej",
"MessageInvalidAsin": "Nieprawidłowy ASIN",
"MessageItemsSelected": "{0} zaznaczone elementy",
"MessageItemsUpdated": "Zaktualizowano {0} elementów",
"MessageJoinUsOn": "Dołącz do nas na",
"MessageLoading": "Ładowanie...",
"MessageLoadingFolders": "Ładowanie folderów...",
@ -756,6 +803,9 @@
"MessageNoCollections": "Brak kolekcji",
"MessageNoCoversFound": "Okładki nieznalezione",
"MessageNoDescription": "Brak opisu",
"MessageNoDevices": "Brak urządzeń",
"MessageNoDownloadsInProgress": "Brak aktualnie trwających pobrań",
"MessageNoDownloadsQueued": "Brak pobrań w kolejce",
"MessageNoEpisodeMatchesFound": "Nie znaleziono pasujących odcinków",
"MessageNoEpisodes": "Brak odcinków",
"MessageNoFoldersAvailable": "Brak dostępnych folderów",
@ -767,25 +817,35 @@
"MessageNoLogs": "Brak logów",
"MessageNoMediaProgress": "Brak postępu",
"MessageNoNotifications": "Brak powiadomień",
"MessageNoPodcastFeed": "Nieprawidłowy podcast: Brak kanału",
"MessageNoPodcastsFound": "Nie znaleziono podcastów",
"MessageNoResults": "Brak wyników",
"MessageNoSearchResultsFor": "Brak wyników wyszukiwania dla \"{0}\"",
"MessageNoSeries": "Brak serii",
"MessageNoTags": "Brak tagów",
"MessageNoTasksRunning": "Brak uruchomionych zadań",
"MessageNoUpdatesWereNecessary": "Brak aktualizacji",
"MessageNoUserPlaylists": "Nie masz żadnych list odtwarzania",
"MessageNoUserPlaylistsHelp": "Listy odtwarzania są prywatne. Tylko użytkownik, który je utworzył, może je zobaczyć.",
"MessageNotYetImplemented": "Jeszcze nie zaimplementowane",
"MessageOpmlPreviewNote": "Uwaga: To jest podgląd sparsowanego pliku OPML. Tytuł podcastu wzięty został z wątku RSS.",
"MessageOr": "lub",
"MessagePauseChapter": "Zatrzymaj odtwarzanie rozdziały",
"MessagePlayChapter": "Rozpocznij odtwarzanie od początku rozdziału",
"MessagePlaylistCreateFromCollection": "Utwórz listę odtwarzania na podstawie kolekcji",
"MessagePleaseWait": "Proszę czekać...",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nie ma adresu url kanału RSS, który mógłby zostać użyty do dopasowania",
"MessagePodcastSearchField": "Wprowadź wyszukiwane hasło lub adres URL kanału RSS",
"MessageQuickEmbedInProgress": "Szybkie osadzanie w toku",
"MessageQuickEmbedQueue": "W kolejce do szybkiego osadzenia ({0} w kolejce)",
"MessageQuickMatchAllEpisodes": "Szybkie dopasowanie wszystkich odcinków",
"MessageQuickMatchDescription": "Wypełnij puste informacje i okładkę pierwszym wynikiem dopasowania z '{0}'. Nie nadpisuje szczegółów, chyba że włączone jest ustawienie serwera 'Preferuj dopasowane metadane'.",
"MessageRemoveChapter": "Usuń rozdział",
"MessageRemoveEpisodes": "Usuń {0} odcinków",
"MessageRemoveFromPlayerQueue": "Usuń z kolejki odtwarzacza",
"MessageRemoveUserWarning": "Czy na pewno chcesz trwale usunąć użytkownika \"{0}\"?",
"MessageReportBugsAndContribute": "Zgłoś błędy, pomysły i pomóż rozwijać aplikację na",
"MessageResetChaptersConfirm": "Czy na pewno chcesz zresetować rozdziały i cofnąć wprowadzone zmiany?",
"MessageRestoreBackupConfirm": "Czy na pewno chcesz przywrócić kopię zapasową utworzoną w dniu",
"MessageRestoreBackupWarning": "Przywrócenie kopii zapasowej spowoduje nadpisanie bazy danych w folderze /config oraz okładek w folderze /metadata/items & /metadata/authors.<br /><br />Kopie zapasowe nie modyfikują żadnego pliku w folderach z plikami audio. Jeśli włączyłeś ustawienia serwera, aby przechowywać okładki i metadane w folderach biblioteki, to nie są one zapisywane w kopii zapasowej lub nadpisywane<br /><br />Wszyscy klienci korzystający z Twojego serwera będą automatycznie odświeżani.",
"MessageSearchResultsFor": "Wyniki wyszukiwania dla",

View File

@ -199,6 +199,7 @@
"HeaderSettingsExperimental": "Экспериментальные функции",
"HeaderSettingsGeneral": "Основные",
"HeaderSettingsScanner": "Сканер",
"HeaderSettingsSecurity": "Безопасность",
"HeaderSettingsWebClient": "Веб-клиент",
"HeaderSleepTimer": "Таймер сна",
"HeaderStatsLargestItems": "Самые большые элементы",
@ -293,6 +294,7 @@
"LabelContinueListening": "Продолжить слушать",
"LabelContinueReading": "Продолжить чтение",
"LabelContinueSeries": "Продолжить серию",
"LabelCorsAllowed": "Разрешённые CORS источники",
"LabelCover": "Обложка",
"LabelCoverImageURL": "URL изображения обложки",
"LabelCoverProvider": "Провайдер обложек",
@ -418,6 +420,7 @@
"LabelLanguages": "Языки",
"LabelLastBookAdded": "Последняя книга добавлена",
"LabelLastBookUpdated": "Последняя книга обновлена",
"LabelLastProgressDate": "Последний прогресс: {0}",
"LabelLastSeen": "Последнее сканирование",
"LabelLastTime": "Последний по времени",
"LabelLastUpdate": "Последний обновленный",
@ -430,6 +433,7 @@
"LabelLibraryFilterSublistEmpty": "Нет {0}",
"LabelLibraryItem": "Элемент библиотеки",
"LabelLibraryName": "Имя библиотеки",
"LabelLibrarySortByProgress": "Прогресс обновлён",
"LabelLimit": "Лимит",
"LabelLineSpacing": "Межстрочный интервал",
"LabelListenAgain": "Послушать снова",
@ -803,6 +807,8 @@
"MessageFeedURLWillBe": "URL канала будет {0}",
"MessageFetching": "Завершается...",
"MessageForceReScanDescription": "будет сканировать все файлы снова, как свежее сканирование. Теги ID3 аудиофайлов, OPF-файлы и текстовые файлы будут сканироваться как новые.",
"MessageHeatmapListeningTimeTooltip": "<strong>{0} прослушивание</strong> на {1}",
"MessageHeatmapNoListeningSessions": "Нет сессий прослушивания на {0}",
"MessageImportantNotice": "Важное замечание!",
"MessageInsertChapterBelow": "Вставить главу ниже",
"MessageInvalidAsin": "Неправильный ASIN",
@ -1030,6 +1036,7 @@
"ToastInvalidImageUrl": "Неверный URL изображения",
"ToastInvalidMaxEpisodesToDownload": "Недопустимое максимальное количество загружаемых эпизодов",
"ToastInvalidUrl": "Неверный URL",
"ToastInvalidUrls": "Один или несколько URL неверны",
"ToastItemCoverUpdateSuccess": "Обложка элемента обновлена",
"ToastItemDeletedFailed": "Не удалось удалить элемент",
"ToastItemDeletedSuccess": "Удаленный элемент",

View File

@ -199,6 +199,7 @@
"HeaderSettingsExperimental": "Експериментальні функції",
"HeaderSettingsGeneral": "Основне",
"HeaderSettingsScanner": "Сканер",
"HeaderSettingsSecurity": "Безпека",
"HeaderSettingsWebClient": "Вебклієнт",
"HeaderSleepTimer": "Таймер вимкнення",
"HeaderStatsLargestItems": "Найбільші елементи",
@ -293,6 +294,7 @@
"LabelContinueListening": "Слухати далі",
"LabelContinueReading": "Читати далі",
"LabelContinueSeries": "Продовжити серії",
"LabelCorsAllowed": "Дозволені джерела CORS",
"LabelCover": "Обкладинка",
"LabelCoverImageURL": "URL-адреса обкладинки",
"LabelCoverProvider": "Постачальник покриття",
@ -1034,6 +1036,7 @@
"ToastInvalidImageUrl": "Невірний URL зображення",
"ToastInvalidMaxEpisodesToDownload": "Невірна кількість епізодів для скачування",
"ToastInvalidUrl": "Невірний URL",
"ToastInvalidUrls": "Одна або декілька URL-адрес недійсні",
"ToastItemCoverUpdateSuccess": "Обкладинку елемента оновлено",
"ToastItemDeletedFailed": "Не вдалося видалити елемент",
"ToastItemDeletedSuccess": "Видалений елемент",

View File

@ -199,6 +199,7 @@
"HeaderSettingsExperimental": "实验功能",
"HeaderSettingsGeneral": "通用",
"HeaderSettingsScanner": "扫描",
"HeaderSettingsSecurity": "安全",
"HeaderSettingsWebClient": "网页客户端",
"HeaderSleepTimer": "睡眠计时",
"HeaderStatsLargestItems": "最大的项目",
@ -293,6 +294,7 @@
"LabelContinueListening": "继续收听",
"LabelContinueReading": "继续阅读",
"LabelContinueSeries": "继续收听系列",
"LabelCorsAllowed": "允许的跨域来源",
"LabelCover": "封面",
"LabelCoverImageURL": "封面图像 URL",
"LabelCoverProvider": "封面提供者",
@ -1034,6 +1036,7 @@
"ToastInvalidImageUrl": "图片网址无效",
"ToastInvalidMaxEpisodesToDownload": "可下载的最大集数无效",
"ToastInvalidUrl": "网址无效",
"ToastInvalidUrls": "一个或多个 URL 无效",
"ToastItemCoverUpdateSuccess": "项目封面已更新",
"ToastItemDeletedFailed": "删除项目失败",
"ToastItemDeletedSuccess": "已删除项目",

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "audiobookshelf",
"version": "2.27.0",
"version": "2.28.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf",
"version": "2.27.0",
"version": "2.28.0",
"license": "GPL-3.0",
"dependencies": {
"axios": "^0.27.2",

View File

@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
"version": "2.27.0",
"version": "2.28.0",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast server",
"main": "index.js",

View File

@ -213,6 +213,7 @@ class Auth {
* @param {Request} req
* @param {Response} res
* @param {string} authMethod - The authentication method, default is 'local'.
* @returns {Object|null} - Returns error object if validation fails, null if successful
*/
paramsToCookies(req, res, authMethod = 'local') {
const TWO_MINUTES = 120000 // 2 minutes in milliseconds
@ -227,13 +228,24 @@ class Auth {
// Validate and store the callback URL
if (!callback) {
return res.status(400).send({ message: 'No callback parameter' })
res.status(400).send({ message: 'No callback parameter' })
return { error: 'No callback parameter' }
}
// Security: Validate callback URL is same-origin only
if (!this.oidcAuthStrategy.isValidWebCallbackUrl(callback, req)) {
Logger.warn(`[Auth] Rejected invalid callback URL: ${callback}`)
res.status(400).send({ message: 'Invalid callback URL - must be same-origin' })
return { error: 'Invalid callback URL - must be same-origin' }
}
res.cookie('auth_cb', callback, { maxAge: TWO_MINUTES, httpOnly: true })
}
// Store the authentication method for long
Logger.debug(`[Auth] paramsToCookies: setting auth_method cookie to ${authMethod}`)
res.cookie('auth_method', authMethod, { maxAge: 1000 * 60 * 60 * 24 * 365 * 10, httpOnly: true })
return null
}
/**
@ -247,6 +259,7 @@ class Auth {
// Handle token generation and get userResponse object
// For API based auth (e.g. mobile), we will return the refresh token in the response
const isApiBased = this.isAuthMethodAPIBased(req.cookies.auth_method)
Logger.debug(`[Auth] handleLoginSuccessBasedOnCookie: isApiBased: ${isApiBased}, auth_method: ${req.cookies.auth_method}`)
const userResponse = await this.handleLoginSuccess(req, res, isApiBased)
if (isApiBased) {
@ -254,7 +267,6 @@ class Auth {
res.json(userResponse)
} else {
// UI request -> check if we have a callback url
// TODO: do we want to somehow limit the values for auth_cb?
if (req.cookies.auth_cb) {
let stateQuery = req.cookies.auth_state ? `&state=${req.cookies.auth_state}` : ''
// UI request -> redirect to auth_cb url and send the jwt token as parameter
@ -288,6 +300,8 @@ class Auth {
userResponse.user.refreshToken = returnTokens ? refreshToken : null
userResponse.user.accessToken = accessToken
Logger.debug(`[Auth] handleLoginSuccess: returnTokens: ${returnTokens}, isRefreshTokenInResponse: ${!!userResponse.user.refreshToken}`)
if (!returnTokens) {
this.tokenManager.setRefreshTokenCookie(req, res, refreshToken)
}
@ -350,7 +364,11 @@ class Auth {
return res.status(authorizationUrlResponse.status).send(authorizationUrlResponse.error)
}
this.paramsToCookies(req, res, authorizationUrlResponse.isMobileFlow ? 'openid-mobile' : 'openid')
// Check if paramsToCookies sent a response (e.g., due to invalid callback URL)
const cookieResult = this.paramsToCookies(req, res, authorizationUrlResponse.isMobileFlow ? 'openid-mobile' : 'openid')
if (cookieResult && cookieResult.error) {
return // Response already sent by paramsToCookies
}
res.redirect(authorizationUrlResponse.authorizationUrl)
})

View File

@ -229,6 +229,10 @@ class Server {
res.setHeader('Content-Security-Policy', "frame-ancestors 'self'")
}
// Security: Prevent referrer leakage to protect against token exposure
// Using 'no-referrer' to completely prevent token leakage in referer headers
res.setHeader('Referrer-Policy', 'no-referrer')
/**
* @temporary
* This is necessary for the ebook & cover API endpoint in the mobile apps
@ -240,8 +244,8 @@ class Server {
* Running in development allows cors to allow testing the mobile apps in the browser
* or env variable ALLOW_CORS = '1'
*/
if (global.AllowCors || Logger.isDev || req.path.match(/\/api\/items\/([a-z0-9-]{36})\/(ebook|cover)(\/[0-9]+)?/)) {
const allowedOrigins = ['capacitor://localhost', 'http://localhost']
if (global.AllowCors || Logger.isDev || req.path.match(/\/api\/items\/([a-z0-9-]{36})\/(ebook|cover)(\/[0-9]+)?/) || global.ServerSettings.allowedOrigins?.length) {
const allowedOrigins = ['capacitor://localhost', 'http://localhost', ...(global.ServerSettings.allowedOrigins ? global.ServerSettings.allowedOrigins : [])]
if (global.AllowCors || Logger.isDev || allowedOrigins.some((o) => o === req.get('origin'))) {
res.header('Access-Control-Allow-Origin', req.get('origin'))
res.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS')

View File

@ -110,6 +110,8 @@ class OidcAuthStrategy {
* @param {Function} done - Passport callback
*/
async verifyCallback(tokenset, userinfo, done) {
let isNewUser = false
let user = null
try {
Logger.debug(`[OidcAuth] openid callback userinfo=`, JSON.stringify(userinfo, null, 2))
@ -121,9 +123,24 @@ class OidcAuthStrategy {
throw new Error(`Group claim ${Database.serverSettings.authOpenIDGroupClaim} not found or empty in userinfo`)
}
let user = await Database.userModel.findOrCreateUserFromOpenIdUserInfo(userinfo)
user = await Database.userModel.findUserFromOpenIdUserInfo(userinfo)
if (!user?.isActive) {
if (user?.error) {
throw new Error('Invalid userinfo or already linked')
}
if (!user) {
// If no existing user was matched, auto-register if configured
if (global.ServerSettings.authOpenIDAutoRegister) {
Logger.info(`[User] openid: Auto-registering user with sub "${userinfo.sub}"`, userinfo)
user = await Database.userModel.createUserFromOpenIdUserInfo(userinfo)
isNewUser = true
} else {
Logger.warn(`[User] openid: User not found and auto-register is disabled`)
}
}
if (!user.isActive) {
throw new Error('User not active or not found')
}
@ -136,6 +153,10 @@ class OidcAuthStrategy {
return done(null, user)
} catch (error) {
Logger.error(`[OidcAuth] openid callback error: ${error?.message}\n${error?.stack}`)
// Remove new user if an error occurs
if (isNewUser && user) {
await user.destroy()
}
return done(null, null, 'Unauthorized')
}
}
@ -483,6 +504,49 @@ class OidcAuthStrategy {
res.status(500).send('Internal Server Error')
}
}
/**
* Validates if a callback URL is safe for redirect (same-origin only)
* @param {string} callbackUrl - The callback URL to validate
* @param {Request} req - Express request object to get current host
* @returns {boolean} - True if the URL is safe (same-origin), false otherwise
*/
isValidWebCallbackUrl(callbackUrl, req) {
if (!callbackUrl) return false
try {
// Handle relative URLs - these are always safe if they start with router base path
if (callbackUrl.startsWith('/')) {
// Only allow relative paths that start with the router base path
if (callbackUrl.startsWith(global.RouterBasePath + '/')) {
return true
}
Logger.warn(`[OidcAuth] Rejected callback URL outside router base path: ${callbackUrl}`)
return false
}
// For absolute URLs, ensure they point to the same origin
const callbackUrlObj = new URL(callbackUrl)
const currentProtocol = req.secure || req.get('x-forwarded-proto') === 'https' ? 'https' : 'http'
const currentHost = req.get('host')
// Check if protocol and host match exactly
if (callbackUrlObj.protocol === currentProtocol + ':' && callbackUrlObj.host === currentHost) {
// Additional check: ensure path starts with router base path
if (callbackUrlObj.pathname.startsWith(global.RouterBasePath + '/')) {
return true
}
Logger.warn(`[OidcAuth] Rejected same-origin callback URL outside router base path: ${callbackUrl}`)
return false
}
Logger.warn(`[OidcAuth] Rejected callback URL to different origin: ${callbackUrl} (expected ${currentProtocol}://${currentHost})`)
return false
} catch (error) {
Logger.error(`[OidcAuth] Invalid callback URL format: ${callbackUrl}`, error)
return false
}
}
}
module.exports = OidcAuthStrategy

View File

@ -4,6 +4,7 @@ const Logger = require('../Logger')
const Database = require('../Database')
const { toNumber, isUUID } = require('../utils/index')
const { getAudioMimeTypeFromExtname, encodeUriPath } = require('../utils/fileUtils')
const { PlayMethod } = require('../utils/constants')
const ShareManager = require('../managers/ShareManager')
@ -299,6 +300,18 @@ class SessionController {
return res.sendStatus(404)
}
// Redirect transcode requests to the HLS router
// Handles bug introduced in android v0.10.0-beta where transcode requests are made to this endpoint
if (playbackSession.playMethod === PlayMethod.TRANSCODE && audioTrack.contentUrl) {
Logger.debug(`[SessionController] Redirecting transcode request to "${audioTrack.contentUrl}"`)
return res.redirect(audioTrack.contentUrl)
}
if (!audioTrack.metadata?.path) {
Logger.error(`[SessionController] Invalid audio track "${audioTrack.index}" for session "${req.params.id}"`)
return res.sendStatus(500)
}
const user = await Database.userModel.getUserById(playbackSession.userId)
Logger.debug(`[SessionController] Serving audio track ${audioTrack.index} for session "${req.params.id}" belonging to user "${user.username}"`)

View File

@ -121,13 +121,11 @@ class PodcastManager {
await fs.mkdir(this.currentDownload.libraryItem.path)
}
let success = false
if (this.currentDownload.isMp3) {
// Download episode and tag it
const ffmpegDownloadResponse = await ffmpegHelpers.downloadPodcastEpisode(this.currentDownload).catch((error) => {
Logger.error(`[PodcastManager] Podcast Episode download failed`, error)
})
success = !!ffmpegDownloadResponse?.success
let success = !!ffmpegDownloadResponse?.success
// If failed due to ffmpeg error, retry without tagging
// e.g. RSS feed may have incorrect file extension and file type
@ -142,15 +140,6 @@ class PodcastManager {
return false
})
}
} else {
// Download episode only
success = await downloadFile(this.currentDownload.url, this.currentDownload.targetPath)
.then(() => true)
.catch((error) => {
Logger.error(`[PodcastManager] Podcast Episode download failed`, error)
return false
})
}
if (success) {
success = await this.scanAddPodcastEpisodeAudioFile()

View File

@ -211,18 +211,18 @@ class User extends Model {
}
/**
* Finds an existing user by OpenID subject identifier, or by email/username based on server settings,
* or creates a new user if configured to do so.
* Finds an existing user by OpenID subject identifier, or by email/username based on server settings
* Returns null if no user is found
*
* @param {Object} userinfo
* @returns {Promise<User>}
* @returns {Promise<User|{error: string}>}
*/
static async findOrCreateUserFromOpenIdUserInfo(userinfo) {
static async findUserFromOpenIdUserInfo(userinfo) {
let user = await this.getUserByOpenIDSub(userinfo.sub)
// Matched by sub
if (user) {
Logger.debug(`[User] openid: User found by sub`)
Logger.debug(`[User] openid: User found by sub "${userinfo.sub}"`)
return user
}
@ -232,20 +232,27 @@ class User extends Model {
// Only disallow when email_verified explicitly set to false (allow both if not set or true)
if (userinfo.email_verified === false) {
Logger.warn(`[User] openid: User not found and email "${userinfo.email}" is not verified`)
return null
return {
error: 'Email not verified'
}
} else {
Logger.info(`[User] openid: User not found, checking existing with email "${userinfo.email}"`)
user = await this.getUserByEmail(userinfo.email)
if (user?.authOpenIDSub) {
Logger.warn(`[User] openid: User found with email "${userinfo.email}" but is already matched with sub "${user.authOpenIDSub}"`)
return null // User is linked to a different OpenID subject; do not proceed.
// User is linked to a different OpenID subject; do not proceed.
return {
error: 'User already linked to a different OpenID subject'
}
}
}
} else {
Logger.warn(`[User] openid: User not found and no email in userinfo`)
// We deny login, because if the admin whishes to match email, it makes sense to require it
return null
return {
error: 'No email in userinfo'
}
}
}
// Match existing user by username
@ -260,22 +267,30 @@ class User extends Model {
username = userinfo.username
} else {
Logger.warn(`[User] openid: User not found and neither preferred_username nor username in userinfo`)
return null
return {
error: 'No username in userinfo'
}
}
user = await this.getUserByUsername(username)
if (user?.authOpenIDSub) {
Logger.warn(`[User] openid: User found with username "${username}" but is already matched with sub "${user.authOpenIDSub}"`)
return null // User is linked to a different OpenID subject; do not proceed.
// User is linked to a different OpenID subject; do not proceed.
return {
error: 'User already linked to a different OpenID subject'
}
}
}
if (!user) {
return null
}
// Found existing user via email or username
if (user) {
if (!user.isActive) {
Logger.warn(`[User] openid: User found but is not active`)
return null
return user
}
// Update user with OpenID sub
@ -288,17 +303,6 @@ class User extends Model {
return user
}
// If no existing user was matched, auto-register if configured
if (global.ServerSettings.authOpenIDAutoRegister) {
Logger.info(`[User] openid: Auto-registering user with sub "${userinfo.sub}"`, userinfo)
user = await this.createUserFromOpenIdUserInfo(userinfo)
return user
}
Logger.warn(`[User] openid: User not found and auto-register is disabled`)
return null
}
/**
* Create user from openid userinfo
* @param {Object} userinfo

View File

@ -63,16 +63,6 @@ class PodcastEpisodeDownload {
const enclosureType = this.rssPodcastEpisode.enclosure.type
return typeof enclosureType === 'string' ? enclosureType : null
}
/**
* RSS feed may have an episode with file extension of mp3 but the specified enclosure type is not mpeg.
* @see https://github.com/advplyr/audiobookshelf/issues/3711
*
* @returns {boolean}
*/
get isMp3() {
if (this.enclosureType && !this.enclosureType.includes('mpeg')) return false
return this.fileExtension === 'mp3'
}
get episodeTitle() {
return this.rssPodcastEpisode.title
}

View File

@ -53,6 +53,7 @@ class ServerSettings {
this.dateFormat = 'MM/dd/yyyy'
this.timeFormat = 'HH:mm'
this.language = 'en-us'
this.allowedOrigins = []
this.logLevel = Logger.logLevel
@ -120,6 +121,7 @@ class ServerSettings {
this.dateFormat = settings.dateFormat || 'MM/dd/yyyy'
this.timeFormat = settings.timeFormat || 'HH:mm'
this.language = settings.language || 'en-us'
this.allowedOrigins = settings.allowedOrigins || []
this.logLevel = settings.logLevel || Logger.logLevel
this.version = settings.version || null
this.buildNumber = settings.buildNumber || 0 // Added v2.4.5
@ -231,6 +233,7 @@ class ServerSettings {
dateFormat: this.dateFormat,
timeFormat: this.timeFormat,
language: this.language,
allowedOrigins: this.allowedOrigins,
logLevel: this.logLevel,
version: this.version,
buildNumber: this.buildNumber,