Compare commits

..

51 Commits

Author SHA1 Message Date
advplyr
a992400d6a Add ENV REACT_CLIENT_PATH to target a Nextjs frontend instead of Nuxt 2025-06-23 16:56:08 -05:00
advplyr
108b2a60f5 Merge pull request #4425 from Vito0912/feat/addExplicit
Add explicit filter
2025-06-21 17:03:25 -05:00
advplyr
af684e6a69 Explicit library filter not shown for users without permission 2025-06-21 17:01:13 -05:00
Vito0912
5336d0525e add explicit to podcasts 2025-06-21 12:29:54 +02:00
Vito0912
bb4eec9355 add explicit 2025-06-21 12:02:44 +02:00
advplyr
28404f37b8 Merge pull request #4422 from advplyr/podcast_episode_duration
Show duration in episode view modal & episode feed modal
2025-06-19 17:35:36 -05:00
advplyr
7b92c15a46 Include durationSeconds on RSS podcast episode parsed from duration 2025-06-19 17:28:21 -05:00
advplyr
c150ed4e98 Update view episode modal to include duration & episode feed modal to include duration & size 2025-06-19 17:14:56 -05:00
advplyr
cb7632b216 Merge pull request #4419 from advplyr/episode-timestamps-clickable
Episode view modal makes timestamps in description clickable
2025-06-18 17:28:55 -05:00
advplyr
b8849677de Episode view modal makes timestamps in description clickable 2025-06-18 17:20:36 -05:00
advplyr
9bf8d7de11 Fix server crash when FantLab provider request times out #4410 2025-06-17 17:21:21 -05:00
advplyr
6634ce8fd4 Merge pull request #4417 from advplyr/book_author_secondary_sort_title
Update book library secondary title sort to use title ignore prefixes
2025-06-17 16:40:59 -05:00
advplyr
9d4303ef7b Update book library secondary title sort to use title ignore prefixes #4414 2025-06-17 16:25:30 -05:00
advplyr
1f7be58124 Fix database cleanup query pulling duplicate mediaProgresses 2025-06-16 17:50:53 -05:00
advplyr
6b8b27b04f Merge pull request #4413 from HadrienPatte/nusqlite3-path
Make `NUSQLITE3_PATH` build arg configurable
2025-06-16 17:22:21 -05:00
Hadrien Patte
ba4061e5a4 Make NUSQLITE3_PATH build arg configurable 2025-06-16 23:03:02 +02:00
advplyr
693dc00fa3 Update local session sync logs to help debug sync errors 2025-06-15 17:21:47 -05:00
advplyr
f3f5f3b9bd Version bump v2.25.1 2025-06-14 17:57:19 -05:00
advplyr
b515c6c746 Remove mediaProgresses duplicate check 2025-06-14 17:56:35 -05:00
advplyr
35e196238a Version bump v2.25.0 2025-06-14 17:18:53 -05:00
advplyr
2dc93258f1 Merge pull request #4364 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-06-13 17:32:53 -05:00
thehijacker
5123f7d240 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1107 of 1107 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2025-06-14 00:29:31 +02:00
Usama Khalil
06d3bd76a8 Translated using Weblate (Arabic)
Currently translated at 100.0% (1107 of 1107 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ar/
2025-06-14 00:29:31 +02:00
Ivan Smoliakov
52196afd99 Translated using Weblate (Russian)
Currently translated at 100.0% (1107 of 1107 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-06-14 00:29:30 +02:00
ugyes
3e44ee6f50 Translated using Weblate (Hungarian)
Currently translated at 100.0% (1107 of 1107 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2025-06-14 00:29:29 +02:00
Максим Горпиніч
9841826e10 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1107 of 1107 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-06-14 00:29:29 +02:00
Dawid Kuźnicki
def93d18ec Translated using Weblate (Polish)
Currently translated at 76.9% (850 of 1105 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-06-14 00:29:28 +02:00
Rekentek
387a3d05b4 Translated using Weblate (Dutch)
Currently translated at 98.5% (1089 of 1105 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/
2025-06-14 00:29:28 +02:00
Daniel Schosser
398d04fc08 Translated using Weblate (German)
Currently translated at 100.0% (1105 of 1105 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-06-14 00:29:27 +02:00
David Havndrup Munch
c5e5e516af Translated using Weblate (Danish)
Currently translated at 98.9% (1093 of 1105 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2025-06-14 00:29:27 +02:00
Plazec
1c6f99b876 Translated using Weblate (Czech)
Currently translated at 99.7% (1102 of 1105 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2025-06-14 00:29:26 +02:00
Grzegorz Orlowski
d0af82e71a Translated using Weblate (Polish)
Currently translated at 76.9% (850 of 1105 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-06-14 00:29:25 +02:00
Usama Khalil
76e7616439 Translated using Weblate (Arabic)
Currently translated at 100.0% (1105 of 1105 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ar/
2025-06-14 00:29:25 +02:00
max grakov
fe99a269bc Translated using Weblate (Russian)
Currently translated at 100.0% (1105 of 1105 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-06-14 00:29:24 +02:00
thehijacker
5315f65023 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1105 of 1105 strings)

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

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-06-14 00:29:23 +02:00
Anders Norman
204ac4f204 Translated using Weblate (Norwegian Bokmål)
Currently translated at 92.6% (1024 of 1105 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2025-06-14 00:29:22 +02:00
Arieh Kellermann
accd5d1096 Translated using Weblate (German)
Currently translated at 100.0% (1105 of 1105 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-06-14 00:29:22 +02:00
advplyr
5025c6a3ea Merge pull request #4383 from JKubovy/improve-podcast-episode-search
Use fuse.js for podcast episode search
2025-06-13 17:29:13 -05:00
advplyr
6d0d1415e4 Add fuse.basic.min.js in libs instead of full npm package, use lower threshold for quick matching 2025-06-13 17:23:24 -05:00
advplyr
514f5c2409 Merge pull request #4394 from Vito0912/feat/addISBNAudible
Added the ISBN for Audible providers (returned data)
2025-06-13 16:21:32 -05:00
advplyr
2cc58b2c8a Merge pull request #4404 from advplyr/podcast_useragents
Update podcast episode downloads to have a fallback user agent string
2025-06-12 17:40:42 -05:00
advplyr
777a055fcd Update podcast episode downloads to have a fallback user agent string 2025-06-12 17:31:12 -05:00
advplyr
b45085d2d6 Update podcast episode download user agent to fix #4401 2025-06-12 17:19:24 -05:00
advplyr
22f6e86a12 Fix pathexists filepath back to posix 2025-06-11 16:37:07 -05:00
advplyr
dc6783ea76 Merge pull request #4398 from advplyr/pathexists_user_access
Update pathexists endpoint to check user has access to library
2025-06-11 16:31:14 -05:00
advplyr
a6f10ca48e Update upload endpoint to check user has access to library 2025-06-11 16:14:51 -05:00
advplyr
aac01d6d9a Update pathexists endpoint to check user has access to library 2025-06-11 16:04:18 -05:00
Vito0912
a617994207 added isbn 2025-06-11 08:12:23 +02:00
advplyr
7a33a412fc Merge pull request #4393 from advplyr/fix_pathexists_join
Fix filesystem pathexists path join
2025-06-10 17:20:23 -05:00
Jan Kubovy
eda7036f70 Use fuse.js for podcast episode search
Replace levenshtein distance with fuse.js fuzzy searching library. Search in episode's title and subtitle
2025-06-06 10:43:52 +00:00
36 changed files with 424 additions and 133 deletions

View File

@@ -57,7 +57,7 @@ WORKDIR /app
# Copy compiled frontend and server from build stages
COPY --from=build-client /client/dist /app/client/dist
COPY --from=build-server /server /app
COPY --from=build-server /usr/local/lib/nusqlite3 /usr/local/lib/nusqlite3
COPY --from=build-server ${NUSQLITE3_PATH} ${NUSQLITE3_PATH}
EXPOSE 80

View File

@@ -94,6 +94,9 @@ export default {
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
userCanAccessExplicitContent() {
return this.$store.getters['user/getUserCanAccessExplicitContent']
},
libraryMediaType() {
return this.$store.getters['libraries/getCurrentLibraryMediaType']
},
@@ -239,6 +242,15 @@ export default {
sublist: false
}
]
if (this.userCanAccessExplicitContent) {
items.push({
text: this.$strings.LabelExplicit,
value: 'explicit',
sublist: false
})
}
if (this.userIsAdminOrUp) {
items.push({
text: this.$strings.LabelShareOpen,
@@ -249,7 +261,7 @@ export default {
return items
},
podcastItems() {
return [
const items = [
{
text: this.$strings.LabelAll,
value: 'all'
@@ -283,6 +295,16 @@ export default {
sublist: false
}
]
if (this.userCanAccessExplicitContent) {
items.push({
text: this.$strings.LabelExplicit,
value: 'explicit',
sublist: false
})
}
return items
},
selectItems() {
if (this.isSeries) return this.seriesItems

View File

@@ -35,7 +35,14 @@
<widgets-podcast-type-indicator :type="episode.episodeType" />
</div>
<p v-if="episode.subtitle" class="mb-1 text-sm text-gray-300 line-clamp-2">{{ episode.subtitle }}</p>
<p class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
<div class="flex items-center space-x-2">
<!-- published -->
<p class="text-xs text-gray-300 w-40">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
<!-- duration -->
<p v-if="episode.durationSeconds && !isNaN(episode.durationSeconds)" class="text-xs text-gray-300 min-w-28">{{ $strings.LabelDuration }}: {{ $elapsedPretty(episode.durationSeconds) }}</p>
<!-- size -->
<p v-if="episode.enclosure?.length && !isNaN(episode.enclosure.length) && Number(episode.enclosure.length) > 0" class="text-xs text-gray-300">{{ $strings.LabelSize }}: {{ $bytesPretty(Number(episode.enclosure.length)) }}</p>
</div>
</div>
</div>
</div>

View File

@@ -16,7 +16,7 @@
</div>
</div>
<p dir="auto" class="text-lg font-semibold mb-6">{{ title }}</p>
<div v-if="description" dir="auto" class="default-style less-spacing" v-html="description" />
<div v-if="description" dir="auto" class="default-style less-spacing" @click="handleDescriptionClick" v-html="description" />
<p v-else class="mb-2">{{ $strings.MessageNoDescription }}</p>
<div class="w-full h-px bg-white/5 my-4" />
@@ -34,6 +34,12 @@
{{ audioFileSize }}
</p>
</div>
<div class="grow">
<p class="font-semibold text-xs mb-1">{{ $strings.LabelDuration }}</p>
<p class="mb-2 text-xs">
{{ audioFileDuration }}
</p>
</div>
</div>
</div>
</modals-modal>
@@ -68,7 +74,7 @@ export default {
return this.episode.title || 'No Episode Title'
},
description() {
return this.episode.description || ''
return this.parseDescription(this.episode.description || '')
},
media() {
return this.libraryItem?.media || {}
@@ -90,11 +96,49 @@ export default {
return this.$bytesPretty(size)
},
audioFileDuration() {
const duration = this.episode.duration || 0
return this.$elapsedPretty(duration)
},
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
}
},
methods: {},
methods: {
handleDescriptionClick(e) {
if (e.target.matches('span.time-marker')) {
const time = parseInt(e.target.dataset.time)
if (!isNaN(time)) {
this.$eventBus.$emit('play-item', {
episodeId: this.episodeId,
libraryItemId: this.libraryItem.id,
startTime: time
})
}
e.preventDefault()
}
},
parseDescription(description) {
const timeMarkerLinkRegex = /<a href="#([^"]*?\b\d{1,2}:\d{1,2}(?::\d{1,2})?)">(.*?)<\/a>/g
const timeMarkerRegex = /\b\d{1,2}:\d{1,2}(?::\d{1,2})?\b/g
function convertToSeconds(time) {
const timeParts = time.split(':').map(Number)
return timeParts.reduce((acc, part, index) => acc * 60 + part, 0)
}
return description
.replace(timeMarkerLinkRegex, (match, href, displayTime) => {
const time = displayTime.match(timeMarkerRegex)[0]
const seekTimeInSeconds = convertToSeconds(time)
return `<span class="time-marker cursor-pointer text-blue-400 hover:text-blue-300" data-time="${seekTimeInSeconds}">${displayTime}</span>`
})
.replace(timeMarkerRegex, (match) => {
const seekTimeInSeconds = convertToSeconds(match)
return `<span class="time-marker cursor-pointer text-blue-400 hover:text-blue-300" data-time="${seekTimeInSeconds}">${match}</span>`
})
}
},
mounted() {}
}
</script>

View File

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

View File

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

View File

@@ -58,6 +58,9 @@ export const getters = {
getUserCanAccessAllLibraries: (state) => {
return !!state.user?.permissions?.accessAllLibraries
},
getUserCanAccessExplicitContent: (state) => {
return !!state.user?.permissions?.accessExplicitContent
},
getLibrariesAccessible: (state, getters) => {
if (!state.user) return []
if (getters.getUserCanAccessAllLibraries) return []

View File

@@ -514,7 +514,7 @@
"LabelPublishers": "الناشرون",
"LabelRSSFeedCustomOwnerEmail": "البريد الالكتروني المخصص للمالك",
"LabelRSSFeedCustomOwnerName": "الاسم المخصص للمالك",
"LabelRSSFeedOpen": "فتح تغذية RSS",
"LabelRSSFeedOpen": "موجز RSS مفتوح",
"LabelRSSFeedPreventIndexing": "منع الفهرسة",
"LabelRSSFeedSlug": "اسم تعريف تغذية RSS",
"LabelRSSFeedURL": "رابط تغذية RSS",
@@ -918,6 +918,8 @@
"NotificationOnBackupCompletedDescription": "يتم تشغيله عند اكتمال النسخ الاحتياطي",
"NotificationOnBackupFailedDescription": "يتم تشغيله عند فشل النسخ الاحتياطي",
"NotificationOnEpisodeDownloadedDescription": "يتم تشغيله عند تنزيل حلقة بودكاست تلقائيًا",
"NotificationOnRSSFeedDisabledDescription": "يتم تشغيله عندما يتم تعطيل تنزيلات الحلقة التلقائية بسبب الكثير من المحاولات الفاشلة",
"NotificationOnRSSFeedFailedDescription": "يتم تشغيله عند فشل طلب تغذية RSS في تنزيل حلقة تلقائية",
"NotificationOnTestDescription": "حدث لاختبار نظام الإشعارات",
"PlaceholderNewCollection": "اسم المجموعة الجديدة",
"PlaceholderNewFolderPath": "مسار المجلد الجديد",

View File

@@ -154,7 +154,7 @@
"HeaderListeningSessions": "Poslechové relace",
"HeaderListeningStats": "Statistiky poslechu",
"HeaderLogin": "Přihlásit",
"HeaderLogs": "Záznamy",
"HeaderLogs": "Logy",
"HeaderManageGenres": "Spravovat žánry",
"HeaderManageTags": "Spravovat štítky",
"HeaderMapDetails": "Podrobnosti mapování",
@@ -177,6 +177,7 @@
"HeaderPlaylist": "Seznam skladeb",
"HeaderPlaylistItems": "Položky seznamu přehrávání",
"HeaderPodcastsToAdd": "Podcasty k přidání",
"HeaderPresets": "Předvolba",
"HeaderPreviewCover": "Náhled obálky",
"HeaderRSSFeedGeneral": "Podrobnosti o RSS",
"HeaderRSSFeedIsOpen": "Informační kanál RSS je otevřený",
@@ -513,9 +514,9 @@
"LabelPublishers": "Vydavatelé",
"LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka",
"LabelRSSFeedCustomOwnerName": "Vlastní jméno vlastníka",
"LabelRSSFeedOpen": "Otevření RSS kanálu",
"LabelRSSFeedOpen": "RSS kanál otevřen",
"LabelRSSFeedPreventIndexing": "Zabránit indexování",
"LabelRSSFeedSlug": "RSS kanál Slug",
"LabelRSSFeedSlug": "Klíčové slovo kanálu RSS",
"LabelRSSFeedURL": "URL RSS kanálu",
"LabelRandomly": "Náhodně",
"LabelReAddSeriesToContinueListening": "Znovu přidat sérii k pokračování poslechu",
@@ -530,6 +531,7 @@
"LabelReleaseDate": "Datum vydání",
"LabelRemoveAllMetadataAbs": "Odebrat všechny soubory metadata.abs",
"LabelRemoveAllMetadataJson": "Smazat všechny soubory metadata.json",
"LabelRemoveAudibleBranding": "Odebrat úvod a závěr Audible z kapitol",
"LabelRemoveCover": "Odstranit obálku",
"LabelRemoveMetadataFile": "Odstranit soubory metadat ve složkách položek knihovny",
"LabelRemoveMetadataFileHelp": "Odstraníte všechny soubory metadata.json a metadata.abs ve svých složkách {0}.",
@@ -549,7 +551,7 @@
"LabelSeries": "Série",
"LabelSeriesName": "Název série",
"LabelSeriesProgress": "Průběh série",
"LabelServerLogLevel": "Úroveň protokolu serveru",
"LabelServerLogLevel": "Úroveň Logování serveru",
"LabelServerYearReview": "Přehled roku na serveru ({0})",
"LabelSetEbookAsPrimary": "Nastavit jako primární",
"LabelSetEbookAsSupplementary": "Nastavit jako doplňkové",
@@ -706,6 +708,7 @@
"MessageAddToPlayerQueue": "Přidat do fronty přehrávače",
"MessageAppriseDescription": "Abyste mohli používat tuto funkci, musíte mít spuštěnou instanci <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> nebo API, které bude zpracovávat stejné požadavky. <br />Adresa URL API Apprise by měla být úplná URL cesta pro odeslání oznámení, např. pokud je vaše instance API obsluhována na adrese <code>http://192.168.1.1:8337</code> pak byste měli zadat <code>http://192.168.1.1:8337/notify</code>.",
"MessageAsinCheck": "Ujistěte se, že používáte ASIN ze správného regionu Audible a ne z Amazonu.",
"MessageAuthenticationOIDCChangesRestart": "Po uložení restartujte server, aby se změny OIDC použily.",
"MessageBackupsDescription": "Zálohy zahrnují uživatele, průběh uživatele, podrobnosti o položkách knihovny, nastavení serveru a obrázky uložené v <code>/metadata/items</code> a <code>/metadata/authors</code>. Zálohy <strong>ne</strong> zahrnují všechny soubory uložené ve složkách knihovny.",
"MessageBackupsLocationEditNote": "Poznámka: Změna umístění záloh nepřesune ani nezmění existující zálohy",
"MessageBackupsLocationNoEditNote": "Poznámka: Umístění záloh je nastavené z proměnných prostředí a nelze zde změnit.",
@@ -787,7 +790,7 @@
"MessageJoinUsOn": "Přidejte se k nám",
"MessageLoading": "Načítá se...",
"MessageLoadingFolders": "Načítám složky...",
"MessageLogsDescription": "Protokoly se ukládají do souborů JSON v <code>/metadata/logs</code>. Protokoly o pádech jsou uloženy v <code>/metadata/logs/crash_logs.txt</code>.",
"MessageLogsDescription": "Logy se ukládají do souborů JSON v <code>/metadata/logs</code>. Logy o pádech jsou uloženy v <code>/metadata/logs/crash_logs.txt</code>.",
"MessageM4BFailed": "M4B se nezdařil!",
"MessageM4BFinished": "M4B dokončen!",
"MessageMapChapterTitles": "Mapování názvů kapitol ke stávajícím kapitolám audioknihy bez úpravy časových razítek",
@@ -811,11 +814,11 @@
"MessageNoEpisodes": "Žádné epizody",
"MessageNoFoldersAvailable": "Nejsou k dispozici žádné složky",
"MessageNoGenres": "Žádné žánry",
"MessageNoIssues": "Žádné výtisk",
"MessageNoIssues": "Žádné problémy",
"MessageNoItems": "Žádné položky",
"MessageNoItemsFound": "Nebyly nalezeny žádné položky",
"MessageNoListeningSessions": "Žádné poslechové relace",
"MessageNoLogs": "Žádné protokoly",
"MessageNoLogs": "Žádné logy",
"MessageNoMediaProgress": "Žádný průběh médií",
"MessageNoNotifications": "Žádná oznámení",
"MessageNoPodcastFeed": "Neplatný podcast: Žádný kanál",
@@ -853,6 +856,7 @@
"MessageScheduleRunEveryWeekdayAtTime": "Spusť každý {0} v {1}",
"MessageSearchResultsFor": "Výsledky hledání pro",
"MessageSelected": "{0} vybráno",
"MessageSeriesSequenceCannotContainSpaces": "Sekvence série nesmí obsahovat mezery",
"MessageServerCouldNotBeReached": "Server je nedostupný",
"MessageSetChaptersFromTracksDescription": "Nastavit kapitoly jako kapitolu a název kapitoly jako název zvukového souboru",
"MessageShareExpirationWillBe": "Expiruje <strong>{0}</strong>",
@@ -971,6 +975,8 @@
"ToastCachePurgeFailed": "Nepodařilo se vyčistit mezipaměť",
"ToastCachePurgeSuccess": "Vyrovnávací paměť úspěšně vyčištěna",
"ToastChaptersHaveErrors": "Kapitoly obsahují chyby",
"ToastChaptersInvalidShiftAmountLast": "Nesprávná délka posunu. Čas začátku poslední kapitoly by přesáhl dobu trvání této audioknihy.",
"ToastChaptersInvalidShiftAmountStart": "Nesprávná délka posunu. První kapitola by měla nulovou nebo zápornou délku a byla by přepsána druhou kapitolou. Zvětšete počáteční délku druhé kapitoly.",
"ToastChaptersMustHaveTitles": "Kapitoly musí mít názvy",
"ToastChaptersRemoved": "Kapitoly odstraněny",
"ToastChaptersUpdated": "Kapitola aktualizována",
@@ -1091,7 +1097,7 @@
"ToastUnlinkOpenIdFailed": "Chyba při odpárování uživatele z OpenID",
"ToastUnlinkOpenIdSuccess": "Uživatel odpárován z uživatele z OpenID",
"ToastUploaderFilepathExistsError": "Soubor \"{0}\" na serveru již existuje",
"ToastUploaderItemExistsInSubdirectoryError": "Položka \"{0}\" používá podsložku nahrávané cesty.",
"ToastUploaderItemExistsInSubdirectoryError": "Položka \"{0}\" používá podadresář cesty pro nahrání.",
"ToastUserDeleteFailed": "Nepodařilo se smazat uživatele",
"ToastUserDeleteSuccess": "Uživatel smazán",
"ToastUserPasswordChangeSuccess": "Heslo bylo změněno úspěšně",

View File

@@ -513,7 +513,7 @@
"LabelPublishers": "Forlag",
"LabelRSSFeedCustomOwnerEmail": "Brugerdefineret ejerens e-mail",
"LabelRSSFeedCustomOwnerName": "Brugerdefineret ejerens navn",
"LabelRSSFeedOpen": "Åben RSS-feed",
"LabelRSSFeedOpen": "RSS-feed åbent",
"LabelRSSFeedPreventIndexing": "Forhindrer indeksering",
"LabelRSSFeedSlug": "RSS-feed-slug",
"LabelRSSFeedURL": "RSS-feed-URL",

View File

@@ -514,7 +514,7 @@
"LabelPublishers": "Herausgeber",
"LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail",
"LabelRSSFeedCustomOwnerName": "Benutzerdefinierter Name des Eigentümers",
"LabelRSSFeedOpen": "RSS Feed offen",
"LabelRSSFeedOpen": "RSS Feed öffnen",
"LabelRSSFeedPreventIndexing": "Indizierung verhindern",
"LabelRSSFeedSlug": "RSS-Feed-Schlagwort",
"LabelRSSFeedURL": "RSS-Feed-URL",
@@ -858,7 +858,7 @@
"MessageSelected": "{0} ausgewählt",
"MessageSeriesSequenceCannotContainSpaces": "Serie Abfolge kann keine Leerzeichen enthalten",
"MessageServerCouldNotBeReached": "Server kann nicht erreicht werden",
"MessageSetChaptersFromTracksDescription": "Kaitelerstellung basiert auf den existierenden einzelnen Audiodateien. Pro existierende Audiodatei wird 1 Kapitel erstellt, wobei deren Kapitelname aus dem Audiodateinamen extrahiert wird",
"MessageSetChaptersFromTracksDescription": "Kapitelerstellung basiert auf den existierenden einzelnen Audiodateien. Pro existierende Audiodatei wird 1 Kapitel erstellt, wobei deren Kapitelname aus dem Audiodateinamen extrahiert wird",
"MessageShareExpirationWillBe": "Läuft am <strong>{0}</strong> ab",
"MessageShareExpiresIn": "Läuft in {0} ab",
"MessageShareURLWillBe": "Der Freigabe Link wird <strong>{0}</strong> sein",

View File

@@ -11,7 +11,7 @@
"ButtonAuthors": "Szerzők",
"ButtonBack": "Vissza",
"ButtonBatchEditPopulateFromExisting": "Létezőből feltöltés",
"ButtonBatchEditPopulateMapDetails": "",
"ButtonBatchEditPopulateMapDetails": "A térkép részleteinek feltöltése",
"ButtonBrowseForFolder": "Mappa keresése",
"ButtonCancel": "Mégse",
"ButtonCancelEncode": "Kódolás megszakítása",
@@ -177,6 +177,7 @@
"HeaderPlaylist": "Lejátszási lista",
"HeaderPlaylistItems": "Lejátszási lista elemek",
"HeaderPodcastsToAdd": "Hozzáadandó podcastok",
"HeaderPresets": "Alapbeállítások",
"HeaderPreviewCover": "Borító előnézete",
"HeaderRSSFeedGeneral": "RSS részletek",
"HeaderRSSFeedIsOpen": "RSS hírcsatorna nyitva van",
@@ -219,6 +220,7 @@
"LabelAccountTypeAdmin": "Adminisztrátor",
"LabelAccountTypeGuest": "Vendég",
"LabelAccountTypeUser": "Felhasználó",
"LabelActivities": "Tevékenységek",
"LabelActivity": "Tevékenység",
"LabelAddToCollection": "Hozzáadás a gyűjteményhez",
"LabelAddToCollectionBatch": "{0} könyv hozzáadása a gyűjteményhez",
@@ -228,6 +230,7 @@
"LabelAddedDate": "{0} Hozzáadva",
"LabelAdminUsersOnly": "Csak admin felhasználók",
"LabelAll": "Összes",
"LabelAllEpisodesDownloaded": "Minden epizód letöltve",
"LabelAllUsers": "Minden felhasználó",
"LabelAllUsersExcludingGuests": "Minden felhasználó, vendégek kivételével",
"LabelAllUsersIncludingGuests": "Minden felhasználó, beleértve a vendégeket is",
@@ -251,7 +254,7 @@
"LabelBackToUser": "Vissza a felhasználóhoz",
"LabelBackupAudioFiles": "Audiófájlok biztonsági mentése",
"LabelBackupLocation": "Biztonsági másolat helye",
"LabelBackupsEnableAutomaticBackups": "Automatikus biztonsági másolatok engedélyezése",
"LabelBackupsEnableAutomaticBackups": "Automatikus biztonsági másolatok",
"LabelBackupsEnableAutomaticBackupsHelp": "Biztonsági másolatok mentése a /metadata/backups mappába",
"LabelBackupsMaxBackupSize": "Maximális biztonsági másolat méret (GB-ban) (0-tól végtelenig)",
"LabelBackupsMaxBackupSizeHelp": "A rossz konfiguráció elleni védelem érdekében a biztonsági másolatok meghiúsulnak, ha meghaladják a beállított méretet.",
@@ -283,6 +286,7 @@
"LabelContinueSeries": "Sorozat folytatása",
"LabelCover": "Borító",
"LabelCoverImageURL": "Borítókép URL",
"LabelCoverProvider": "Borító Szolgáltató",
"LabelCreatedAt": "Létrehozás ideje",
"LabelCronExpression": "Cron kifejezés",
"LabelCurrent": "Jelenlegi",
@@ -391,7 +395,8 @@
"LabelIntervalEvery6Hours": "Minden 6 órában",
"LabelIntervalEveryDay": "Minden nap",
"LabelIntervalEveryHour": "Minden órában",
"LabelInvert": "Megfordítás",
"LabelIntervalEveryMinute": "Minden percben",
"LabelInvert": "Inverz",
"LabelItem": "Elem",
"LabelJumpBackwardAmount": "Visszafelé ugrás mennyisége",
"LabelJumpForwardAmount": "Előre ugrás mennyisége",
@@ -486,6 +491,7 @@
"LabelPersonalYearReview": "Az éved összefoglalása ({0})",
"LabelPhotoPathURL": "Fénykép útvonal/URL",
"LabelPlayMethod": "Lejátszási módszer",
"LabelPlaybackRateIncrementDecrement": "Lejátszási sebesség növelés/csökkentés értéke",
"LabelPlayerChapterNumberMarker": "{0} a {1} -ből",
"LabelPlaylists": "Lejátszási listák",
"LabelPodcast": "Podcast",
@@ -508,7 +514,7 @@
"LabelPublishers": "Kiadók",
"LabelRSSFeedCustomOwnerEmail": "Egyéni tulajdonos e-mail",
"LabelRSSFeedCustomOwnerName": "Egyéni tulajdonos neve",
"LabelRSSFeedOpen": "RSS hírcsatorna nyitva",
"LabelRSSFeedOpen": "RSS-hírcsatorna nyitva",
"LabelRSSFeedPreventIndexing": "Indexelés megakadályozása",
"LabelRSSFeedSlug": "RSS hírcsatorna slug",
"LabelRSSFeedURL": "RSS hírcsatorna URL",
@@ -525,6 +531,7 @@
"LabelReleaseDate": "Megjelenés dátuma",
"LabelRemoveAllMetadataAbs": "Az összes metadata.abs fájl eltávolítása",
"LabelRemoveAllMetadataJson": "Az összes metadata.json fájl eltávolítása",
"LabelRemoveAudibleBranding": "Audible intro és outro eltávolítása a fejezetekből",
"LabelRemoveCover": "Borító eltávolítása",
"LabelRemoveMetadataFile": "Metaadatfájlok eltávolítása a könyvtár elemek mappáiból",
"LabelRemoveMetadataFileHelp": "A metadata.json és metadata.abs fájlokat eltávolítása a {0} mappáidból.",
@@ -554,6 +561,8 @@
"LabelSettingsBookshelfViewHelp": "Skeuomorfikus dizájn fa polcokkal",
"LabelSettingsChromecastSupport": "Chromecast támogatás",
"LabelSettingsDateFormat": "Dátumformátum",
"LabelSettingsEnableWatcher": "Változások automatikus vizsgálata a könyvtárakban",
"LabelSettingsEnableWatcherForLibrary": "Változások automatikus vizsgálata a könyvtárban",
"LabelSettingsEnableWatcherHelp": "Engedélyezi az automatikus elem hozzáadás/frissítés funkciót, amikor fájlváltozásokat észlel. *Szerver újraindítása szükséges",
"LabelSettingsEpubsAllowScriptedContent": "Szkriptelt tartalmak engedélyezése epub-okban",
"LabelSettingsEpubsAllowScriptedContentHelp": "Megengedi, hogy az epub fájlok szkripteket hajtsanak végre. Ezt a beállítást kikapcsolva ajánlott tartani, kivéve, ha megbízik az epub fájlok forrásában.",
@@ -597,6 +606,7 @@
"LabelSlug": "Rövid cím",
"LabelSortAscending": "Emelkedő",
"LabelSortDescending": "Csökkenő",
"LabelSortPubDate": "Rendezés megjelenés dátuma szerint",
"LabelStart": "Kezdés",
"LabelStartTime": "Kezdési idő",
"LabelStarted": "Elkezdődött",
@@ -697,12 +707,17 @@
"LabelYourProgress": "Haladásod",
"MessageAddToPlayerQueue": "Hozzáadás a lejátszó sorhoz",
"MessageAppriseDescription": "Ennek a funkció használatához futtatnia kell egy <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> példányt vagy egy olyan API-t, amely kezeli ezeket a kéréseket. <br />Az Apprise API URL-nek a teljes URL útvonalat kell tartalmaznia az értesítés elküldéséhez, például, ha az API példánya a <code>http://192.168.1.1:8337</code> címen szolgáltatva, akkor <code>http://192.168.1.1:8337/notify</code> értéket kell megadnia.",
"MessageAsinCheck": "Győződjön meg róla, hogy az ASIN-t a megfelelő Audible régióból használja, nem az Amazonból.",
"MessageAuthenticationOIDCChangesRestart": "A mentés után indítsa újra a szervert az OIDC módosítások alkalmazásához.",
"MessageBackupsDescription": "A biztonsági másolatok tartalmazzák a felhasználókat, a felhasználói haladást, a könyvtári elem részleteit, a szerver beállításait és a képeket, amelyek a <code>/metadata/items</code> és <code>/metadata/authors</code> mappákban vannak tárolva. A biztonsági másolatok <strong>nem</strong> tartalmazzák a könyvtári mappákban tárolt fájlokat.",
"MessageBackupsLocationEditNote": "Megjegyzés: A biztonsági mentés helyének frissítése nem mozgatja vagy módosítja a meglévő biztonsági mentéseket",
"MessageBackupsLocationNoEditNote": "Megjegyzés: A biztonsági mentés helye egy környezeti változóval van beállítva, és itt nem módosítható.",
"MessageBackupsLocationPathEmpty": "A biztonsági mentés helyének elérési útvonala nem lehet üres",
"MessageBatchEditPopulateMapDetailsAllHelp": "Az engedélyezett mezők feltöltése az összes elem adatával. A több értéket tartalmazó mezők összevonásra kerülnek",
"MessageBatchEditPopulateMapDetailsItemHelp": "A térkép engedélyezett adatmezőinek feltöltése ezen elem adataival",
"MessageBatchQuickMatchDescription": "A Gyors egyeztetés megpróbálja hozzáadni a hiányzó borítókat és metaadatokat a kiválasztott elemekhez. Engedélyezze az alábbi opciókat, hogy a Gyors egyeztetés felülírhassa a meglévő borítókat és/vagy metaadatokat.",
"MessageBookshelfNoCollections": "Még nem készített gyűjteményeket",
"MessageBookshelfNoCollectionsHelp": "A gyűjtemények nyilvánosak. Minden, a könyvtárhoz hozzáféréssel rendelkező felhasználó láthatja őket.",
"MessageBookshelfNoRSSFeeds": "Nincsenek nyitott RSS hírcsatornák",
"MessageBookshelfNoResultsForFilter": "Nincs eredmény a \"{0}: {1}\" szűrőre",
"MessageBookshelfNoResultsForQuery": "Nincs eredmény a lekérdezéshez",
@@ -712,6 +727,7 @@
"MessageChapterErrorStartGteDuration": "Érvénytelen kezdési idő, kevesebbnek kell lennie, mint a hangoskönyv időtartama",
"MessageChapterErrorStartLtPrev": "Érvénytelen kezdési idő, nagyobbnak kell lennie, mint az előző fejezet kezdési ideje",
"MessageChapterStartIsAfter": "A fejezet kezdete a hangoskönyv végét követi",
"MessageChaptersNotFound": "Fejezetek nem találhatók",
"MessageCheckingCron": "Cron ellenőrzése...",
"MessageConfirmCloseFeed": "Biztosan be szeretné zárni ezt a hírcsatornát?",
"MessageConfirmDeleteBackup": "Biztosan törölni szeretné a(z) {0} biztonsági másolatot?",
@@ -768,6 +784,7 @@
"MessageForceReScanDescription": "minden fájlt újra szkennel, mint egy friss szkennelés. Az audiofájlok ID3 címkéi, OPF fájlok és szövegfájlok újként lesznek szkennelve.",
"MessageImportantNotice": "Fontos közlemény!",
"MessageInsertChapterBelow": "Fejezet beszúrása alulra",
"MessageInvalidAsin": "Érvénytelen ASIN",
"MessageItemsSelected": "{0} kiválasztott elem",
"MessageItemsUpdated": "{0} frissített elem",
"MessageJoinUsOn": "Csatlakozzon hozzánk a",
@@ -813,6 +830,7 @@
"MessageNoTasksRunning": "Nincsenek futó feladatok",
"MessageNoUpdatesWereNecessary": "Nem volt szükség frissítésekre",
"MessageNoUserPlaylists": "Nincsenek felhasználói lejátszási listák",
"MessageNoUserPlaylistsHelp": "A lejátszási listák személyesek. Csak az a felhasználó láthatja őket, aki létrehozta őket.",
"MessageNotYetImplemented": "Még nem implementált",
"MessageOpmlPreviewNote": "Megjegyzés: Ez egy előnézeti kép az elemzett OPML fájlról. A podcast tényleges címe az RSS hírcsatornából származik.",
"MessageOr": "vagy",
@@ -835,8 +853,10 @@
"MessageRestoreBackupConfirm": "Biztosan vissza szeretné állítani a biztonsági másolatot, amely ekkor készült:",
"MessageRestoreBackupWarning": "A biztonsági mentés visszaállítása felülírja az egész adatbázist, amely a /config mappában található, valamint a borítóképeket a /metadata/items és /metadata/authors mappákban.<br /><br />A biztonsági mentések nem módosítják a könyvtár mappáiban található fájlokat. Ha engedélyezte a szerverbeállításokat a borítóképek és a metaadatok könyvtármappákban való tárolására, akkor ezek nem kerülnek biztonsági mentésre vagy felülírásra.<br /><br />A szerver használó összes kliens automatikusan frissül.",
"MessageScheduleLibraryScanNote": "A legtöbb felhasználó számára ajánlott ezt a funkciót kikapcsolva hagyni, és engedélyezni a mappafigyelő beállítást. A mappafigyelő automatikusan észleli a könyvtári mappák változásait. A mappafigyelő nem működik minden fájlrendszernél (mint például az NFS), ezért helyette ütemezett könyvtárellenőrzéseket lehet használni.",
"MessageScheduleRunEveryWeekdayAtTime": "Futás minden {1} óra {0}-kor",
"MessageSearchResultsFor": "Keresési eredmények",
"MessageSelected": "{0} kiválasztva",
"MessageSeriesSequenceCannotContainSpaces": "Sorozat sorrend nem tartalmazhat szóközt",
"MessageServerCouldNotBeReached": "A szervert nem lehet elérni",
"MessageSetChaptersFromTracksDescription": "Fejezetek beállítása minden egyes hangfájlt egy fejezetként használva, és a fejezet címét a hangfájl neveként",
"MessageShareExpirationWillBe": "A lejárat: <strong>{0}</strong>",
@@ -861,6 +881,7 @@
"MessageTaskNoFilesToScan": "Nincs beolvasandó fájl",
"MessageTaskOpmlImport": "OPML import",
"MessageTaskOpmlImportDescription": "Podcastok létrehozása {0} RSS hírcsatornából",
"MessageTaskOpmlImportFeed": "OPML import hírcsatorna",
"MessageTaskOpmlImportFeedDescription": "RSS feed „{0}” importálása",
"MessageTaskOpmlImportFeedFailed": "Nem sikerült letölteni a podcast feedet",
"MessageTaskOpmlImportFeedPodcastDescription": "„{0}” podcast létrehozása",
@@ -869,6 +890,7 @@
"MessageTaskOpmlImportFinished": "{0} podcast hozzáadva",
"MessageTaskOpmlParseFailed": "Az OPML fájl elemzése nem sikerült",
"MessageTaskOpmlParseFastFail": "Érvénytelen OPML fájl: <opml> tag nem található VAGY nem találtak <outline> taget",
"MessageTaskOpmlParseNoneFound": "Nem található feed az OPML fájlban",
"MessageTaskScanItemsAdded": "{0} hozzáadva",
"MessageTaskScanItemsMissing": "{0} hiányzik",
"MessageTaskScanItemsUpdated": "{0} frissítve",
@@ -896,6 +918,8 @@
"NotificationOnBackupCompletedDescription": "A biztonsági mentés befejezésekor aktiválódik",
"NotificationOnBackupFailedDescription": "A biztonsági mentés sikertelensége esetén aktiválódik",
"NotificationOnEpisodeDownloadedDescription": "Egy podcast epizód automatikus letöltésekor aktiválódik",
"NotificationOnRSSFeedDisabledDescription": "Akkor lép működésbe, ha az automatikus epizódletöltés a túl sok sikertelen próbálkozás miatt letiltásra kerül",
"NotificationOnRSSFeedFailedDescription": "Akkor aktiválódik, ha az RSS feed kérés sikertelen az automatikus epizódletöltésnél",
"NotificationOnTestDescription": "Esemény az értesítési rendszer teszteléséhez",
"PlaceholderNewCollection": "Új gyűjtemény neve",
"PlaceholderNewFolderPath": "Új mappa útvonala",
@@ -940,8 +964,11 @@
"ToastBackupRestoreFailed": "A biztonsági mentés visszaállítása sikertelen",
"ToastBackupUploadFailed": "A biztonsági mentés feltöltése sikertelen",
"ToastBackupUploadSuccess": "Biztonsági mentés feltöltve",
"ToastBatchApplyDetailsToItemsSuccess": "Tételekre alkalmazott részletek",
"ToastBatchDeleteFailed": "A tömeges törlés nem sikerült",
"ToastBatchDeleteSuccess": "Sikeres tömeges törlés",
"ToastBatchQuickMatchFailed": "Tömeges Gyors Egyeztetés sikertelen!",
"ToastBatchQuickMatchStarted": "{0} könyv Tömeges Gyors Egyeztetése elkezdődött!",
"ToastBatchUpdateFailed": "Kötegelt frissítés sikertelen",
"ToastBatchUpdateSuccess": "Kötegelt frissítés sikeres",
"ToastBookmarkCreateFailed": "Könyvjelző létrehozása sikertelen",
@@ -950,9 +977,12 @@
"ToastCachePurgeFailed": "A gyorsítótár törlése sikertelen",
"ToastCachePurgeSuccess": "A gyorsítótár sikeresen törölve",
"ToastChaptersHaveErrors": "A fejezetek hibákat tartalmaznak",
"ToastChaptersInvalidShiftAmountLast": "Érvénytelen eltolási érték. Az utolsó fejezet kezdési időpontja túlnyúlna a hangoskönyv időtartamán.",
"ToastChaptersInvalidShiftAmountStart": "Érvénytelen eltolási érték. Az első fejezet hossza nulla vagy negatív lenne, és a második fejezet felülírná. Növelje a második fejezet kezdő időtartamát.",
"ToastChaptersMustHaveTitles": "A fejezeteknek címekkel kell rendelkezniük",
"ToastChaptersRemoved": "Fejezetek eltávolítva",
"ToastChaptersUpdated": "Fejezetek frissítve",
"ToastCollectionItemsAddFailed": "A tétel(ek) hozzáadása gyűjteményhez sikertelen",
"ToastCollectionRemoveSuccess": "Gyűjtemény eltávolítva",
"ToastCollectionUpdateSuccess": "Gyűjtemény frissítve",
"ToastCoverUpdateFailed": "A borító frissítése nem sikerült",
@@ -967,6 +997,7 @@
"ToastEncodeCancelFailed": "A kódolás törlése sikertelen volt",
"ToastEncodeCancelSucces": "Kódolás törölve",
"ToastEpisodeDownloadQueueClearFailed": "Nem sikerült törölni a várólistát",
"ToastEpisodeDownloadQueueClearSuccess": "Epizód letöltési várólista törölve",
"ToastEpisodeUpdateSuccess": "{0} epizód frissítve",
"ToastErrorCannotShare": "Ezen az eszközön nem lehet natívan megosztani",
"ToastFailedToLoadData": "Sikertelen adatbetöltés",
@@ -974,6 +1005,7 @@
"ToastFailedToShare": "Nem sikerült megosztani",
"ToastFailedToUpdate": "Nem sikerült frissíteni",
"ToastInvalidImageUrl": "Érvénytelen a kép URL címe",
"ToastInvalidMaxEpisodesToDownload": "A letölthető epizódok száma érvénytelen",
"ToastInvalidUrl": "Érvénytelen URL",
"ToastItemCoverUpdateSuccess": "Elem borítója frissítve",
"ToastItemDeletedFailed": "Nem sikerült törölni az elemet",
@@ -1011,8 +1043,11 @@
"ToastNoUpdatesNecessary": "Nincs szükség frissítésre",
"ToastNotificationCreateFailed": "Értesítés létrehozása sikertelen",
"ToastNotificationDeleteFailed": "Értesítés törlése sikertelen",
"ToastNotificationFailedMaximum": "A sikertelen kísérletek maximális száma >= 0 kell, hogy legyen",
"ToastNotificationQueueMaximum": "Az értesítési sor maximális száma >= 0 kell, hogy legyen",
"ToastNotificationSettingsUpdateSuccess": "Értesítési beállítások frissítve",
"ToastNotificationTestTriggerFailed": "Nem sikerült a tesztértesítést elindítani",
"ToastNotificationTestTriggerSuccess": "Kiváltott tesztértesítés",
"ToastNotificationUpdateSuccess": "Értesítés frissítve",
"ToastPlaylistCreateFailed": "Lejátszási lista létrehozása sikertelen",
"ToastPlaylistCreateSuccess": "Lejátszási lista létrehozva",
@@ -1020,6 +1055,7 @@
"ToastPlaylistUpdateSuccess": "Lejátszási lista frissítve",
"ToastPodcastCreateFailed": "Podcast létrehozása sikertelen",
"ToastPodcastCreateSuccess": "A podcast sikeresen létrehozva",
"ToastPodcastGetFeedFailed": "Nem sikerült podcast feedet kapni",
"ToastPodcastNoEpisodesInFeed": "Nincsenek epizódok az RSS hírcsatornában",
"ToastPodcastNoRssFeed": "A podcastnak nincs RSS-hírcsatornája",
"ToastProgressIsNotBeingSynced": "Az előrehaladás nem szinkronizálódik, a lejátszás újraindul",
@@ -1032,10 +1068,18 @@
"ToastRemoveFailed": "Sikertelen eltávolítás",
"ToastRemoveItemFromCollectionFailed": "Tétel eltávolítása a gyűjteményből sikertelen",
"ToastRemoveItemFromCollectionSuccess": "Tétel eltávolítva a gyűjteményből",
"ToastRemoveItemsWithIssuesFailed": "Nem sikerült eltávolítani a hibás könyvtárelemeket",
"ToastRemoveItemsWithIssuesSuccess": "Hibás könyvtárelemek eltávolítva",
"ToastRenameFailed": "Sikertelen átnevezés",
"ToastRescanFailed": "Sikertelen újrakeresés a következőnél: {0}",
"ToastRescanRemoved": "A teljes újrabeolvasás befejezve, elem eltávolítva",
"ToastRescanUpToDate": "A teljes újrabeolvasás befejezve, elem naprakész volt",
"ToastRescanUpdated": "A teljes újrabeolvasás befejezve, elem frissítve",
"ToastScanFailed": "Nem sikerült beolvasni a könyvtárelemet",
"ToastSelectAtLeastOneUser": "Válasszon legalább egy felhasználót",
"ToastSendEbookToDeviceFailed": "E-könyv küldése az eszközre sikertelen",
"ToastSendEbookToDeviceSuccess": "E-könyv elküldve az eszközre \"{0}\"",
"ToastSeriesSubmitFailedSameName": "Nem lehet két azonos nevű sorozatot hozzáadni",
"ToastSeriesUpdateFailed": "Sorozat frissítése sikertelen",
"ToastSeriesUpdateSuccess": "Sorozat frissítése sikeres",
"ToastServerSettingsUpdateSuccess": "Szerver beállítások frissítve",
@@ -1043,6 +1087,8 @@
"ToastSessionDeleteFailed": "Munkamenet törlése sikertelen",
"ToastSessionDeleteSuccess": "Munkamenet törölve",
"ToastSleepTimerDone": "Alvásidőzítő kész... zZzzZZz",
"ToastSlugMustChange": "A Slug érvénytelen karaktereket tartalmaz",
"ToastSlugRequired": "Slug szükséges",
"ToastSocketConnected": "Socket csatlakoztatva",
"ToastSocketDisconnected": "Socket lecsatlakoztatva",
"ToastSocketFailedToConnect": "A Socket csatlakoztatása sikertelen",
@@ -1050,9 +1096,14 @@
"ToastSortingPrefixesUpdateSuccess": "Rendezési előtagok frissítése ({0} elem)",
"ToastTitleRequired": "A cím kötelező",
"ToastUnknownError": "Ismeretlen hiba",
"ToastUnlinkOpenIdFailed": "Nem sikerült leválasztani a felhasználót az OpenID-ről",
"ToastUnlinkOpenIdSuccess": "Felhasználó leválasztva az OpenID-ről",
"ToastUploaderFilepathExistsError": "A \"{0}\" fájl elérési útja már létezik a szerveren",
"ToastUploaderItemExistsInSubdirectoryError": "A „{0}” elem a feltöltési útvonal egy alkönyvtárát használja.",
"ToastUserDeleteFailed": "Felhasználó törlése sikertelen",
"ToastUserDeleteSuccess": "Felhasználó törölve",
"ToastUserPasswordChangeSuccess": "Jelszó sikeresen megváltoztatva",
"ToastUserPasswordMismatch": "A jelszavak nem egyeznek",
"ToastUserPasswordMustChange": "Az új jelszó nem egyezik a régi jelszóval",
"ToastUserRootRequireName": "Egy root felhasználónevet kell megadnia"
}

View File

@@ -394,6 +394,7 @@
"LabelIntervalEvery6Hours": "Iedere 6 uur",
"LabelIntervalEveryDay": "Iedere dag",
"LabelIntervalEveryHour": "Ieder uur",
"LabelIntervalEveryMinute": "Elke minuut",
"LabelInvert": "Omdraaien",
"LabelItem": "Onderdeel",
"LabelJumpBackwardAmount": "Terugspoelen hoeveelheid",
@@ -424,7 +425,7 @@
"LabelLookForNewEpisodesAfterDate": "Zoek naar nieuwe afleveringen na deze datum",
"LabelLowestPriority": "Laagste Prioriteit",
"LabelMatchExistingUsersBy": "Bestaande gebruikers matchen op",
"LabelMatchExistingUsersByDescription": "Wordt gebruikt om bestaande gebruikers te verbinden. Zodra ze verbonden zijn, worden gebruikers gekoppeld aan een unieke id van uw SSO-provider.",
"LabelMatchExistingUsersByDescription": "Wordt gebruikt om bestaande gebruikers te verbinden. Zodra ze verbonden zijn, worden gebruikers gekoppeld aan een unieke id van uw SSO-provider",
"LabelMaxEpisodesToDownload": "Maximale # afleveringen om te downloaden. Gebruik 0 voor ongelimiteerd.",
"LabelMaxEpisodesToDownloadPerCheck": "Maximale # nieuwe afleveringen om te downloaden per check",
"LabelMaxEpisodesToKeep": "Maximale # afleveringen om te houden",
@@ -529,6 +530,7 @@
"LabelReleaseDate": "Verschijningsdatum",
"LabelRemoveAllMetadataAbs": "Verwijder alle metadata.abs bestanden",
"LabelRemoveAllMetadataJson": "Verwijder alle metadata.json bestanden",
"LabelRemoveAudibleBranding": "Verwijder Audible intro en outro uit hoofdstukken",
"LabelRemoveCover": "Verwijder cover",
"LabelRemoveMetadataFile": "Verwijder metadata bestanden in bibliotheek item folders",
"LabelRemoveMetadataFileHelp": "Verwijder alle metadata.json en metadata.abs bestanden in uw {0} folders.",
@@ -558,6 +560,8 @@
"LabelSettingsBookshelfViewHelp": "Skeumorphisch design met houten planken",
"LabelSettingsChromecastSupport": "Chromecast ondersteuning",
"LabelSettingsDateFormat": "Datum format",
"LabelSettingsEnableWatcher": "Bibliotheken automatisch scannen op wijzigingen",
"LabelSettingsEnableWatcherForLibrary": "Bibliotheek automatisch scannen op wijzigingen",
"LabelSettingsEnableWatcherHelp": "Zorgt voor het automatisch toevoegen/bijwerken van onderdelen als bestandswijzigingen worden gedetecteerd. *Vereist herstarten van server",
"LabelSettingsEpubsAllowScriptedContent": "Sta scripted content toe in epubs",
"LabelSettingsEpubsAllowScriptedContentHelp": "Sta toe dat epub-bestanden scripts uitvoeren. Het wordt aanbevolen om deze instelling uitgeschakeld te houden, tenzij u de bron van de epub-bestanden vertrouwt.",
@@ -719,6 +723,7 @@
"MessageChapterErrorStartGteDuration": "Ongeldig: starttijd moet kleiner zijn dan duur van audioboek",
"MessageChapterErrorStartLtPrev": "Ongeldig: starttijd moet be groter zijn dan of equal aan starttijd van vorig hoofdstuk",
"MessageChapterStartIsAfter": "Start van hoofdstuk is na het einde van je audioboek",
"MessageChaptersNotFound": "Hoofdstukken niet gevonden",
"MessageCheckingCron": "Cron aan het checken...",
"MessageConfirmCloseFeed": "Ben je zeker dat je deze feed wil sluiten?",
"MessageConfirmDeleteBackup": "Weet je zeker dat je de backup voor {0} wil verwijderen?",
@@ -775,6 +780,7 @@
"MessageForceReScanDescription": "zal alle bestanden opnieuw scannen als een verse scan. Audiobestanden ID3-tags, OPF-bestanden en textbestanden zullen als nieuw worden gescand.",
"MessageImportantNotice": "Belangrijke opmerking!",
"MessageInsertChapterBelow": "Hoofdstuk hieronder invoegen",
"MessageInvalidAsin": "Ongeldige ASIN",
"MessageItemsSelected": "{0} onderdelen geselecteerd",
"MessageItemsUpdated": "{0} onderdelen bijgewerkt",
"MessageJoinUsOn": "Doe mee op",
@@ -808,7 +814,7 @@
"MessageNoItems": "Geen onderdelen",
"MessageNoItemsFound": "Geen onderdelen gevonden",
"MessageNoListeningSessions": "Geen luistersessies",
"MessageNoLogs": "Geen logs",
"MessageNoLogs": "Geen logbestanden",
"MessageNoMediaProgress": "Geen mediavoortgang",
"MessageNoNotifications": "Geen notificaties",
"MessageNoPodcastFeed": "Ongeldige podcast: Geen Feed",

View File

@@ -8,7 +8,7 @@
"ButtonAddYourFirstLibrary": "Legg til ditt første bibliotek",
"ButtonApply": "Bruk",
"ButtonApplyChapters": "Bruk kapittel",
"ButtonAuthors": "Forfatter",
"ButtonAuthors": "Forfattere",
"ButtonBack": "Tilbake",
"ButtonBrowseForFolder": "Bla gjennom mappe",
"ButtonCancel": "Avbryt",
@@ -175,6 +175,7 @@
"HeaderPlaylist": "Spilleliste",
"HeaderPlaylistItems": "Spillelisteelement",
"HeaderPodcastsToAdd": "Podcaster å legge til",
"HeaderPresets": "Forhåndsinnstillinger",
"HeaderPreviewCover": "Forhåndsvis omslag",
"HeaderRSSFeedGeneral": "RSS Detailer",
"HeaderRSSFeedIsOpen": "RSS Feed er åpen",
@@ -217,6 +218,7 @@
"LabelAccountTypeAdmin": "Administrator",
"LabelAccountTypeGuest": "Gjest",
"LabelAccountTypeUser": "Bruker",
"LabelActivities": "Aktiviteter",
"LabelActivity": "Aktivitet",
"LabelAddToCollection": "Legg til i samling",
"LabelAddToCollectionBatch": "Legg {0} bøker til samling",
@@ -226,6 +228,7 @@
"LabelAddedDate": "La til {0}",
"LabelAdminUsersOnly": "Kun administratorer",
"LabelAll": "Alle",
"LabelAllEpisodesDownloaded": "Alle nedlastede episoder",
"LabelAllUsers": "Alle brukere",
"LabelAllUsersExcludingGuests": "Alle brukere bortsett fra gjester",
"LabelAllUsersIncludingGuests": "Alle brukere inkludert gjester",
@@ -281,6 +284,7 @@
"LabelContinueSeries": "Fortsett serier",
"LabelCover": "Omslag",
"LabelCoverImageURL": "Omslagsbilde URL",
"LabelCoverProvider": "Tilbyder av omslagsbilde",
"LabelCreatedAt": "Dato opprettet",
"LabelCronExpression": "Cron uttrykk",
"LabelCurrent": "Nåværende",
@@ -389,6 +393,7 @@
"LabelIntervalEvery6Hours": "Hver 6. timer",
"LabelIntervalEveryDay": "Hver dag",
"LabelIntervalEveryHour": "Hver time",
"LabelIntervalEveryMinute": "Hvert minutt",
"LabelInvert": "Inverter",
"LabelItem": "Enhet",
"LabelJumpBackwardAmount": "Hopp bakover med",
@@ -464,6 +469,7 @@
"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",
"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",
"LabelOverwrite": "Overskriv",
@@ -521,6 +527,7 @@
"LabelReleaseDate": "Utgivelsesdato",
"LabelRemoveAllMetadataAbs": "Fjern alle metadata.abs filer",
"LabelRemoveAllMetadataJson": "Fjern alle metadata.json filer",
"LabelRemoveAudibleBranding": "Fjern Audible inn- og utledning fra kapitler",
"LabelRemoveCover": "Fjern omslag",
"LabelRemoveMetadataFile": "Fjern metadata-filer fra biblioteks-mapper",
"LabelRemoveMetadataFileHelp": "Fjern alle metadata.json og metadata.abs i alle {0} mappene.",
@@ -550,6 +557,8 @@
"LabelSettingsBookshelfViewHelp": "Skeuomorf design med hyller av ved",
"LabelSettingsChromecastSupport": "Chromecast støtte",
"LabelSettingsDateFormat": "Dato Format",
"LabelSettingsEnableWatcher": "Skann biblioteker automatisk for endringer",
"LabelSettingsEnableWatcherForLibrary": "Skann bibliotek automatisk for endringer",
"LabelSettingsEnableWatcherHelp": "Aktiverer automatisk opprettelse/oppdatering av enheter når filendringer er oppdaget. *Krever restart av server*",
"LabelSettingsEpubsAllowScriptedContent": "Tillat scripting i innholdet i ebub-bøker",
"LabelSettingsEpubsAllowScriptedContentHelp": "Tillat epub-filer å kjøre script. Det er anbefalt å slå av denne innstillingen med mindre du stoler på kilden til epub-filene.",
@@ -593,6 +602,7 @@
"LabelSlug": "Slug",
"LabelSortAscending": "Stigende",
"LabelSortDescending": "Synkende",
"LabelSortPubDate": "Sorter etter publiseringsdato",
"LabelStart": "Start",
"LabelStartTime": "Start Tid",
"LabelStarted": "Startet",
@@ -693,6 +703,8 @@
"LabelYourProgress": "Din fremgang",
"MessageAddToPlayerQueue": "Legg til i kø",
"MessageAppriseDescription": "For å bruke denne funksjonen trenger du en instans av <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> kjørende eller et API som håndterer disse forespørslene. <br />Apprise API URL skal være hele URL-en til varslingen, f.eks., hvis din API-instans er på <code>http://192.168.1.1:8337</code> så skal du bruke <code>http://192.168.1.1:8337/notify</code>.",
"MessageAsinCheck": "Påse at du bruker ASIN fra den riktige Audible-regionen, ikke Amazon.",
"MessageAuthenticationOIDCChangesRestart": "Etter å ha lagret, start serveren din på nytt for at OIDC-endringene skal tre i kraft.",
"MessageBackupsDescription": "Sikkerhetskopier inkluderer, brukerfremgang, detaljer om bibliotekgjenstander, tjener instillinger og bilder lagret under <code>/metadata/items</code> og <code>/metadata/authors</code>. Sikkerhetskopier <strong>vil ikke</strong> inkludere filer som er lagret i bibliotek mappene.",
"MessageBackupsLocationEditNote": "Viktig: Endring av mappen for sikkerhetskopi hverken endrer eller flytter eksisterende sikkerhetskopier!",
"MessageBackupsLocationNoEditNote": "NB: Mappen for sikkerhetskopi settes i en miljøvariabel og kan ikke endres her.",

View File

@@ -329,7 +329,9 @@
"LabelEpisode": "Odcinek",
"LabelEpisodeTitle": "Tytuł odcinka",
"LabelEpisodeType": "Typ odcinka",
"LabelEpisodeUrlFromRssFeed": "Adres URL odcinka z kanału RSS",
"LabelEpisodes": "Epizody",
"LabelEpisodic": "Epizodyczny",
"LabelExample": "Przykład",
"LabelExpandSeries": "Rozwiń serie",
"LabelExpandSubSeries": "Rozwiń podserie",
@@ -357,6 +359,7 @@
"LabelFontScale": "Rozmiar czcionki",
"LabelFontStrikethrough": "Przekreślony",
"LabelFormat": "Format",
"LabelFull": "Pełny",
"LabelGenre": "Gatunek",
"LabelGenres": "Gatunki",
"LabelHardDeleteFile": "Usuń trwale plik",
@@ -381,6 +384,7 @@
"LabelIntervalEvery6Hours": "Co 6 godzin",
"LabelIntervalEveryDay": "Każdego dnia",
"LabelIntervalEveryHour": "Każdej godziny",
"LabelIntervalEveryMinute": "Co minutę",
"LabelInvert": "Inversja",
"LabelItem": "Pozycja",
"LabelJumpBackwardAmount": "Przeskocz do tyłu o:",
@@ -412,6 +416,9 @@
"LabelLowestPriority": "Najniższy priorytet",
"LabelMatchExistingUsersBy": "Dopasuje istniejących użytkowników poprzez",
"LabelMatchExistingUsersByDescription": "Służy do łączenia istniejących użytkowników. Po połączeniu użytkownicy zostaną dopasowani za pomocą unikalnego identyfikatora od dostawcy SSO",
"LabelMaxEpisodesToDownload": "Maksymalna liczba odcinków do pobrania. Użyj 0, aby wyłączyć ograniczenie.",
"LabelMaxEpisodesToKeep": "Maksymalna liczba odcinków do zachowania",
"LabelMaxEpisodesToKeepHelp": "Wartość 0 wyłącza maksymalny limit. Po automatycznym pobraniu nowego odcinka, najstarszy odcinek zostanie usunięty, jeśli masz ich więcej niż X. Spowoduje to usunięcie tylko 1 odcinka na nowe pobieranie.",
"LabelMediaPlayer": "Odtwarzacz",
"LabelMediaType": "Typ mediów",
"LabelMetaTag": "Tag",
@@ -424,6 +431,7 @@
"LabelMissingEbook": "Nie posiada ebooka",
"LabelMissingSupplementaryEbook": "Nie posiada dodatkowego ebooka",
"LabelMobileRedirectURIs": "Dozwolone URI przekierowań mobilnych",
"LabelMobileRedirectURIsDescription": "To jest biała lista prawidłowych adresów URI przekierowań dla aplikacji mobilnych. Domyślny adres to <code>audiobookshelf://oauth</code>, który można usunąć lub dodać inne adresy URI w celu integracji z aplikacjami innych firm. Użycie gwiazdki (<code>*</code>) jako jedynego wpisu zezwala na dowolny URI.",
"LabelMore": "Więcej",
"LabelMoreInfo": "Więcej informacji",
"LabelName": "Nazwa",
@@ -453,12 +461,14 @@
"LabelNumberOfEpisodes": "# Odcinków",
"LabelOpenRSSFeed": "Otwórz kanał RSS",
"LabelOverwrite": "Nadpisz",
"LabelPaginationPageXOfY": "Strona {0} z {1}",
"LabelPassword": "Hasło",
"LabelPath": "Ścieżka",
"LabelPermanent": "Stałe",
"LabelPermissionsAccessAllLibraries": "Ma dostęp do wszystkich bibliotek",
"LabelPermissionsAccessAllTags": "Ma dostęp do wszystkich tagów",
"LabelPermissionsAccessExplicitContent": "Ma dostęp do treści oznacznych jako nieprzyzwoite",
"LabelPermissionsCreateEreader": "Możliwość stworzenia czytnika e-booków",
"LabelPermissionsDelete": "Ma możliwość usuwania",
"LabelPermissionsDownload": "Ma możliwość pobierania",
"LabelPermissionsUpdate": "Ma możliwość aktualizowania",
@@ -466,19 +476,25 @@
"LabelPersonalYearReview": "Podsumowanie twojego roku ({0})",
"LabelPhotoPathURL": "Scieżka/URL do zdjęcia",
"LabelPlayMethod": "Metoda odtwarzania",
"LabelPlayerChapterNumberMarker": "{0} z {1}",
"LabelPlaylists": "Listy odtwarzania",
"LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "Obszar wyszukiwania podcastów",
"LabelPodcastType": "Typ podcastu",
"LabelPodcasts": "Podcasty",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Ignorowane prefiksy (wielkość liter nie ma znaczenia)",
"LabelPreventIndexing": "Zapobiega indeksowaniu przez iTunes i Google",
"LabelPrimaryEbook": "Główny ebook",
"LabelProgress": "Postęp",
"LabelProvider": "Dostawca",
"LabelProviderAuthorizationValue": "Wartość nagłówka autoryzacji",
"LabelPubDate": "Data publikacji",
"LabelPublishYear": "Rok publikacji",
"LabelPublishedDate": "Opublikowano {0}",
"LabelPublisher": "Wydawca",
"LabelPublishers": "Wydawcy",
"LabelRSSFeedOpen": "RSS Feed otwarty",
"LabelRSSFeedOpen": "Otwarty Kanał RSS",
"LabelRSSFeedPreventIndexing": "Zapobiegaj indeksowaniu",
"LabelRSSFeedURL": "URL kanały RSS",
"LabelRandomly": "Losowo",
@@ -490,15 +506,22 @@
"LabelRecentlyAdded": "Niedawno dodane",
"LabelRecommended": "Polecane",
"LabelRedo": "Wycofaj",
"LabelRegion": "Region",
"LabelReleaseDate": "Data wydania",
"LabelRemoveAllMetadataAbs": "Usuń wszystkie pliki metadata.abs",
"LabelRemoveAllMetadataJson": "Usuń wszystkie pliki metadata.json",
"LabelRemoveCover": "Usuń okładkę",
"LabelRemoveMetadataFile": "Usuń pliki metadanych z folderów biblioteki",
"LabelRemoveMetadataFileHelp": "Usuń wszystkie pliki metadata.json i metadata.abs z {0} folderów.",
"LabelRowsPerPage": "Wierszy na stronę",
"LabelSearchTerm": "Wyszukiwanie frazy",
"LabelSearchTitle": "Wyszukaj tytuł",
"LabelSearchTitleOrASIN": "Szukaj tytuł lub ASIN",
"LabelSeason": "Sezon",
"LabelSeasonNumber": "Sezon #{0}",
"LabelSelectAll": "Wybierz wszystko",
"LabelSelectAllEpisodes": "Wybierz wszystkie odcinki",
"LabelSelectEpisodesShowing": "Wybierz {0} wyświetlanych odcinków",
"LabelSelectUsers": "Wybór użytkowników",
"LabelSendEbookToDevice": "Wyślij ebook do...",
"LabelSequence": "Kolejność",
@@ -513,6 +536,8 @@
"LabelSettingsBookshelfViewHelp": "Widok półki z książkami",
"LabelSettingsChromecastSupport": "Wsparcie Chromecast",
"LabelSettingsDateFormat": "Format daty",
"LabelSettingsEnableWatcher": "Automatyczne skanowanie bibliotek w poszukiwaniu zmian",
"LabelSettingsEnableWatcherForLibrary": "Automatyczne skanowanie biblioteki w poszukiwaniu zmian",
"LabelSettingsEnableWatcherHelp": "Włącza automatyczne dodawanie/aktualizację pozycji gdy wykryte zostaną zmiany w plikach. Wymaga restartu serwera",
"LabelSettingsEpubsAllowScriptedContent": "Zezwalanie na skrypty w plikach epub",
"LabelSettingsEpubsAllowScriptedContentHelp": "Zezwala plikom epub na wykonywanie skryptów. Zaleca się mieć to ustawienie wyłączone, chyba że ma się zaufanie do źródła plików epub.",
@@ -524,6 +549,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Serie, które posiadają tylko jedną książkę, nie będą pokazywane na stronie z seriami i na stronie domowej z półkami.",
"LabelSettingsHomePageBookshelfView": "Widok półki z książkami na stronie głównej",
"LabelSettingsLibraryBookshelfView": "Widok półki z książkami na stronie biblioteki",
"LabelSettingsLibraryMarkAsFinishedWhen": "Oznacz element multimedialny jako ukończony, gdy",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Pomiń poprzednie książki przy kontynuacji 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\"",
"LabelSettingsPreferMatchedMetadata": "Preferowanie dopasowanych metadanych",
@@ -547,6 +574,9 @@
"LabelShowSubtitles": "Pokaż Napisy",
"LabelSize": "Rozmiar",
"LabelSleepTimer": "Wyłącznik czasowy",
"LabelSortAscending": "Rosnąco",
"LabelSortDescending": "Malejąco",
"LabelSortPubDate": "Sortuj według daty publikacji",
"LabelStart": "Rozpocznij",
"LabelStartTime": "Czas rozpoczęcia",
"LabelStarted": "Rozpoczęty",
@@ -568,14 +598,21 @@
"LabelStatsWeekListening": "Tydzień słuchania",
"LabelSubtitle": "Podtytuł",
"LabelSupportedFileTypes": "Obsługiwane typy plików",
"LabelTag": "Znacznik",
"LabelTags": "Tagi",
"LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika",
"LabelTagsNotAccessibleToUser": "Znaczniki niedostępne dla użytkownika",
"LabelTasks": "Uruchomione zadania",
"LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Lista numerowana",
"LabelTextEditorUnlink": "Usuń link",
"LabelThemeDark": "Ciemny",
"LabelThemeLight": "Jasny",
"LabelTimeDurationXHours": "{0} godzin",
"LabelTimeDurationXMinutes": "{0} minuty",
"LabelTimeDurationXSeconds": "{0} sekundy",
"LabelTimeInMinutes": "Czas w minutach",
"LabelTimeLeft": "pozostało {0}",
"LabelTimeListened": "Czas odtwarzania",
"LabelTimeListenedToday": "Czas odtwarzania dzisiaj",
"LabelTimeRemaining": "Pozostało {0}",
@@ -583,6 +620,7 @@
"LabelTitle": "Tytuł",
"LabelToolsEmbedMetadata": "Załącz metadane",
"LabelToolsEmbedMetadataDescription": "Załącz metadane do plików audio (okładkę oraz znaczniki rozdziałów).",
"LabelToolsM4bEncoder": "Enkoder M4B",
"LabelToolsMakeM4b": "Generuj plik M4B",
"LabelToolsMakeM4bDescription": "Tworzy plik w formacie .M4B, który zawiera metadane, okładkę oraz rozdziały.",
"LabelToolsSplitM4b": "Podziel plik .M4B na pliki .MP3",
@@ -595,12 +633,14 @@
"LabelType": "Typ",
"LabelUndo": "Wycofaj",
"LabelUnknown": "Nieznany",
"LabelUnknownPublishDate": "Nieznana data publikacji",
"LabelUpdateCover": "Zaktalizuj odkładkę",
"LabelUpdateCoverHelp": "Umożliwienie nadpisania istniejących okładek dla wybranych książek w przypadku znalezienia dopasowania",
"LabelUpdateDetails": "Zaktualizuj szczegóły",
"LabelUpdateDetailsHelp": "Umożliwienie nadpisania istniejących szczegółów dla wybranych książek w przypadku znalezienia dopasowania",
"LabelUpdatedAt": "Zaktualizowano",
"LabelUploaderDragAndDrop": "Przeciągnij i puść foldery lub pliki",
"LabelUploaderDragAndDropFilesOnly": "Przeciągnij i upuść pliki",
"LabelUploaderDropFiles": "Puść pliki",
"LabelUploaderItemFetchMetadataHelp": "Automatycznie pobierz tytuł, autora i serie",
"LabelUseChapterTrack": "Użyj ścieżki rozdziału",

View File

@@ -348,7 +348,7 @@
"LabelExpandSubSeries": "Развернуть подсерию",
"LabelExplicit": "18+",
"LabelExplicitChecked": "18+ (отмечено)",
"LabelExplicitUnchecked": "Не явно (не отмечено)",
"LabelExplicitUnchecked": "+18 (не отмечено)",
"LabelExportOPML": "Экспорт OPML",
"LabelFeedURL": "URL канала",
"LabelFetchingMetadata": "Извлечение метаданных",
@@ -514,7 +514,7 @@
"LabelPublishers": "Издатели",
"LabelRSSFeedCustomOwnerEmail": "Пользовательский Email владельца",
"LabelRSSFeedCustomOwnerName": "Пользовательское Имя владельца",
"LabelRSSFeedOpen": "Открыть RSS-канал",
"LabelRSSFeedOpen": "Открыть RSS-ленту",
"LabelRSSFeedPreventIndexing": "Запретить индексирование",
"LabelRSSFeedSlug": "Встроить RSS-канал",
"LabelRSSFeedURL": "URL RSS-канала",
@@ -918,6 +918,8 @@
"NotificationOnBackupCompletedDescription": "Запускается при завершении резервного копирования",
"NotificationOnBackupFailedDescription": "Срабатывает при сбое резервного копирования",
"NotificationOnEpisodeDownloadedDescription": "Запускается при автоматической загрузке эпизода подкаста",
"NotificationOnRSSFeedDisabledDescription": "Срабатывает, когда автоматическая загрузка эпизодов отключена из-за слишком большого количества неудачных попыток",
"NotificationOnRSSFeedFailedDescription": "Срабатывает при сбое запроса RSS-канала на автоматическую загрузку эпизода",
"NotificationOnTestDescription": "Событие для тестирования системы оповещения",
"PlaceholderNewCollection": "Новое имя коллекции",
"PlaceholderNewFolderPath": "Путь к новой папке",

View File

@@ -514,7 +514,7 @@
"LabelPublishers": "Izdajatelji",
"LabelRSSFeedCustomOwnerEmail": "E-pošta lastnika po meri",
"LabelRSSFeedCustomOwnerName": "Ime lastnika po meri",
"LabelRSSFeedOpen": "Odprt vir RSS",
"LabelRSSFeedOpen": "RSS vir je odprt",
"LabelRSSFeedPreventIndexing": "Prepreči indeksiranje",
"LabelRSSFeedSlug": "Slug RSS vira",
"LabelRSSFeedURL": "URL vira RSS",
@@ -918,6 +918,8 @@
"NotificationOnBackupCompletedDescription": "Sproži se, ko je varnostno kopiranje končano",
"NotificationOnBackupFailedDescription": "Sproži se, ko varnostno kopiranje ne uspe",
"NotificationOnEpisodeDownloadedDescription": "Sproži se, ko se epizoda podcasta samodejno prenese",
"NotificationOnRSSFeedDisabledDescription": "Sproži se, ko so samodejni prenosi epizod onemogočeni zaradi preveč neuspelih poskusov",
"NotificationOnRSSFeedFailedDescription": "Sproži se, ko zahteva za vir RSS za samodejni prenos epizode ne uspe",
"NotificationOnTestDescription": "Dogodek za testiranje sistema obveščanja",
"PlaceholderNewCollection": "Novo ime zbirke",
"PlaceholderNewFolderPath": "Pot nove mape",

View File

@@ -514,7 +514,7 @@
"LabelPublishers": "Видавці",
"LabelRSSFeedCustomOwnerEmail": "Користувацька електронна адреса власника",
"LabelRSSFeedCustomOwnerName": "Користувацьке ім'я власника",
"LabelRSSFeedOpen": "RSS-канал відкрито",
"LabelRSSFeedOpen": "RSS-канал відкритий",
"LabelRSSFeedPreventIndexing": "Запобігати індексації",
"LabelRSSFeedSlug": "Назва RSS-каналу",
"LabelRSSFeedURL": "Адреса RSS-каналу",
@@ -918,6 +918,8 @@
"NotificationOnBackupCompletedDescription": "Запускається після завершення резервного копіювання",
"NotificationOnBackupFailedDescription": "Срабатывает при збої резервного копіювання",
"NotificationOnEpisodeDownloadedDescription": "Запускається при автоматичному завантаженні епізоду подкасту",
"NotificationOnRSSFeedDisabledDescription": "Активується, коли автоматичне завантаження епізодів вимкнено через занадто багато невдалих спроб",
"NotificationOnRSSFeedFailedDescription": "Активується, коли запит RSS-каналу не вдається виконати для автоматичного завантаження епізоду",
"NotificationOnTestDescription": "Подія для тестування системи сповіщень",
"PlaceholderNewCollection": "Нова назва добірки",
"PlaceholderNewFolderPath": "Новий шлях до теки",

View File

@@ -28,6 +28,7 @@ if (isDev) {
if (devEnv.SkipBinariesCheck) process.env.SKIP_BINARIES_CHECK = '1'
if (devEnv.AllowIframe) process.env.ALLOW_IFRAME = '1'
if (devEnv.BackupPath) process.env.BACKUP_PATH = devEnv.BackupPath
if (devEnv.ReactClientPath) process.env.REACT_CLIENT_PATH = devEnv.ReactClientPath
process.env.SOURCE = 'local'
process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath ?? '/audiobookshelf'
}

4
package-lock.json generated
View File

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

View File

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

View File

@@ -442,7 +442,17 @@ class Auth {
// Local strategy login route (takes username and password)
router.post('/login', passport.authenticate('local'), async (req, res) => {
// return the user login response json if the login was successfull
res.json(await this.getUserLoginResponsePayload(req.user))
const userResponse = await this.getUserLoginResponsePayload(req.user)
// Experimental Next.js client uses bearer token in cookies
res.cookie('auth_token', userResponse.user.token, {
httpOnly: true,
secure: req.secure || req.get('x-forwarded-proto') === 'https',
sameSite: 'strict',
maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
})
res.json(userResponse)
})
// openid strategy login route (this redirects to the configured openid login provider)
@@ -718,6 +728,7 @@ class Auth {
const authMethod = req.cookies.auth_method
res.clearCookie('auth_method')
res.clearCookie('auth_token')
let logoutUrl = null

View File

@@ -766,8 +766,19 @@ class Database {
Logger.warn(`Removed ${badSessionsRemoved} sessions that were 3 seconds or less`)
}
// Remove mediaProgresses with duplicate mediaItemId (remove the oldest updatedAt)
const [duplicateMediaProgresses] = await this.sequelize.query(`SELECT id, mediaItemId FROM mediaProgresses WHERE (mediaItemId, updatedAt) IN (SELECT mediaItemId, MIN(updatedAt) FROM mediaProgresses GROUP BY mediaItemId HAVING COUNT(*) > 1)`)
// Remove mediaProgresses with duplicate mediaItemId (remove the oldest updatedAt or if updatedAt is the same, remove arbitrary one)
const [duplicateMediaProgresses] = await this.sequelize.query(`SELECT mp1.id, mp1.mediaItemId
FROM mediaProgresses mp1
WHERE EXISTS (
SELECT 1
FROM mediaProgresses mp2
WHERE mp2.mediaItemId = mp1.mediaItemId
AND mp2.userId = mp1.userId
AND (
mp2.updatedAt > mp1.updatedAt
OR (mp2.updatedAt = mp1.updatedAt AND mp2.id < mp1.id)
)
)`)
for (const duplicateMediaProgress of duplicateMediaProgresses) {
Logger.warn(`Found duplicate mediaProgress for mediaItem "${duplicateMediaProgress.mediaItemId}" - removing it`)
await this.mediaProgressModel.destroy({

View File

@@ -220,6 +220,7 @@ class Server {
async start() {
Logger.info('=== Starting Server ===')
this.initProcessEventListeners()
await this.init()
@@ -281,6 +282,7 @@ class Server {
await this.auth.initPassportJs()
const router = express.Router()
// if RouterBasePath is set, modify all requests to include the base path
app.use((req, res, next) => {
const urlStartsWithRouterBasePath = req.url.startsWith(global.RouterBasePath)
@@ -313,10 +315,6 @@ class Server {
router.use('/hls', this.hlsRouter.router)
router.use('/public', this.publicRouter.router)
// Static path to generated nuxt
const distPath = Path.join(global.appRoot, '/client/dist')
router.use(express.static(distPath))
// Static folder
router.use(express.static(Path.join(global.appRoot, 'static')))
@@ -336,32 +334,6 @@ class Server {
// Auth routes
await this.auth.initAuthRoutes(router)
// Client dynamic routes
const dynamicRoutes = [
'/item/:id',
'/author/:id',
'/audiobook/:id/chapters',
'/audiobook/:id/edit',
'/audiobook/:id/manage',
'/library/:library',
'/library/:library/search',
'/library/:library/bookshelf/:id?',
'/library/:library/authors',
'/library/:library/narrators',
'/library/:library/stats',
'/library/:library/series/:id?',
'/library/:library/podcast/search',
'/library/:library/podcast/latest',
'/library/:library/podcast/download-queue',
'/config/users/:id',
'/config/users/:id/sessions',
'/config/item-metadata-utils/:id',
'/collection/:id',
'/playlist/:id',
'/share/:slug'
]
dynamicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
router.post('/init', (req, res) => {
if (Database.hasRootUser) {
Logger.error(`[Server] attempt to init server when server already has a root user`)
@@ -392,6 +364,48 @@ class Server {
})
router.get('/healthcheck', (req, res) => res.sendStatus(200))
const ReactClientPath = process.env.REACT_CLIENT_PATH
if (!ReactClientPath) {
// Static path to generated nuxt
const distPath = Path.join(global.appRoot, '/client/dist')
router.use(express.static(distPath))
// Client dynamic routes
const dynamicRoutes = [
'/item/:id',
'/author/:id',
'/audiobook/:id/chapters',
'/audiobook/:id/edit',
'/audiobook/:id/manage',
'/library/:library',
'/library/:library/search',
'/library/:library/bookshelf/:id?',
'/library/:library/authors',
'/library/:library/narrators',
'/library/:library/stats',
'/library/:library/series/:id?',
'/library/:library/podcast/search',
'/library/:library/podcast/latest',
'/library/:library/podcast/download-queue',
'/config/users/:id',
'/config/users/:id/sessions',
'/config/item-metadata-utils/:id',
'/collection/:id',
'/playlist/:id',
'/share/:slug'
]
dynamicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
} else {
// This is for using the experimental Next.js client
Logger.info(`Using React client at ${ReactClientPath}`)
const nextPath = Path.join(ReactClientPath, 'node_modules/next')
const next = require(nextPath)
const nextApp = next({ dev: Logger.isDev, dir: ReactClientPath })
const handle = nextApp.getRequestHandler()
await nextApp.prepare()
router.get('*', (req, res) => handle(req, res))
}
const unixSocketPrefix = 'unix/'
if (this.Host?.startsWith(unixSocketPrefix)) {
const sockPath = this.Host.slice(unixSocketPrefix.length)

View File

@@ -108,7 +108,13 @@ class FileSystemController {
return res.sendStatus(404)
}
const filepath = Path.join(libraryFolder.path, directory)
if (!req.user.checkCanAccessLibrary(libraryFolder.libraryId)) {
Logger.error(`[FileSystemController] User "${req.user.username}" attempting to check path exists for library "${libraryFolder.libraryId}" without access`)
return res.sendStatus(403)
}
let filepath = Path.join(libraryFolder.path, directory)
filepath = fileUtils.filePathToPOSIX(filepath)
// Ensure filepath is inside library folder (prevents directory traversal)
if (!filepath.startsWith(libraryFolder.path)) {

View File

@@ -59,6 +59,12 @@ class MiscController {
if (!library) {
return res.status(404).send('Library not found')
}
if (!req.user.checkCanAccessLibrary(library.id)) {
Logger.error(`[MiscController] User "${req.user.username}" attempting to upload to library "${library.id}" without access`)
return res.sendStatus(403)
}
const folder = library.libraryFolders.find((fold) => fold.id === folderId)
if (!folder) {
return res.status(404).send('Folder not found')

View File

File diff suppressed because one or more lines are too long

View File

@@ -107,7 +107,7 @@ class PlaybackSessionManager {
const syncResults = []
for (const sessionJson of sessions) {
Logger.info(`[PlaybackSessionManager] Syncing local session "${sessionJson.displayTitle}" (${sessionJson.id})`)
Logger.info(`[PlaybackSessionManager] Syncing local session "${sessionJson.displayTitle}" (${sessionJson.id}) (updatedAt: ${sessionJson.updatedAt})`)
const result = await this.syncLocalSession(user, sessionJson, deviceInfo)
syncResults.push(result)
}
@@ -230,9 +230,9 @@ class PlaybackSessionManager {
let userProgressForItem = user.getMediaProgress(mediaItemId)
if (userProgressForItem) {
if (userProgressForItem.updatedAt.valueOf() > session.updatedAt) {
Logger.debug(`[PlaybackSessionManager] Not updating progress for "${session.displayTitle}" because it has been updated more recently`)
Logger.info(`[PlaybackSessionManager] Not updating progress for "${session.displayTitle}" because it has been updated more recently (${userProgressForItem.updatedAt.valueOf()} > ${session.updatedAt}) (incoming currentTime: ${session.currentTime}) (current currentTime: ${userProgressForItem.currentTime})`)
} else {
Logger.debug(`[PlaybackSessionManager] Updating progress for "${session.displayTitle}" with current time ${session.currentTime} (previously ${userProgressForItem.currentTime})`)
Logger.info(`[PlaybackSessionManager] Updating progress for "${session.displayTitle}" with current time ${session.currentTime} (previously ${userProgressForItem.currentTime})`)
const updateResponse = await user.createUpdateMediaProgressFromPayload({
libraryItemId: libraryItem.id,
episodeId: session.episodeId,
@@ -246,7 +246,7 @@ class PlaybackSessionManager {
}
}
} else {
Logger.debug(`[PlaybackSessionManager] Creating new media progress for media item "${session.displayTitle}"`)
Logger.info(`[PlaybackSessionManager] Creating new media progress for media item "${session.displayTitle}"`)
const updateResponse = await user.createUpdateMediaProgressFromPayload({
libraryItemId: libraryItem.id,
episodeId: session.episodeId,

View File

@@ -222,13 +222,13 @@ class MediaProgress extends Model {
const markAsFinishedPercentComplete = Number(progressPayload.markAsFinishedPercentComplete) / 100
shouldMarkAsFinished = markAsFinishedPercentComplete < this.progress
if (shouldMarkAsFinished) {
Logger.debug(`[MediaProgress] Marking media progress as finished because progress (${this.progress}) is greater than ${markAsFinishedPercentComplete}`)
Logger.info(`[MediaProgress] Marking media progress as finished because progress (${this.progress}) is greater than ${markAsFinishedPercentComplete} (media item ${this.mediaItemId})`)
}
} else {
const markAsFinishedTimeRemaining = isNullOrNaN(progressPayload.markAsFinishedTimeRemaining) ? 10 : Number(progressPayload.markAsFinishedTimeRemaining)
shouldMarkAsFinished = timeRemaining < markAsFinishedTimeRemaining
if (shouldMarkAsFinished) {
Logger.debug(`[MediaProgress] Marking media progress as finished because time remaining (${timeRemaining}) is less than ${markAsFinishedTimeRemaining} seconds`)
Logger.info(`[MediaProgress] Marking media progress as finished because time remaining (${timeRemaining}) is less than ${markAsFinishedTimeRemaining} seconds (media item ${this.mediaItemId})`)
}
}
}
@@ -246,6 +246,7 @@ class MediaProgress extends Model {
// For local sync
if (progressPayload.lastUpdate) {
this.updatedAt = progressPayload.lastUpdate
Logger.info(`[MediaProgress] Manually setting updatedAt to ${this.updatedAt} (media item ${this.mediaItemId})`)
this.changed('updatedAt', true)
}

View File

@@ -41,7 +41,7 @@ class Audible {
}
cleanResult(item) {
const { title, subtitle, asin, authors, narrators, publisherName, summary, releaseDate, image, genres, seriesPrimary, seriesSecondary, language, runtimeLengthMin, formatType } = item
const { title, subtitle, asin, authors, narrators, publisherName, summary, releaseDate, image, genres, seriesPrimary, seriesSecondary, language, runtimeLengthMin, formatType, isbn } = item
const series = []
if (seriesPrimary) {
@@ -70,6 +70,7 @@ class Audible {
description: summary || null,
cover: image,
asin,
isbn,
genres: genresFiltered.length ? genresFiltered : null,
tags: tagsFiltered.length ? tagsFiltered.join(', ') : null,
series: series.length ? series : null,

View File

@@ -52,9 +52,7 @@ class FantLab {
return []
})
return Promise.all(items.map(async (item) => await this.getWork(item, timeout))).then((resArray) => {
return resArray.filter((res) => res)
})
return Promise.all(items.map(async (item) => await this.getWork(item, timeout))).then((resArray) => resArray.filter(Boolean))
}
/**
@@ -83,6 +81,10 @@ class FantLab {
return null
})
if (!bookData) {
return null
}
return this.cleanBookData(bookData, timeout)
}

View File

@@ -370,7 +370,7 @@ class Scanner {
let numEpisodesUpdated = 0
for (const episode of episodesToQuickMatch) {
const episodeMatches = findMatchingEpisodesInFeed(feed, episode.title)
const episodeMatches = findMatchingEpisodesInFeed(feed, episode.title, 0.1)
if (episodeMatches?.length) {
const wasUpdated = await this.updateEpisodeWithMatch(episode, episodeMatches[0].episode, options)
if (wasUpdated) numEpisodesUpdated++

View File

@@ -103,18 +103,39 @@ module.exports.resizeImage = resizeImage
*/
module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
return new Promise(async (resolve) => {
const response = await axios({
url: podcastEpisodeDownload.url,
method: 'GET',
responseType: 'stream',
headers: {
'User-Agent': 'audiobookshelf (+https://audiobookshelf.org)'
},
timeout: global.PodcastDownloadTimeout
}).catch((error) => {
Logger.error(`[ffmpegHelpers] Failed to download podcast episode with url "${podcastEpisodeDownload.url}"`, error)
return null
})
// Some podcasts fail due to user agent strings
// See: https://github.com/advplyr/audiobookshelf/issues/3246 (requires iTMS user agent)
// See: https://github.com/advplyr/audiobookshelf/issues/4401 (requires no iTMS user agent)
const userAgents = ['audiobookshelf (+https://audiobookshelf.org; like iTMS)', 'audiobookshelf (+https://audiobookshelf.org)']
let response = null
let lastError = null
for (const userAgent of userAgents) {
try {
response = await axios({
url: podcastEpisodeDownload.url,
method: 'GET',
responseType: 'stream',
headers: {
'User-Agent': userAgent
},
timeout: global.PodcastDownloadTimeout
})
Logger.debug(`[ffmpegHelpers] Successfully connected with User-Agent: ${userAgent}`)
break
} catch (error) {
lastError = error
Logger.warn(`[ffmpegHelpers] Failed to download podcast episode with User-Agent "${userAgent}" for url "${podcastEpisodeDownload.url}"`, error.message)
// If this is the last attempt, log the full error
if (userAgent === userAgents[userAgents.length - 1]) {
Logger.error(`[ffmpegHelpers] All User-Agent attempts failed for url "${podcastEpisodeDownload.url}"`, lastError)
}
}
}
if (!response) {
return resolve({
success: false

View File

@@ -1,8 +1,9 @@
const axios = require('axios')
const ssrfFilter = require('ssrf-req-filter')
const Logger = require('../Logger')
const { xmlToJSON, levenshteinDistance, timestampToSeconds } = require('./index')
const { xmlToJSON, timestampToSeconds } = require('./index')
const htmlSanitizer = require('../utils/htmlSanitizer')
const Fuse = require('../libs/fusejs')
/**
* @typedef RssPodcastChapter
@@ -24,6 +25,7 @@ const htmlSanitizer = require('../utils/htmlSanitizer')
* @property {string} episode
* @property {string} author
* @property {string} duration
* @property {number|null} durationSeconds - Parsed from duration string if duration is valid
* @property {string} explicit
* @property {number} publishedAt - Unix timestamp
* @property {{ url: string, type?: string, length?: string }} enclosure
@@ -216,8 +218,9 @@ function extractEpisodeData(item) {
})
// Extract psc:chapters if duration is set
let episodeDuration = !isNaN(episode.duration) ? timestampToSeconds(episode.duration) : null
if (item['psc:chapters']?.[0]?.['psc:chapter']?.length && episodeDuration) {
episode.durationSeconds = episode.duration ? timestampToSeconds(episode.duration) : null
if (item['psc:chapters']?.[0]?.['psc:chapter']?.length && episode.durationSeconds) {
// Example chapter:
// {"id":0,"start":0,"end":43.004286,"title":"chapter 1"}
@@ -243,7 +246,7 @@ function extractEpisodeData(item) {
} else {
episode.chapters = cleanedChapters.map((chapter, index) => {
const nextChapter = cleanedChapters[index + 1]
const end = nextChapter ? nextChapter.start : episodeDuration
const end = nextChapter ? nextChapter.start : episode.durationSeconds
return {
id: chapter.id,
title: chapter.title,
@@ -272,6 +275,7 @@ function cleanEpisodeData(data) {
episode: data.episode || '',
author: data.author || '',
duration: data.duration || '',
durationSeconds: data.durationSeconds || null,
explicit: data.explicit || '',
publishedAt,
enclosure: data.enclosure,
@@ -407,7 +411,7 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => {
})
}
// Return array of episodes ordered by closest match (Levenshtein distance of 6 or less)
// Return array of episodes ordered by closest match using fuse.js
module.exports.findMatchingEpisodes = async (feedUrl, searchTitle) => {
const feed = await this.getPodcastFeed(feedUrl).catch(() => {
return null
@@ -420,32 +424,29 @@ module.exports.findMatchingEpisodes = async (feedUrl, searchTitle) => {
*
* @param {RssPodcast} feed
* @param {string} searchTitle
* @returns {Array<{ episode: RssPodcastEpisode, levenshtein: number }>}
* @param {number} [threshold=0.4] - 0.0 for perfect match, 1.0 for match anything
* @returns {Array<{ episode: RssPodcastEpisode }>}
*/
module.exports.findMatchingEpisodesInFeed = (feed, searchTitle) => {
searchTitle = searchTitle.toLowerCase().trim()
module.exports.findMatchingEpisodesInFeed = (feed, searchTitle, threshold = 0.4) => {
if (!feed?.episodes) {
return null
}
const fuseOptions = {
ignoreDiacritics: true,
threshold,
keys: [
{ name: 'title', weight: 0.7 }, // prefer match in title
{ name: 'subtitle', weight: 0.3 }
]
}
const fuse = new Fuse(feed.episodes, fuseOptions)
const matches = []
feed.episodes.forEach((ep) => {
if (!ep.title) return
const epTitle = ep.title.toLowerCase().trim()
if (epTitle === searchTitle) {
matches.push({
episode: ep,
levenshtein: 0
})
} else {
const levenshtein = levenshteinDistance(searchTitle, epTitle, true)
if (levenshtein <= 6 && epTitle.length > levenshtein) {
matches.push({
episode: ep,
levenshtein
})
}
}
fuse.search(searchTitle).forEach((match) => {
matches.push({
episode: match.item
})
})
return matches.sort((a, b) => a.levenshtein - b.levenshtein)
return matches
}

View File

@@ -186,6 +186,8 @@ module.exports = {
mediaWhere['$series.id$'] = null
} else if (group === 'abridged') {
mediaWhere['abridged'] = true
} else if (group === 'explicit') {
mediaWhere['explicit'] = true
} else if (['genres', 'tags', 'narrators'].includes(group)) {
mediaWhere[group] = Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(${group}) WHERE json_valid(${group}) AND json_each.value = :filterValue)`), {
[Sequelize.Op.gte]: 1
@@ -251,6 +253,15 @@ module.exports = {
*/
getOrder(sortBy, sortDesc, collapseseries) {
const dir = sortDesc ? 'DESC' : 'ASC'
const getTitleOrder = () => {
if (global.ServerSettings.sortingIgnorePrefix) {
return [Sequelize.literal('`libraryItem`.`titleIgnorePrefix` COLLATE NOCASE'), dir]
} else {
return [Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir]
}
}
if (sortBy === 'addedAt') {
return [[Sequelize.literal('libraryItem.createdAt'), dir]]
} else if (sortBy === 'size') {
@@ -264,25 +275,16 @@ module.exports = {
} else if (sortBy === 'media.metadata.publishedYear') {
return [[Sequelize.literal(`CAST(\`book\`.\`publishedYear\` AS INTEGER)`), dir]]
} else if (sortBy === 'media.metadata.authorNameLF') {
return [
[Sequelize.literal('`libraryItem`.`authorNamesLastFirst` COLLATE NOCASE'), dir],
[Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir]
]
// Sort by author name last first, secondary sort by title
return [[Sequelize.literal('`libraryItem`.`authorNamesLastFirst` COLLATE NOCASE'), dir], getTitleOrder()]
} else if (sortBy === 'media.metadata.authorName') {
return [
[Sequelize.literal('`libraryItem`.`authorNamesFirstLast` COLLATE NOCASE'), dir],
[Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir]
]
// Sort by author name first last, secondary sort by title
return [[Sequelize.literal('`libraryItem`.`authorNamesFirstLast` COLLATE NOCASE'), dir], getTitleOrder()]
} else if (sortBy === 'media.metadata.title') {
if (collapseseries) {
return [[Sequelize.literal('display_title COLLATE NOCASE'), dir]]
}
if (global.ServerSettings.sortingIgnorePrefix) {
return [[Sequelize.literal('`libraryItem`.`titleIgnorePrefix` COLLATE NOCASE'), dir]]
} else {
return [[Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir]]
}
return [getTitleOrder()]
} else if (sortBy === 'sequence') {
const nullDir = sortDesc ? 'DESC NULLS FIRST' : 'ASC NULLS LAST'
return [[Sequelize.literal(`CAST(\`series.bookSeries.sequence\` AS FLOAT) ${nullDir}`)]]

View File

@@ -59,6 +59,8 @@ module.exports = {
replacements.filterValue = value
} else if (group === 'languages') {
mediaWhere['language'] = value
} else if (group === 'explicit') {
mediaWhere['explicit'] = true
}
return {