Compare commits

...

34 Commits

Author SHA1 Message Date
advplyr ce213c3d89 Version bump v2.13.4 2024-09-09 16:15:44 -05:00
advplyr 32cd0360e6 Merge pull request #3371 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-09-09 16:11:21 -05:00
Mario 1ec23a5699 Translated using Weblate (German)
Currently translated at 100.0% (974 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2024-09-09 23:04:25 +02:00
Soaibuzzaman 48330f6432 Translated using Weblate (Bengali)
Currently translated at 100.0% (974 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/bn/
2024-09-09 23:04:25 +02:00
thehijacker 28358debbc Translated using Weblate (Slovenian)
Currently translated at 100.0% (974 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-09-09 23:04:25 +02:00
SunSpring 54b7ed6117 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (974 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2024-09-09 23:04:25 +02:00
gallegonovato 0cfd2ee63b Translated using Weblate (Spanish)
Currently translated at 99.7% (972 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2024-09-09 23:04:25 +02:00
thehijacker 37a0990741 Translated using Weblate (Slovenian)
Currently translated at 100.0% (974 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-09-09 23:04:25 +02:00
advplyr 7a0cd1eb34 Merge pull request #3396 from mikiher/custom-provider-try-catch
Add a try-catch block around custom provider search
2024-09-09 16:04:20 -05:00
advplyr ac3277da09 Merge pull request #3395 from mikiher/quick-match-new-series
Fix crash when quick match adds new series
2024-09-09 16:03:30 -05:00
advplyr 65d1e7be56 Merge pull request #3394 from mikiher/webp-embed
Convert webp images to jpeg during metadata embed
2024-09-09 16:02:17 -05:00
mikiher 80685afa7e Add a try-catch block around custom provider search 2024-09-09 19:23:26 +03:00
mikiher f892453892 Fix crash when quick match adds new series 2024-09-09 18:36:12 +03:00
mikiher 422bb8c31c Convert webp images to jpeg during metadata embed 2024-09-09 15:28:53 +03:00
advplyr 4ddd2788f0 Fix:Byte conversion to use 1000 instead of 1024 to be accurate with abbrevs #3386 2024-09-07 16:52:42 -05:00
advplyr 423a2129d1 Update:Format number for entity total in bookshelf toolbar #3370 2024-09-06 17:01:48 -05:00
advplyr a338097514 Update:Cleanup logging on library item update #3362 2024-09-06 16:58:40 -05:00
advplyr 84b67abb03 Fix:Get all collections API endpoint crashing server #3372 2024-09-05 17:15:38 -05:00
advplyr 5ec8406653 Cleanup Collection model to remove oldCollection references 2024-09-04 18:00:59 -05:00
advplyr 0344a63b48 Clean out old unused objects 2024-09-03 17:04:58 -05:00
advplyr 24923c0009 Version bump v2.13.3 2024-09-02 17:09:34 -05:00
advplyr a9036c9738 Merge pull request #3360 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-09-02 16:53:30 -05:00
Hosted Weblate f9f7fbed33 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/
2024-09-02 23:50:30 +02:00
thehijacker 53b5bee736 Translated using Weblate (Slovenian)
Currently translated at 100.0% (974 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-09-02 23:50:29 +02:00
Kamil Pomykała d0b3726905 Translated using Weblate (Polish)
Currently translated at 81.8% (797 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2024-09-02 23:50:28 +02:00
Andrej Kralj 7a6864507e Translated using Weblate (Slovenian)
Currently translated at 100.0% (974 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-09-02 23:50:27 +02:00
Soaibuzzaman e20563f2e1 Translated using Weblate (Bengali)
Currently translated at 82.0% (799 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/bn/
2024-09-02 23:50:26 +02:00
advplyr fea5f8f3d4 Update:Batch edit page show confirmation before navigating away with unsaved changes #3369 2024-09-02 16:50:22 -05:00
advplyr f9bb529b85 Fix:Unlink OpenID button translation string 2024-09-02 16:15:26 -05:00
advplyr 60e348fcc1 Fix:Updating root user #3366 2024-09-02 16:12:57 -05:00
advplyr f194c5be0e Merge pull request #3368 from nichwall/fix_tag_permissions
Fix tag permissions
2024-09-02 15:58:05 -05:00
advplyr 47712e63f1 Update user default permissions 2024-09-02 15:55:25 -05:00
Nicholas Wallace 790c1fb34a Allow update of default permission keys missing for user 2024-09-02 10:28:03 -07:00
Nicholas Wallace 9cca731acc Add: missing default user permission property 2024-09-02 10:08:17 -07:00
55 changed files with 1333 additions and 1782 deletions
-1
View File
@@ -264,7 +264,6 @@ export default {
libraryItems.forEach((item) => { libraryItems.forEach((item) => {
let subtitle = '' let subtitle = ''
if (item.mediaType === 'book') subtitle = item.media.metadata.authors.map((au) => au.name).join(', ') if (item.mediaType === 'book') subtitle = item.media.metadata.authors.map((au) => au.name).join(', ')
else if (item.mediaType === 'music') subtitle = item.media.metadata.artists.join(', ')
queueItems.push({ queueItems.push({
libraryItemId: item.id, libraryItemId: item.id,
libraryId: item.libraryId, libraryId: item.libraryId,
+3 -7
View File
@@ -50,7 +50,7 @@
{{ seriesName }} {{ seriesName }}
</p> </p>
<div class="w-6 h-6 rounded-full bg-black bg-opacity-30 flex items-center justify-center ml-3"> <div class="w-6 h-6 rounded-full bg-black bg-opacity-30 flex items-center justify-center ml-3">
<span class="font-mono">{{ numShowing }}</span> <span class="font-mono">{{ $formatNumber(numShowing) }}</span>
</div> </div>
<div class="flex-grow" /> <div class="flex-grow" />
@@ -63,7 +63,7 @@
</template> </template>
<!-- library & collections page --> <!-- library & collections page -->
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome"> <template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome">
<p class="hidden md:block">{{ numShowing }} {{ entityName }}</p> <p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p>
<div class="flex-grow hidden sm:inline-block" /> <div class="flex-grow hidden sm:inline-block" />
@@ -80,7 +80,7 @@
<controls-sort-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesSortBy" :descending.sync="settings.seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" /> <controls-sort-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesSortBy" :descending.sync="settings.seriesSortDesc" :items="seriesSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesSort" />
<!-- issues page remove all button --> <!-- issues page remove all button -->
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn> <ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ $formatNumber(numShowing) }} {{ entityName }}</ui-btn>
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" /> <ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
</template> </template>
@@ -246,9 +246,6 @@ export default {
isPodcastLibrary() { isPodcastLibrary() {
return this.currentLibraryMediaType === 'podcast' return this.currentLibraryMediaType === 'podcast'
}, },
isMusicLibrary() {
return this.currentLibraryMediaType === 'music'
},
isLibraryPage() { isLibraryPage() {
return this.page === '' return this.page === ''
}, },
@@ -281,7 +278,6 @@ export default {
}, },
entityName() { entityName() {
if (this.isAlbumsPage) return 'Albums' if (this.isAlbumsPage) return 'Albums'
if (this.isMusicLibrary) return 'Tracks'
if (this.isPodcastLibrary) return this.$strings.LabelPodcasts if (this.isPodcastLibrary) return this.$strings.LabelPodcasts
if (!this.page) return this.$strings.LabelBooks if (!this.page) return this.$strings.LabelBooks
+2 -11
View File
@@ -1,10 +1,9 @@
<template> <template>
<div v-if="streamLibraryItem" id="mediaPlayerContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 lg:h-40 z-50 bg-primary px-2 lg:px-4 pb-1 lg:pb-4 pt-2"> <div v-if="streamLibraryItem" id="mediaPlayerContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 lg:h-40 z-50 bg-primary px-2 lg:px-4 pb-1 lg:pb-4 pt-2">
<div id="videoDock" />
<div class="absolute left-2 top-2 lg:left-4 cursor-pointer"> <div class="absolute left-2 top-2 lg:left-4 cursor-pointer">
<covers-book-cover expand-on-click :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" /> <covers-book-cover expand-on-click :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
</div> </div>
<div class="flex items-start mb-6 lg:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'"> <div class="flex items-start mb-6 lg:mb-0" :class="isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
<div class="min-w-0 w-full"> <div class="min-w-0 w-full">
<div class="flex items-center"> <div class="flex items-center">
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate"> <nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate">
@@ -12,10 +11,9 @@
</nuxt-link> </nuxt-link>
<widgets-explicit-indicator v-if="isExplicit" /> <widgets-explicit-indicator v-if="isExplicit" />
</div> </div>
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5"> <div class="text-gray-400 flex items-center w-1/2 sm:w-4/5 lg:w-2/5">
<span class="material-symbols text-sm">person</span> <span class="material-symbols text-sm">person</span>
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div> <div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
<div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div>
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate"> <div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base truncate">
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link> <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
</div> </div>
@@ -140,9 +138,6 @@ export default {
isPodcast() { isPodcast() {
return this.streamLibraryItem?.mediaType === 'podcast' return this.streamLibraryItem?.mediaType === 'podcast'
}, },
isMusic() {
return this.streamLibraryItem?.mediaType === 'music'
},
isExplicit() { isExplicit() {
return !!this.mediaMetadata.explicit return !!this.mediaMetadata.explicit
}, },
@@ -174,10 +169,6 @@ export default {
if (!this.isPodcast) return null if (!this.isPodcast) return null
return this.mediaMetadata.author || 'Unknown' return this.mediaMetadata.author || 'Unknown'
}, },
musicArtists() {
if (!this.isMusic) return null
return this.mediaMetadata.artists.join(', ')
},
hasNextItemInQueue() { hasNextItemInQueue() {
return this.currentPlayerQueueIndex < this.playerQueueItems.length - 1 return this.currentPlayerQueueIndex < this.playerQueueItems.length - 1
}, },
-14
View File
@@ -95,14 +95,6 @@
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> <div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isMusicLibrary" :to="`/library/${currentLibraryId}/bookshelf/albums`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isMusicAlbumsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-symbols text-xl">album</span>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">Albums</p>
<div v-show="isMusicAlbumsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-symbols text-2xl">&#xf090;</span> <span class="material-symbols text-2xl">&#xf090;</span>
@@ -172,9 +164,6 @@ export default {
isPodcastLibrary() { isPodcastLibrary() {
return this.currentLibraryMediaType === 'podcast' return this.currentLibraryMediaType === 'podcast'
}, },
isMusicLibrary() {
return this.currentLibraryMediaType === 'music'
},
isPodcastDownloadQueuePage() { isPodcastDownloadQueuePage() {
return this.$route.name === 'library-library-podcast-download-queue' return this.$route.name === 'library-library-podcast-download-queue'
}, },
@@ -184,9 +173,6 @@ export default {
isPodcastLatestPage() { isPodcastLatestPage() {
return this.$route.name === 'library-library-podcast-latest' return this.$route.name === 'library-library-podcast-latest'
}, },
isMusicAlbumsPage() {
return this.paramId === 'albums'
},
homePage() { homePage() {
return this.$route.name === 'library-library' return this.$route.name === 'library-library'
}, },
+1 -8
View File
@@ -226,9 +226,6 @@ export default {
isPodcast() { isPodcast() {
return this.mediaType === 'podcast' || this.store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast' return this.mediaType === 'podcast' || this.store.getters['libraries/getCurrentLibraryMediaType'] === 'podcast'
}, },
isMusic() {
return this.mediaType === 'music'
},
isExplicit() { isExplicit() {
return this.mediaMetadata.explicit || false return this.mediaMetadata.explicit || false
}, },
@@ -336,7 +333,6 @@ export default {
displayLineTwo() { displayLineTwo() {
if (this.recentEpisode) return this.title if (this.recentEpisode) return this.title
if (this.isPodcast) return this.author if (this.isPodcast) return this.author
if (this.isMusic) return this.artist
if (this.collapsedSeries) return '' if (this.collapsedSeries) return ''
if (this.isAuthorBookshelfView) { if (this.isAuthorBookshelfView) {
return this.mediaMetadata.publishedYear || '' return this.mediaMetadata.publishedYear || ''
@@ -364,7 +360,6 @@ export default {
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id) return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
}, },
userProgress() { userProgress() {
if (this.isMusic) return null
if (this.episodeProgress) return this.episodeProgress if (this.episodeProgress) return this.episodeProgress
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId) return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
}, },
@@ -420,7 +415,7 @@ export default {
return !this.isSelectionMode && !this.showPlayButton && this.ebookFormat return !this.isSelectionMode && !this.showPlayButton && this.ebookFormat
}, },
showPlayButton() { showPlayButton() {
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode || this.isMusic) return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode)
}, },
showSmallEBookIcon() { showSmallEBookIcon() {
return !this.isSelectionMode && this.ebookFormat return !this.isSelectionMode && this.ebookFormat
@@ -464,8 +459,6 @@ export default {
return this.store.getters['user/getIsAdminOrUp'] return this.store.getters['user/getIsAdminOrUp']
}, },
moreMenuItems() { moreMenuItems() {
if (this.isMusic) return []
if (this.recentEpisode) { if (this.recentEpisode) {
const items = [ const items = [
{ {
@@ -27,38 +27,6 @@
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=publishers.${$encode(publisher)}`" class="hover:underline">{{ publisher }}</nuxt-link> <nuxt-link :to="`/library/${libraryId}/bookshelf?filter=publishers.${$encode(publisher)}`" class="hover:underline">{{ publisher }}</nuxt-link>
</div> </div>
</div> </div>
<div v-if="musicAlbum" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">Album</span>
</div>
<div>
{{ musicAlbum }}
</div>
</div>
<div v-if="musicAlbumArtist" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">Album Artist</span>
</div>
<div>
{{ musicAlbumArtist }}
</div>
</div>
<div v-if="musicTrackPretty" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">Track</span>
</div>
<div>
{{ musicTrackPretty }}
</div>
</div>
<div v-if="musicDiscPretty" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">Disc</span>
</div>
<div>
{{ musicDiscPretty }}
</div>
</div>
<div v-if="podcastType" class="flex py-0.5"> <div v-if="podcastType" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32"> <div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPodcastType }}</span> <span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPodcastType }}</span>
@@ -97,7 +65,7 @@
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=languages.${$encode(language)}`" class="hover:underline">{{ language }}</nuxt-link> <nuxt-link :to="`/library/${libraryId}/bookshelf?filter=languages.${$encode(language)}`" class="hover:underline">{{ language }}</nuxt-link>
</div> </div>
</div> </div>
<div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5"> <div v-if="tracks.length || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32"> <div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span> <span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
</div> </div>
@@ -134,10 +102,6 @@ export default {
isPodcast() { isPodcast() {
return this.libraryItem.mediaType === 'podcast' return this.libraryItem.mediaType === 'podcast'
}, },
audioFile() {
// Music track
return this.media.audioFile
},
media() { media() {
return this.libraryItem.media || {} return this.libraryItem.media || {}
}, },
@@ -168,25 +132,6 @@ export default {
publisher() { publisher() {
return this.mediaMetadata.publisher || '' return this.mediaMetadata.publisher || ''
}, },
musicArtists() {
return this.mediaMetadata.artists || []
},
musicAlbum() {
return this.mediaMetadata.album || ''
},
musicAlbumArtist() {
return this.mediaMetadata.albumArtist || ''
},
musicTrackPretty() {
if (!this.mediaMetadata.trackNumber) return null
if (!this.mediaMetadata.trackTotal) return this.mediaMetadata.trackNumber
return `${this.mediaMetadata.trackNumber} / ${this.mediaMetadata.trackTotal}`
},
musicDiscPretty() {
if (!this.mediaMetadata.discNumber) return null
if (!this.mediaMetadata.discTotal) return this.mediaMetadata.discNumber
return `${this.mediaMetadata.discNumber} / ${this.mediaMetadata.discTotal}`
},
narrators() { narrators() {
return this.mediaMetadata.narrators || [] return this.mediaMetadata.narrators || []
}, },
@@ -220,4 +165,4 @@ export default {
methods: {}, methods: {},
mounted() {} mounted() {}
} }
</script> </script>
@@ -98,9 +98,6 @@ export default {
isPodcast() { isPodcast() {
return this.libraryMediaType === 'podcast' return this.libraryMediaType === 'podcast'
}, },
isMusic() {
return this.libraryMediaType === 'music'
},
seriesItems() { seriesItems() {
return [ return [
{ {
@@ -274,35 +271,9 @@ export default {
} }
] ]
}, },
musicItems() {
return [
{
text: this.$strings.LabelAll,
value: 'all'
},
{
text: this.$strings.LabelGenre,
textPlural: this.$strings.LabelGenres,
value: 'genres',
sublist: true
},
{
text: this.$strings.LabelTag,
textPlural: this.$strings.LabelTags,
value: 'tags',
sublist: true
},
{
text: this.$strings.ButtonIssues,
value: 'issues',
sublist: false
}
]
},
selectItems() { selectItems() {
if (this.isSeries) return this.seriesItems if (this.isSeries) return this.seriesItems
if (this.isPodcast) return this.podcastItems if (this.isPodcast) return this.podcastItems
if (this.isMusic) return this.musicItems
return this.bookItems return this.bookItems
}, },
selectedItemSublist() { selectedItemSublist() {
@@ -56,9 +56,6 @@ export default {
isPodcast() { isPodcast() {
return this.libraryMediaType === 'podcast' return this.libraryMediaType === 'podcast'
}, },
isMusic() {
return this.libraryMediaType === 'music'
},
podcastItems() { podcastItems() {
return [ return [
{ {
@@ -148,40 +145,10 @@ export default {
} }
] ]
}, },
musicItems() {
return [
{
text: this.$strings.LabelTitle,
value: 'media.metadata.title'
},
{
text: this.$strings.LabelAddedAt,
value: 'addedAt'
},
{
text: this.$strings.LabelSize,
value: 'size'
},
{
text: this.$strings.LabelDuration,
value: 'media.duration'
},
{
text: this.$strings.LabelFileBirthtime,
value: 'birthtimeMs'
},
{
text: this.$strings.LabelFileModified,
value: 'mtimeMs'
}
]
},
selectItems() { selectItems() {
let items = null let items = null
if (this.isPodcast) { if (this.isPodcast) {
items = this.podcastItems items = this.podcastItems
} else if (this.isMusic) {
items = this.musicItems
} else if (this.$store.getters['user/getUserSetting']('filterBy').startsWith('series.')) { } else if (this.$store.getters['user/getUserSetting']('filterBy').startsWith('series.')) {
items = this.seriesItems items = this.seriesItems
} else { } else {
-16
View File
@@ -178,22 +178,6 @@ export default {
methods: { methods: {
toggleFullscreen(isFullscreen) { toggleFullscreen(isFullscreen) {
this.$store.commit('setPlayerIsFullscreen', isFullscreen) this.$store.commit('setPlayerIsFullscreen', isFullscreen)
var videoPlayerEl = document.getElementById('video-player')
if (videoPlayerEl) {
if (isFullscreen) {
videoPlayerEl.style.width = '100vw'
videoPlayerEl.style.height = '100vh'
videoPlayerEl.style.top = '0px'
videoPlayerEl.style.left = '0px'
} else {
videoPlayerEl.style.width = '384px'
videoPlayerEl.style.height = '216px'
videoPlayerEl.style.top = 'unset'
videoPlayerEl.style.bottom = '80px'
videoPlayerEl.style.left = '16px'
}
}
}, },
setDuration(duration) { setDuration(duration) {
this.duration = duration this.duration = duration
+23 -16
View File
@@ -3,67 +3,67 @@
<form class="w-full h-full px-2 md:px-4 py-6" @submit.prevent="submitForm"> <form class="w-full h-full px-2 md:px-4 py-6" @submit.prevent="submitForm">
<div class="flex flex-wrap -mx-1"> <div class="flex flex-wrap -mx-1">
<div class="w-full md:w-1/2 px-1"> <div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" /> <ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" @input="handleInputChange" />
</div> </div>
<div class="flex-grow px-1 mt-2 md:mt-0"> <div class="flex-grow px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" :label="$strings.LabelSubtitle" /> <ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" :label="$strings.LabelSubtitle" @input="handleInputChange" />
</div> </div>
</div> </div>
<div class="flex flex-wrap mt-2 -mx-1"> <div class="flex flex-wrap mt-2 -mx-1">
<div class="w-full md:w-3/4 px-1"> <div class="w-full md:w-3/4 px-1">
<!-- Authors filter only contains authors in this library, uses filter data --> <!-- Authors filter only contains authors in this library, uses filter data -->
<ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" :label="$strings.LabelAuthors" filter-key="authors" /> <ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" :label="$strings.LabelAuthors" filter-key="authors" @input="handleInputChange" />
</div> </div>
<div class="flex-grow px-1 mt-2 md:mt-0"> <div class="flex-grow px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" :label="$strings.LabelPublishYear" /> <ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" :label="$strings.LabelPublishYear" @input="handleInputChange" />
</div> </div>
</div> </div>
<div class="flex mt-2 -mx-1"> <div class="flex mt-2 -mx-1">
<div class="flex-grow px-1"> <div class="flex-grow px-1">
<widgets-series-input-widget v-model="details.series" /> <widgets-series-input-widget v-model="details.series" @input="handleInputChange" />
</div> </div>
</div> </div>
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" /> <ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" />
<div class="flex flex-wrap mt-2 -mx-1"> <div class="flex flex-wrap mt-2 -mx-1">
<div class="w-full md:w-1/2 px-1"> <div class="w-full md:w-1/2 px-1">
<ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" /> <ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" @input="handleInputChange" />
</div> </div>
<div class="flex-grow px-1 mt-2 md:mt-0"> <div class="flex-grow px-1 mt-2 md:mt-0">
<ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" /> <ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" @input="handleInputChange" />
</div> </div>
</div> </div>
<div class="flex flex-wrap mt-2 -mx-1"> <div class="flex flex-wrap mt-2 -mx-1">
<div class="w-full md:w-1/2 px-1"> <div class="w-full md:w-1/2 px-1">
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" :label="$strings.LabelNarrators" :items="narrators" /> <ui-multi-select ref="narratorsSelect" v-model="details.narrators" :label="$strings.LabelNarrators" :items="narrators" @input="handleInputChange" />
</div> </div>
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0"> <div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" /> <ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" @input="handleInputChange" />
</div> </div>
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0"> <div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" /> <ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" @input="handleInputChange" />
</div> </div>
</div> </div>
<div class="flex flex-wrap mt-2 -mx-1"> <div class="flex flex-wrap mt-2 -mx-1">
<div class="w-full md:w-1/4 px-1"> <div class="w-full md:w-1/4 px-1">
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" /> <ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" @input="handleInputChange" />
</div> </div>
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0"> <div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" /> <ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" @input="handleInputChange" />
</div> </div>
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0"> <div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
<div class="flex justify-center"> <div class="flex justify-center">
<ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" /> <ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" @input="handleInputChange" />
</div> </div>
</div> </div>
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0"> <div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
<div class="flex justify-center"> <div class="flex justify-center">
<ui-checkbox v-model="details.abridged" :label="$strings.LabelAbridged" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" /> <ui-checkbox v-model="details.abridged" :label="$strings.LabelAbridged" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" @input="handleInputChange" />
</div> </div>
</div> </div>
</div> </div>
@@ -132,6 +132,12 @@ export default {
} }
}, },
methods: { methods: {
handleInputChange() {
this.$emit('change', {
libraryItemId: this.libraryItem.id,
hasChanges: this.checkForChanges().hasChanges
})
},
getDetails() { getDetails() {
this.forceBlur() this.forceBlur()
return this.checkForChanges() return this.checkForChanges()
@@ -172,6 +178,7 @@ export default {
} }
} }
} }
this.handleInputChange()
}, },
forceBlur() { forceBlur() {
if (this.$refs.titleInput) this.$refs.titleInput.blur() if (this.$refs.titleInput) this.$refs.titleInput.blur()
@@ -286,4 +293,4 @@ export default {
}, },
mounted() {} mounted() {}
} }
</script> </script>
@@ -3,45 +3,45 @@
<form class="w-full h-full px-4 py-6" @submit.prevent="submitForm"> <form class="w-full h-full px-4 py-6" @submit.prevent="submitForm">
<div class="flex -mx-1"> <div class="flex -mx-1">
<div class="w-1/2 px-1"> <div class="w-1/2 px-1">
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" /> <ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" @input="handleInputChange" />
</div> </div>
<div class="flex-grow px-1"> <div class="flex-grow px-1">
<ui-text-input-with-label ref="authorInput" v-model="details.author" :label="$strings.LabelAuthor" /> <ui-text-input-with-label ref="authorInput" v-model="details.author" :label="$strings.LabelAuthor" @input="handleInputChange" />
</div> </div>
</div> </div>
<ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" :label="$strings.LabelRSSFeedURL" class="mt-2" /> <ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" :label="$strings.LabelRSSFeedURL" class="mt-2" @input="handleInputChange" />
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" /> <ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" />
<div class="flex mt-2 -mx-1"> <div class="flex mt-2 -mx-1">
<div class="w-1/2 px-1"> <div class="w-1/2 px-1">
<ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" /> <ui-multi-select ref="genresSelect" v-model="details.genres" :label="$strings.LabelGenres" :items="genres" @input="handleInputChange" />
</div> </div>
<div class="flex-grow px-1"> <div class="flex-grow px-1">
<ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" /> <ui-multi-select ref="tagsSelect" v-model="newTags" :label="$strings.LabelTags" :items="tags" @input="handleInputChange" />
</div> </div>
</div> </div>
<div class="flex mt-2 -mx-1"> <div class="flex mt-2 -mx-1">
<div class="w-1/4 px-1"> <div class="w-1/4 px-1">
<ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" :label="$strings.LabelReleaseDate" /> <ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" :label="$strings.LabelReleaseDate" @input="handleInputChange" />
</div> </div>
<div class="w-1/4 px-1"> <div class="w-1/4 px-1">
<ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" /> <ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" @input="handleInputChange" />
</div> </div>
<div class="w-1/4 px-1"> <div class="w-1/4 px-1">
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" /> <ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" @input="handleInputChange" />
</div> </div>
<div class="flex-grow px-1 pt-6"> <div class="flex-grow px-1 pt-6">
<div class="flex justify-center"> <div class="flex justify-center">
<ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" /> <ui-checkbox v-model="details.explicit" :label="$strings.LabelExplicit" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-base font-semibold" @input="handleInputChange" />
</div> </div>
</div> </div>
</div> </div>
<div class="flex mt-2 -mx-1"> <div class="flex mt-2 -mx-1">
<div class="w-1/4 px-1"> <div class="w-1/4 px-1">
<ui-dropdown :label="$strings.LabelPodcastType" v-model="details.type" :items="podcastTypes" small class="max-w-52" /> <ui-dropdown :label="$strings.LabelPodcastType" v-model="details.type" :items="podcastTypes" small class="max-w-52" @input="handleInputChange" />
</div> </div>
</div> </div>
</form> </form>
@@ -105,6 +105,12 @@ export default {
} }
}, },
methods: { methods: {
handleInputChange() {
this.$emit('change', {
libraryItemId: this.libraryItem.id,
hasChanges: this.checkForChanges().hasChanges
})
},
getDetails() { getDetails() {
this.forceBlur() this.forceBlur()
return this.checkForChanges() return this.checkForChanges()
@@ -136,6 +142,8 @@ export default {
} }
} }
} }
this.handleInputChange()
}, },
forceBlur() { forceBlur() {
if (this.$refs.titleInput) this.$refs.titleInput.blur() if (this.$refs.titleInput) this.$refs.titleInput.blur()
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.13.2", "version": "2.13.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.13.2", "version": "2.13.4",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@nuxtjs/axios": "^5.13.6", "@nuxtjs/axios": "^5.13.6",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.13.2", "version": "2.13.4",
"buildNumber": 1, "buildNumber": 1,
"description": "Self-hosted audiobook and podcast client", "description": "Self-hosted audiobook and podcast client",
"main": "index.js", "main": "index.js",
+38 -36
View File
@@ -97,8 +97,8 @@
<div class="flex justify-center flex-wrap"> <div class="flex justify-center flex-wrap">
<template v-for="libraryItem in libraryItemCopies"> <template v-for="libraryItem in libraryItemCopies">
<div :key="libraryItem.id" class="w-full max-w-3xl border border-black-300 p-6 -ml-px -mt-px"> <div :key="libraryItem.id" class="w-full max-w-3xl border border-black-300 p-6 -ml-px -mt-px">
<widgets-book-details-edit v-if="libraryItem.mediaType === 'book'" :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" /> <widgets-book-details-edit v-if="libraryItem.mediaType === 'book'" :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" @change="handleItemChange" />
<widgets-podcast-details-edit v-else :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" /> <widgets-podcast-details-edit v-else :ref="`itemForm-${libraryItem.id}`" :library-item="libraryItem" @change="handleItemChange" />
</div> </div>
</template> </template>
</div> </div>
@@ -108,7 +108,7 @@
<div :class="isScrollable ? 'fixed left-0 box-shadow-lg-up bg-primary' : ''" class="w-full h-20 px-4 flex items-center border-t border-bg z-40" :style="{ bottom: streamLibraryItem ? '165px' : '0px' }"> <div :class="isScrollable ? 'fixed left-0 box-shadow-lg-up bg-primary' : ''" class="w-full h-20 px-4 flex items-center border-t border-bg z-40" :style="{ bottom: streamLibraryItem ? '165px' : '0px' }">
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn color="success" :padding-x="8" class="text-lg" :loading="isProcessing" @click.prevent="saveClick">{{ $strings.ButtonSave }}</ui-btn> <ui-btn color="success" :padding-x="8" class="text-lg" :loading="isProcessing" :disabled="!hasChanges" @click.prevent="saveClick">{{ $strings.ButtonSave }}</ui-btn>
</div> </div>
</div> </div>
</template> </template>
@@ -170,7 +170,8 @@ export default {
abridged: false abridged: false
}, },
appendableKeys: ['authors', 'genres', 'tags', 'narrators', 'series'], appendableKeys: ['authors', 'genres', 'tags', 'narrators', 'series'],
openMapOptions: false openMapOptions: false,
itemsWithChanges: []
} }
}, },
computed: { computed: {
@@ -221,9 +222,19 @@ export default {
}, },
hasSelectedBatchUsage() { hasSelectedBatchUsage() {
return Object.values(this.selectedBatchUsage).some((b) => !!b) return Object.values(this.selectedBatchUsage).some((b) => !!b)
},
hasChanges() {
return this.itemsWithChanges.length > 0
} }
}, },
methods: { methods: {
handleItemChange(itemChange) {
if (!itemChange.hasChanges) {
this.itemsWithChanges = this.itemsWithChanges.filter((id) => id !== itemChange.libraryItemId)
} else if (!this.itemsWithChanges.includes(itemChange.libraryItemId)) {
this.itemsWithChanges.push(itemChange.libraryItemId)
}
},
blurBatchForm() { blurBatchForm() {
if (this.$refs.seriesSelect && this.$refs.seriesSelect.isFocused) { if (this.$refs.seriesSelect && this.$refs.seriesSelect.isFocused) {
this.$refs.seriesSelect.forceBlur() this.$refs.seriesSelect.forceBlur()
@@ -283,38 +294,10 @@ export default {
removedSeriesItem(item) {}, removedSeriesItem(item) {},
newNarratorItem(item) {}, newNarratorItem(item) {},
removedNarratorItem(item) {}, removedNarratorItem(item) {},
newTagItem(item) { newTagItem(item) {},
// if (item && !this.newTagItems.includes(item)) { removedTagItem(item) {},
// this.newTagItems.push(item) newGenreItem(item) {},
// } removedGenreItem(item) {},
},
removedTagItem(item) {
// If newly added, remove if not used on any other items
// if (item && this.newTagItems.includes(item)) {
// var usedByOtherAb = this.libraryItemCopies.find((ab) => {
// return ab.tags && ab.tags.includes(item)
// })
// if (!usedByOtherAb) {
// this.newTagItems = this.newTagItems.filter((t) => t !== item)
// }
// }
},
newGenreItem(item) {
// if (item && !this.newGenreItems.includes(item)) {
// this.newGenreItems.push(item)
// }
},
removedGenreItem(item) {
// If newly added, remove if not used on any other items
// if (item && this.newGenreItems.includes(item)) {
// var usedByOtherAb = this.libraryItemCopies.find((ab) => {
// return ab.book.genres && ab.book.genres.includes(item)
// })
// if (!usedByOtherAb) {
// this.newGenreItems = this.newGenreItems.filter((t) => t !== item)
// }
// }
},
init() { init() {
// TODO: Better deep cloning of library items // TODO: Better deep cloning of library items
this.libraryItemCopies = this.libraryItems.map((li) => { this.libraryItemCopies = this.libraryItems.map((li) => {
@@ -376,6 +359,7 @@ export default {
.then((data) => { .then((data) => {
this.isProcessing = false this.isProcessing = false
if (data.updates) { if (data.updates) {
this.itemsWithChanges = []
this.$toast.success(`Successfully updated ${data.updates} items`) this.$toast.success(`Successfully updated ${data.updates} items`)
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf`) this.$router.replace(`/library/${this.currentLibraryId}/bookshelf`)
} else { } else {
@@ -387,10 +371,28 @@ export default {
this.$toast.error('Failed to batch update') this.$toast.error('Failed to batch update')
this.isProcessing = false this.isProcessing = false
}) })
},
beforeUnload(e) {
if (!e || !this.hasChanges) return
e.preventDefault()
e.returnValue = ''
}
},
beforeRouteLeave(to, from, next) {
if (this.hasChanges) {
next(false)
window.location = to.path
} else {
next()
} }
}, },
mounted() { mounted() {
this.init() this.init()
window.addEventListener('beforeunload', this.beforeUnload)
},
beforeDestroy() {
window.removeEventListener('beforeunload', this.beforeUnload)
} }
} }
</script> </script>
+7 -31
View File
@@ -39,16 +39,11 @@
><span :key="index" v-if="index < seriesList.length - 1">, </span> ><span :key="index" v-if="index < seriesList.length - 1">, </span>
</template> </template>
<template v-if="!isVideo"> <p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">{{ $getString('LabelByAuthor', [podcastAuthor]) }}</p>
<p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">{{ $getString('LabelByAuthor', [podcastAuthor]) }}</p> <p v-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden overflow-ellipsis">
<p v-else-if="musicArtists.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden overflow-ellipsis"> by <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
<nuxt-link v-for="(artist, index) in musicArtists" :key="index" :to="`/artist/${$encode(artist)}`" class="hover:underline">{{ artist }}<span v-if="index < musicArtists.length - 1">,&nbsp;</span></nuxt-link> </p>
</p> <p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
<p v-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden overflow-ellipsis">
by <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">,&nbsp;</span></nuxt-link>
</p>
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
</template>
<content-library-item-details :library-item="libraryItem" /> <content-library-item-details :library-item="libraryItem" />
</div> </div>
@@ -109,7 +104,7 @@
<ui-icon-btn icon="&#xe3c9;" outlined class="mx-0.5" @click="editClick" /> <ui-icon-btn icon="&#xe3c9;" outlined class="mx-0.5" @click="editClick" />
</ui-tooltip> </ui-tooltip>
<ui-tooltip v-if="!isPodcast && !isMusic" :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top"> <ui-tooltip v-if="!isPodcast" :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="mx-0.5" @click="toggleFinished" /> <ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="mx-0.5" @click="toggleFinished" />
</ui-tooltip> </ui-tooltip>
@@ -220,12 +215,6 @@ export default {
isPodcast() { isPodcast() {
return this.libraryItem.mediaType === 'podcast' return this.libraryItem.mediaType === 'podcast'
}, },
isVideo() {
return this.libraryItem.mediaType === 'video'
},
isMusic() {
return this.libraryItem.mediaType === 'music'
},
isMissing() { isMissing() {
return this.libraryItem.isMissing return this.libraryItem.isMissing
}, },
@@ -240,8 +229,6 @@ export default {
}, },
showPlayButton() { showPlayButton() {
if (this.isMissing || this.isInvalid) return false if (this.isMissing || this.isInvalid) return false
if (this.isMusic) return !!this.audioFile
if (this.isVideo) return !!this.videoFile
if (this.isPodcast) return this.podcastEpisodes.length if (this.isPodcast) return this.podcastEpisodes.length
return this.tracks.length return this.tracks.length
}, },
@@ -292,9 +279,6 @@ export default {
authors() { authors() {
return this.mediaMetadata.authors || [] return this.mediaMetadata.authors || []
}, },
musicArtists() {
return this.mediaMetadata.artists || []
},
series() { series() {
return this.mediaMetadata.series || [] return this.mediaMetadata.series || []
}, },
@@ -309,7 +293,7 @@ export default {
}) })
}, },
duration() { duration() {
if (!this.tracks.length && !this.audioFile) return 0 if (!this.tracks.length) return 0
return this.media.duration return this.media.duration
}, },
libraryFiles() { libraryFiles() {
@@ -321,18 +305,10 @@ export default {
ebookFile() { ebookFile() {
return this.media.ebookFile return this.media.ebookFile
}, },
videoFile() {
return this.media.videoFile
},
audioFile() {
// Music track
return this.media.audioFile
},
description() { description() {
return this.mediaMetadata.description || '' return this.mediaMetadata.description || ''
}, },
userMediaProgress() { userMediaProgress() {
if (this.isMusic) return null
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId) return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
}, },
userIsFinished() { userIsFinished() {
-260
View File
@@ -1,260 +0,0 @@
import Hls from 'hls.js'
import EventEmitter from 'events'
export default class LocalVideoPlayer extends EventEmitter {
constructor(ctx) {
super()
this.ctx = ctx
this.player = null
this.libraryItem = null
this.videoTrack = null
this.isHlsTranscode = null
this.hlsInstance = null
this.usingNativeplayer = false
this.startTime = 0
this.playWhenReady = false
this.defaultPlaybackRate = 1
this.playableMimeTypes = []
this.initialize()
}
initialize() {
if (document.getElementById('video-player')) {
document.getElementById('video-player').remove()
}
var videoEl = document.createElement('video')
videoEl.id = 'video-player'
// videoEl.style.display = 'none'
videoEl.className = 'absolute bg-black z-50'
videoEl.style.height = '216px'
videoEl.style.width = '384px'
videoEl.style.bottom = '80px'
videoEl.style.left = '16px'
document.body.appendChild(videoEl)
this.player = videoEl
this.player.addEventListener('play', this.evtPlay.bind(this))
this.player.addEventListener('pause', this.evtPause.bind(this))
this.player.addEventListener('progress', this.evtProgress.bind(this))
this.player.addEventListener('ended', this.evtEnded.bind(this))
this.player.addEventListener('error', this.evtError.bind(this))
this.player.addEventListener('loadedmetadata', this.evtLoadedMetadata.bind(this))
this.player.addEventListener('timeupdate', this.evtTimeupdate.bind(this))
var mimeTypes = ['video/mp4']
var mimeTypeCanPlayMap = {}
mimeTypes.forEach((mt) => {
var canPlay = this.player.canPlayType(mt)
mimeTypeCanPlayMap[mt] = canPlay
if (canPlay) this.playableMimeTypes.push(mt)
})
console.log(`[LocalVideoPlayer] Supported mime types`, mimeTypeCanPlayMap, this.playableMimeTypes)
}
evtPlay() {
this.emit('stateChange', 'PLAYING')
}
evtPause() {
this.emit('stateChange', 'PAUSED')
}
evtProgress() {
var lastBufferTime = this.getLastBufferedTime()
this.emit('buffertimeUpdate', lastBufferTime)
}
evtEnded() {
console.log(`[LocalVideoPlayer] Ended`)
this.emit('finished')
}
evtError(error) {
console.error('Player error', error)
this.emit('error', error)
}
evtLoadedMetadata(data) {
if (!this.isHlsTranscode) {
this.player.currentTime = this.startTime
}
this.emit('stateChange', 'LOADED')
if (this.playWhenReady) {
this.playWhenReady = false
this.play()
}
}
evtTimeupdate() {
if (this.player.paused) {
this.emit('timeupdate', this.getCurrentTime())
}
}
destroy() {
this.destroyHlsInstance()
if (this.player) {
this.player.remove()
}
}
set(libraryItem, videoTrack, isHlsTranscode, startTime, playWhenReady = false) {
this.libraryItem = libraryItem
this.videoTrack = videoTrack
this.isHlsTranscode = isHlsTranscode
this.playWhenReady = playWhenReady
this.startTime = startTime
if (this.hlsInstance) {
this.destroyHlsInstance()
}
if (this.isHlsTranscode) {
this.setHlsStream()
} else {
this.setDirectPlay()
}
}
setHlsStream() {
// iOS does not support Media Elements but allows for HLS in the native video player
if (!Hls.isSupported()) {
console.warn('HLS is not supported - fallback to using video element')
this.usingNativeplayer = true
this.player.src = this.videoTrack.relativeContentUrl
this.player.currentTime = this.startTime
return
}
var hlsOptions = {
startPosition: this.startTime || -1
// No longer needed because token is put in a query string
// xhrSetup: (xhr) => {
// xhr.setRequestHeader('Authorization', `Bearer ${this.token}`)
// }
}
this.hlsInstance = new Hls(hlsOptions)
this.hlsInstance.attachMedia(this.player)
this.hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => {
this.hlsInstance.loadSource(this.videoTrack.relativeContentUrl)
this.hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
console.log('[HLS] Manifest Parsed')
})
this.hlsInstance.on(Hls.Events.ERROR, (e, data) => {
console.error('[HLS] Error', data.type, data.details, data)
if (data.details === Hls.ErrorDetails.BUFFER_STALLED_ERROR) {
console.error('[HLS] BUFFER STALLED ERROR')
}
})
this.hlsInstance.on(Hls.Events.DESTROYING, () => {
console.log('[HLS] Destroying HLS Instance')
})
})
}
setDirectPlay() {
this.player.src = this.videoTrack.relativeContentUrl
console.log(`[LocalVideoPlayer] Loading track src ${this.videoTrack.relativeContentUrl}`)
this.player.load()
}
destroyHlsInstance() {
if (!this.hlsInstance) return
if (this.hlsInstance.destroy) {
var temp = this.hlsInstance
temp.destroy()
}
this.hlsInstance = null
}
async resetStream(startTime) {
this.destroyHlsInstance()
await new Promise((resolve) => setTimeout(resolve, 1000))
this.set(this.libraryItem, this.videoTrack, this.isHlsTranscode, startTime, true)
}
playPause() {
if (!this.player) return
if (this.player.paused) this.play()
else this.pause()
}
play() {
if (this.player) this.player.play()
}
pause() {
if (this.player) this.player.pause()
}
getCurrentTime() {
return this.player ? this.player.currentTime : 0
}
getDuration() {
return this.videoTrack.duration
}
setPlaybackRate(playbackRate) {
if (!this.player) return
this.defaultPlaybackRate = playbackRate
this.player.playbackRate = playbackRate
}
seek(time) {
if (!this.player) return
this.player.currentTime = Math.max(0, time)
}
setVolume(volume) {
if (!this.player) return
this.player.volume = volume
}
// Utils
isValidDuration(duration) {
if (duration && !isNaN(duration) && duration !== Number.POSITIVE_INFINITY && duration !== Number.NEGATIVE_INFINITY) {
return true
}
return false
}
getBufferedRanges() {
if (!this.player) return []
const ranges = []
const seekable = this.player.buffered || []
let offset = 0
for (let i = 0, length = seekable.length; i < length; i++) {
let start = seekable.start(i)
let end = seekable.end(i)
if (!this.isValidDuration(start)) {
start = 0
}
if (!this.isValidDuration(end)) {
end = 0
continue
}
ranges.push({
start: start + offset,
end: end + offset
})
}
return ranges
}
getLastBufferedTime() {
var bufferedRanges = this.getBufferedRanges()
if (!bufferedRanges.length) return 0
var buff = bufferedRanges.find((buff) => buff.start < this.player.currentTime && buff.end > this.player.currentTime)
if (buff) return buff.end
var last = bufferedRanges[bufferedRanges.length - 1]
return last.end
}
}
+11 -36
View File
@@ -1,8 +1,6 @@
import LocalAudioPlayer from './LocalAudioPlayer' import LocalAudioPlayer from './LocalAudioPlayer'
import LocalVideoPlayer from './LocalVideoPlayer'
import CastPlayer from './CastPlayer' import CastPlayer from './CastPlayer'
import AudioTrack from './AudioTrack' import AudioTrack from './AudioTrack'
import VideoTrack from './VideoTrack'
export default class PlayerHandler { export default class PlayerHandler {
constructor(ctx) { constructor(ctx) {
@@ -16,8 +14,6 @@ export default class PlayerHandler {
this.player = null this.player = null
this.playerState = 'IDLE' this.playerState = 'IDLE'
this.isHlsTranscode = false this.isHlsTranscode = false
this.isVideo = false
this.isMusic = false
this.currentSessionId = null this.currentSessionId = null
this.startTimeOverride = undefined // Used for starting playback at a specific time (i.e. clicking bookmark from library item page) this.startTimeOverride = undefined // Used for starting playback at a specific time (i.e. clicking bookmark from library item page)
this.startTime = 0 this.startTime = 0
@@ -65,12 +61,10 @@ export default class PlayerHandler {
load(libraryItem, episodeId, playWhenReady, playbackRate, startTimeOverride = undefined) { load(libraryItem, episodeId, playWhenReady, playbackRate, startTimeOverride = undefined) {
this.libraryItem = libraryItem this.libraryItem = libraryItem
this.isVideo = libraryItem.mediaType === 'video'
this.isMusic = libraryItem.mediaType === 'music'
this.episodeId = episodeId this.episodeId = episodeId
this.playWhenReady = playWhenReady this.playWhenReady = playWhenReady
this.initialPlaybackRate = this.isMusic ? 1 : playbackRate this.initialPlaybackRate = playbackRate
this.startTimeOverride = startTimeOverride == null || isNaN(startTimeOverride) ? undefined : Number(startTimeOverride) this.startTimeOverride = startTimeOverride == null || isNaN(startTimeOverride) ? undefined : Number(startTimeOverride)
@@ -97,7 +91,7 @@ export default class PlayerHandler {
this.playWhenReady = playWhenReady this.playWhenReady = playWhenReady
this.prepare() this.prepare()
} }
} else if (!this.isCasting && !(this.player instanceof LocalAudioPlayer) && !(this.player instanceof LocalVideoPlayer)) { } else if (!this.isCasting && !(this.player instanceof LocalAudioPlayer)) {
console.log('[PlayerHandler] Switching to local player') console.log('[PlayerHandler] Switching to local player')
this.stopPlayInterval() this.stopPlayInterval()
@@ -107,11 +101,7 @@ export default class PlayerHandler {
this.player.destroy() this.player.destroy()
} }
if (this.isVideo) { this.player = new LocalAudioPlayer(this.ctx)
this.player = new LocalVideoPlayer(this.ctx)
} else {
this.player = new LocalAudioPlayer(this.ctx)
}
this.setPlayerListeners() this.setPlayerListeners()
@@ -203,7 +193,7 @@ export default class PlayerHandler {
supportedMimeTypes: this.player.playableMimeTypes, supportedMimeTypes: this.player.playableMimeTypes,
mediaPlayer: this.isCasting ? 'chromecast' : 'html5', mediaPlayer: this.isCasting ? 'chromecast' : 'html5',
forceTranscode, forceTranscode,
forceDirectPlay: this.isCasting || this.isVideo // TODO: add transcode support for chromecast forceDirectPlay: this.isCasting // TODO: add transcode support for chromecast
} }
const path = this.episodeId ? `/api/items/${this.libraryItem.id}/play/${this.episodeId}` : `/api/items/${this.libraryItem.id}/play` const path = this.episodeId ? `/api/items/${this.libraryItem.id}/play/${this.episodeId}` : `/api/items/${this.libraryItem.id}/play`
@@ -218,7 +208,6 @@ export default class PlayerHandler {
if (!this.player) this.switchPlayer() // Must set player first for open sessions if (!this.player) this.switchPlayer() // Must set player first for open sessions
this.libraryItem = session.libraryItem this.libraryItem = session.libraryItem
this.isVideo = session.libraryItem.mediaType === 'video'
this.playWhenReady = false this.playWhenReady = false
this.initialPlaybackRate = playbackRate this.initialPlaybackRate = playbackRate
this.startTimeOverride = undefined this.startTimeOverride = undefined
@@ -237,28 +226,16 @@ export default class PlayerHandler {
console.log('[PlayerHandler] Preparing Session', session) console.log('[PlayerHandler] Preparing Session', session)
if (session.videoTrack) { var audioTracks = session.audioTracks.map((at) => new AudioTrack(at, this.userToken))
var videoTrack = new VideoTrack(session.videoTrack, this.userToken)
this.ctx.playerLoading = true this.ctx.playerLoading = true
this.isHlsTranscode = true this.isHlsTranscode = true
if (session.playMethod === this.ctx.$constants.PlayMethod.DIRECTPLAY) { if (session.playMethod === this.ctx.$constants.PlayMethod.DIRECTPLAY) {
this.isHlsTranscode = false this.isHlsTranscode = false
}
this.player.set(this.libraryItem, videoTrack, this.isHlsTranscode, this.startTime, this.playWhenReady)
} else {
var audioTracks = session.audioTracks.map((at) => new AudioTrack(at, this.userToken))
this.ctx.playerLoading = true
this.isHlsTranscode = true
if (session.playMethod === this.ctx.$constants.PlayMethod.DIRECTPLAY) {
this.isHlsTranscode = false
}
this.player.set(this.libraryItem, audioTracks, this.isHlsTranscode, this.startTime, this.playWhenReady)
} }
this.player.set(this.libraryItem, audioTracks, this.isHlsTranscode, this.startTime, this.playWhenReady)
// browser media session api // browser media session api
this.ctx.setMediaSession() this.ctx.setMediaSession()
} }
@@ -333,8 +310,6 @@ export default class PlayerHandler {
} }
sendProgressSync(currentTime) { sendProgressSync(currentTime) {
if (this.isMusic) return
const diffSinceLastSync = Math.abs(this.lastSyncTime - currentTime) const diffSinceLastSync = Math.abs(this.lastSyncTime - currentTime)
if (diffSinceLastSync < 1) return if (diffSinceLastSync < 1) return
-32
View File
@@ -1,32 +0,0 @@
export default class VideoTrack {
constructor(track, userToken) {
this.index = track.index || 0
this.startOffset = track.startOffset || 0 // Total time of all previous tracks
this.duration = track.duration || 0
this.title = track.title || ''
this.contentUrl = track.contentUrl || null
this.mimeType = track.mimeType
this.metadata = track.metadata || {}
this.userToken = userToken
}
get fullContentUrl() {
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
if (process.env.NODE_ENV === 'development') {
return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
}
return `${window.location.origin}${this.contentUrl}?token=${this.userToken}`
}
get relativeContentUrl() {
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
if (process.env.NODE_ENV === 'development') {
return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
}
return this.contentUrl + `?token=${this.userToken}`
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
if (isNaN(bytes) || bytes == 0) { if (isNaN(bytes) || bytes == 0) {
return '0 Bytes' return '0 Bytes'
} }
const k = 1024 const k = 1000
const dm = decimals < 0 ? 0 : decimals const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const i = Math.floor(Math.log(bytes) / Math.log(k)) const i = Math.floor(Math.log(bytes) / Math.log(k))
+209 -10
View File
@@ -9,6 +9,7 @@
"ButtonApply": "প্রয়োগ করুন", "ButtonApply": "প্রয়োগ করুন",
"ButtonApplyChapters": "অধ্যায় প্রয়োগ করুন", "ButtonApplyChapters": "অধ্যায় প্রয়োগ করুন",
"ButtonAuthors": "লেখক", "ButtonAuthors": "লেখক",
"ButtonBack": "পেছনে যান",
"ButtonBrowseForFolder": "ফোল্ডারের জন্য ব্রাউজ করুন", "ButtonBrowseForFolder": "ফোল্ডারের জন্য ব্রাউজ করুন",
"ButtonCancel": "বাতিল করুন", "ButtonCancel": "বাতিল করুন",
"ButtonCancelEncode": "এনকোড বাতিল করুন", "ButtonCancelEncode": "এনকোড বাতিল করুন",
@@ -18,6 +19,7 @@
"ButtonChooseFiles": "ফাইল চয়ন করুন", "ButtonChooseFiles": "ফাইল চয়ন করুন",
"ButtonClearFilter": "ফিল্টার পরিষ্কার করুন", "ButtonClearFilter": "ফিল্টার পরিষ্কার করুন",
"ButtonCloseFeed": "ফিড বন্ধ করুন", "ButtonCloseFeed": "ফিড বন্ধ করুন",
"ButtonCloseSession": "খোলা সেশন বন্ধ করুন",
"ButtonCollections": "সংগ্রহ", "ButtonCollections": "সংগ্রহ",
"ButtonConfigureScanner": "স্ক্যানার কনফিগার করুন", "ButtonConfigureScanner": "স্ক্যানার কনফিগার করুন",
"ButtonCreate": "তৈরি করুন", "ButtonCreate": "তৈরি করুন",
@@ -27,6 +29,9 @@
"ButtonEdit": "সম্পাদনা করুন", "ButtonEdit": "সম্পাদনা করুন",
"ButtonEditChapters": "অধ্যায় সম্পাদনা করুন", "ButtonEditChapters": "অধ্যায় সম্পাদনা করুন",
"ButtonEditPodcast": "পডকাস্ট সম্পাদনা করুন", "ButtonEditPodcast": "পডকাস্ট সম্পাদনা করুন",
"ButtonEnable": "সক্রিয় করুন",
"ButtonFireAndFail": "সক্রিয় এবং ব্যর্থ",
"ButtonFireOnTest": "পরীক্ষামূলক ইভেন্টে সক্রিয় করুন",
"ButtonForceReScan": "জোরপূর্বক পুনরায় স্ক্যান করুন", "ButtonForceReScan": "জোরপূর্বক পুনরায় স্ক্যান করুন",
"ButtonFullPath": "সম্পূর্ণ পথ", "ButtonFullPath": "সম্পূর্ণ পথ",
"ButtonHide": "লুকান", "ButtonHide": "লুকান",
@@ -45,6 +50,7 @@
"ButtonNevermind": "কিছু মনে করবেন না", "ButtonNevermind": "কিছু মনে করবেন না",
"ButtonNext": "পরবর্তী", "ButtonNext": "পরবর্তী",
"ButtonNextChapter": "পরবর্তী অধ্যায়", "ButtonNextChapter": "পরবর্তী অধ্যায়",
"ButtonNextItemInQueue": "সারিতে পরের আইটেম",
"ButtonOk": "ঠিক আছে", "ButtonOk": "ঠিক আছে",
"ButtonOpenFeed": "ফিড খুলুন", "ButtonOpenFeed": "ফিড খুলুন",
"ButtonOpenManager": "ম্যানেজার খুলুন", "ButtonOpenManager": "ম্যানেজার খুলুন",
@@ -54,13 +60,17 @@
"ButtonPlaylists": "প্লেলিস্ট", "ButtonPlaylists": "প্লেলিস্ট",
"ButtonPrevious": "পূর্ববর্তী", "ButtonPrevious": "পূর্ববর্তী",
"ButtonPreviousChapter": "আগের অধ্যায়", "ButtonPreviousChapter": "আগের অধ্যায়",
"ButtonProbeAudioFile": "প্রোব অডিও ফাইল",
"ButtonPurgeAllCache": "সমস্ত ক্যাশে পরিষ্কার করুন", "ButtonPurgeAllCache": "সমস্ত ক্যাশে পরিষ্কার করুন",
"ButtonPurgeItemsCache": "আইটেম ক্যাশে পরিষ্কার করুন", "ButtonPurgeItemsCache": "আইটেম ক্যাশে পরিষ্কার করুন",
"ButtonQueueAddItem": "সারিতে যোগ করুন", "ButtonQueueAddItem": "সারিতে যোগ করুন",
"ButtonQueueRemoveItem": "সারি থেকে মুছে ফেলুন", "ButtonQueueRemoveItem": "সারি থেকে মুছে ফেলুন",
"ButtonQuickEmbedMetadata": "মেটাডেটা দ্রুত এম্বেড করুন",
"ButtonQuickMatch": "দ্রুত ম্যাচ", "ButtonQuickMatch": "দ্রুত ম্যাচ",
"ButtonReScan": "পুনরায় স্ক্যান", "ButtonReScan": "পুনরায় স্ক্যান",
"ButtonRead": "পড়ুন", "ButtonRead": "পড়ুন",
"ButtonReadLess": "সংক্ষিপ্ত",
"ButtonReadMore": "বিস্তারিত পড়ুন",
"ButtonRefresh": "রিফ্রেশ", "ButtonRefresh": "রিফ্রেশ",
"ButtonRemove": "মুছে ফেলুন", "ButtonRemove": "মুছে ফেলুন",
"ButtonRemoveAll": "সব মুছে ফেলুন", "ButtonRemoveAll": "সব মুছে ফেলুন",
@@ -85,8 +95,10 @@
"ButtonShow": "দেখান", "ButtonShow": "দেখান",
"ButtonStartM4BEncode": "M4B এনকোড শুরু করুন", "ButtonStartM4BEncode": "M4B এনকোড শুরু করুন",
"ButtonStartMetadataEmbed": "মেটাডেটা এম্বেড শুরু করুন", "ButtonStartMetadataEmbed": "মেটাডেটা এম্বেড শুরু করুন",
"ButtonStats": "পরিসংখ্যান",
"ButtonSubmit": "জমা দিন", "ButtonSubmit": "জমা দিন",
"ButtonTest": "পরীক্ষা", "ButtonTest": "পরীক্ষা",
"ButtonUnlinkOpenId": "ওপেন আইডি লিঙ্কমুক্ত করুন",
"ButtonUpload": "আপলোড", "ButtonUpload": "আপলোড",
"ButtonUploadBackup": "আপলোড ব্যাকআপ", "ButtonUploadBackup": "আপলোড ব্যাকআপ",
"ButtonUploadCover": "কভার আপলোড করুন", "ButtonUploadCover": "কভার আপলোড করুন",
@@ -99,9 +111,10 @@
"ErrorUploadFetchMetadataNoResults": "মেটাডেটা আনা যায়নি - শিরোনাম এবং/অথবা লেখক আপডেট করার চেষ্টা করুন", "ErrorUploadFetchMetadataNoResults": "মেটাডেটা আনা যায়নি - শিরোনাম এবং/অথবা লেখক আপডেট করার চেষ্টা করুন",
"ErrorUploadLacksTitle": "একটি শিরোনাম থাকতে হবে", "ErrorUploadLacksTitle": "একটি শিরোনাম থাকতে হবে",
"HeaderAccount": "অ্যাকাউন্ট", "HeaderAccount": "অ্যাকাউন্ট",
"HeaderAddCustomMetadataProvider": "কাস্টম মেটাডেটা সরবরাহকারী যোগ করুন",
"HeaderAdvanced": "অ্যাডভান্সড", "HeaderAdvanced": "অ্যাডভান্সড",
"HeaderAppriseNotificationSettings": "বিজ্ঞপ্তি সেটিংস অবহিত করুন", "HeaderAppriseNotificationSettings": "বিজ্ঞপ্তি সেটিংস অবহিত করুন",
"HeaderAudioTracks": "অডিও ট্র্যাকস", "HeaderAudioTracks": "অডিও ট্র্যাকসগুলো",
"HeaderAudiobookTools": "অডিওবই ফাইল ম্যানেজমেন্ট টুলস", "HeaderAudiobookTools": "অডিওবই ফাইল ম্যানেজমেন্ট টুলস",
"HeaderAuthentication": "প্রমাণীকরণ", "HeaderAuthentication": "প্রমাণীকরণ",
"HeaderBackups": "ব্যাকআপ", "HeaderBackups": "ব্যাকআপ",
@@ -112,6 +125,7 @@
"HeaderCollectionItems": "সংগ্রহ আইটেম", "HeaderCollectionItems": "সংগ্রহ আইটেম",
"HeaderCover": "কভার", "HeaderCover": "কভার",
"HeaderCurrentDownloads": "বর্তমান ডাউনলোডগুলি", "HeaderCurrentDownloads": "বর্তমান ডাউনলোডগুলি",
"HeaderCustomMessageOnLogin": "লগইন এ কাস্টম বার্তা",
"HeaderCustomMetadataProviders": "কাস্টম মেটাডেটা প্রদানকারী", "HeaderCustomMetadataProviders": "কাস্টম মেটাডেটা প্রদানকারী",
"HeaderDetails": "বিস্তারিত", "HeaderDetails": "বিস্তারিত",
"HeaderDownloadQueue": "ডাউনলোড সারি", "HeaderDownloadQueue": "ডাউনলোড সারি",
@@ -143,6 +157,8 @@
"HeaderMetadataToEmbed": "এম্বেড করার জন্য মেটাডেটা", "HeaderMetadataToEmbed": "এম্বেড করার জন্য মেটাডেটা",
"HeaderNewAccount": "নতুন অ্যাকাউন্ট", "HeaderNewAccount": "নতুন অ্যাকাউন্ট",
"HeaderNewLibrary": "নতুন লাইব্রেরি", "HeaderNewLibrary": "নতুন লাইব্রেরি",
"HeaderNotificationCreate": "বিজ্ঞপ্তি তৈরি করুন",
"HeaderNotificationUpdate": "বিজ্ঞপ্তি আপডেট করুন",
"HeaderNotifications": "বিজ্ঞপ্তি", "HeaderNotifications": "বিজ্ঞপ্তি",
"HeaderOpenIDConnectAuthentication": "ওপেনআইডি সংযোগ প্রমাণীকরণ", "HeaderOpenIDConnectAuthentication": "ওপেনআইডি সংযোগ প্রমাণীকরণ",
"HeaderOpenRSSFeed": "আরএসএস ফিড খুলুন", "HeaderOpenRSSFeed": "আরএসএস ফিড খুলুন",
@@ -150,6 +166,7 @@
"HeaderPasswordAuthentication": "পাসওয়ার্ড প্রমাণীকরণ", "HeaderPasswordAuthentication": "পাসওয়ার্ড প্রমাণীকরণ",
"HeaderPermissions": "অনুমতি", "HeaderPermissions": "অনুমতি",
"HeaderPlayerQueue": "প্লেয়ার সারি", "HeaderPlayerQueue": "প্লেয়ার সারি",
"HeaderPlayerSettings": "প্লেয়ার সেটিংস",
"HeaderPlaylist": "প্লেলিস্ট", "HeaderPlaylist": "প্লেলিস্ট",
"HeaderPlaylistItems": "প্লেলিস্ট আইটেম", "HeaderPlaylistItems": "প্লেলিস্ট আইটেম",
"HeaderPodcastsToAdd": "যোগ করার জন্য পডকাস্ট", "HeaderPodcastsToAdd": "যোগ করার জন্য পডকাস্ট",
@@ -186,6 +203,9 @@
"HeaderYearReview": "বাৎসরিক পর্যালোচনা {0}", "HeaderYearReview": "বাৎসরিক পর্যালোচনা {0}",
"HeaderYourStats": "আপনার পরিসংখ্যান", "HeaderYourStats": "আপনার পরিসংখ্যান",
"LabelAbridged": "সংক্ষিপ্ত", "LabelAbridged": "সংক্ষিপ্ত",
"LabelAbridgedChecked": "সংক্ষিপ্ত (চেক)",
"LabelAbridgedUnchecked": "অসংক্ষেপিত (চেক করা হয়নি)",
"LabelAccessibleBy": "দ্বারা প্রবেশযোগ্য",
"LabelAccountType": "অ্যাকাউন্টের প্রকার", "LabelAccountType": "অ্যাকাউন্টের প্রকার",
"LabelAccountTypeAdmin": "প্রশাসন", "LabelAccountTypeAdmin": "প্রশাসন",
"LabelAccountTypeGuest": "অতিথি", "LabelAccountTypeGuest": "অতিথি",
@@ -196,6 +216,7 @@
"LabelAddToPlaylist": "প্লেলিস্টে যোগ করুন", "LabelAddToPlaylist": "প্লেলিস্টে যোগ করুন",
"LabelAddToPlaylistBatch": "প্লেলিস্টে {0}টি আইটেম যোগ করুন", "LabelAddToPlaylistBatch": "প্লেলিস্টে {0}টি আইটেম যোগ করুন",
"LabelAddedAt": "এতে যোগ করা হয়েছে", "LabelAddedAt": "এতে যোগ করা হয়েছে",
"LabelAddedDate": "যোগ করা হয়েছে {0}",
"LabelAdminUsersOnly": "শুধু অ্যাডমিন ব্যবহারকারী", "LabelAdminUsersOnly": "শুধু অ্যাডমিন ব্যবহারকারী",
"LabelAll": "সব", "LabelAll": "সব",
"LabelAllUsers": "সমস্ত ব্যবহারকারী", "LabelAllUsers": "সমস্ত ব্যবহারকারী",
@@ -218,13 +239,14 @@
"LabelBackupLocation": "ব্যাকআপ অবস্থান", "LabelBackupLocation": "ব্যাকআপ অবস্থান",
"LabelBackupsEnableAutomaticBackups": "স্বয়ংক্রিয় ব্যাকআপ সক্ষম করুন", "LabelBackupsEnableAutomaticBackups": "স্বয়ংক্রিয় ব্যাকআপ সক্ষম করুন",
"LabelBackupsEnableAutomaticBackupsHelp": "ব্যাকআপগুলি /মেটাডাটা/ব্যাকআপে সংরক্ষিত", "LabelBackupsEnableAutomaticBackupsHelp": "ব্যাকআপগুলি /মেটাডাটা/ব্যাকআপে সংরক্ষিত",
"LabelBackupsMaxBackupSize": "সর্বোচ্চ ব্যাকআপ আকার (GB-তে)", "LabelBackupsMaxBackupSize": "সর্বোচ্চ ব্যাকআপ আকার (GB-তে) (অসীমের জন্য 0)",
"LabelBackupsMaxBackupSizeHelp": "ভুল কনফিগারেশনের বিরুদ্ধে সুরক্ষা হিসেবে ব্যাকআপগুলি ব্যর্থ হবে যদি তারা কনফিগার করা আকার অতিক্রম করে।", "LabelBackupsMaxBackupSizeHelp": "ভুল কনফিগারেশনের বিরুদ্ধে সুরক্ষা হিসেবে ব্যাকআপগুলি ব্যর্থ হবে যদি তারা কনফিগার করা আকার অতিক্রম করে।",
"LabelBackupsNumberToKeep": "ব্যাকআপের সংখ্যা রাখুন", "LabelBackupsNumberToKeep": "ব্যাকআপের সংখ্যা রাখুন",
"LabelBackupsNumberToKeepHelp": "এক সময়ে শুধুমাত্র ১ টি ব্যাকআপ সরানো হবে তাই যদি আপনার কাছে ইতিমধ্যে এর চেয়ে বেশি ব্যাকআপ থাকে তাহলে আপনাকে ম্যানুয়ালি সেগুলি সরিয়ে ফেলতে হবে।", "LabelBackupsNumberToKeepHelp": "এক সময়ে শুধুমাত্র ১ টি ব্যাকআপ সরানো হবে তাই যদি আপনার কাছে ইতিমধ্যে এর চেয়ে বেশি ব্যাকআপ থাকে তাহলে আপনাকে ম্যানুয়ালি সেগুলি সরিয়ে ফেলতে হবে।",
"LabelBitrate": "বিটরেট", "LabelBitrate": "বিটরেট",
"LabelBooks": "বইগুলো", "LabelBooks": "বইগুলো",
"LabelButtonText": "ঘর পাঠ্য", "LabelButtonText": "ঘর পাঠ্য",
"LabelByAuthor": "দ্বারা {0}",
"LabelChangePassword": "পাসওয়ার্ড পরিবর্তন করুন", "LabelChangePassword": "পাসওয়ার্ড পরিবর্তন করুন",
"LabelChannels": "চ্যানেল", "LabelChannels": "চ্যানেল",
"LabelChapterTitle": "অধ্যায়ের শিরোনাম", "LabelChapterTitle": "অধ্যায়ের শিরোনাম",
@@ -234,6 +256,7 @@
"LabelClosePlayer": "প্লেয়ার বন্ধ করুন", "LabelClosePlayer": "প্লেয়ার বন্ধ করুন",
"LabelCodec": "কোডেক", "LabelCodec": "কোডেক",
"LabelCollapseSeries": "সিরিজ সঙ্কুচিত করুন", "LabelCollapseSeries": "সিরিজ সঙ্কুচিত করুন",
"LabelCollapseSubSeries": "উপ-সিরিজ সঙ্কুচিত করুন",
"LabelCollection": "সংগ্রহ", "LabelCollection": "সংগ্রহ",
"LabelCollections": "সংগ্রহ", "LabelCollections": "সংগ্রহ",
"LabelComplete": "সম্পূর্ণ", "LabelComplete": "সম্পূর্ণ",
@@ -249,6 +272,7 @@
"LabelCurrently": "বর্তমানে:", "LabelCurrently": "বর্তমানে:",
"LabelCustomCronExpression": "কাস্টম Cron এক্সপ্রেশন:", "LabelCustomCronExpression": "কাস্টম Cron এক্সপ্রেশন:",
"LabelDatetime": "তারিখ সময়", "LabelDatetime": "তারিখ সময়",
"LabelDays": "দিনগুলো",
"LabelDeleteFromFileSystemCheckbox": "ফাইল সিস্টেম থেকে মুছে ফেলুন (শুধু ডাটাবেস থেকে সরাতে টিক চিহ্ন মুক্ত করুন)", "LabelDeleteFromFileSystemCheckbox": "ফাইল সিস্টেম থেকে মুছে ফেলুন (শুধু ডাটাবেস থেকে সরাতে টিক চিহ্ন মুক্ত করুন)",
"LabelDescription": "বিবরণ", "LabelDescription": "বিবরণ",
"LabelDeselectAll": "সমস্ত অনির্বাচিত করুন", "LabelDeselectAll": "সমস্ত অনির্বাচিত করুন",
@@ -262,29 +286,42 @@
"LabelDownload": "ডাউনলোড করুন", "LabelDownload": "ডাউনলোড করুন",
"LabelDownloadNEpisodes": "{0}টি পর্ব ডাউনলোড করুন", "LabelDownloadNEpisodes": "{0}টি পর্ব ডাউনলোড করুন",
"LabelDuration": "সময়কাল", "LabelDuration": "সময়কাল",
"LabelDurationComparisonExactMatch": "(সঠিক মিল)",
"LabelDurationComparisonLonger": "({0} দীর্ঘ)",
"LabelDurationComparisonShorter": "({0} ছোট)",
"LabelDurationFound": "সময়কাল পাওয়া গেছে:", "LabelDurationFound": "সময়কাল পাওয়া গেছে:",
"LabelEbook": "ই-বই", "LabelEbook": "ই-বই",
"LabelEbooks": "ই-বইগুলো", "LabelEbooks": "ই-বইগুলো",
"LabelEdit": "সম্পাদনা করুন", "LabelEdit": "সম্পাদনা করুন",
"LabelEmail": "ইমেইল", "LabelEmail": "ইমেইল",
"LabelEmailSettingsFromAddress": "ঠিকানা থেকে", "LabelEmailSettingsFromAddress": "ঠিকানা থেকে",
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to।", "LabelEmailSettingsRejectUnauthorized": "অননুমোদিত সার্টিফিকেট প্রত্যাখ্যান করুন",
"LabelEmailSettingsRejectUnauthorizedHelp": "SSL প্রমাণপত্রের বৈধতা নিষ্ক্রিয় করা আপনার সংযোগকে নিরাপত্তা ঝুঁকিতে ফেলতে পারে, যেমন ম্যান-ইন-দ্য-মিডল আক্রমণ। শুধুমাত্র এই বিকল্পটি নিষ্ক্রিয় করুন যদি আপনি এর প্রভাবগুলি বুঝতে পারেন এবং আপনি যে মেইল সার্ভারের সাথে সংযোগ করছেন তাকে বিশ্বাস করেন।",
"LabelEmailSettingsSecure": "নিরাপদ", "LabelEmailSettingsSecure": "নিরাপদ",
"LabelEmailSettingsSecureHelp": "যদি সত্য হয় সার্ভারের সাথে সংযোগ করার সময় সংযোগটি TLS ব্যবহার করবে। মিথ্যা হলে TLS ব্যবহার করা হবে যদি সার্ভার STARTTLS এক্সটেনশন সমর্থন করে। বেশিরভাগ ক্ষেত্রে এই মানটিকে সত্য হিসাবে সেট করুন যদি আপনি পোর্ট 465-এর সাথে সংযোগ করছেন। পোর্ট 587 বা পোর্টের জন্য 25 এটি মিথ্যা রাখুন। (nodemailer.com/smtp/#authentication থেকে)", "LabelEmailSettingsSecureHelp": "যদি সত্য হয় সার্ভারের সাথে সংযোগ করার সময় সংযোগটি TLS ব্যবহার করবে। মিথ্যা হলে TLS ব্যবহার করা হবে যদি সার্ভার STARTTLS এক্সটেনশন সমর্থন করে। বেশিরভাগ ক্ষেত্রে এই মানটিকে সত্য হিসাবে সেট করুন যদি আপনি পোর্ট 465-এর সাথে সংযোগ করছেন। পোর্ট 587 বা পোর্টের জন্য 25 এটি মিথ্যা রাখুন। (nodemailer.com/smtp/#authentication থেকে)",
"LabelEmailSettingsTestAddress": "পরীক্ষার ঠিকানা", "LabelEmailSettingsTestAddress": "পরীক্ষার ঠিকানা",
"LabelEmbeddedCover": "এম্বেডেড কভার", "LabelEmbeddedCover": "এম্বেডেড কভার",
"LabelEnable": "সক্ষম করুন", "LabelEnable": "সক্ষম করুন",
"LabelEnd": "সমাপ্ত", "LabelEnd": "সমাপ্ত",
"LabelEndOfChapter": "অধ্যায়ের সমাপ্তি",
"LabelEpisode": "পর্ব", "LabelEpisode": "পর্ব",
"LabelEpisodeTitle": "পর্বের শিরোনাম", "LabelEpisodeTitle": "পর্বের শিরোনাম",
"LabelEpisodeType": "পর্বের ধরন", "LabelEpisodeType": "পর্বের ধরন",
"LabelEpisodes": "পর্বগুলো",
"LabelExample": "উদাহরণ", "LabelExample": "উদাহরণ",
"LabelExpandSeries": "সিরিজ প্রসারিত করুন",
"LabelExpandSubSeries": "সাব সিরিজ প্রসারিত করুন",
"LabelExplicit": "বিশদ", "LabelExplicit": "বিশদ",
"LabelExplicitChecked": "সুস্পষ্ট (পরীক্ষিত)",
"LabelExplicitUnchecked": "অস্পষ্ট (অপরিক্ষীত)",
"LabelExportOPML": "OPML এক্সপোর্ট করুন",
"LabelFeedURL": "ফিড ইউআরএল", "LabelFeedURL": "ফিড ইউআরএল",
"LabelFetchingMetadata": "মেটাডেটা আনা হচ্ছে", "LabelFetchingMetadata": "মেটাডেটা আনা হচ্ছে",
"LabelFile": "ফাইল", "LabelFile": "ফাইল",
"LabelFileBirthtime": "ফাইল জন্মের সময়", "LabelFileBirthtime": "ফাইল জন্মের সময়",
"LabelFileBornDate": "জন্ম {0}",
"LabelFileModified": "ফাইল পরিবর্তিত", "LabelFileModified": "ফাইল পরিবর্তিত",
"LabelFileModifiedDate": "পরিবর্তিত {0}",
"LabelFilename": "ফাইলের নাম", "LabelFilename": "ফাইলের নাম",
"LabelFilterByUser": "ব্যবহারকারী দ্বারা ফিল্টারকৃত", "LabelFilterByUser": "ব্যবহারকারী দ্বারা ফিল্টারকৃত",
"LabelFindEpisodes": "পর্বগুলো খুঁজুন", "LabelFindEpisodes": "পর্বগুলো খুঁজুন",
@@ -292,7 +329,8 @@
"LabelFolder": "ফোল্ডার", "LabelFolder": "ফোল্ডার",
"LabelFolders": "ফোল্ডারগুলো", "LabelFolders": "ফোল্ডারগুলো",
"LabelFontBold": "বোল্ড", "LabelFontBold": "বোল্ড",
"LabelFontFamily": "ফন্ট পরিবার", "LabelFontBoldness": "হরফ বোল্ডনেস",
"LabelFontFamily": "হরফ পরিবার",
"LabelFontItalic": "ইটালিক", "LabelFontItalic": "ইটালিক",
"LabelFontScale": "ফন্ট স্কেল", "LabelFontScale": "ফন্ট স্কেল",
"LabelFontStrikethrough": "অবচ্ছেদন রেখা", "LabelFontStrikethrough": "অবচ্ছেদন রেখা",
@@ -302,9 +340,11 @@
"LabelHardDeleteFile": "জোরপূর্বক ফাইল মুছে ফেলুন", "LabelHardDeleteFile": "জোরপূর্বক ফাইল মুছে ফেলুন",
"LabelHasEbook": "ই-বই আছে", "LabelHasEbook": "ই-বই আছে",
"LabelHasSupplementaryEbook": "পরিপূরক ই-বই আছে", "LabelHasSupplementaryEbook": "পরিপূরক ই-বই আছে",
"LabelHideSubtitles": "সাবটাইটেল লুকান",
"LabelHighestPriority": "সর্বোচ্চ অগ্রাধিকার", "LabelHighestPriority": "সর্বোচ্চ অগ্রাধিকার",
"LabelHost": "নিমন্ত্রণকর্তা", "LabelHost": "নিমন্ত্রণকর্তা",
"LabelHour": "ঘন্টা", "LabelHour": "ঘন্টা",
"LabelHours": "ঘন্টা",
"LabelIcon": "আইকন", "LabelIcon": "আইকন",
"LabelImageURLFromTheWeb": "ওয়েব থেকে ছবির ইউআরএল", "LabelImageURLFromTheWeb": "ওয়েব থেকে ছবির ইউআরএল",
"LabelInProgress": "প্রগতিতে আছে", "LabelInProgress": "প্রগতিতে আছে",
@@ -321,8 +361,11 @@
"LabelIntervalEveryHour": "প্রতি ঘন্টা", "LabelIntervalEveryHour": "প্রতি ঘন্টা",
"LabelInvert": "উল্টানো", "LabelInvert": "উল্টানো",
"LabelItem": "আইটেম", "LabelItem": "আইটেম",
"LabelJumpBackwardAmount": "পিছন দিকে ঝাঁপের পরিমাণ",
"LabelJumpForwardAmount": "সামনের দিকে ঝাঁপের পরিমাণ",
"LabelLanguage": "ভাষা", "LabelLanguage": "ভাষা",
"LabelLanguageDefaultServer": "সার্ভারের ডিফল্ট ভাষা", "LabelLanguageDefaultServer": "সার্ভারের ডিফল্ট ভাষা",
"LabelLanguages": "ভাষাসমূহ",
"LabelLastBookAdded": "শেষ বই যোগ করা হয়েছে", "LabelLastBookAdded": "শেষ বই যোগ করা হয়েছে",
"LabelLastBookUpdated": "শেষ বই আপডেট করা হয়েছে", "LabelLastBookUpdated": "শেষ বই আপডেট করা হয়েছে",
"LabelLastSeen": "শেষ দেখা", "LabelLastSeen": "শেষ দেখা",
@@ -334,6 +377,7 @@
"LabelLess": "কম", "LabelLess": "কম",
"LabelLibrariesAccessibleToUser": "ব্যবহারকারীর কাছে অ্যাক্সেসযোগ্য লাইব্রেরি", "LabelLibrariesAccessibleToUser": "ব্যবহারকারীর কাছে অ্যাক্সেসযোগ্য লাইব্রেরি",
"LabelLibrary": "লাইব্রেরি", "LabelLibrary": "লাইব্রেরি",
"LabelLibraryFilterSublistEmpty": "না {0}",
"LabelLibraryItem": "লাইব্রেরি আইটেম", "LabelLibraryItem": "লাইব্রেরি আইটেম",
"LabelLibraryName": "লাইব্রেরির নাম", "LabelLibraryName": "লাইব্রেরির নাম",
"LabelLimit": "সীমা", "LabelLimit": "সীমা",
@@ -353,6 +397,7 @@
"LabelMetadataOrderOfPrecedenceDescription": "উচ্চ অগ্রাধিকারের মেটাডেটার উৎসগুলো নিম্ন অগ্রাধিকারের মেটাডেটা উৎসগুলোকে ওভাররাইড করবে", "LabelMetadataOrderOfPrecedenceDescription": "উচ্চ অগ্রাধিকারের মেটাডেটার উৎসগুলো নিম্ন অগ্রাধিকারের মেটাডেটা উৎসগুলোকে ওভাররাইড করবে",
"LabelMetadataProvider": "মেটাডেটা প্রদানকারী", "LabelMetadataProvider": "মেটাডেটা প্রদানকারী",
"LabelMinute": "মিনিট", "LabelMinute": "মিনিট",
"LabelMinutes": "মিনিটস",
"LabelMissing": "নিখোঁজ", "LabelMissing": "নিখোঁজ",
"LabelMissingEbook": "কোনও ই-বই নেই", "LabelMissingEbook": "কোনও ই-বই নেই",
"LabelMissingSupplementaryEbook": "কোনও সম্পূরক ই-বই নেই", "LabelMissingSupplementaryEbook": "কোনও সম্পূরক ই-বই নেই",
@@ -369,6 +414,7 @@
"LabelNewestEpisodes": "নতুনতম পর্ব", "LabelNewestEpisodes": "নতুনতম পর্ব",
"LabelNextBackupDate": "পরবর্তী ব্যাকআপ তারিখ", "LabelNextBackupDate": "পরবর্তী ব্যাকআপ তারিখ",
"LabelNextScheduledRun": "পরবর্তী নির্ধারিত দৌড়", "LabelNextScheduledRun": "পরবর্তী নির্ধারিত দৌড়",
"LabelNoCustomMetadataProviders": "কোনো কাস্টম মেটাডেটা প্রদানকারী নেই",
"LabelNoEpisodesSelected": "কোন পর্ব নির্বাচন করা হয়নি", "LabelNoEpisodesSelected": "কোন পর্ব নির্বাচন করা হয়নি",
"LabelNotFinished": "সমাপ্ত হয়নি", "LabelNotFinished": "সমাপ্ত হয়নি",
"LabelNotStarted": "শুরু হয়নি", "LabelNotStarted": "শুরু হয়নি",
@@ -391,6 +437,7 @@
"LabelOverwrite": "পুনঃলিখিত", "LabelOverwrite": "পুনঃলিখিত",
"LabelPassword": "পাসওয়ার্ড", "LabelPassword": "পাসওয়ার্ড",
"LabelPath": "পথ", "LabelPath": "পথ",
"LabelPermanent": "স্থায়ী",
"LabelPermissionsAccessAllLibraries": "সমস্ত লাইব্রেরি অ্যাক্সেস করতে পারবে", "LabelPermissionsAccessAllLibraries": "সমস্ত লাইব্রেরি অ্যাক্সেস করতে পারবে",
"LabelPermissionsAccessAllTags": "সমস্ত ট্যাগ অ্যাক্সেস করতে পারবে", "LabelPermissionsAccessAllTags": "সমস্ত ট্যাগ অ্যাক্সেস করতে পারবে",
"LabelPermissionsAccessExplicitContent": "স্পষ্ট বিষয়বস্তু অ্যাক্সেস করতে পারে", "LabelPermissionsAccessExplicitContent": "স্পষ্ট বিষয়বস্তু অ্যাক্সেস করতে পারে",
@@ -401,6 +448,7 @@
"LabelPersonalYearReview": "আপনার বছরের পর্যালোচনা ({0})", "LabelPersonalYearReview": "আপনার বছরের পর্যালোচনা ({0})",
"LabelPhotoPathURL": "ছবি পথ/ইউআরএল", "LabelPhotoPathURL": "ছবি পথ/ইউআরএল",
"LabelPlayMethod": "প্লে পদ্ধতি", "LabelPlayMethod": "প্লে পদ্ধতি",
"LabelPlayerChapterNumberMarker": "{1} এর মধ্যে {0}",
"LabelPlaylists": "প্লেলিস্ট", "LabelPlaylists": "প্লেলিস্ট",
"LabelPodcast": "পডকাস্ট", "LabelPodcast": "পডকাস্ট",
"LabelPodcastSearchRegion": "পডকাস্ট অনুসন্ধান অঞ্চল", "LabelPodcastSearchRegion": "পডকাস্ট অনুসন্ধান অঞ্চল",
@@ -412,15 +460,20 @@
"LabelPrimaryEbook": "প্রাথমিক ই-বই", "LabelPrimaryEbook": "প্রাথমিক ই-বই",
"LabelProgress": "প্রগতি", "LabelProgress": "প্রগতি",
"LabelProvider": "প্রদানকারী", "LabelProvider": "প্রদানকারী",
"LabelProviderAuthorizationValue": "অনুমোদন শিরোনামের মান",
"LabelPubDate": "প্রকাশের তারিখ", "LabelPubDate": "প্রকাশের তারিখ",
"LabelPublishYear": "প্রকাশের বছর", "LabelPublishYear": "প্রকাশের বছর",
"LabelPublishedDate": "প্রকাশিত {0}",
"LabelPublisher": "প্রকাশক", "LabelPublisher": "প্রকাশক",
"LabelPublishers": "প্রকাশকরা",
"LabelRSSFeedCustomOwnerEmail": "কাস্টম মালিকের ইমেইল", "LabelRSSFeedCustomOwnerEmail": "কাস্টম মালিকের ইমেইল",
"LabelRSSFeedCustomOwnerName": "কাস্টম মালিকের নাম", "LabelRSSFeedCustomOwnerName": "কাস্টম মালিকের নাম",
"LabelRSSFeedOpen": "আরএসএস ফিড খুলুন", "LabelRSSFeedOpen": "আরএসএস ফিড খুলুন",
"LabelRSSFeedPreventIndexing": "সূচীকরণ প্রতিরোধ করুন", "LabelRSSFeedPreventIndexing": "সূচীকরণ প্রতিরোধ করুন",
"LabelRSSFeedSlug": "আরএসএস ফিড স্লাগ", "LabelRSSFeedSlug": "আরএসএস ফিড স্লাগ",
"LabelRSSFeedURL": "আরএসএস ফিড ইউআরএল", "LabelRSSFeedURL": "আরএসএস ফিড ইউআরএল",
"LabelRandomly": "এলোমেলোভাবে",
"LabelReAddSeriesToContinueListening": "শোনা চালিয়ে যেতে সিরিজ পুনরায় যোগ করুন",
"LabelRead": "পড়ুন", "LabelRead": "পড়ুন",
"LabelReadAgain": "আবার পড়ুন", "LabelReadAgain": "আবার পড়ুন",
"LabelReadEbookWithoutProgress": "প্রগতি না রেখে ই-বই পড়ুন", "LabelReadEbookWithoutProgress": "প্রগতি না রেখে ই-বই পড়ুন",
@@ -436,6 +489,7 @@
"LabelSearchTitle": "অনুসন্ধান শিরোনাম", "LabelSearchTitle": "অনুসন্ধান শিরোনাম",
"LabelSearchTitleOrASIN": "অনুসন্ধান শিরোনাম বা ASIN", "LabelSearchTitleOrASIN": "অনুসন্ধান শিরোনাম বা ASIN",
"LabelSeason": "সেশন", "LabelSeason": "সেশন",
"LabelSelectAll": "সব নির্বাচন করুন",
"LabelSelectAllEpisodes": "সমস্ত পর্ব নির্বাচন করুন", "LabelSelectAllEpisodes": "সমস্ত পর্ব নির্বাচন করুন",
"LabelSelectEpisodesShowing": "দেখানো {0}টি পর্ব নির্বাচন করুন", "LabelSelectEpisodesShowing": "দেখানো {0}টি পর্ব নির্বাচন করুন",
"LabelSelectUsers": "ব্যবহারকারী নির্বাচন করুন", "LabelSelectUsers": "ব্যবহারকারী নির্বাচন করুন",
@@ -458,7 +512,8 @@
"LabelSettingsEnableWatcher": "প্রহরী সক্ষম করুন", "LabelSettingsEnableWatcher": "প্রহরী সক্ষম করুন",
"LabelSettingsEnableWatcherForLibrary": "লাইব্রেরির জন্য ফোল্ডার প্রহরী সক্ষম করুন", "LabelSettingsEnableWatcherForLibrary": "লাইব্রেরির জন্য ফোল্ডার প্রহরী সক্ষম করুন",
"LabelSettingsEnableWatcherHelp": "ফাইলের পরিবর্তন শনাক্ত হলে আইটেমগুলির স্বয়ংক্রিয় যোগ/আপডেট সক্ষম করবে। *সার্ভার পুনরায় চালু করতে হবে", "LabelSettingsEnableWatcherHelp": "ফাইলের পরিবর্তন শনাক্ত হলে আইটেমগুলির স্বয়ংক্রিয় যোগ/আপডেট সক্ষম করবে। *সার্ভার পুনরায় চালু করতে হবে",
"LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files।", "LabelSettingsEpubsAllowScriptedContent": "ইপাবে স্ক্রিপ্ট করা বিষয়বস্তুর অনুমতি দিন",
"LabelSettingsEpubsAllowScriptedContentHelp": "ইপাব ফাইলগুলিকে স্ক্রিপ্ট চালানোর অনুমতি দিন। আপনি ইপাব ফাইলগুলির উৎসকে বিশ্বাস না করলে এই সেটিংটি নিষ্ক্রিয় রাখার সুপারিশ করা হলো।",
"LabelSettingsExperimentalFeatures": "পরীক্ষামূলক বৈশিষ্ট্য", "LabelSettingsExperimentalFeatures": "পরীক্ষামূলক বৈশিষ্ট্য",
"LabelSettingsExperimentalFeaturesHelp": "ফিচারের বৈশিষ্ট্য যা আপনার প্রতিক্রিয়া ব্যবহার করতে পারে এবং পরীক্ষায় সহায়তা করতে পারে। গিটহাব আলোচনা খুলতে ক্লিক করুন।", "LabelSettingsExperimentalFeaturesHelp": "ফিচারের বৈশিষ্ট্য যা আপনার প্রতিক্রিয়া ব্যবহার করতে পারে এবং পরীক্ষায় সহায়তা করতে পারে। গিটহাব আলোচনা খুলতে ক্লিক করুন।",
"LabelSettingsFindCovers": "কভার খুঁজুন", "LabelSettingsFindCovers": "কভার খুঁজুন",
@@ -468,7 +523,7 @@
"LabelSettingsHomePageBookshelfView": "নীড় পেজে বুকশেলফ ভিউ ব্যবহার করুন", "LabelSettingsHomePageBookshelfView": "নীড় পেজে বুকশেলফ ভিউ ব্যবহার করুন",
"LabelSettingsLibraryBookshelfView": "লাইব্রেরি বুকশেলফ ভিউ ব্যবহার করুন", "LabelSettingsLibraryBookshelfView": "লাইব্রেরি বুকশেলফ ভিউ ব্যবহার করুন",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "কন্টিনিউ সিরিজে আগের বইগুলো এড়িয়ে যান", "LabelSettingsOnlyShowLaterBooksInContinueSeries": "কন্টিনিউ সিরিজে আগের বইগুলো এড়িয়ে যান",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "কন্টিনিউ সিরিজের হোম পেজ শেল্ফ প্রথম বইটি দেখায় যেটি সিরিজে শুরু হয়নি যেটিতে অন্তত একটি বই শেষ হয়েছে এবং কোনো বই চলছে না। এই সেটিংটি সক্ষম করা হলে তা শুরু না হওয়া প্রথম বইটির পরিবর্তে সবচেয়ে দূরের সম্পূর্ণ বই থেকে সিরিজ চালিয়ে যাবে।", "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "কন্টিনিউ সিরিজের নীড় পেজ শেল্ফ দেখায় যে সিরিজে শুরু হয়নি এমন প্রথম বই যার অন্তত একটি বই শেষ হয়েছে এবং কোনো বই চলছে না। এই সেটিংটি সক্ষম করলে শুরু না হওয়া প্রথম বইটির পরিবর্তে সবচেয়ে দূরের সম্পূর্ণ বই থেকে সিরিজ চলতে থাকবে।",
"LabelSettingsParseSubtitles": "সাবটাইটেল পার্স করুন", "LabelSettingsParseSubtitles": "সাবটাইটেল পার্স করুন",
"LabelSettingsParseSubtitlesHelp": "অডিওবুক ফোল্ডারের নাম থেকে সাবটাইটেল বের করুন৷<br>সাবটাইটেল অবশ্যই \" - \"<br>অর্থাৎ \"বুকের শিরোনাম - এখানে একটি সাবটাইটেল\" এর সাবটাইটেল আছে \"এখানে একটি সাবটাইটেল\"", "LabelSettingsParseSubtitlesHelp": "অডিওবুক ফোল্ডারের নাম থেকে সাবটাইটেল বের করুন৷<br>সাবটাইটেল অবশ্যই \" - \"<br>অর্থাৎ \"বুকের শিরোনাম - এখানে একটি সাবটাইটেল\" এর সাবটাইটেল আছে \"এখানে একটি সাবটাইটেল\"",
"LabelSettingsPreferMatchedMetadata": "মিলিত মেটাডেটা পছন্দ করুন", "LabelSettingsPreferMatchedMetadata": "মিলিত মেটাডেটা পছন্দ করুন",
@@ -484,7 +539,12 @@
"LabelSettingsStoreMetadataWithItem": "আইটেমের সাথে মেটাডেটা সংরক্ষণ করুন", "LabelSettingsStoreMetadataWithItem": "আইটেমের সাথে মেটাডেটা সংরক্ষণ করুন",
"LabelSettingsStoreMetadataWithItemHelp": "ডিফল্টরূপে মেটাডেটা ফাইলগুলি /মেটাডাটা/আইটেমগুলি -এ সংরক্ষণ করা হয়, এই সেটিংটি সক্ষম করলে মেটাডেটা ফাইলগুলি আপনার লাইব্রেরি আইটেম ফোল্ডারে সংরক্ষণ করা হবে", "LabelSettingsStoreMetadataWithItemHelp": "ডিফল্টরূপে মেটাডেটা ফাইলগুলি /মেটাডাটা/আইটেমগুলি -এ সংরক্ষণ করা হয়, এই সেটিংটি সক্ষম করলে মেটাডেটা ফাইলগুলি আপনার লাইব্রেরি আইটেম ফোল্ডারে সংরক্ষণ করা হবে",
"LabelSettingsTimeFormat": "সময় বিন্যাস", "LabelSettingsTimeFormat": "সময় বিন্যাস",
"LabelShare": "শেয়ার করুন",
"LabelShareOpen": "শেয়ার খোলা",
"LabelShareURL": "শেয়ার ইউআরএল",
"LabelShowAll": "সব দেখান", "LabelShowAll": "সব দেখান",
"LabelShowSeconds": "সেকেন্ড দেখান",
"LabelShowSubtitles": "সহ-শিরোনাম দেখান",
"LabelSize": "আকার", "LabelSize": "আকার",
"LabelSleepTimer": "স্লিপ টাইমার", "LabelSleepTimer": "স্লিপ টাইমার",
"LabelSlug": "স্লাগ", "LabelSlug": "স্লাগ",
@@ -522,6 +582,10 @@
"LabelThemeDark": "অন্ধকার", "LabelThemeDark": "অন্ধকার",
"LabelThemeLight": "আলো", "LabelThemeLight": "আলো",
"LabelTimeBase": "সময় বেস", "LabelTimeBase": "সময় বেস",
"LabelTimeDurationXHours": "{0} ঘণ্টা",
"LabelTimeDurationXMinutes": "{0} মিনিট",
"LabelTimeDurationXSeconds": "{0} সেকেন্ড",
"LabelTimeInMinutes": "মিনিটে সময়",
"LabelTimeListened": "সময় শোনা হয়েছে", "LabelTimeListened": "সময় শোনা হয়েছে",
"LabelTimeListenedToday": "আজ শোনার সময়", "LabelTimeListenedToday": "আজ শোনার সময়",
"LabelTimeRemaining": "{0}টি অবশিষ্ট", "LabelTimeRemaining": "{0}টি অবশিষ্ট",
@@ -545,6 +609,7 @@
"LabelUnabridged": "অসংলগ্ন", "LabelUnabridged": "অসংলগ্ন",
"LabelUndo": "পূর্বাবস্থা", "LabelUndo": "পূর্বাবস্থা",
"LabelUnknown": "অজানা", "LabelUnknown": "অজানা",
"LabelUnknownPublishDate": "প্রকাশের তারিখ অজানা",
"LabelUpdateCover": "কভার আপডেট করুন", "LabelUpdateCover": "কভার আপডেট করুন",
"LabelUpdateCoverHelp": "একটি মিল থাকা অবস্থায় নির্বাচিত বইগুলির বিদ্যমান কভারগুলি ওভাররাইট করার অনুমতি দিন", "LabelUpdateCoverHelp": "একটি মিল থাকা অবস্থায় নির্বাচিত বইগুলির বিদ্যমান কভারগুলি ওভাররাইট করার অনুমতি দিন",
"LabelUpdateDetails": "বিশদ আপডেট করুন", "LabelUpdateDetails": "বিশদ আপডেট করুন",
@@ -561,9 +626,12 @@
"LabelVersion": "সংস্করণ", "LabelVersion": "সংস্করণ",
"LabelViewBookmarks": "বুকমার্ক দেখুন", "LabelViewBookmarks": "বুকমার্ক দেখুন",
"LabelViewChapters": "অধ্যায় দেখুন", "LabelViewChapters": "অধ্যায় দেখুন",
"LabelViewPlayerSettings": "প্লেয়ার সেটিংস দেখুন",
"LabelViewQueue": "প্লেয়ার সারি দেখুন", "LabelViewQueue": "প্লেয়ার সারি দেখুন",
"LabelVolume": "ভলিউম", "LabelVolume": "ভলিউম",
"LabelWeekdaysToRun": "চলতে হবে সপ্তাহের দিন", "LabelWeekdaysToRun": "চলতে হবে সপ্তাহের দিন",
"LabelXBooks": "{0}টি বই",
"LabelXItems": "{0}টি আইটেম",
"LabelYearReviewHide": "পর্যালোচনার বছর লুকান", "LabelYearReviewHide": "পর্যালোচনার বছর লুকান",
"LabelYearReviewShow": "পর্যালোচনার বছর দেখুন", "LabelYearReviewShow": "পর্যালোচনার বছর দেখুন",
"LabelYourAudiobookDuration": "আপনার অডিওবুকের সময়কাল", "LabelYourAudiobookDuration": "আপনার অডিওবুকের সময়কাল",
@@ -571,12 +639,16 @@
"LabelYourPlaylists": "আপনার প্লেলিস্ট", "LabelYourPlaylists": "আপনার প্লেলিস্ট",
"LabelYourProgress": "আপনার অগ্রগতি", "LabelYourProgress": "আপনার অগ্রগতি",
"MessageAddToPlayerQueue": "প্লেয়ার সারিতে যোগ করুন", "MessageAddToPlayerQueue": "প্লেয়ার সারিতে যোগ করুন",
"MessageAppriseDescription": "এই বৈশিষ্ট্যটি ব্যবহার করার জন্য আপনাকে <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">এর একটি উদাহরণ থাকতে হবে </a> চলমান বা একটি এপিআই যা সেই একই অনুরোধগুলি পরিচালনা করবে <br /> বিজ্ঞপ্তি পাঠানোর জন্য Apprise API Url সম্পূর্ণ URLথ হওয়া উচিত, যেমন, যদি আপনার API উদাহরণ <code>http://192.168 এ পরিবেশিত হয়৷ 1.1:8337</code> তারপর আপনি <code>http://192.168.1.1:8337/notify</code> লিখবেন।", "MessageAppriseDescription": "এই বৈশিষ্ট্যটি ব্যবহার করার জন্য আপনাকে <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> চালানোর একটি উদাহরণ বা একটি এপিআই পরিচালনা করতে হবে যে একই অনুরোধ পরিচালনা করবে <br />অ্যাপ্রাইজ এপিআই ইউআরএলটি বিজ্ঞপ্তি পাঠানোর জন্য সম্পূর্ণ ইউআরএল পথ হওয়া উচিত, যেমন, যদি আপনার API ইনস্ট্যান্স <code>http://192.168.1.1:8337</code> এ পরিবেশিত হয় তাহলে আপনি <code> রাখবেন >http://192.168.1.1:8337/notify</code>।",
"MessageBackupsDescription": "ব্যাকআপের মধ্যে রয়েছে ব্যবহারকারী, ব্যবহারকারীর অগ্রগতি, লাইব্রেরি আইটেমের বিবরণ, সার্ভার সেটিংস এবং <code>/metadata/items</code> & <code>/metadata/authors</code>-এ সংরক্ষিত ছবি। ব্যাকআপগুলি <strong> আপনার লাইব্রেরি ফোল্ডারে সঞ্চিত কোনো ফাইল >অন্তর্ভুক্ত করবেন না</strong>।", "MessageBackupsDescription": "ব্যাকআপের মধ্যে রয়েছে ব্যবহারকারী, ব্যবহারকারীর অগ্রগতি, লাইব্রেরি আইটেমের বিবরণ, সার্ভার সেটিংস এবং <code>/metadata/items</code> & <code>/metadata/authors</code>-এ সংরক্ষিত ছবি। ব্যাকআপগুলি <strong> আপনার লাইব্রেরি ফোল্ডারে সঞ্চিত কোনো ফাইল >অন্তর্ভুক্ত করবেন না</strong>।",
"MessageBackupsLocationEditNote": "দ্রষ্টব্য: ব্যাকআপ অবস্থান আপডেট করলে বিদ্যমান ব্যাকআপগুলি সরানো বা সংশোধন করা হবে না",
"MessageBackupsLocationNoEditNote": "দ্রষ্টব্য: ব্যাকআপ অবস্থান একটি পরিবেশ পরিবর্তনশীল মাধ্যমে স্থির করা হয়েছে এবং এখানে পরিবর্তন করা যাবে না।",
"MessageBackupsLocationPathEmpty": "ব্যাকআপ অবস্থানের পথ খালি থাকতে পারবে না",
"MessageBatchQuickMatchDescription": "কুইক ম্যাচ নির্বাচিত আইটেমগুলির জন্য অনুপস্থিত কভার এবং মেটাডেটা যোগ করার চেষ্টা করবে। বিদ্যমান কভার এবং/অথবা মেটাডেটা ওভাররাইট করার জন্য দ্রুত ম্যাচকে অনুমতি দিতে নীচের বিকল্পগুলি সক্ষম করুন।", "MessageBatchQuickMatchDescription": "কুইক ম্যাচ নির্বাচিত আইটেমগুলির জন্য অনুপস্থিত কভার এবং মেটাডেটা যোগ করার চেষ্টা করবে। বিদ্যমান কভার এবং/অথবা মেটাডেটা ওভাররাইট করার জন্য দ্রুত ম্যাচকে অনুমতি দিতে নীচের বিকল্পগুলি সক্ষম করুন।",
"MessageBookshelfNoCollections": "আপনি এখনও কোনো সংগ্রহ করেননি", "MessageBookshelfNoCollections": "আপনি এখনও কোনো সংগ্রহ করেননি",
"MessageBookshelfNoRSSFeeds": "কোনও RSS ফিড খোলা নেই", "MessageBookshelfNoRSSFeeds": "কোনও RSS ফিড খোলা নেই",
"MessageBookshelfNoResultsForFilter": "ফিল্টার \"{0}: {1}\" এর জন্য কোন ফলাফল নেই", "MessageBookshelfNoResultsForFilter": "ফিল্টার \"{0}: {1}\" এর জন্য কোন ফলাফল নেই",
"MessageBookshelfNoResultsForQuery": "প্রশ্নের জন্য কোন ফলাফল নেই",
"MessageBookshelfNoSeries": "আপনার কোনো সিরিজ নেই", "MessageBookshelfNoSeries": "আপনার কোনো সিরিজ নেই",
"MessageChapterEndIsAfter": "অধ্যায়ের সমাপ্তি আপনার অডিওবুকের শেষে", "MessageChapterEndIsAfter": "অধ্যায়ের সমাপ্তি আপনার অডিওবুকের শেষে",
"MessageChapterErrorFirstNotZero": "প্রথম অধ্যায় 0 এ শুরু হতে হবে", "MessageChapterErrorFirstNotZero": "প্রথম অধ্যায় 0 এ শুরু হতে হবে",
@@ -586,16 +658,24 @@
"MessageCheckingCron": "ক্রন পরীক্ষা করা হচ্ছে...", "MessageCheckingCron": "ক্রন পরীক্ষা করা হচ্ছে...",
"MessageConfirmCloseFeed": "আপনি কি নিশ্চিত যে আপনি এই ফিডটি বন্ধ করতে চান?", "MessageConfirmCloseFeed": "আপনি কি নিশ্চিত যে আপনি এই ফিডটি বন্ধ করতে চান?",
"MessageConfirmDeleteBackup": "আপনি কি নিশ্চিত যে আপনি {0} এর ব্যাকআপ মুছে ফেলতে চান?", "MessageConfirmDeleteBackup": "আপনি কি নিশ্চিত যে আপনি {0} এর ব্যাকআপ মুছে ফেলতে চান?",
"MessageConfirmDeleteDevice": "আপনি কি নিশ্চিতভাবে ই-রিডার ডিভাইস \"{0}\" মুছতে চান?",
"MessageConfirmDeleteFile": "এটি আপনার ফাইল সিস্টেম থেকে ফাইলটি মুছে দেবে। আপনি কি নিশ্চিত?", "MessageConfirmDeleteFile": "এটি আপনার ফাইল সিস্টেম থেকে ফাইলটি মুছে দেবে। আপনি কি নিশ্চিত?",
"MessageConfirmDeleteLibrary": "আপনি কি নিশ্চিত যে আপনি স্থায়ীভাবে লাইব্রেরি \"{0}\" মুছে ফেলতে চান?", "MessageConfirmDeleteLibrary": "আপনি কি নিশ্চিত যে আপনি স্থায়ীভাবে লাইব্রেরি \"{0}\" মুছে ফেলতে চান?",
"MessageConfirmDeleteLibraryItem": "এটি ডাটাবেস এবং আপনার ফাইল সিস্টেম থেকে লাইব্রেরি আইটেমটি মুছে ফেলবে। আপনি কি নিশ্চিত?", "MessageConfirmDeleteLibraryItem": "এটি ডাটাবেস এবং আপনার ফাইল সিস্টেম থেকে লাইব্রেরি আইটেমটি মুছে ফেলবে। আপনি কি নিশ্চিত?",
"MessageConfirmDeleteLibraryItems": "এটি ডাটাবেস এবং আপনার ফাইল সিস্টেম থেকে {0}টি লাইব্রেরি আইটেম মুছে ফেলবে। আপনি কি নিশ্চিত?", "MessageConfirmDeleteLibraryItems": "এটি ডাটাবেস এবং আপনার ফাইল সিস্টেম থেকে {0}টি লাইব্রেরি আইটেম মুছে ফেলবে। আপনি কি নিশ্চিত?",
"MessageConfirmDeleteMetadataProvider": "আপনি কি নিশ্চিতভাবে কাস্টম মেটাডেটা প্রদানকারী \"{0}\" মুছতে চান?",
"MessageConfirmDeleteNotification": "আপনি কি নিশ্চিতভাবে এই বিজ্ঞপ্তিটি মুছতে চান?",
"MessageConfirmDeleteSession": "আপনি কি নিশ্চিত আপনি এই অধিবেশন মুছে দিতে চান?", "MessageConfirmDeleteSession": "আপনি কি নিশ্চিত আপনি এই অধিবেশন মুছে দিতে চান?",
"MessageConfirmForceReScan": "আপনি কি নিশ্চিত যে আপনি জোর করে পুনরায় স্ক্যান করতে চান?", "MessageConfirmForceReScan": "আপনি কি নিশ্চিত যে আপনি জোর করে পুনরায় স্ক্যান করতে চান?",
"MessageConfirmMarkAllEpisodesFinished": "আপনি কি নিশ্চিত যে আপনি সমস্ত পর্ব সমাপ্ত হিসাবে চিহ্নিত করতে চান?", "MessageConfirmMarkAllEpisodesFinished": "আপনি কি নিশ্চিত যে আপনি সমস্ত পর্ব সমাপ্ত হিসাবে চিহ্নিত করতে চান?",
"MessageConfirmMarkAllEpisodesNotFinished": "আপনি কি নিশ্চিত যে আপনি সমস্ত পর্বকে শেষ হয়নি বলে চিহ্নিত করতে চান?", "MessageConfirmMarkAllEpisodesNotFinished": "আপনি কি নিশ্চিত যে আপনি সমস্ত পর্বকে শেষ হয়নি বলে চিহ্নিত করতে চান?",
"MessageConfirmMarkItemFinished": "আপনি কি \"{0}\" কে সমাপ্ত হিসাবে চিহ্নিত করার বিষয়ে নিশ্চিত?",
"MessageConfirmMarkItemNotFinished": "আপনি কি \"{0}\" শেষ হয়নি বলে চিহ্নিত করার বিষয়ে নিশ্চিত?",
"MessageConfirmMarkSeriesFinished": "আপনি কি নিশ্চিত যে আপনি এই সিরিজের সমস্ত বইকে সমাপ্ত হিসাবে চিহ্নিত করতে চান?", "MessageConfirmMarkSeriesFinished": "আপনি কি নিশ্চিত যে আপনি এই সিরিজের সমস্ত বইকে সমাপ্ত হিসাবে চিহ্নিত করতে চান?",
"MessageConfirmMarkSeriesNotFinished": "আপনি কি নিশ্চিত যে আপনি এই সিরিজের সমস্ত বইকে শেষ হয়নি বলে চিহ্নিত করতে চান?", "MessageConfirmMarkSeriesNotFinished": "আপনি কি নিশ্চিত যে আপনি এই সিরিজের সমস্ত বইকে শেষ হয়নি বলে চিহ্নিত করতে চান?",
"MessageConfirmNotificationTestTrigger": "পরীক্ষার তথ্য দিয়ে এই বিজ্ঞপ্তিটি ট্রিগার করবেন?",
"MessageConfirmPurgeCache": "ক্যাশে পরিষ্কারক <code>/metadata/cache</code>-এ সম্পূর্ণ ডিরেক্টরি মুছে ফেলবে। <br /><br />আপনি কি নিশ্চিত আপনি ক্যাশে ডিরেক্টরি সরাতে চান?",
"MessageConfirmPurgeItemsCache": "আইটেম ক্যাশে পরিষ্কারক <code>/metadata/cache/items</code>-এ সম্পূর্ণ ডিরেক্টরি মুছে ফেলবে।<br />আপনি কি নিশ্চিত?",
"MessageConfirmQuickEmbed": "সতর্কতা! দ্রুত এম্বেড আপনার অডিও ফাইলের ব্যাকআপ করবে না। নিশ্চিত করুন যে আপনার অডিও ফাইলগুলির একটি ব্যাকআপ আছে। <br><br>আপনি কি চালিয়ে যেতে চান?", "MessageConfirmQuickEmbed": "সতর্কতা! দ্রুত এম্বেড আপনার অডিও ফাইলের ব্যাকআপ করবে না। নিশ্চিত করুন যে আপনার অডিও ফাইলগুলির একটি ব্যাকআপ আছে। <br><br>আপনি কি চালিয়ে যেতে চান?",
"MessageConfirmReScanLibraryItems": "আপনি কি নিশ্চিত যে আপনি {0}টি আইটেম পুনরায় স্ক্যান করতে চান?", "MessageConfirmReScanLibraryItems": "আপনি কি নিশ্চিত যে আপনি {0}টি আইটেম পুনরায় স্ক্যান করতে চান?",
"MessageConfirmRemoveAllChapters": "আপনি কি নিশ্চিত যে আপনি সমস্ত অধ্যায় সরাতে চান?", "MessageConfirmRemoveAllChapters": "আপনি কি নিশ্চিত যে আপনি সমস্ত অধ্যায় সরাতে চান?",
@@ -612,12 +692,15 @@
"MessageConfirmRenameTag": "আপনি কি সব আইটেমের জন্য \"{0}\" ট্যাগের নাম পরিবর্তন করে \"{1}\" করার বিষয়ে নিশ্চিত?", "MessageConfirmRenameTag": "আপনি কি সব আইটেমের জন্য \"{0}\" ট্যাগের নাম পরিবর্তন করে \"{1}\" করার বিষয়ে নিশ্চিত?",
"MessageConfirmRenameTagMergeNote": "দ্রষ্টব্য: এই ট্যাগটি আগে থেকেই বিদ্যমান তাই সেগুলিকে একত্র করা হবে।", "MessageConfirmRenameTagMergeNote": "দ্রষ্টব্য: এই ট্যাগটি আগে থেকেই বিদ্যমান তাই সেগুলিকে একত্র করা হবে।",
"MessageConfirmRenameTagWarning": "সতর্কতা! একটি ভিন্ন কেসিং সহ একটি অনুরূপ ট্যাগ ইতিমধ্যেই বিদ্যমান \"{0}\"।", "MessageConfirmRenameTagWarning": "সতর্কতা! একটি ভিন্ন কেসিং সহ একটি অনুরূপ ট্যাগ ইতিমধ্যেই বিদ্যমান \"{0}\"।",
"MessageConfirmResetProgress": "আপনি কি আপনার অগ্রগতি রিসেট করার বিষয়ে নিশ্চিত?",
"MessageConfirmSendEbookToDevice": "আপনি কি নিশ্চিত যে আপনি \"{2}\" ডিভাইসে {0} ইবুক \"{1}\" পাঠাতে চান?", "MessageConfirmSendEbookToDevice": "আপনি কি নিশ্চিত যে আপনি \"{2}\" ডিভাইসে {0} ইবুক \"{1}\" পাঠাতে চান?",
"MessageConfirmUnlinkOpenId": "আপনি কি এই ব্যবহারকারীকে ওপেনআইডি থেকে লিঙ্কমুক্ত করার বিষয়ে নিশ্চিত?",
"MessageDownloadingEpisode": "ডাউনলোডিং পর্ব", "MessageDownloadingEpisode": "ডাউনলোডিং পর্ব",
"MessageDragFilesIntoTrackOrder": "সঠিক ট্র্যাক অর্ডারে ফাইল টেনে আনুন", "MessageDragFilesIntoTrackOrder": "সঠিক ট্র্যাক অর্ডারে ফাইল টেনে আনুন",
"MessageEmbedFailed": "এম্বেড ব্যর্থ হয়েছে!",
"MessageEmbedFinished": "এম্বেড করা শেষ!", "MessageEmbedFinished": "এম্বেড করা শেষ!",
"MessageEpisodesQueuedForDownload": "{0} পর্ব(গুলি) ডাউনলোডের জন্য সারিবদ্ধ", "MessageEpisodesQueuedForDownload": "{0} পর্ব(গুলি) ডাউনলোডের জন্য সারিবদ্ধ",
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below।", "MessageEreaderDevices": "ই-বুক সরবরাহ নিশ্চিত করতে, আপনাকে নীচে তালিকাভুক্ত প্রতিটি ডিভাইসের জন্য একটি বৈধ প্রেরক হিসাবে উপরের ইমেল ঠিকানাটি যুক্ত করতে হতে পারে।",
"MessageFeedURLWillBe": "ফিড URL হবে {0}", "MessageFeedURLWillBe": "ফিড URL হবে {0}",
"MessageFetching": "আনয় হচ্ছে...", "MessageFetching": "আনয় হচ্ছে...",
"MessageForceReScanDescription": "সকল ফাইল আবার নতুন স্ক্যানের মত স্ক্যান করবে। অডিও ফাইল ID3 ট্যাগ, OPF ফাইল, এবং টেক্সট ফাইলগুলি নতুন হিসাবে স্ক্যান করা হবে।", "MessageForceReScanDescription": "সকল ফাইল আবার নতুন স্ক্যানের মত স্ক্যান করবে। অডিও ফাইল ID3 ট্যাগ, OPF ফাইল, এবং টেক্সট ফাইলগুলি নতুন হিসাবে স্ক্যান করা হবে।",
@@ -629,7 +712,7 @@
"MessageListeningSessionsInTheLastYear": "গত বছরে {0}টি শোনার সেশন", "MessageListeningSessionsInTheLastYear": "গত বছরে {0}টি শোনার সেশন",
"MessageLoading": "লোড হচ্ছে...", "MessageLoading": "লোড হচ্ছে...",
"MessageLoadingFolders": "ফোল্ডার লোড হচ্ছে...", "MessageLoadingFolders": "ফোল্ডার লোড হচ্ছে...",
"MessageLogsDescription": "Logs are stored in <code>/metadata/logs</code> as JSON files. Crash logs are stored in <code>/metadata/logs/crash_logs.txt</code>।", "MessageLogsDescription": "লগগুলি JSON ফাইল হিসাবে <code>/metadata/logs</code>-এ সংরক্ষণ করা হয়। ক্র্যাশ লগগুলি <code>/metadata/logs/crash_logs.txt</code>-এ সংরক্ষণ করা হয়।",
"MessageM4BFailed": "M4B ব্যর্থ!", "MessageM4BFailed": "M4B ব্যর্থ!",
"MessageM4BFinished": "M4B সমাপ্ত!", "MessageM4BFinished": "M4B সমাপ্ত!",
"MessageMapChapterTitles": "টাইমস্ট্যাম্প সামঞ্জস্য না করে আপনার বিদ্যমান অডিওবুক অধ্যায়গুলিতে অধ্যায়ের শিরোনাম ম্যাপ করুন", "MessageMapChapterTitles": "টাইমস্ট্যাম্প সামঞ্জস্য না করে আপনার বিদ্যমান অডিওবুক অধ্যায়গুলিতে অধ্যায়ের শিরোনাম ম্যাপ করুন",
@@ -646,6 +729,7 @@
"MessageNoCollections": "কোন সংগ্রহ নেই", "MessageNoCollections": "কোন সংগ্রহ নেই",
"MessageNoCoversFound": "কোন কভার পাওয়া যায়নি", "MessageNoCoversFound": "কোন কভার পাওয়া যায়নি",
"MessageNoDescription": "কোন বর্ণনা নেই", "MessageNoDescription": "কোন বর্ণনা নেই",
"MessageNoDevices": "কোনো ডিভাইস নেই",
"MessageNoDownloadsInProgress": "বর্তমানে কোনো ডাউনলোড চলছে না", "MessageNoDownloadsInProgress": "বর্তমানে কোনো ডাউনলোড চলছে না",
"MessageNoDownloadsQueued": "কোনও ডাউনলোড সারি নেই", "MessageNoDownloadsQueued": "কোনও ডাউনলোড সারি নেই",
"MessageNoEpisodeMatchesFound": "কোন পর্বের মিল পাওয়া যায়নি", "MessageNoEpisodeMatchesFound": "কোন পর্বের মিল পাওয়া যায়নি",
@@ -668,10 +752,12 @@
"MessageNoUpdatesWereNecessary": "কোন আপডেটের প্রয়োজন ছিল না", "MessageNoUpdatesWereNecessary": "কোন আপডেটের প্রয়োজন ছিল না",
"MessageNoUserPlaylists": "আপনার কোনো প্লেলিস্ট নেই", "MessageNoUserPlaylists": "আপনার কোনো প্লেলিস্ট নেই",
"MessageNotYetImplemented": "এখনও বাস্তবায়িত হয়নি", "MessageNotYetImplemented": "এখনও বাস্তবায়িত হয়নি",
"MessageOpmlPreviewNote": "দ্রষ্টব্য: এটি পার্স করা OPML ফাইলের একটি পূর্বরূপ। প্রকৃত পডকাস্ট শিরোনাম RSS ফিড থেকে নেওয়া হবে।",
"MessageOr": "বা", "MessageOr": "বা",
"MessagePauseChapter": "পজ অধ্যায় প্লেব্যাক", "MessagePauseChapter": "পজ অধ্যায় প্লেব্যাক",
"MessagePlayChapter": "অধ্যায়ের শুরুতে শুনুন", "MessagePlayChapter": "অধ্যায়ের শুরুতে শুনুন",
"MessagePlaylistCreateFromCollection": "সংগ্রহ থেকে প্লেলিস্ট তৈরি করুন", "MessagePlaylistCreateFromCollection": "সংগ্রহ থেকে প্লেলিস্ট তৈরি করুন",
"MessagePleaseWait": "অনুগ্রহ করে অপেক্ষা করুন..।",
"MessagePodcastHasNoRSSFeedForMatching": "পডকাস্টের সাথে মিলের জন্য ব্যবহার করার জন্য কোন RSS ফিড ইউআরএল নেই", "MessagePodcastHasNoRSSFeedForMatching": "পডকাস্টের সাথে মিলের জন্য ব্যবহার করার জন্য কোন RSS ফিড ইউআরএল নেই",
"MessageQuickMatchDescription": "খালি আইটেমের বিশদ বিবরণ এবং '{0}' থেকে প্রথম ম্যাচের ফলাফলের সাথে কভার করুন। সার্ভার সেটিং সক্ষম না থাকলে বিশদ ওভাররাইট করে না।", "MessageQuickMatchDescription": "খালি আইটেমের বিশদ বিবরণ এবং '{0}' থেকে প্রথম ম্যাচের ফলাফলের সাথে কভার করুন। সার্ভার সেটিং সক্ষম না থাকলে বিশদ ওভাররাইট করে না।",
"MessageRemoveChapter": "অধ্যায় সরান", "MessageRemoveChapter": "অধ্যায় সরান",
@@ -686,6 +772,9 @@
"MessageSelected": "{0}টি নির্বাচিত", "MessageSelected": "{0}টি নির্বাচিত",
"MessageServerCouldNotBeReached": "সার্ভারে পৌঁছানো যায়নি", "MessageServerCouldNotBeReached": "সার্ভারে পৌঁছানো যায়নি",
"MessageSetChaptersFromTracksDescription": "প্রতিটি অডিও ফাইলকে অধ্যায় হিসেবে ব্যবহার করে অধ্যায় সেট করুন এবং অডিও ফাইলের নাম হিসেবে অধ্যায়ের শিরোনাম করুন", "MessageSetChaptersFromTracksDescription": "প্রতিটি অডিও ফাইলকে অধ্যায় হিসেবে ব্যবহার করে অধ্যায় সেট করুন এবং অডিও ফাইলের নাম হিসেবে অধ্যায়ের শিরোনাম করুন",
"MessageShareExpirationWillBe": "মেয়াদ শেষ হবে <strong>{0}</strong>",
"MessageShareExpiresIn": "মেয়াদ শেষ হবে {0}",
"MessageShareURLWillBe": "শেয়ার করা ইউআরএল হবে <strong>{0}</strong>",
"MessageStartPlaybackAtTime": "\"{0}\" এর জন্য {1} এ প্লেব্যাক শুরু করবেন?", "MessageStartPlaybackAtTime": "\"{0}\" এর জন্য {1} এ প্লেব্যাক শুরু করবেন?",
"MessageThinking": "চিন্তা করছি...", "MessageThinking": "চিন্তা করছি...",
"MessageUploaderItemFailed": "আপলোড করতে ব্যর্থ", "MessageUploaderItemFailed": "আপলোড করতে ব্যর্থ",
@@ -709,20 +798,48 @@
"PlaceholderNewPlaylist": "নতুন প্লেলিস্টের নাম", "PlaceholderNewPlaylist": "নতুন প্লেলিস্টের নাম",
"PlaceholderSearch": "অনুসন্ধান..", "PlaceholderSearch": "অনুসন্ধান..",
"PlaceholderSearchEpisode": "অনুসন্ধান পর্ব..", "PlaceholderSearchEpisode": "অনুসন্ধান পর্ব..",
"StatsAuthorsAdded": "লেখক যোগ করা হয়েছে",
"StatsBooksAdded": "বই যোগ করা হয়েছে",
"StatsBooksAdditional": "কিছু সংযোজনের মধ্যে রয়েছে…",
"StatsBooksFinished": "বই সমাপ্ত",
"StatsBooksFinishedThisYear": "এ বছর শেষ হওয়া কিছু বই …",
"StatsBooksListenedTo": "বই শোনা হয়েছে",
"StatsCollectionGrewTo": "আপনার বইয়ের সংগ্রহ বেড়েছে…",
"StatsSessions": "অধিবেশনসমূহ",
"StatsSpentListening": "শুনে কাটিয়েছেন",
"StatsTopAuthor": "শীর্ষস্থানীয় লেখক",
"StatsTopAuthors": "শীর্ষস্থানীয় লেখকগণ",
"StatsTopGenre": "শীর্ষ ঘরানা",
"StatsTopGenres": "শীর্ষ ঘরানাগুলো",
"StatsTopMonth": "সেরা মাস",
"StatsTopNarrator": "শীর্ষ কথক",
"StatsTopNarrators": "শীর্ষ কথকগণ",
"StatsTotalDuration": "মোট সময়কাল…",
"StatsYearInReview": "বাৎসরিক পর্যালোচনা",
"ToastAccountUpdateFailed": "অ্যাকাউন্ট আপডেট করতে ব্যর্থ", "ToastAccountUpdateFailed": "অ্যাকাউন্ট আপডেট করতে ব্যর্থ",
"ToastAccountUpdateSuccess": "অ্যাকাউন্ট আপডেট করা হয়েছে", "ToastAccountUpdateSuccess": "অ্যাকাউন্ট আপডেট করা হয়েছে",
"ToastAppriseUrlRequired": "একটি Apprise ইউআরএল লিখতে হবে",
"ToastAuthorImageRemoveSuccess": "লেখকের ছবি সরানো হয়েছে", "ToastAuthorImageRemoveSuccess": "লেখকের ছবি সরানো হয়েছে",
"ToastAuthorNotFound": "লেখক \"{0}\" খুঁজে পাওয়া যায়নি",
"ToastAuthorRemoveSuccess": "লেখক সরানো হয়েছে",
"ToastAuthorSearchNotFound": "লেখক পাওয়া যায়নি",
"ToastAuthorUpdateFailed": "লেখক আপডেট করতে ব্যর্থ", "ToastAuthorUpdateFailed": "লেখক আপডেট করতে ব্যর্থ",
"ToastAuthorUpdateMerged": "লেখক একত্রিত হয়েছে", "ToastAuthorUpdateMerged": "লেখক একত্রিত হয়েছে",
"ToastAuthorUpdateSuccess": "লেখক আপডেট করেছেন", "ToastAuthorUpdateSuccess": "লেখক আপডেট করেছেন",
"ToastAuthorUpdateSuccessNoImageFound": "লেখক আপডেট করেছেন (কোন ছবি পাওয়া যায়নি)", "ToastAuthorUpdateSuccessNoImageFound": "লেখক আপডেট করেছেন (কোন ছবি পাওয়া যায়নি)",
"ToastBackupAppliedSuccess": "ব্যাকআপ প্রয়োগ করা হয়েছে",
"ToastBackupCreateFailed": "ব্যাকআপ তৈরি করতে ব্যর্থ", "ToastBackupCreateFailed": "ব্যাকআপ তৈরি করতে ব্যর্থ",
"ToastBackupCreateSuccess": "ব্যাকআপ তৈরি করা হয়েছে", "ToastBackupCreateSuccess": "ব্যাকআপ তৈরি করা হয়েছে",
"ToastBackupDeleteFailed": "ব্যাকআপ মুছে ফেলতে ব্যর্থ", "ToastBackupDeleteFailed": "ব্যাকআপ মুছে ফেলতে ব্যর্থ",
"ToastBackupDeleteSuccess": "ব্যাকআপ মুছে ফেলা হয়েছে", "ToastBackupDeleteSuccess": "ব্যাকআপ মুছে ফেলা হয়েছে",
"ToastBackupInvalidMaxKeep": "রাখার জন্য অকার্যকর ব্যাকআপের সংখ্যা",
"ToastBackupInvalidMaxSize": "অকার্যকর সর্বোচ্চ ব্যাকআপ আকার",
"ToastBackupPathUpdateFailed": "ব্যাকআপ পথ আপডেট করতে ব্যর্থ হয়েছে",
"ToastBackupRestoreFailed": "ব্যাকআপ পুনরুদ্ধার করতে ব্যর্থ", "ToastBackupRestoreFailed": "ব্যাকআপ পুনরুদ্ধার করতে ব্যর্থ",
"ToastBackupUploadFailed": "ব্যাকআপ আপলোড করতে ব্যর্থ", "ToastBackupUploadFailed": "ব্যাকআপ আপলোড করতে ব্যর্থ",
"ToastBackupUploadSuccess": "ব্যাকআপ আপলোড হয়েছে", "ToastBackupUploadSuccess": "ব্যাকআপ আপলোড হয়েছে",
"ToastBatchDeleteFailed": "ব্যাচ মুছে ফেলতে ব্যর্থ হয়েছে",
"ToastBatchDeleteSuccess": "ব্যাচ মুছে ফেলা সফল হয়েছে",
"ToastBatchUpdateFailed": "ব্যাচ আপডেট ব্যর্থ হয়েছে", "ToastBatchUpdateFailed": "ব্যাচ আপডেট ব্যর্থ হয়েছে",
"ToastBatchUpdateSuccess": "ব্যাচ আপডেট সাফল্য", "ToastBatchUpdateSuccess": "ব্যাচ আপডেট সাফল্য",
"ToastBookmarkCreateFailed": "বুকমার্ক তৈরি করতে ব্যর্থ", "ToastBookmarkCreateFailed": "বুকমার্ক তৈরি করতে ব্যর্থ",
@@ -730,20 +847,50 @@
"ToastBookmarkRemoveSuccess": "বুকমার্ক সরানো হয়েছে", "ToastBookmarkRemoveSuccess": "বুকমার্ক সরানো হয়েছে",
"ToastBookmarkUpdateFailed": "বুকমার্ক আপডেট করতে ব্যর্থ", "ToastBookmarkUpdateFailed": "বুকমার্ক আপডেট করতে ব্যর্থ",
"ToastBookmarkUpdateSuccess": "বুকমার্ক আপডেট করা হয়েছে", "ToastBookmarkUpdateSuccess": "বুকমার্ক আপডেট করা হয়েছে",
"ToastCachePurgeFailed": "ক্যাশে পরিষ্কার করতে ব্যর্থ হয়েছে",
"ToastCachePurgeSuccess": "ক্যাশে সফলভাবে পরিষ্কার করা হয়েছে",
"ToastChaptersHaveErrors": "অধ্যায়ে ত্রুটি আছে", "ToastChaptersHaveErrors": "অধ্যায়ে ত্রুটি আছে",
"ToastChaptersMustHaveTitles": "অধ্যায়ের শিরোনাম থাকতে হবে", "ToastChaptersMustHaveTitles": "অধ্যায়ের শিরোনাম থাকতে হবে",
"ToastChaptersRemoved": "অধ্যায়গুলো মুছে ফেলা হয়েছে",
"ToastCollectionItemsAddFailed": "আইটেম(গুলি) সংগ্রহে যোগ করা ব্যর্থ হয়েছে",
"ToastCollectionItemsAddSuccess": "আইটেম(গুলি) সংগ্রহে যোগ করা সফল হয়েছে",
"ToastCollectionItemsRemoveSuccess": "আইটেম(গুলি) সংগ্রহ থেকে সরানো হয়েছে", "ToastCollectionItemsRemoveSuccess": "আইটেম(গুলি) সংগ্রহ থেকে সরানো হয়েছে",
"ToastCollectionRemoveSuccess": "সংগ্রহ সরানো হয়েছে", "ToastCollectionRemoveSuccess": "সংগ্রহ সরানো হয়েছে",
"ToastCollectionUpdateFailed": "সংগ্রহ আপডেট করতে ব্যর্থ", "ToastCollectionUpdateFailed": "সংগ্রহ আপডেট করতে ব্যর্থ",
"ToastCollectionUpdateSuccess": "সংগ্রহ আপডেট করা হয়েছে", "ToastCollectionUpdateSuccess": "সংগ্রহ আপডেট করা হয়েছে",
"ToastCoverUpdateFailed": "কভার আপডেট ব্যর্থ হয়েছে",
"ToastDeleteFileFailed": "ফাইল মুছে ফেলতে ব্যর্থ হয়েছে",
"ToastDeleteFileSuccess": "ফাইল মুছে ফেলা হয়েছে",
"ToastDeviceAddFailed": "ডিভাইস যোগ করতে ব্যর্থ হয়েছে",
"ToastDeviceNameAlreadyExists": "এই নামের ইরিডার ডিভাইস ইতিমধ্যেই বিদ্যমান",
"ToastDeviceTestEmailFailed": "পরীক্ষামূলক ইমেল পাঠাতে ব্যর্থ হয়েছে",
"ToastDeviceTestEmailSuccess": "পরীক্ষামূলক ইমেল পাঠানো হয়েছে",
"ToastDeviceUpdateFailed": "ডিভাইস আপডেট করতে ব্যর্থ হয়েছে",
"ToastEmailSettingsUpdateFailed": "ইমেল সেটিংস আপডেট করতে ব্যর্থ হয়েছে",
"ToastEmailSettingsUpdateSuccess": "ইমেল সেটিংস আপডেট করা হয়েছে",
"ToastEncodeCancelFailed": "এনকোড বাতিল করতে ব্যর্থ হয়েছে",
"ToastEncodeCancelSucces": "এনকোড বাতিল করা হয়েছে",
"ToastEpisodeDownloadQueueClearFailed": "সারি সাফ করতে ব্যর্থ হয়েছে",
"ToastEpisodeDownloadQueueClearSuccess": "পর্ব ডাউনলোড সারি পরিষ্কার করা হয়েছে",
"ToastErrorCannotShare": "এই ডিভাইসে স্থানীয়ভাবে শেয়ার করা যাবে না",
"ToastFailedToLoadData": "ডেটা লোড করা যায়নি",
"ToastFailedToShare": "শেয়ার করতে ব্যর্থ",
"ToastFailedToUpdateAccount": "অ্যাকাউন্ট আপডেট করতে ব্যর্থ",
"ToastFailedToUpdateUser": "ব্যবহারকারী আপডেট করতে ব্যর্থ",
"ToastInvalidImageUrl": "অকার্যকর ছবির ইউআরএল",
"ToastInvalidUrl": "অকার্যকর ইউআরএল",
"ToastItemCoverUpdateFailed": "আইটেম কভার আপডেট করতে ব্যর্থ হয়েছে", "ToastItemCoverUpdateFailed": "আইটেম কভার আপডেট করতে ব্যর্থ হয়েছে",
"ToastItemCoverUpdateSuccess": "আইটেম কভার আপডেট করা হয়েছে", "ToastItemCoverUpdateSuccess": "আইটেম কভার আপডেট করা হয়েছে",
"ToastItemDeletedFailed": "আইটেম মুছে ফেলতে ব্যর্থ",
"ToastItemDeletedSuccess": "মুছে ফেলা আইটেম",
"ToastItemDetailsUpdateFailed": "আইটেমের বিবরণ আপডেট করতে ব্যর্থ", "ToastItemDetailsUpdateFailed": "আইটেমের বিবরণ আপডেট করতে ব্যর্থ",
"ToastItemDetailsUpdateSuccess": "আইটেমের বিবরণ আপডেট করা হয়েছে", "ToastItemDetailsUpdateSuccess": "আইটেমের বিবরণ আপডেট করা হয়েছে",
"ToastItemMarkedAsFinishedFailed": "সমাপ্ত হিসাবে চিহ্নিত করতে ব্যর্থ", "ToastItemMarkedAsFinishedFailed": "সমাপ্ত হিসাবে চিহ্নিত করতে ব্যর্থ",
"ToastItemMarkedAsFinishedSuccess": "আইটেম সমাপ্ত হিসাবে চিহ্নিত", "ToastItemMarkedAsFinishedSuccess": "আইটেম সমাপ্ত হিসাবে চিহ্নিত",
"ToastItemMarkedAsNotFinishedFailed": "সমাপ্ত হয়নি হিসাবে চিহ্নিত করতে ব্যর্থ", "ToastItemMarkedAsNotFinishedFailed": "সমাপ্ত হয়নি হিসাবে চিহ্নিত করতে ব্যর্থ",
"ToastItemMarkedAsNotFinishedSuccess": "আইটেম সমাপ্ত হয়নি বলে চিহ্নিত", "ToastItemMarkedAsNotFinishedSuccess": "আইটেম সমাপ্ত হয়নি বলে চিহ্নিত",
"ToastItemUpdateFailed": "আইটেম আপডেট করতে ব্যর্থ",
"ToastItemUpdateSuccess": "আইটেম আপডেট করা হয়েছে",
"ToastLibraryCreateFailed": "লাইব্রেরি তৈরি করতে ব্যর্থ", "ToastLibraryCreateFailed": "লাইব্রেরি তৈরি করতে ব্যর্থ",
"ToastLibraryCreateSuccess": "লাইব্রেরি \"{0}\" তৈরি করা হয়েছে", "ToastLibraryCreateSuccess": "লাইব্রেরি \"{0}\" তৈরি করা হয়েছে",
"ToastLibraryDeleteFailed": "লাইব্রেরি মুছে ফেলতে ব্যর্থ", "ToastLibraryDeleteFailed": "লাইব্রেরি মুছে ফেলতে ব্যর্থ",
@@ -752,6 +899,25 @@
"ToastLibraryScanStarted": "লাইব্রেরি স্ক্যান শুরু হয়েছে", "ToastLibraryScanStarted": "লাইব্রেরি স্ক্যান শুরু হয়েছে",
"ToastLibraryUpdateFailed": "লাইব্রেরি আপডেট করতে ব্যর্থ", "ToastLibraryUpdateFailed": "লাইব্রেরি আপডেট করতে ব্যর্থ",
"ToastLibraryUpdateSuccess": "লাইব্রেরি \"{0}\" আপডেট করা হয়েছে", "ToastLibraryUpdateSuccess": "লাইব্রেরি \"{0}\" আপডেট করা হয়েছে",
"ToastNameEmailRequired": "নাম এবং ইমেইল আবশ্যক",
"ToastNameRequired": "নাম আবশ্যক",
"ToastNewUserCreatedFailed": "অ্যাকাউন্ট তৈরি করতে ব্যর্থ: \"{0}\"",
"ToastNewUserCreatedSuccess": "নতুন একাউন্ট তৈরি হয়েছে",
"ToastNewUserLibraryError": "অন্তত একটি লাইব্রেরি নির্বাচন করতে হবে",
"ToastNewUserPasswordError": "অন্তত একটি পাসওয়ার্ড থাকতে হবে, শুধুমাত্র রুট ব্যবহারকারীর একটি খালি পাসওয়ার্ড থাকতে পারে",
"ToastNewUserTagError": "অন্তত একটি ট্যাগ নির্বাচন করতে হবে",
"ToastNewUserUsernameError": "একটি ব্যবহারকারীর নাম লিখুন",
"ToastNoUpdatesNecessary": "কোন আপডেটের প্রয়োজন নেই",
"ToastNotificationCreateFailed": "বিজ্ঞপ্তি তৈরি করতে ব্যর্থ",
"ToastNotificationDeleteFailed": "বিজ্ঞপ্তি মুছে ফেলতে ব্যর্থ",
"ToastNotificationFailedMaximum": "সর্বাধিক ব্যর্থ প্রচেষ্টা >= 0 হতে হবে",
"ToastNotificationQueueMaximum": "সর্বাধিক বিজ্ঞপ্তি সারি >= 0 হতে হবে",
"ToastNotificationSettingsUpdateFailed": "বিজ্ঞপ্তি সেটিংস আপডেট করতে ব্যর্থ",
"ToastNotificationSettingsUpdateSuccess": "বিজ্ঞপ্তি সেটিংস আপডেট করা হয়েছে",
"ToastNotificationTestTriggerFailed": "পরীক্ষামূলক বিজ্ঞপ্তি ট্রিগার করতে ব্যর্থ হয়েছে",
"ToastNotificationTestTriggerSuccess": "পরীক্ষামুলক বিজ্ঞপ্তি ট্রিগার হয়েছে",
"ToastNotificationUpdateFailed": "বিজ্ঞপ্তি আপডেট করতে ব্যর্থ",
"ToastNotificationUpdateSuccess": "বিজ্ঞপ্তি আপডেট হয়েছে",
"ToastPlaylistCreateFailed": "প্লেলিস্ট তৈরি করতে ব্যর্থ", "ToastPlaylistCreateFailed": "প্লেলিস্ট তৈরি করতে ব্যর্থ",
"ToastPlaylistCreateSuccess": "প্লেলিস্ট তৈরি করা হয়েছে", "ToastPlaylistCreateSuccess": "প্লেলিস্ট তৈরি করা হয়েছে",
"ToastPlaylistRemoveSuccess": "প্লেলিস্ট সরানো হয়েছে", "ToastPlaylistRemoveSuccess": "প্লেলিস্ট সরানো হয়েছে",
@@ -759,19 +925,52 @@
"ToastPlaylistUpdateSuccess": "প্লেলিস্ট আপডেট করা হয়েছে", "ToastPlaylistUpdateSuccess": "প্লেলিস্ট আপডেট করা হয়েছে",
"ToastPodcastCreateFailed": "পডকাস্ট তৈরি করতে ব্যর্থ", "ToastPodcastCreateFailed": "পডকাস্ট তৈরি করতে ব্যর্থ",
"ToastPodcastCreateSuccess": "পডকাস্ট সফলভাবে তৈরি করা হয়েছে", "ToastPodcastCreateSuccess": "পডকাস্ট সফলভাবে তৈরি করা হয়েছে",
"ToastPodcastGetFeedFailed": "পডকাস্ট ফিড পেতে ব্যর্থ হয়েছে",
"ToastPodcastNoEpisodesInFeed": "আরএসএস ফিডে কোনো পর্ব পাওয়া যায়নি",
"ToastPodcastNoRssFeed": "পডকাস্টের কোন আরএসএস ফিড নেই",
"ToastProviderCreatedFailed": "প্রদানকারী যোগ করতে ব্যর্থ হয়েছে",
"ToastProviderCreatedSuccess": "নতুন প্রদানকারী যোগ করা হয়েছে",
"ToastProviderNameAndUrlRequired": "নাম এবং ইউআরএল আবশ্যক",
"ToastProviderRemoveSuccess": "প্রদানকারী সরানো হয়েছে",
"ToastRSSFeedCloseFailed": "RSS ফিড বন্ধ করতে ব্যর্থ", "ToastRSSFeedCloseFailed": "RSS ফিড বন্ধ করতে ব্যর্থ",
"ToastRSSFeedCloseSuccess": "RSS ফিড বন্ধ", "ToastRSSFeedCloseSuccess": "RSS ফিড বন্ধ",
"ToastRemoveFailed": "মুছে ফেলতে ব্যর্থ হয়েছে",
"ToastRemoveItemFromCollectionFailed": "সংগ্রহ থেকে আইটেম সরাতে ব্যর্থ", "ToastRemoveItemFromCollectionFailed": "সংগ্রহ থেকে আইটেম সরাতে ব্যর্থ",
"ToastRemoveItemFromCollectionSuccess": "সংগ্রহ থেকে আইটেম সরানো হয়েছে", "ToastRemoveItemFromCollectionSuccess": "সংগ্রহ থেকে আইটেম সরানো হয়েছে",
"ToastRemoveItemsWithIssuesFailed": "সমস্যাযুক্ত লাইব্রেরি আইটেমগুলি সরাতে ব্যর্থ হয়েছে",
"ToastRemoveItemsWithIssuesSuccess": "সমস্যাযুক্ত লাইব্রেরি আইটেম সরানো হয়েছে",
"ToastRenameFailed": "পুনঃনামকরণ ব্যর্থ হয়েছে",
"ToastRescanFailed": "{0} এর জন্য পুনরায় স্ক্যান করা ব্যর্থ হয়েছে",
"ToastRescanRemoved": "পুনরায় স্ক্যান সম্পূর্ণ,আইটেম সরানো হয়েছে",
"ToastRescanUpToDate": "পুনরায় স্ক্যান সম্পূর্ণ, আইটেম সাম্প্রতিক ছিল",
"ToastRescanUpdated": "পুনরায় স্ক্যান সম্পূর্ণ, আইটেম আপডেট করা হয়েছে",
"ToastScanFailed": "লাইব্রেরি আইটেম স্ক্যান করতে ব্যর্থ হয়েছে",
"ToastSelectAtLeastOneUser": "অন্তত একজন ব্যবহারকারী নির্বাচন করুন",
"ToastSendEbookToDeviceFailed": "ডিভাইসে ইবুক পাঠাতে ব্যর্থ", "ToastSendEbookToDeviceFailed": "ডিভাইসে ইবুক পাঠাতে ব্যর্থ",
"ToastSendEbookToDeviceSuccess": "ইবুক \"{0}\" ডিভাইসে পাঠানো হয়েছে", "ToastSendEbookToDeviceSuccess": "ইবুক \"{0}\" ডিভাইসে পাঠানো হয়েছে",
"ToastSeriesUpdateFailed": "সিরিজ আপডেট ব্যর্থ হয়েছে", "ToastSeriesUpdateFailed": "সিরিজ আপডেট ব্যর্থ হয়েছে",
"ToastSeriesUpdateSuccess": "সিরিজ আপডেট সাফল্য", "ToastSeriesUpdateSuccess": "সিরিজ আপডেট সাফল্য",
"ToastServerSettingsUpdateFailed": "সার্ভার সেটিংস আপডেট করতে ব্যর্থ হয়েছে",
"ToastServerSettingsUpdateSuccess": "সার্ভার সেটিংস আপডেট করা হয়েছে",
"ToastSessionCloseFailed": "অধিবেশন বন্ধ করতে ব্যর্থ হয়েছে",
"ToastSessionDeleteFailed": "সেশন মুছে ফেলতে ব্যর্থ", "ToastSessionDeleteFailed": "সেশন মুছে ফেলতে ব্যর্থ",
"ToastSessionDeleteSuccess": "সেশন মুছে ফেলা হয়েছে", "ToastSessionDeleteSuccess": "সেশন মুছে ফেলা হয়েছে",
"ToastSlugMustChange": "স্লাগে অবৈধ অক্ষর রয়েছে",
"ToastSlugRequired": "স্লাগ আবশ্যক",
"ToastSocketConnected": "সকেট সংযুক্ত", "ToastSocketConnected": "সকেট সংযুক্ত",
"ToastSocketDisconnected": "সকেট সংযোগ বিচ্ছিন্ন", "ToastSocketDisconnected": "সকেট সংযোগ বিচ্ছিন্ন",
"ToastSocketFailedToConnect": "সকেট সংযোগ করতে ব্যর্থ হয়েছে", "ToastSocketFailedToConnect": "সকেট সংযোগ করতে ব্যর্থ হয়েছে",
"ToastSortingPrefixesEmptyError": "কমপক্ষে ১ টি সাজানোর উপসর্গ থাকতে হবে",
"ToastSortingPrefixesUpdateFailed": "বাছাই উপসর্গ আপডেট করতে ব্যর্থ হয়েছে",
"ToastSortingPrefixesUpdateSuccess": "বাছাই করা উপসর্গ আপডেট করা হয়েছে ({0}টি আইটেম)",
"ToastTitleRequired": "শিরোনাম আবশ্যক",
"ToastUnknownError": "অজানা ত্রুটি",
"ToastUnlinkOpenIdFailed": "OpenID থেকে ব্যবহারকারীকে আনলিঙ্ক করতে ব্যর্থ হয়েছে",
"ToastUnlinkOpenIdSuccess": "OpenID থেকে ব্যবহারকারীকে লিঙ্কমুক্ত করা হয়েছে",
"ToastUserDeleteFailed": "ব্যবহারকারী মুছতে ব্যর্থ", "ToastUserDeleteFailed": "ব্যবহারকারী মুছতে ব্যর্থ",
"ToastUserDeleteSuccess": "ব্যবহারকারী মুছে ফেলা হয়েছে" "ToastUserDeleteSuccess": "ব্যবহারকারী মুছে ফেলা হয়েছে",
"ToastUserPasswordChangeSuccess": "পাসওয়ার্ড সফলভাবে পরিবর্তন করা হয়েছে",
"ToastUserPasswordMismatch": "পাসওয়ার্ড মিলছে না",
"ToastUserPasswordMustChange": "নতুন পাসওয়ার্ড পুরানো পাসওয়ার্ডের সাথে মিলতে পারবে না",
"ToastUserRootRequireName": "একটি রুট ব্যবহারকারীর নাম লিখতে হবে"
} }
+1 -1
View File
@@ -98,7 +98,7 @@
"ButtonStats": "Statistiken", "ButtonStats": "Statistiken",
"ButtonSubmit": "Ok", "ButtonSubmit": "Ok",
"ButtonTest": "Test", "ButtonTest": "Test",
"ButtonUnlinkOpedId": "OpenID trennen", "ButtonUnlinkOpenId": "OpenID trennen",
"ButtonUpload": "Hochladen", "ButtonUpload": "Hochladen",
"ButtonUploadBackup": "Sicherung hochladen", "ButtonUploadBackup": "Sicherung hochladen",
"ButtonUploadCover": "Titelbild hochladen", "ButtonUploadCover": "Titelbild hochladen",
+1 -1
View File
@@ -98,7 +98,7 @@
"ButtonStats": "Stats", "ButtonStats": "Stats",
"ButtonSubmit": "Submit", "ButtonSubmit": "Submit",
"ButtonTest": "Test", "ButtonTest": "Test",
"ButtonUnlinkOpedId": "Unlink OpenID", "ButtonUnlinkOpenId": "Unlink OpenID",
"ButtonUpload": "Upload", "ButtonUpload": "Upload",
"ButtonUploadBackup": "Upload Backup", "ButtonUploadBackup": "Upload Backup",
"ButtonUploadCover": "Upload Cover", "ButtonUploadCover": "Upload Cover",
+1 -1
View File
@@ -97,7 +97,7 @@
"ButtonStats": "Estadísticas", "ButtonStats": "Estadísticas",
"ButtonSubmit": "Enviar", "ButtonSubmit": "Enviar",
"ButtonTest": "Prueba", "ButtonTest": "Prueba",
"ButtonUnlinkOpedId": "Desvincular OpenID", "ButtonUnlinkOpenId": "Desvincular OpenID",
"ButtonUpload": "Subir", "ButtonUpload": "Subir",
"ButtonUploadBackup": "Subir Respaldo", "ButtonUploadBackup": "Subir Respaldo",
"ButtonUploadCover": "Subir Portada", "ButtonUploadCover": "Subir Portada",
-1
View File
@@ -98,7 +98,6 @@
"ButtonStats": "Statistiques", "ButtonStats": "Statistiques",
"ButtonSubmit": "Soumettre", "ButtonSubmit": "Soumettre",
"ButtonTest": "Test", "ButtonTest": "Test",
"ButtonUnlinkOpedId": "Dissocier OpenID",
"ButtonUpload": "Téléverser", "ButtonUpload": "Téléverser",
"ButtonUploadBackup": "Téléverser une sauvegarde", "ButtonUploadBackup": "Téléverser une sauvegarde",
"ButtonUploadCover": "Téléverser une couverture", "ButtonUploadCover": "Téléverser une couverture",
-1
View File
@@ -98,7 +98,6 @@
"ButtonStats": "Statistika", "ButtonStats": "Statistika",
"ButtonSubmit": "Podnesi", "ButtonSubmit": "Podnesi",
"ButtonTest": "Test", "ButtonTest": "Test",
"ButtonUnlinkOpedId": "Odspoji OpenID",
"ButtonUpload": "Učitaj", "ButtonUpload": "Učitaj",
"ButtonUploadBackup": "Učitaj sigurnosnu kopiju", "ButtonUploadBackup": "Učitaj sigurnosnu kopiju",
"ButtonUploadCover": "Učitaj naslovnicu", "ButtonUploadCover": "Učitaj naslovnicu",
+38 -1
View File
@@ -19,6 +19,7 @@
"ButtonChooseFiles": "Wybierz pliki", "ButtonChooseFiles": "Wybierz pliki",
"ButtonClearFilter": "Wyczyść filtr", "ButtonClearFilter": "Wyczyść filtr",
"ButtonCloseFeed": "Zamknij kanał", "ButtonCloseFeed": "Zamknij kanał",
"ButtonCloseSession": "Zamknij otwartą sesję",
"ButtonCollections": "Kolekcje", "ButtonCollections": "Kolekcje",
"ButtonConfigureScanner": "Skonfiguruj skaner", "ButtonConfigureScanner": "Skonfiguruj skaner",
"ButtonCreate": "Utwórz", "ButtonCreate": "Utwórz",
@@ -28,6 +29,7 @@
"ButtonEdit": "Edycja", "ButtonEdit": "Edycja",
"ButtonEditChapters": "Edytuj rozdziały", "ButtonEditChapters": "Edytuj rozdziały",
"ButtonEditPodcast": "Edytuj podcast", "ButtonEditPodcast": "Edytuj podcast",
"ButtonEnable": "Włącz",
"ButtonForceReScan": "Wymuś ponowne skanowanie", "ButtonForceReScan": "Wymuś ponowne skanowanie",
"ButtonFullPath": "Pełna ścieżka", "ButtonFullPath": "Pełna ścieżka",
"ButtonHide": "Ukryj", "ButtonHide": "Ukryj",
@@ -47,8 +49,10 @@
"ButtonNext": "Następny", "ButtonNext": "Następny",
"ButtonNextChapter": "Następny rozdział", "ButtonNextChapter": "Następny rozdział",
"ButtonNextItemInQueue": "Następny element w kolejce", "ButtonNextItemInQueue": "Następny element w kolejce",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Otwórz feed", "ButtonOpenFeed": "Otwórz feed",
"ButtonOpenManager": "Otwórz menadżera", "ButtonOpenManager": "Otwórz menadżera",
"ButtonPause": "Wstrzymaj",
"ButtonPlay": "Odtwarzaj", "ButtonPlay": "Odtwarzaj",
"ButtonPlaying": "Odtwarzane", "ButtonPlaying": "Odtwarzane",
"ButtonPlaylists": "Listy odtwarzania", "ButtonPlaylists": "Listy odtwarzania",
@@ -90,6 +94,7 @@
"ButtonStartMetadataEmbed": "Osadź metadane", "ButtonStartMetadataEmbed": "Osadź metadane",
"ButtonStats": "Statystyki", "ButtonStats": "Statystyki",
"ButtonSubmit": "Zaloguj", "ButtonSubmit": "Zaloguj",
"ButtonTest": "Test",
"ButtonUpload": "Wgraj", "ButtonUpload": "Wgraj",
"ButtonUploadBackup": "Wgraj kopię zapasową", "ButtonUploadBackup": "Wgraj kopię zapasową",
"ButtonUploadCover": "Wgraj okładkę", "ButtonUploadCover": "Wgraj okładkę",
@@ -102,6 +107,7 @@
"ErrorUploadFetchMetadataNoResults": "Nie można pobrać metadanych — spróbuj zaktualizować tytuł i/lub autora", "ErrorUploadFetchMetadataNoResults": "Nie można pobrać metadanych — spróbuj zaktualizować tytuł i/lub autora",
"ErrorUploadLacksTitle": "Musi mieć tytuł", "ErrorUploadLacksTitle": "Musi mieć tytuł",
"HeaderAccount": "Konto", "HeaderAccount": "Konto",
"HeaderAddCustomMetadataProvider": "Dodaj niestandardowego dostawcę metadanych",
"HeaderAdvanced": "Zaawansowane", "HeaderAdvanced": "Zaawansowane",
"HeaderAppriseNotificationSettings": "Ustawienia powiadomień Apprise", "HeaderAppriseNotificationSettings": "Ustawienia powiadomień Apprise",
"HeaderAudioTracks": "Ścieżki audio", "HeaderAudioTracks": "Ścieżki audio",
@@ -120,6 +126,7 @@
"HeaderDetails": "Szczegóły", "HeaderDetails": "Szczegóły",
"HeaderDownloadQueue": "Kolejka do ściągania", "HeaderDownloadQueue": "Kolejka do ściągania",
"HeaderEbookFiles": "Pliki Ebook", "HeaderEbookFiles": "Pliki Ebook",
"HeaderEmail": "E-mail",
"HeaderEmailSettings": "Ustawienia e-mail", "HeaderEmailSettings": "Ustawienia e-mail",
"HeaderEpisodes": "Rozdziały", "HeaderEpisodes": "Rozdziały",
"HeaderEreaderDevices": "Czytniki", "HeaderEreaderDevices": "Czytniki",
@@ -145,6 +152,8 @@
"HeaderMetadataToEmbed": "Osadź metadane", "HeaderMetadataToEmbed": "Osadź metadane",
"HeaderNewAccount": "Nowe konto", "HeaderNewAccount": "Nowe konto",
"HeaderNewLibrary": "Nowa biblioteka", "HeaderNewLibrary": "Nowa biblioteka",
"HeaderNotificationCreate": "Utwórz powiadomienie",
"HeaderNotificationUpdate": "Zaktualizuj powiadomienie",
"HeaderNotifications": "Powiadomienia", "HeaderNotifications": "Powiadomienia",
"HeaderOpenIDConnectAuthentication": "Uwierzytelnianie OpenID Connect", "HeaderOpenIDConnectAuthentication": "Uwierzytelnianie OpenID Connect",
"HeaderOpenRSSFeed": "Utwórz kanał RSS", "HeaderOpenRSSFeed": "Utwórz kanał RSS",
@@ -157,7 +166,9 @@
"HeaderPlaylistItems": "Pozycje listy odtwarzania", "HeaderPlaylistItems": "Pozycje listy odtwarzania",
"HeaderPodcastsToAdd": "Podcasty do dodania", "HeaderPodcastsToAdd": "Podcasty do dodania",
"HeaderPreviewCover": "Podgląd okładki", "HeaderPreviewCover": "Podgląd okładki",
"HeaderRSSFeedGeneral": "Szczegóły RSS",
"HeaderRSSFeedIsOpen": "Kanał RSS jest otwarty", "HeaderRSSFeedIsOpen": "Kanał RSS jest otwarty",
"HeaderRSSFeeds": "Kanały RSS",
"HeaderRemoveEpisode": "Usuń odcinek", "HeaderRemoveEpisode": "Usuń odcinek",
"HeaderRemoveEpisodes": "Usuń {0} odcinków", "HeaderRemoveEpisodes": "Usuń {0} odcinków",
"HeaderSavedMediaProgress": "Zapisany postęp", "HeaderSavedMediaProgress": "Zapisany postęp",
@@ -200,6 +211,7 @@
"LabelAddToPlaylist": "Dodaj do playlisty", "LabelAddToPlaylist": "Dodaj do playlisty",
"LabelAddToPlaylistBatch": "Dodaj {0} pozycji do playlisty", "LabelAddToPlaylistBatch": "Dodaj {0} pozycji do playlisty",
"LabelAddedAt": "Dodano", "LabelAddedAt": "Dodano",
"LabelAddedDate": "Dodano {0}",
"LabelAdminUsersOnly": "Tylko użytkownicy administracyjni", "LabelAdminUsersOnly": "Tylko użytkownicy administracyjni",
"LabelAll": "Wszystkie", "LabelAll": "Wszystkie",
"LabelAllUsers": "Wszyscy użytkownicy", "LabelAllUsers": "Wszyscy użytkownicy",
@@ -215,15 +227,19 @@
"LabelAutoFetchMetadata": "Automatycznie pobierz metadane", "LabelAutoFetchMetadata": "Automatycznie pobierz metadane",
"LabelAutoFetchMetadataHelp": "Pobiera metadane dotyczące tytułu, autora i serii, aby usprawnić przesyłanie. Po przesłaniu może być konieczne dopasowanie dodatkowych metadanych.", "LabelAutoFetchMetadataHelp": "Pobiera metadane dotyczące tytułu, autora i serii, aby usprawnić przesyłanie. Po przesłaniu może być konieczne dopasowanie dodatkowych metadanych.",
"LabelAutoLaunch": "Uruchom automatycznie", "LabelAutoLaunch": "Uruchom automatycznie",
"LabelAutoRegister": "Automatyczna rejestracja",
"LabelAutoRegisterDescription": "Automatycznie utwórz nowych użytkowników po zalogowaniu",
"LabelBackToUser": "Powrót", "LabelBackToUser": "Powrót",
"LabelBackupLocation": "Lokalizacja kopii zapasowej", "LabelBackupLocation": "Lokalizacja kopii zapasowej",
"LabelBackupsEnableAutomaticBackups": "Włącz automatyczne kopie zapasowe", "LabelBackupsEnableAutomaticBackups": "Włącz automatyczne kopie zapasowe",
"LabelBackupsEnableAutomaticBackupsHelp": "Kopie zapasowe są zapisywane w folderze /metadata/backups", "LabelBackupsEnableAutomaticBackupsHelp": "Kopie zapasowe są zapisywane w folderze /metadata/backups",
"LabelBackupsMaxBackupSize": "Maksymalny rozmiar kopii zapasowej (w GB)", "LabelBackupsMaxBackupSize": "Maksymalny rozmiar kopii zapasowej (w GB) (0 oznacza nieograniczony)",
"LabelBackupsMaxBackupSizeHelp": "Jako zabezpieczenie przed błędną konfiguracją, kopie zapasowe nie będą wykonywane, jeśli przekroczą skonfigurowany rozmiar.", "LabelBackupsMaxBackupSizeHelp": "Jako zabezpieczenie przed błędną konfiguracją, kopie zapasowe nie będą wykonywane, jeśli przekroczą skonfigurowany rozmiar.",
"LabelBackupsNumberToKeep": "Liczba kopii zapasowych do przechowywania", "LabelBackupsNumberToKeep": "Liczba kopii zapasowych do przechowywania",
"LabelBackupsNumberToKeepHelp": "Tylko 1 kopia zapasowa zostanie usunięta, więc jeśli masz już więcej kopii zapasowych, powinieneś je ręcznie usunąć.", "LabelBackupsNumberToKeepHelp": "Tylko 1 kopia zapasowa zostanie usunięta, więc jeśli masz już więcej kopii zapasowych, powinieneś je ręcznie usunąć.",
"LabelBitrate": "Bitrate",
"LabelBooks": "Książki", "LabelBooks": "Książki",
"LabelButtonText": "Tekst przycisku",
"LabelByAuthor": "autorstwa {0}", "LabelByAuthor": "autorstwa {0}",
"LabelChangePassword": "Zmień hasło", "LabelChangePassword": "Zmień hasło",
"LabelChannels": "Kanały", "LabelChannels": "Kanały",
@@ -232,6 +248,7 @@
"LabelChaptersFound": "Znalezione rozdziały", "LabelChaptersFound": "Znalezione rozdziały",
"LabelClickForMoreInfo": "Kliknij po więcej szczegółów", "LabelClickForMoreInfo": "Kliknij po więcej szczegółów",
"LabelClosePlayer": "Zamknij odtwarzacz", "LabelClosePlayer": "Zamknij odtwarzacz",
"LabelCodec": "Kodek",
"LabelCollapseSeries": "Podsumuj serię", "LabelCollapseSeries": "Podsumuj serię",
"LabelCollapseSubSeries": "Zwiń podserie", "LabelCollapseSubSeries": "Zwiń podserie",
"LabelCollection": "Kolekcja", "LabelCollection": "Kolekcja",
@@ -247,6 +264,7 @@
"LabelCronExpression": "Wyrażenie CRON", "LabelCronExpression": "Wyrażenie CRON",
"LabelCurrent": "Aktualny", "LabelCurrent": "Aktualny",
"LabelCurrently": "Obecnie:", "LabelCurrently": "Obecnie:",
"LabelCustomCronExpression": "Niestandardowe wyrażenie Cron:",
"LabelDatetime": "Data i godzina", "LabelDatetime": "Data i godzina",
"LabelDays": "Dni", "LabelDays": "Dni",
"LabelDeleteFromFileSystemCheckbox": "Usuń z systemu plików (odznacz, aby usunąć tylko z bazy danych)", "LabelDeleteFromFileSystemCheckbox": "Usuń z systemu plików (odznacz, aby usunąć tylko z bazy danych)",
@@ -254,6 +272,7 @@
"LabelDeselectAll": "Odznacz wszystko", "LabelDeselectAll": "Odznacz wszystko",
"LabelDevice": "Urządzenie", "LabelDevice": "Urządzenie",
"LabelDeviceInfo": "Informacja o urządzeniu", "LabelDeviceInfo": "Informacja o urządzeniu",
"LabelDeviceIsAvailableTo": "Urządzenie jest dostępne do...",
"LabelDirectory": "Katalog", "LabelDirectory": "Katalog",
"LabelDiscFromFilename": "Oznaczenie dysku z nazwy pliku", "LabelDiscFromFilename": "Oznaczenie dysku z nazwy pliku",
"LabelDiscFromMetadata": "Oznaczenie dysku z metadanych", "LabelDiscFromMetadata": "Oznaczenie dysku z metadanych",
@@ -261,16 +280,20 @@
"LabelDownload": "Pobierz", "LabelDownload": "Pobierz",
"LabelDownloadNEpisodes": "Ściąganie {0} odcinków", "LabelDownloadNEpisodes": "Ściąganie {0} odcinków",
"LabelDuration": "Czas trwania", "LabelDuration": "Czas trwania",
"LabelDurationComparisonExactMatch": "(dokładne dopasowanie)",
"LabelDurationComparisonLonger": "({0} dłużej)", "LabelDurationComparisonLonger": "({0} dłużej)",
"LabelDurationComparisonShorter": "({0} krócej)", "LabelDurationComparisonShorter": "({0} krócej)",
"LabelDurationFound": "Znaleziona długość:", "LabelDurationFound": "Znaleziona długość:",
"LabelEbook": "Ebook",
"LabelEbooks": "Ebooki", "LabelEbooks": "Ebooki",
"LabelEdit": "Edytuj", "LabelEdit": "Edytuj",
"LabelEmail": "E-mail",
"LabelEmailSettingsFromAddress": "Z adresu", "LabelEmailSettingsFromAddress": "Z adresu",
"LabelEmailSettingsRejectUnauthorized": "Odrzuć nieautoryzowane certyfikaty", "LabelEmailSettingsRejectUnauthorized": "Odrzuć nieautoryzowane certyfikaty",
"LabelEmailSettingsRejectUnauthorizedHelp": "Wyłączenie walidacji certyfikatów SSL może narazić cię na ryzyka bezpieczeństwa, takie jak ataki man-in-the-middle. Wyłącz tą opcję wyłącznie jeśli rozumiesz tego skutki i ufasz serwerowi pocztowemu, do którego się podłączasz.", "LabelEmailSettingsRejectUnauthorizedHelp": "Wyłączenie walidacji certyfikatów SSL może narazić cię na ryzyka bezpieczeństwa, takie jak ataki man-in-the-middle. Wyłącz tą opcję wyłącznie jeśli rozumiesz tego skutki i ufasz serwerowi pocztowemu, do którego się podłączasz.",
"LabelEmailSettingsSecure": "Bezpieczeństwo", "LabelEmailSettingsSecure": "Bezpieczeństwo",
"LabelEmailSettingsSecureHelp": "Jeśli włączysz, połączenie będzie korzystać z TLS podczas łączenia do serwera. Jeśli wyłączysz, TLS będzie wykorzystane jeśli serwer wspiera rozszerzenie STARTTLS. W większości przypadków włącz to ustawienie jeśli łączysz się do portu 465. Dla portów 587 lub 25 pozostaw to ustawienie wyłączone. (na podstawie nodemailer.com/smtp/#authentication)", "LabelEmailSettingsSecureHelp": "Jeśli włączysz, połączenie będzie korzystać z TLS podczas łączenia do serwera. Jeśli wyłączysz, TLS będzie wykorzystane jeśli serwer wspiera rozszerzenie STARTTLS. W większości przypadków włącz to ustawienie jeśli łączysz się do portu 465. Dla portów 587 lub 25 pozostaw to ustawienie wyłączone. (na podstawie nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Adres testowy",
"LabelEmbeddedCover": "Wbudowana okładka", "LabelEmbeddedCover": "Wbudowana okładka",
"LabelEnable": "Włącz", "LabelEnable": "Włącz",
"LabelEnd": "Zakończ", "LabelEnd": "Zakończ",
@@ -278,16 +301,21 @@
"LabelEpisode": "Odcinek", "LabelEpisode": "Odcinek",
"LabelEpisodeTitle": "Tytuł odcinka", "LabelEpisodeTitle": "Tytuł odcinka",
"LabelEpisodeType": "Typ odcinka", "LabelEpisodeType": "Typ odcinka",
"LabelEpisodes": "Epizody",
"LabelExample": "Przykład", "LabelExample": "Przykład",
"LabelExpandSeries": "Rozwiń serie", "LabelExpandSeries": "Rozwiń serie",
"LabelExpandSubSeries": "Rozwiń podserie", "LabelExpandSubSeries": "Rozwiń podserie",
"LabelExplicit": "Nieprzyzwoite", "LabelExplicit": "Nieprzyzwoite",
"LabelExplicitChecked": "Nieprzyzwoite (sprawdzone)",
"LabelExplicitUnchecked": "Przyzwoite (niesprawdzone)",
"LabelExportOPML": "Wyeksportuj OPML", "LabelExportOPML": "Wyeksportuj OPML",
"LabelFeedURL": "URL kanału", "LabelFeedURL": "URL kanału",
"LabelFetchingMetadata": "Pobieranie metadanych", "LabelFetchingMetadata": "Pobieranie metadanych",
"LabelFile": "Plik", "LabelFile": "Plik",
"LabelFileBirthtime": "Data utworzenia pliku", "LabelFileBirthtime": "Data utworzenia pliku",
"LabelFileBornDate": "Utworzony {0}",
"LabelFileModified": "Data modyfikacji pliku", "LabelFileModified": "Data modyfikacji pliku",
"LabelFileModifiedDate": "Modyfikowany {0}",
"LabelFilename": "Nazwa pliku", "LabelFilename": "Nazwa pliku",
"LabelFilterByUser": "Filtruj według danego użytkownika", "LabelFilterByUser": "Filtruj według danego użytkownika",
"LabelFindEpisodes": "Znajdź odcinki", "LabelFindEpisodes": "Znajdź odcinki",
@@ -297,8 +325,10 @@
"LabelFontBold": "Pogrubiony", "LabelFontBold": "Pogrubiony",
"LabelFontBoldness": "Grubość czcionki", "LabelFontBoldness": "Grubość czcionki",
"LabelFontFamily": "Rodzina czcionek", "LabelFontFamily": "Rodzina czcionek",
"LabelFontItalic": "Kursywa",
"LabelFontScale": "Rozmiar czcionki", "LabelFontScale": "Rozmiar czcionki",
"LabelFontStrikethrough": "Przekreślony", "LabelFontStrikethrough": "Przekreślony",
"LabelFormat": "Format",
"LabelGenre": "Gatunek", "LabelGenre": "Gatunek",
"LabelGenres": "Gatunki", "LabelGenres": "Gatunki",
"LabelHardDeleteFile": "Usuń trwale plik", "LabelHardDeleteFile": "Usuń trwale plik",
@@ -306,6 +336,7 @@
"LabelHasSupplementaryEbook": "Posiada dodatkowy ebook", "LabelHasSupplementaryEbook": "Posiada dodatkowy ebook",
"LabelHideSubtitles": "Ukryj napisy", "LabelHideSubtitles": "Ukryj napisy",
"LabelHighestPriority": "Najwyższy priorytet", "LabelHighestPriority": "Najwyższy priorytet",
"LabelHost": "Host",
"LabelHour": "Godzina", "LabelHour": "Godzina",
"LabelHours": "Godziny", "LabelHours": "Godziny",
"LabelIcon": "Ikona", "LabelIcon": "Ikona",
@@ -324,6 +355,7 @@
"LabelIntervalEveryHour": "Każdej godziny", "LabelIntervalEveryHour": "Każdej godziny",
"LabelInvert": "Inversja", "LabelInvert": "Inversja",
"LabelItem": "Pozycja", "LabelItem": "Pozycja",
"LabelJumpBackwardAmount": "Rozmiar skoku do przodu",
"LabelLanguage": "Język", "LabelLanguage": "Język",
"LabelLanguageDefaultServer": "Domyślny język serwera", "LabelLanguageDefaultServer": "Domyślny język serwera",
"LabelLanguages": "Języki", "LabelLanguages": "Języki",
@@ -338,10 +370,13 @@
"LabelLess": "Mniej", "LabelLess": "Mniej",
"LabelLibrariesAccessibleToUser": "Biblioteki dostępne dla użytkownika", "LabelLibrariesAccessibleToUser": "Biblioteki dostępne dla użytkownika",
"LabelLibrary": "Biblioteka", "LabelLibrary": "Biblioteka",
"LabelLibraryFilterSublistEmpty": "Brak {0}",
"LabelLibraryItem": "Element biblioteki", "LabelLibraryItem": "Element biblioteki",
"LabelLibraryName": "Nazwa biblioteki", "LabelLibraryName": "Nazwa biblioteki",
"LabelLimit": "Limit",
"LabelLineSpacing": "Odstęp między wierszami", "LabelLineSpacing": "Odstęp między wierszami",
"LabelListenAgain": "Słuchaj ponownie", "LabelListenAgain": "Słuchaj ponownie",
"LabelLogLevelDebug": "Debugowanie",
"LabelLogLevelInfo": "Informacja", "LabelLogLevelInfo": "Informacja",
"LabelLogLevelWarn": "Ostrzeżenie", "LabelLogLevelWarn": "Ostrzeżenie",
"LabelLookForNewEpisodesAfterDate": "Szukaj nowych odcinków po dacie", "LabelLookForNewEpisodesAfterDate": "Szukaj nowych odcinków po dacie",
@@ -351,6 +386,7 @@
"LabelMediaPlayer": "Odtwarzacz", "LabelMediaPlayer": "Odtwarzacz",
"LabelMediaType": "Typ mediów", "LabelMediaType": "Typ mediów",
"LabelMetaTag": "Tag", "LabelMetaTag": "Tag",
"LabelMetaTags": "Meta Tagi",
"LabelMetadataOrderOfPrecedenceDescription": "Źródła metadanych o wyższym priorytecie będą zastępują źródła o niższym priorytecie", "LabelMetadataOrderOfPrecedenceDescription": "Źródła metadanych o wyższym priorytecie będą zastępują źródła o niższym priorytecie",
"LabelMetadataProvider": "Dostawca metadanych", "LabelMetadataProvider": "Dostawca metadanych",
"LabelMinute": "Minuta", "LabelMinute": "Minuta",
@@ -358,6 +394,7 @@
"LabelMissing": "Brakujący", "LabelMissing": "Brakujący",
"LabelMissingEbook": "Nie posiada ebooka", "LabelMissingEbook": "Nie posiada ebooka",
"LabelMissingSupplementaryEbook": "Nie posiada dodatkowego ebooka", "LabelMissingSupplementaryEbook": "Nie posiada dodatkowego ebooka",
"LabelMobileRedirectURIs": "Dozwolone URI przekierowań mobilnych",
"LabelMore": "Więcej", "LabelMore": "Więcej",
"LabelMoreInfo": "Więcej informacji", "LabelMoreInfo": "Więcej informacji",
"LabelName": "Nazwa", "LabelName": "Nazwa",
-1
View File
@@ -98,7 +98,6 @@
"ButtonStats": "Статистика", "ButtonStats": "Статистика",
"ButtonSubmit": "Применить", "ButtonSubmit": "Применить",
"ButtonTest": "Тест", "ButtonTest": "Тест",
"ButtonUnlinkOpedId": "Отвязать OpenID",
"ButtonUpload": "Загрузить", "ButtonUpload": "Загрузить",
"ButtonUploadBackup": "Загрузить бэкап", "ButtonUploadBackup": "Загрузить бэкап",
"ButtonUploadCover": "Загрузить обложку", "ButtonUploadCover": "Загрузить обложку",
+547 -21
View File
@@ -5,7 +5,7 @@
"ButtonAddLibrary": "Dodaj knjižnico", "ButtonAddLibrary": "Dodaj knjižnico",
"ButtonAddPodcasts": "Dodaj podcast", "ButtonAddPodcasts": "Dodaj podcast",
"ButtonAddUser": "Dodaj uporabnika", "ButtonAddUser": "Dodaj uporabnika",
"ButtonAddYourFirstLibrary": "Dodaj tvojo prvo knjižnico", "ButtonAddYourFirstLibrary": "Dodajte svojo prvo knjižnico",
"ButtonApply": "Uveljavi", "ButtonApply": "Uveljavi",
"ButtonApplyChapters": "Uveljavi poglavja", "ButtonApplyChapters": "Uveljavi poglavja",
"ButtonAuthors": "Avtorji", "ButtonAuthors": "Avtorji",
@@ -15,13 +15,13 @@
"ButtonCancelEncode": "Prekliči prekodiranje", "ButtonCancelEncode": "Prekliči prekodiranje",
"ButtonChangeRootPassword": "Zamenjaj korensko geslo", "ButtonChangeRootPassword": "Zamenjaj korensko geslo",
"ButtonCheckAndDownloadNewEpisodes": "Preveri in prenesi nove epizode", "ButtonCheckAndDownloadNewEpisodes": "Preveri in prenesi nove epizode",
"ButtonChooseAFolder": "Izberi mapo", "ButtonChooseAFolder": "Izberite mapo",
"ButtonChooseFiles": "Izberi datoteke", "ButtonChooseFiles": "Izberite datoteke",
"ButtonClearFilter": "Počisti filter", "ButtonClearFilter": "Počisti filter",
"ButtonCloseFeed": "Zapri vir", "ButtonCloseFeed": "Zapri vir",
"ButtonCloseSession": "Zapri odprto sejo", "ButtonCloseSession": "Zapri odprto sejo",
"ButtonCollections": "Zbirke", "ButtonCollections": "Zbirke",
"ButtonConfigureScanner": "Nastavi skener", "ButtonConfigureScanner": "Nastavi pregledovalnik",
"ButtonCreate": "Ustvari", "ButtonCreate": "Ustvari",
"ButtonCreateBackup": "Ustvari varnostno kopijo", "ButtonCreateBackup": "Ustvari varnostno kopijo",
"ButtonDelete": "Izbriši", "ButtonDelete": "Izbriši",
@@ -30,9 +30,9 @@
"ButtonEditChapters": "Uredi poglavja", "ButtonEditChapters": "Uredi poglavja",
"ButtonEditPodcast": "Uredi podcast", "ButtonEditPodcast": "Uredi podcast",
"ButtonEnable": "Omogoči", "ButtonEnable": "Omogoči",
"ButtonFireAndFail": "Zaženi in je neuspešno", "ButtonFireAndFail": "Zaženi in je bilo neuspešno",
"ButtonFireOnTest": "Zaženi samo na dogodku onTest", "ButtonFireOnTest": "Zaženi samo na dogodku onTest",
"ButtonForceReScan": "Prisilno ponovno skeniranje", "ButtonForceReScan": "Prisilno ponovno pregledovanje",
"ButtonFullPath": "Polna pot", "ButtonFullPath": "Polna pot",
"ButtonHide": "Skrij", "ButtonHide": "Skrij",
"ButtonHome": "Domov", "ButtonHome": "Domov",
@@ -67,7 +67,7 @@
"ButtonQueueRemoveItem": "Odstrani iz čakalne vrste", "ButtonQueueRemoveItem": "Odstrani iz čakalne vrste",
"ButtonQuickEmbedMetadata": "Hitra vdelava metapodatkov", "ButtonQuickEmbedMetadata": "Hitra vdelava metapodatkov",
"ButtonQuickMatch": "Hitro ujemanje", "ButtonQuickMatch": "Hitro ujemanje",
"ButtonReScan": "Ponovno iskanje", "ButtonReScan": "Ponovno pregledovanje",
"ButtonRead": "Preberi", "ButtonRead": "Preberi",
"ButtonReadLess": "Preberi manj", "ButtonReadLess": "Preberi manj",
"ButtonReadMore": "Preberi več", "ButtonReadMore": "Preberi več",
@@ -84,10 +84,10 @@
"ButtonSave": "Shrani", "ButtonSave": "Shrani",
"ButtonSaveAndClose": "Shrani iz zapri", "ButtonSaveAndClose": "Shrani iz zapri",
"ButtonSaveTracklist": "Shrani seznam skladb", "ButtonSaveTracklist": "Shrani seznam skladb",
"ButtonScan": "Skeniranje", "ButtonScan": "Pregledovanje",
"ButtonScanLibrary": "Skeniraj knjižnico", "ButtonScanLibrary": "Preglej knjižnico",
"ButtonSearch": "Poišči", "ButtonSearch": "Poišči",
"ButtonSelectFolderPath": "Izberi pot mape", "ButtonSelectFolderPath": "Izberite pot do mape",
"ButtonSeries": "Serije", "ButtonSeries": "Serije",
"ButtonSetChaptersFromTracks": "Nastavi poglavja za posnetke", "ButtonSetChaptersFromTracks": "Nastavi poglavja za posnetke",
"ButtonShare": "Deli", "ButtonShare": "Deli",
@@ -98,7 +98,7 @@
"ButtonStats": "Statistika", "ButtonStats": "Statistika",
"ButtonSubmit": "Posreduj", "ButtonSubmit": "Posreduj",
"ButtonTest": "Test", "ButtonTest": "Test",
"ButtonUnlinkOpedId": "Prekini povezavo OpenID", "ButtonUnlinkOpenId": "Prekini povezavo OpenID",
"ButtonUpload": "Naloži", "ButtonUpload": "Naloži",
"ButtonUploadBackup": "Naloži varnostno kopijo", "ButtonUploadBackup": "Naloži varnostno kopijo",
"ButtonUploadCover": "Naloži naslovnico", "ButtonUploadCover": "Naloži naslovnico",
@@ -120,7 +120,7 @@
"HeaderBackups": "Varnostne kopije", "HeaderBackups": "Varnostne kopije",
"HeaderChangePassword": "Zamenjaj geslo", "HeaderChangePassword": "Zamenjaj geslo",
"HeaderChapters": "Poglavja", "HeaderChapters": "Poglavja",
"HeaderChooseAFolder": "Izberi mapo", "HeaderChooseAFolder": "Izberite mapo",
"HeaderCollection": "Zbirka", "HeaderCollection": "Zbirka",
"HeaderCollectionItems": "Elementi zbirke", "HeaderCollectionItems": "Elementi zbirke",
"HeaderCover": "Naslovnica", "HeaderCover": "Naslovnica",
@@ -178,7 +178,7 @@
"HeaderRemoveEpisodes": "Odstrani {0} epizod", "HeaderRemoveEpisodes": "Odstrani {0} epizod",
"HeaderSavedMediaProgress": "Shranjen napredek predstavnosti", "HeaderSavedMediaProgress": "Shranjen napredek predstavnosti",
"HeaderSchedule": "Načrtovanje", "HeaderSchedule": "Načrtovanje",
"HeaderScheduleLibraryScans": "Načrtuj samodejno skeniranje knjižnice", "HeaderScheduleLibraryScans": "Načrtuj samodejno pregledovanje knjižnice",
"HeaderSession": "Seja", "HeaderSession": "Seja",
"HeaderSetBackupSchedule": "Nastavite urnik varnostnega kopiranja", "HeaderSetBackupSchedule": "Nastavite urnik varnostnega kopiranja",
"HeaderSettings": "Nastavitve", "HeaderSettings": "Nastavitve",
@@ -189,7 +189,7 @@
"HeaderSleepTimer": "Časovnik za izklop", "HeaderSleepTimer": "Časovnik za izklop",
"HeaderStatsLargestItems": "Največji elementi", "HeaderStatsLargestItems": "Največji elementi",
"HeaderStatsLongestItems": "Najdaljši elementi (ure)", "HeaderStatsLongestItems": "Najdaljši elementi (ure)",
"HeaderStatsMinutesListeningChart": "Minute poslušanja (zadnjih 7 dni)", "HeaderStatsMinutesListeningChart": "Minut poslušanja (zadnjih 7 dni)",
"HeaderStatsRecentSessions": "Nedavne seje", "HeaderStatsRecentSessions": "Nedavne seje",
"HeaderStatsTop10Authors": "Najboljših 10 avtorjev", "HeaderStatsTop10Authors": "Najboljših 10 avtorjev",
"HeaderStatsTop5Genres": "Najboljših 5 žanrov", "HeaderStatsTop5Genres": "Najboljših 5 žanrov",
@@ -290,8 +290,8 @@
"LabelDurationComparisonLonger": "({0} dlje)", "LabelDurationComparisonLonger": "({0} dlje)",
"LabelDurationComparisonShorter": "({0} krajše)", "LabelDurationComparisonShorter": "({0} krajše)",
"LabelDurationFound": "Najdeno trajanje:", "LabelDurationFound": "Najdeno trajanje:",
"LabelEbook": "Eknjiga", "LabelEbook": "E-knjiga",
"LabelEbooks": "Eknjige", "LabelEbooks": "E-knjige",
"LabelEdit": "Uredi", "LabelEdit": "Uredi",
"LabelEmail": "E-pošta", "LabelEmail": "E-pošta",
"LabelEmailSettingsFromAddress": "Iz naslova", "LabelEmailSettingsFromAddress": "Iz naslova",
@@ -338,8 +338,8 @@
"LabelGenre": "Žanr", "LabelGenre": "Žanr",
"LabelGenres": "Žanri", "LabelGenres": "Žanri",
"LabelHardDeleteFile": "Trdo brisanje datoteke", "LabelHardDeleteFile": "Trdo brisanje datoteke",
"LabelHasEbook": "Ima eknjigo", "LabelHasEbook": "Ima e-knjigo",
"LabelHasSupplementaryEbook": "Ima dodatno eknjigo", "LabelHasSupplementaryEbook": "Ima dodatno e-knjigo",
"LabelHideSubtitles": "Skrij podnapise", "LabelHideSubtitles": "Skrij podnapise",
"LabelHighestPriority": "Najvišja prioriteta", "LabelHighestPriority": "Najvišja prioriteta",
"LabelHost": "Gostitelj", "LabelHost": "Gostitelj",
@@ -406,8 +406,8 @@
"LabelMore": "Več", "LabelMore": "Več",
"LabelMoreInfo": "Več informacij", "LabelMoreInfo": "Več informacij",
"LabelName": "Naziv", "LabelName": "Naziv",
"LabelNarrator": "Pripovedovalec", "LabelNarrator": "Bralec",
"LabelNarrators": "Pripovedovalci", "LabelNarrators": "Bralci",
"LabelNew": "Novo", "LabelNew": "Novo",
"LabelNewPassword": "Novo geslo", "LabelNewPassword": "Novo geslo",
"LabelNewestAuthors": "Najnovejši avtorji", "LabelNewestAuthors": "Najnovejši avtorji",
@@ -445,6 +445,532 @@
"LabelPermissionsDownload": "Lahko prenaša", "LabelPermissionsDownload": "Lahko prenaša",
"LabelPermissionsUpdate": "Lahko posodablja", "LabelPermissionsUpdate": "Lahko posodablja",
"LabelPermissionsUpload": "Lahko nalaga", "LabelPermissionsUpload": "Lahko nalaga",
"LabelPersonalYearReview": "Pregled tvojega leta ({0})",
"LabelPhotoPathURL": "Slika pot/URL",
"LabelPlayMethod": "Metoda predvajanja",
"LabelPlayerChapterNumberMarker": "{0} od {1}",
"LabelPlaylists": "Seznami predvajanja",
"LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "Regija iskanja podcastov",
"LabelPodcastType": "Vrsta podcasta",
"LabelPodcasts": "Podcasti",
"LabelPort": "Vrata",
"LabelPrefixesToIgnore": "Predpone, ki jih je treba prezreti (neobčutljivo na velike in male črke)",
"LabelPreventIndexing": "Preprečite, da bi vaš vir indeksirali imeniki podcastov iTunes in Google",
"LabelPrimaryEbook": "Primarna e-knjiga",
"LabelProgress": "Napredek",
"LabelProvider": "Ponudnik",
"LabelProviderAuthorizationValue": "Vrednost glave avtorizacije",
"LabelPubDate": "Datum objave",
"LabelPublishYear": "Leto objave",
"LabelPublishedDate": "Objavljeno {0}",
"LabelPublisher": "Založnik",
"LabelPublishers": "Založniki",
"LabelRSSFeedCustomOwnerEmail": "E-pošta lastnika po meri",
"LabelRSSFeedCustomOwnerName": "Ime lastnika po meri",
"LabelRSSFeedOpen": "Odprt vir RSS",
"LabelRSSFeedPreventIndexing": "Prepreči indeksiranje",
"LabelRSSFeedSlug": "Slug RSS vira",
"LabelRSSFeedURL": "URL vira RSS",
"LabelRandomly": "Naključno",
"LabelReAddSeriesToContinueListening": "Znova dodaj serijo za nadaljevanje poslušanja",
"LabelRead": "Preberi",
"LabelReadAgain": "Ponovno preberi",
"LabelReadEbookWithoutProgress": "Preberi eknjigo brez ohranjanja napredka",
"LabelRecentSeries": "Nedavne serije",
"LabelRecentlyAdded": "Nedavno dodano",
"LabelRecommended": "Priporočeno",
"LabelRedo": "Ponovi",
"LabelRegion": "Regija",
"LabelReleaseDate": "Datum izdaje",
"LabelRemoveCover": "Odstrani naslovnico",
"LabelRowsPerPage": "Vrstic na stran",
"LabelSearchTerm": "Iskalni pojem",
"LabelSearchTitle": "Naslov iskanja",
"LabelSearchTitleOrASIN": "Naslov iskanja ali ASIN",
"LabelSeason": "Sezona",
"LabelSelectAll": "Izberite vse",
"LabelSelectAllEpisodes": "Izberite vse epizode",
"LabelSelectEpisodesShowing": "Izberi {0} prikazanih epizod",
"LabelSelectUsers": "Izberite uporabnike",
"LabelSendEbookToDevice": "Pošlji eknjigo k...",
"LabelSequence": "Zaporedje",
"LabelSeries": "Serije",
"LabelSeriesName": "Ime serije",
"LabelSeriesProgress": "Napredek serije",
"LabelServerYearReview": "Pregled leta strežnika ({0})",
"LabelSetEbookAsPrimary": "Nastavi kot primarno",
"LabelSetEbookAsSupplementary": "Nastavi kot dodatno",
"LabelSettingsAudiobooksOnly": "Samo zvočne knjige",
"LabelSettingsAudiobooksOnlyHelp": "Če omogočite to nastavitev, bodo datoteke eknjig prezrte, razen če so znotraj mape zvočnih knjig, v tem primeru bodo nastavljene kot dodatne e-knjige",
"LabelSettingsBookshelfViewHelp": "Skeuomorfna oblika z lesenimi policami",
"LabelSettingsChromecastSupport": "Podpora za Chromecast",
"LabelSettingsDateFormat": "Oblika datuma",
"LabelSettingsDisableWatcher": "Onemogoči pregledovalca",
"LabelSettingsDisableWatcherForLibrary": "Onemogoči pregledovalca map za knjižnico",
"LabelSettingsDisableWatcherHelp": "Onemogoči samodejno dodajanje/posodabljanje elementov, ko so zaznane spremembe datoteke. *Potreben je ponovni zagon strežnika",
"LabelSettingsEnableWatcher": "Omogoči pregledovalca",
"LabelSettingsEnableWatcherForLibrary": "Omogoči pregledovalca map za knjižnico",
"LabelSettingsEnableWatcherHelp": "Omogoča samodejno dodajanje/posodabljanje elementov, ko so zaznane spremembe datoteke. *Potreben je ponovni zagon strežnika",
"LabelSettingsEpubsAllowScriptedContent": "Dovoli skriptirano vsebino v epubih",
"LabelSettingsEpubsAllowScriptedContentHelp": "Dovoli datotekam epub izvajanje skript. Priporočljivo je, da to nastavitev pustite onemogočeno, razen če zaupate viru datotek epub.",
"LabelSettingsExperimentalFeatures": "Eksperimentalne funkcije",
"LabelSettingsExperimentalFeaturesHelp": "Funkcije v razvoju, ki bi lahko uporabile vaše povratne informacije in pomoč pri testiranju. Kliknite, da odprete razpravo na githubu.",
"LabelSettingsFindCovers": "Poišči naslovnice",
"LabelSettingsFindCoversHelp": "Če vaša zvočna knjiga nima vdelane naslovnice ali slike naslovnice v mapi, bo pregledovalnik poskušal najti naslovnico.<br>Opomba: To bo podaljšalo čas pregledovanja",
"LabelSettingsHideSingleBookSeries": "Skrij serije s samo eno knjigo",
"LabelSettingsHideSingleBookSeriesHelp": "Serije, ki imajo eno knjigo, bodo skrite na strani serije in policah domače strani.",
"LabelSettingsHomePageBookshelfView": "Domača stran bo imela pogled knjižne police",
"LabelSettingsLibraryBookshelfView": "Knjižnična uporaba pogleda knjižne police",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Preskoči prejšnje knjige v nadaljevanju serije",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Polica z domačo stranjo Nadaljuj serijo prikazuje prvo nezačeto knjigo v seriji, ki ima vsaj eno dokončano knjigo in ni nobene knjige v teku. Če omogočite to nastavitev, se bo serija nadaljevala od najbolj dokončane knjige namesto od prve nezačete knjige.",
"LabelSettingsParseSubtitles": "Uporabi podnapise",
"LabelSettingsParseSubtitlesHelp": "Izvleci podnapise iz imen map zvočnih knjig.<br>Podnaslov mora biti ločen z \" - \"<br>npr. »Naslov knjige Tu podnapis« ima podnaslov »Tu podnapis«",
"LabelSettingsPreferMatchedMetadata": "Prednost imajo ujemajoči se metapodatki",
"LabelSettingsPreferMatchedMetadataHelp": "Pri uporabi hitrega ujemanja bodo ujemajoči se podatki preglasili podrobnosti artikla. Hitro ujemanje bo privzeto izpolnil samo manjkajoče podrobnosti.",
"LabelSettingsSkipMatchingBooksWithASIN": "Preskoči ujemajoče se knjige, ki že imajo ASIN",
"LabelSettingsSkipMatchingBooksWithISBN": "Preskoči ujemajoče se knjige, ki že imajo oznako ISBN",
"LabelSettingsSortingIgnorePrefixes": "Pri razvrščanju ne upoštevajte predpon",
"LabelSettingsSortingIgnorePrefixesHelp": "npr. za naslov knjige s predpono \"the\" bi se \"The Book Title\" razvrstil kot \"Book Title, The\"",
"LabelSettingsSquareBookCovers": "Uporabi kvadratne platnice knjig",
"LabelSettingsSquareBookCoversHelp": "Raje uporabi kvadratne platnice kot standardne knjižne platnice 1.6:1",
"LabelSettingsStoreCoversWithItem": "Shrani naslovnice skupaj z elementom",
"LabelSettingsStoreCoversWithItemHelp": "Naslovnice so privzeto shranjene v /metadata/items, če omogočite to nastavitev, bodo platnice shranjene v mapi elementov knjižnice. Shranjena bo samo ena datoteka z imenom \"cover\"",
"LabelSettingsStoreMetadataWithItem": "Shrani metapodatke skupaj z elementom",
"LabelSettingsStoreMetadataWithItemHelp": "Datoteke z metapodatki so privzeto shranjene v /metadata/items, če omogočite to nastavitev, boste datoteke z metapodatki shranili v mape elementov vaše knjižnice",
"LabelSettingsTimeFormat": "Oblika časa",
"LabelShare": "Deli",
"LabelShareOpen": "Deli odprto",
"LabelShareURL": "Deli URL",
"LabelShowAll": "Prikaži vse",
"LabelShowSeconds": "Prikaži sekunde",
"LabelShowSubtitles": "Prikaži podnapise",
"LabelSize": "Velikost",
"LabelSleepTimer": "Časovnik za spanje",
"LabelSlug": "Slug",
"LabelStart": "Začetek",
"LabelStartTime": "Začetni čas",
"LabelStarted": "Začeto",
"LabelStartedAt": "Začeto ob",
"LabelStatsAudioTracks": "Zvočni posnetki",
"LabelStatsAuthors": "Avtorji",
"LabelStatsBestDay": "Najboljši dan",
"LabelStatsDailyAverage": "Dnevno povprečje",
"LabelStatsDays": "Dnevi",
"LabelStatsDaysListened": "Poslušani dnevi",
"LabelStatsHours": "Ure",
"LabelStatsInARow": "v vrsti",
"LabelStatsItemsFinished": "Končani elementi",
"LabelStatsItemsInLibrary": "Elementi v knjižnici",
"LabelStatsMinutes": "minute",
"LabelStatsMinutesListening": "Poslušane minute",
"LabelStatsOverallDays": "Skupaj dnevi",
"LabelStatsOverallHours": "Skupaj ure",
"LabelStatsWeekListening": "Tednov poslušanja",
"LabelSubtitle": "Podnapis",
"LabelSupportedFileTypes": "Podprte vrste datotek",
"LabelTag": "Oznaka",
"LabelTags": "Oznake",
"LabelTagsAccessibleToUser": "Oznake, dostopne uporabniku",
"LabelTagsNotAccessibleToUser": "Oznake, ki niso dostopne uporabniku",
"LabelTasks": "Tekoče naloge",
"LabelTextEditorBulletedList": "Seznam z oznakami",
"LabelTextEditorLink": "Povezava",
"LabelTextEditorNumberedList": "Številčni seznam",
"LabelTextEditorUnlink": "Odveži",
"LabelTheme": "Tema",
"LabelThemeDark": "Temna",
"LabelThemeLight": "Svetla",
"LabelTimeBase": "Odvisna od časa",
"LabelTimeDurationXHours": "{0} ur",
"LabelTimeDurationXMinutes": "{0} minut",
"LabelTimeDurationXSeconds": "{0} sekund",
"LabelTimeInMinutes": "Čas v minutah",
"LabelTimeListened": "Čas poslušanja",
"LabelTimeListenedToday": "Čas poslušanja danes",
"LabelTimeRemaining": "Še {0}",
"LabelTimeToShift": "Čas prestavljanja v sekundah",
"LabelTitle": "Naslov",
"LabelToolsEmbedMetadata": "Vdelaj metapodatke",
"LabelToolsEmbedMetadataDescription": "Vdelajte metapodatke v zvočne datoteke, vključno s sliko naslovnice in poglavji.",
"LabelToolsMakeM4b": "Ustvari datoteko zvočne knjige M4B",
"LabelToolsMakeM4bDescription": "Ustvarite datoteko zvočne knjige .M4B z vdelanimi metapodatki, sliko naslovnice in poglavji.",
"LabelToolsSplitM4b": "Razdeli M4B v MP3 datoteke",
"LabelToolsSplitM4bDescription": "Ustvarite MP3 datoteke iz datoteke M4B, razdeljene po poglavjih z vdelanimi metapodatki, naslovno sliko in poglavji.",
"LabelTotalDuration": "Skupno trajanje",
"LabelTotalTimeListened": "Skupni čas poslušanja",
"LabelTrackFromFilename": "Posnetek iz datoteke",
"LabelTrackFromMetadata": "Posnetek iz metapodatkov",
"LabelTracks": "Posnetki",
"LabelTracksMultiTrack": "Več posnetkov",
"LabelTracksNone": "Brez posnetka",
"LabelTracksSingleTrack": "Enojni posnetek",
"LabelType": "Vrsta",
"LabelUnabridged": "Neskrajšano",
"LabelUndo": "Razveljavi",
"LabelUnknown": "Neznano",
"LabelUnknownPublishDate": "Neznan datum objave",
"LabelUpdateCover": "Posodobi naslovnico",
"LabelUpdateCoverHelp": "Dovoli prepisovanje obstoječih naslovnic za izbrane knjige, ko se najde ujemanje",
"LabelUpdateDetails": "Posodobi podrobnosti",
"LabelUpdateDetailsHelp": "Dovoli prepisovanje obstoječih podrobnosti za izbrane knjige, ko se najde ujemanje",
"LabelUpdatedAt": "Posodobljeno ob",
"LabelUploaderDragAndDrop": "Povleci in spusti datoteke ali mape",
"LabelUploaderDropFiles": "Spusti datoteke",
"LabelUploaderItemFetchMetadataHelp": "Samodejno pridobi naslov, avtorja in serijo",
"LabelUseChapterTrack": "Uporabi posnetek poglavij",
"LabelUseFullTrack": "Uporabi celoten posnetek",
"LabelUser": "Uporabnik",
"LabelUsername": "Uporabniško ime",
"LabelValue": "Vrednost",
"LabelVersion": "Verzija",
"LabelViewBookmarks": "Ogled zaznamkov",
"LabelViewChapters": "Ogled poglavij",
"LabelViewPlayerSettings": "Ogled nastavitev predvajalnika",
"LabelViewQueue": "Ogled čakalno vrsto predvajalnika",
"LabelVolume": "Glasnost",
"LabelWeekdaysToRun": "Delovni dnevi predvajanja",
"LabelXBooks": "{0} knjig",
"LabelXItems": "{0} elementov",
"LabelYearReviewHide": "Skrij pregled leta",
"LabelYearReviewShow": "Poglej pregled leta",
"LabelYourAudiobookDuration": "Trajanje tvojih zvočnih knjig",
"LabelYourBookmarks": "Tvoji zaznamki",
"LabelYourPlaylists": "Tvoje seznami predvajanj",
"LabelYourProgress": "Tvoj napredek",
"MessageAddToPlayerQueue": "Dodaj v čakalno vrsto predvajalnika",
"MessageAppriseDescription": "Če želite uporabljati to funkcijo, morate imeti zagnan primerek <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> ali API, ki bo obravnaval te iste zahteve. <br />Url API-ja Apprise mora biti celotna pot URL-ja za pošiljanje obvestila, npr. če je vaš primerek API-ja postrežen na <code>http://192.168.1.1:8337</code>, bi morali vnesti <code >http://192.168.1.1:8337/notify</code>.",
"MessageBackupsDescription": "Varnostne kopije vključujejo uporabnike, napredek uporabnikov, podrobnosti elementov knjižnice, nastavitve strežnika in slike, shranjene v <code>/metadata/items</code> & <code>/metadata/authors</code>. Varnostne kopije <strong>ne</strong> vključujejo datotek, shranjenih v mapah vaše knjižnice.",
"MessageBackupsLocationEditNote": "Opomba: Posodabljanje lokacije varnostne kopije ne bo premaknilo ali spremenilo obstoječih varnostnih kopij",
"MessageBackupsLocationNoEditNote": "Opomba: Lokacija varnostne kopije je nastavljena s spremenljivko okolja in je tu ni mogoče spremeniti.",
"MessageBackupsLocationPathEmpty": "Pot do lokacije varnostne kopije ne sme biti prazna",
"MessageBatchQuickMatchDescription": "Hitro ujemanje bo poskušal dodati manjkajoče naslovnice in metapodatke za izbrane elemente. Omogočite spodnje možnosti, da omogočite hitremu ujemanju, da prepiše obstoječe naslovnice in/ali metapodatke.",
"MessageBookshelfNoCollections": "Ustvaril nisi še nobene zbirke",
"MessageBookshelfNoRSSFeeds": "Noben vir RSS ni odprt",
"MessageBookshelfNoResultsForFilter": "Ni rezultatov za filter \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Ni rezultatov za poizvedbo",
"MessageBookshelfNoSeries": "Nimate serij",
"MessageChapterEndIsAfter": "Konec poglavja je za koncem vaše zvočne knjige",
"MessageChapterErrorFirstNotZero": "Prvo poglavje se mora začeti pri 0",
"MessageChapterErrorStartGteDuration": "Neveljaven začetni čas mora biti krajši od trajanja zvočne knjige",
"MessageChapterErrorStartLtPrev": "Neveljaven začetni čas mora biti večji od ali enak začetnemu času prejšnjega poglavja",
"MessageChapterStartIsAfter": "Začetek poglavja je po koncu vaše zvočne knjige",
"MessageCheckingCron": "Preverjam cron...",
"MessageConfirmCloseFeed": "Ali ste prepričani, da želite zapreti ta vir?",
"MessageConfirmDeleteBackup": "Ali ste prepričani, da želite izbrisati varnostno kopijo za {0}?",
"MessageConfirmDeleteDevice": "Ali ste prepričani, da želite izbrisati e-bralnik \"{0}\"?",
"MessageConfirmDeleteFile": "To bo izbrisalo datoteko iz vašega datotečnega sistema. Ali ste prepričani?",
"MessageConfirmDeleteLibrary": "Ali ste prepričani, da želite trajno izbrisati knjižnico \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "S tem boste element knjižnice izbrisali iz baze podatkov in vašega datotečnega sistema. Ste prepričani?",
"MessageConfirmDeleteLibraryItems": "To bo izbrisalo {0} elementov knjižnice iz baze podatkov in vašega datotečnega sistema. Ste prepričani?",
"MessageConfirmDeleteMetadataProvider": "Ali ste prepričani, da želite izbrisati ponudnika metapodatkov po meri \"{0}\"?",
"MessageConfirmDeleteNotification": "Ali ste prepričani, da želite izbrisati to obvestilo?",
"MessageConfirmDeleteSession": "Ali ste prepričani, da želite izbrisati to sejo?",
"MessageConfirmForceReScan": "Ali ste prepričani, da želite vsiliti ponovno iskanje?",
"MessageConfirmMarkAllEpisodesFinished": "Ali ste prepričani, da želite označiti vse epizode kot dokončane?",
"MessageConfirmMarkAllEpisodesNotFinished": "Ali ste prepričani, da želite vse epizode označiti kot nedokončane?",
"MessageConfirmMarkItemFinished": "Ali ste prepričani, da želite \"{0}\" označiti kot dokončanega?",
"MessageConfirmMarkItemNotFinished": "Ali ste prepričani, da želite \"{0}\" označiti kot nedokončanega?",
"MessageConfirmMarkSeriesFinished": "Ali ste prepričani, da želite vse knjige v tej seriji označiti kot dokončane?",
"MessageConfirmMarkSeriesNotFinished": "Ali ste prepričani, da želite vse knjige v tej seriji označiti kot nedokončane?",
"MessageConfirmNotificationTestTrigger": "Želite sprožiti to obvestilo s testnimi podatki?",
"MessageConfirmPurgeCache": "Čiščenje predpomnilnika bo izbrisalo celoten imenik v <code>/metadata/cache</code>. <br /><br />Ali ste prepričani, da želite odstraniti imenik predpomnilnika?", "MessageConfirmPurgeCache": "Čiščenje predpomnilnika bo izbrisalo celoten imenik v <code>/metadata/cache</code>. <br /><br />Ali ste prepričani, da želite odstraniti imenik predpomnilnika?",
"MessageConfirmPurgeItemsCache": "Čiščenje predpomnilnika elementov bo izbrisalo celoten imenik na <code>/metadata/cache/items</code>.<br />Ste prepričani?" "MessageConfirmPurgeItemsCache": "Čiščenje predpomnilnika elementov bo izbrisalo celoten imenik na <code>/metadata/cache/items</code>.<br />Ste prepričani?",
"MessageConfirmQuickEmbed": "Opozorilo! Hitra vdelava ne bo varnostno kopirala vaših zvočnih datotek. Prepričajte se, da imate varnostno kopijo zvočnih datotek. <br><br>Ali želite nadaljevati?",
"MessageConfirmReScanLibraryItems": "Ali ste prepričani, da želite ponovno poiskati {0} elementov?",
"MessageConfirmRemoveAllChapters": "Ali ste prepričani, da želite odstraniti vsa poglavja?",
"MessageConfirmRemoveAuthor": "Ali ste prepričani, da želite odstraniti avtorja \"{0}\"?",
"MessageConfirmRemoveCollection": "Ali ste prepričani, da želite odstraniti zbirko \"{0}\"?",
"MessageConfirmRemoveEpisode": "Ali ste prepričani, da želite odstraniti epizodo \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Ali ste prepričani, da želite odstraniti {0} epizod?",
"MessageConfirmRemoveListeningSessions": "Ali ste prepričani, da želite odstraniti {0} sej poslušanja?",
"MessageConfirmRemoveNarrator": "Ali ste prepričani, da želite odstraniti bralca \"{0}\"?",
"MessageConfirmRemovePlaylist": "Ali ste prepričani, da želite odstraniti svoj seznam predvajanja \"{0}\"?",
"MessageConfirmRenameGenre": "Ali ste prepričani, da želite preimenovati žanr \"{0}\" v \"{1}\" za vse elemente?",
"MessageConfirmRenameGenreMergeNote": "Opomba: Ta žanr že obstaja, zato bosta združeni.",
"MessageConfirmRenameGenreWarning": "Opozorilo! Podoben žanr z različnimi velikosti črk že obstaja \"{0}\".",
"MessageConfirmRenameTag": "Ali ste prepričani, da želite preimenovati oznako \"{0}\" v \"{1}\" za vse elemente?",
"MessageConfirmRenameTagMergeNote": "Opomba: Ta oznaka že obstaja, zato bosta združeni.",
"MessageConfirmRenameTagWarning": "Opozorilo! Podobna oznaka z različnimi velikosti črk že obstaja \"{0}\".",
"MessageConfirmResetProgress": "Ali ste prepričani, da želite ponastaviti svoj napredek?",
"MessageConfirmSendEbookToDevice": "Ali ste prepričani, da želite poslati {0} e-knjigo \"{1}\" v napravo \"{2}\"?",
"MessageConfirmUnlinkOpenId": "Ali ste prepričani, da želite prekiniti povezavo tega uporabnika z OpenID?",
"MessageDownloadingEpisode": "Prenašam epizodo",
"MessageDragFilesIntoTrackOrder": "Povlecite datoteke v pravilen vrstni red posnetkov",
"MessageEmbedFailed": "Vdelava ni uspela!",
"MessageEmbedFinished": "Vdelava končana!",
"MessageEpisodesQueuedForDownload": "{0} epizod v čakalni vrsti za prenos",
"MessageEreaderDevices": "Da zagotovite dostavo e-knjig, boste morda morali dodati zgornji e-poštni naslov kot veljavnega pošiljatelja za vsako spodaj navedeno napravo.",
"MessageFeedURLWillBe": "URL vira bo {0}",
"MessageFetching": "Pridobivam...",
"MessageForceReScanDescription": "bo znova pregledal vse datoteke kot nov pregled. Oznake ID3 zvočnih datotek, datoteke OPF in besedilne datoteke bodo pregledane kot nove.",
"MessageImportantNotice": "Pomembno obvestilo!",
"MessageInsertChapterBelow": "Spodaj vstavite poglavje",
"MessageItemsSelected": "{0} izbranih elementov",
"MessageItemsUpdated": "Št. posodobljenih elementov: {0}",
"MessageJoinUsOn": "Pridružite se nam",
"MessageListeningSessionsInTheLastYear": "{0} sej poslušanja v zadnjem letu",
"MessageLoading": "Nalagam...",
"MessageLoadingFolders": "Nalagam mape...",
"MessageLogsDescription": "Dnevniki so shranjeni v <code>/metadata/logs</code> kot datoteke JSON. Dnevniki zrušitev so shranjeni v <code>/metadata/logs/crash_logs.txt</code>.",
"MessageM4BFailed": "M4B ni uspel!",
"MessageM4BFinished": "M4B končan!",
"MessageMapChapterTitles": "Preslikajte naslove poglavij v obstoječa poglavja zvočne knjige brez prilagajanja časovnih žigov",
"MessageMarkAllEpisodesFinished": "Označi vse epizode kot končane",
"MessageMarkAllEpisodesNotFinished": "Označi vse epizode kot nedokončane",
"MessageMarkAsFinished": "Označi kot dokončano",
"MessageMarkAsNotFinished": "Označi kot nedokončano",
"MessageMatchBooksDescription": "bo poskušal povezati knjige v knjižnici s knjigo izbranega ponudnika iskanja in izpolniti prazne podatke in naslovnico. Ne prepisujejo se pa podrobnosti.",
"MessageNoAudioTracks": "Ni zvočnih posnetkov",
"MessageNoAuthors": "Brez avtorjev",
"MessageNoBackups": "Brez varnostnih kopij",
"MessageNoBookmarks": "Brez zaznamkov",
"MessageNoChapters": "Brez poglavij",
"MessageNoCollections": "Brez zbirk",
"MessageNoCoversFound": "Ni naslovnic",
"MessageNoDescription": "Ni opisa",
"MessageNoDevices": "Ni naprav",
"MessageNoDownloadsInProgress": "Trenutno ni prenosov v teku",
"MessageNoDownloadsQueued": "Ni prenosov v čakalni vrsti",
"MessageNoEpisodeMatchesFound": "Ni zadetkov za epizodo",
"MessageNoEpisodes": "Ni epizod",
"MessageNoFoldersAvailable": "Ni na voljo nobene mape",
"MessageNoGenres": "Ni žanrov",
"MessageNoIssues": "Ni težav",
"MessageNoItems": "Ni elementov",
"MessageNoItemsFound": "Ni najdenih elementov",
"MessageNoListeningSessions": "Ni sej poslušanja",
"MessageNoLogs": "Ni dnevnikov",
"MessageNoMediaProgress": "Ni medijskega napredka",
"MessageNoNotifications": "Ni obvestil",
"MessageNoPodcastsFound": "Ni podcastov",
"MessageNoResults": "Ni rezultatov",
"MessageNoSearchResultsFor": "Ni rezultatov iskanja za \"{0}\"",
"MessageNoSeries": "Ni serij",
"MessageNoTags": "Ni oznak",
"MessageNoTasksRunning": "Nobeno opravili ne teče",
"MessageNoUpdatesWereNecessary": "Posodobitve niso bile potrebne",
"MessageNoUserPlaylists": "Nimate seznamov predvajanja",
"MessageNotYetImplemented": "Še ni implementirano",
"MessageOpmlPreviewNote": "Opomba: To je predogled razčlenjene datoteke OPML. Dejanski naslov podcasta bo vzet iz vira RSS.",
"MessageOr": "ali",
"MessagePauseChapter": "Začasno ustavite predvajanje poglavja",
"MessagePlayChapter": "Poslušajte začetek poglavja",
"MessagePlaylistCreateFromCollection": "Ustvari seznam predvajanja iz zbirke",
"MessagePleaseWait": "Prosim počakajte...",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nima URL-ja vira RSS, ki bi ga lahko uporabil za ujemanje",
"MessageQuickMatchDescription": "Izpolni prazne podrobnosti elementa in naslovnico s prvim rezultatom ujemanja iz '{0}'. Ne prepiše podrobnosti, razen če je omogočena nastavitev strežnika 'Prednostno ujemajoči se metapodatki'.",
"MessageRemoveChapter": "Odstrani poglavje",
"MessageRemoveEpisodes": "Odstrani toliko epizod: {0}",
"MessageRemoveFromPlayerQueue": "Odstrani iz čakalne vrste predvajalnika",
"MessageRemoveUserWarning": "Ali ste prepričani, da želite trajno izbrisati uporabnika \"{0}\"?",
"MessageReportBugsAndContribute": "Prijavite hrošče, zahtevajte nove funkcije in prispevajte še naprej",
"MessageResetChaptersConfirm": "Ali ste prepričani, da želite ponastaviti poglavja in razveljaviti spremembe, ki ste jih naredili?",
"MessageRestoreBackupConfirm": "Ali ste prepričani, da želite obnoviti varnostno kopijo, ustvarjeno ob",
"MessageRestoreBackupWarning": "Obnovitev varnostne kopije bo prepisala celotno zbirko podatkov, ki se nahaja v /config, in zajema slike v /metadata/items in /metadata/authors.<br /><br />Varnostne kopije ne spreminjajo nobenih datotek v mapah vaše knjižnice. Če ste omogočili nastavitve strežnika za shranjevanje naslovnic in metapodatkov v mapah vaše knjižnice, potem ti niso varnostno kopirani ali prepisani.<br /><br />Vsi odjemalci, ki uporabljajo vaš strežnik, bodo samodejno osveženi.",
"MessageSearchResultsFor": "Rezultati iskanja za",
"MessageSelected": "{0} izbrano",
"MessageServerCouldNotBeReached": "Strežnika ni bilo mogoče doseči",
"MessageSetChaptersFromTracksDescription": "Nastavite poglavja z uporabo vsake zvočne datoteke kot poglavja in naslova poglavja kot imena zvočne datoteke",
"MessageShareExpirationWillBe": "Potečeno bo <strong>{0}</strong>",
"MessageShareExpiresIn": "Poteče čez {0}",
"MessageShareURLWillBe": "URL za skupno rabo bo <strong>{0}</strong>",
"MessageStartPlaybackAtTime": "Začni predvajanje za \"{0}\" ob {1}?",
"MessageThinking": "Razmišljam...",
"MessageUploaderItemFailed": "Nalaganje ni uspelo",
"MessageUploaderItemSuccess": "Uspešno naloženo!",
"MessageUploading": "Nalaganje...",
"MessageValidCronExpression": "Veljaven cron izraz",
"MessageWatcherIsDisabledGlobally": "Pregledovalec je globalno onemogočen v nastavitvah strežnika",
"MessageXLibraryIsEmpty": "{0} Knjižnica je prazna!",
"MessageYourAudiobookDurationIsLonger": "Trajanje vaše zvočne knjige je daljše od ugotovljenega trajanja",
"MessageYourAudiobookDurationIsShorter": "Trajanje vaše zvočne knjige je krajše od ugotovljenega trajanja",
"NoteChangeRootPassword": "Korenski uporabnik je edini uporabnik, ki ima lahko prazno geslo",
"NoteChapterEditorTimes": "Opomba: Začetni čas prvega poglavja mora ostati pri 0:00 in zadnji čas začetka poglavja ne sme preseči tega trajanja zvočne knjige.",
"NoteFolderPicker": "Opomba: že preslikane mape ne bodo prikazane",
"NoteRSSFeedPodcastAppsHttps": "Opozorilo: večina aplikacij za podcaste bo zahtevala, da URL vira RSS uporablja HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Opozorilo: 1 ali več vaših epizod nima datuma objave. Nekatere aplikacije za podcaste to zahtevajo.",
"NoteUploaderFoldersWithMediaFiles": "Mape z predstavnostnimi datotekami bodo obravnavane kot ločene postavke knjižnice.",
"NoteUploaderOnlyAudioFiles": "Če nalagate samo zvočne datoteke, bo vsaka zvočna datoteka obravnavana kot ločena zvočna knjiga.",
"NoteUploaderUnsupportedFiles": "Nepodprte datoteke so prezrte. Ko izberete ali spustite mapo, se druge datoteke, ki niso v mapi elementov, prezrejo.",
"PlaceholderNewCollection": "Novo ime zbirke",
"PlaceholderNewFolderPath": "Pot nove mape",
"PlaceholderNewPlaylist": "Novo ime seznama predvajanja",
"PlaceholderSearch": "Poišči..",
"PlaceholderSearchEpisode": "Poišči epizodo...",
"StatsAuthorsAdded": "dodanih avtorjev",
"StatsBooksAdded": "dodanih knjig",
"StatsBooksAdditional": "Nekateri dodatki vključujejo…",
"StatsBooksFinished": "končane knjige",
"StatsBooksFinishedThisYear": "Nekaj knjig, ki so bile dokončane letos…",
"StatsBooksListenedTo": "poslušane knjige",
"StatsCollectionGrewTo": "Vaša zbirka knjig se je povečala na …",
"StatsSessions": "seje",
"StatsSpentListening": "porabil za poslušanje",
"StatsTopAuthor": "TOP AVTOR",
"StatsTopAuthors": "TOP AVTORJI",
"StatsTopGenre": "TOP ŽANR",
"StatsTopGenres": "TOP ŽANRI",
"StatsTopMonth": "TOP MESEC",
"StatsTopNarrator": "TOP BRALEC",
"StatsTopNarrators": "TOP BRALCI",
"StatsTotalDuration": "S skupnim trajanjem…",
"StatsYearInReview": "PREGLED LETA",
"ToastAccountUpdateFailed": "Računa ni bilo mogoče posodobiti",
"ToastAccountUpdateSuccess": "Račun posodobljen",
"ToastAppriseUrlRequired": "Vnesti morate Apprise URL",
"ToastAuthorImageRemoveSuccess": "Slika avtorja je odstranjena",
"ToastAuthorNotFound": "Avtor \"{0}\" ni bil najden",
"ToastAuthorRemoveSuccess": "Avtor odstranjen",
"ToastAuthorSearchNotFound": "Ne najdem avtorja",
"ToastAuthorUpdateFailed": "Avtorja ni bilo mogoče posodobiti",
"ToastAuthorUpdateMerged": "Avtor združen",
"ToastAuthorUpdateSuccess": "Avtor posodobljen",
"ToastAuthorUpdateSuccessNoImageFound": "Avtor posodobljen (ne najdem slike)",
"ToastBackupAppliedSuccess": "Uporabljena varnostna kopija",
"ToastBackupCreateFailed": "Varnostne kopije ni bilo mogoče ustvariti",
"ToastBackupCreateSuccess": "Varnostna kopija ustvarjena",
"ToastBackupDeleteFailed": "Varnostne kopije ni bilo mogoče izbrisati",
"ToastBackupDeleteSuccess": "Varnostna kopija izbrisana",
"ToastBackupInvalidMaxKeep": "Neveljavno število varnostnih kopij za ohranjanje",
"ToastBackupInvalidMaxSize": "Neveljavna največja velikost varnostne kopije",
"ToastBackupPathUpdateFailed": "Posodobitev poti varnostnih kopij ni uspela",
"ToastBackupRestoreFailed": "Varnostne kopije ni bilo mogoče obnoviti",
"ToastBackupUploadFailed": "Nalaganje varnostne kopije ni uspelo",
"ToastBackupUploadSuccess": "Varnostna kopija je naložena",
"ToastBatchDeleteFailed": "Paketno brisanje ni uspelo",
"ToastBatchDeleteSuccess": "Paketno brisanje je bilo uspešno",
"ToastBatchUpdateFailed": "Paketna posodobitev ni uspela",
"ToastBatchUpdateSuccess": "Paketna posodobitev je uspela",
"ToastBookmarkCreateFailed": "Zaznamka ni bilo mogoče ustvariti",
"ToastBookmarkCreateSuccess": "Zaznamek dodan",
"ToastBookmarkRemoveSuccess": "Zaznamek odstranjen",
"ToastBookmarkUpdateFailed": "Zaznamka ni bilo mogoče posodobiti",
"ToastBookmarkUpdateSuccess": "Zaznamek posodobljen",
"ToastCachePurgeFailed": "Čiščenje predpomnilnika ni uspelo",
"ToastCachePurgeSuccess": "Predpomnilnik je bil uspešno očiščen",
"ToastChaptersHaveErrors": "Poglavja imajo napake",
"ToastChaptersMustHaveTitles": "Poglavja morajo imeti naslove",
"ToastChaptersRemoved": "Poglavja so odstranjena",
"ToastCollectionItemsAddFailed": "Dodajanje elementov v zbirko ni uspelo",
"ToastCollectionItemsAddSuccess": "Dodajanje elementov v zbirko je bilo uspešno",
"ToastCollectionItemsRemoveSuccess": "Elementi so bili odstranjeni iz zbirke",
"ToastCollectionRemoveSuccess": "Zbirka je bila odstranjena",
"ToastCollectionUpdateFailed": "Zbirke ni bilo mogoče posodobiti",
"ToastCollectionUpdateSuccess": "Zbirka je bila posodobljena",
"ToastCoverUpdateFailed": "Posodobitev naslovnice ni uspela",
"ToastDeleteFileFailed": "Brisanje datoteke ni uspelo",
"ToastDeleteFileSuccess": "Datoteka je bila izbrisana",
"ToastDeviceAddFailed": "Naprave ni bilo mogoče dodati",
"ToastDeviceNameAlreadyExists": "Elektronska naprava s tem imenom že obstaja",
"ToastDeviceTestEmailFailed": "Pošiljanje testnega e-poštnega sporočila ni uspelo",
"ToastDeviceTestEmailSuccess": "Testno e-poštno sporočilo je poslano",
"ToastDeviceUpdateFailed": "Naprave ni bilo mogoče posodobiti",
"ToastEmailSettingsUpdateFailed": "E-poštnih nastavitev ni bilo mogoče posodobiti",
"ToastEmailSettingsUpdateSuccess": "E-poštne nastavitve so bile posodobljene",
"ToastEncodeCancelFailed": "Napaka pri preklicu prekodiranja",
"ToastEncodeCancelSucces": "Prekodiranje prekinjeno",
"ToastEpisodeDownloadQueueClearFailed": "Čiščenje čakalne vrste ni uspelo",
"ToastEpisodeDownloadQueueClearSuccess": "Čakalna vrsta za prenos epizod je počiščena",
"ToastErrorCannotShare": "V tej napravi ni mogoče dati v skupno rabo",
"ToastFailedToLoadData": "Podatkov ni bilo mogoče naložiti",
"ToastFailedToShare": "Skupna raba ni uspela",
"ToastFailedToUpdateAccount": "Računa ni bilo mogoče posodobiti",
"ToastFailedToUpdateUser": "Uporabnika ni bilo mogoče posodobiti",
"ToastInvalidImageUrl": "Neveljaven URL slike",
"ToastInvalidUrl": "Neveljaven URL",
"ToastItemCoverUpdateFailed": "Naslovnice elementa ni bilo mogoče posodobiti",
"ToastItemCoverUpdateSuccess": "Naslovnica elementa je bila posodobljena",
"ToastItemDeletedFailed": "Elementa ni bilo mogoče izbrisati",
"ToastItemDeletedSuccess": "Element je bil izbrisan",
"ToastItemDetailsUpdateFailed": "Posodobitev podrobnosti elementa ni uspela",
"ToastItemDetailsUpdateSuccess": "Podrobnosti elementa so bile posodobjene",
"ToastItemMarkedAsFinishedFailed": "Označevanje kot dokončano ni uspelo",
"ToastItemMarkedAsFinishedSuccess": "Element je označen kot dokončan",
"ToastItemMarkedAsNotFinishedFailed": "Ni bilo mogoče označiti kot nedokončano",
"ToastItemMarkedAsNotFinishedSuccess": "Element označen kot nedokončan",
"ToastItemUpdateFailed": "Elementa ni bilo mogoče posodobiti",
"ToastItemUpdateSuccess": "Element je bil posodobljen",
"ToastLibraryCreateFailed": "Knjižnice ni bilo mogoče ustvariti",
"ToastLibraryCreateSuccess": "Knjižnica \"{0}\" je bila ustvarjena",
"ToastLibraryDeleteFailed": "Knjižnice ni bilo mogoče izbrisati",
"ToastLibraryDeleteSuccess": "Knjižnica je bila izbrisana",
"ToastLibraryScanFailedToStart": "Pregleda ni bilo mogoče začeti",
"ToastLibraryScanStarted": "Pregled knjižnice se je začel",
"ToastLibraryUpdateFailed": "Knjižnice ni bilo mogoče posodobiti",
"ToastLibraryUpdateSuccess": "Knjižnica \"{0}\" je bila posodobljena",
"ToastNameEmailRequired": "Ime in e-pošta sta obvezna",
"ToastNameRequired": "Ime je obvezno",
"ToastNewUserCreatedFailed": "Računa ni bilo mogoče ustvariti: \"{0}\"",
"ToastNewUserCreatedSuccess": "Nov račun je bil ustvarjen",
"ToastNewUserLibraryError": "Izbrati morate vsaj eno knjižnico",
"ToastNewUserPasswordError": "Mora imeti geslo, samo korenski uporabnik ima lahko prazno geslo",
"ToastNewUserTagError": "Izbrati morate vsaj eno oznako",
"ToastNewUserUsernameError": "Vnesite uporabniško ime",
"ToastNoUpdatesNecessary": "Posodobitve niso potrebne",
"ToastNotificationCreateFailed": "Obvestila ni bilo mogoče ustvariti",
"ToastNotificationDeleteFailed": "Brisanje obvestila ni uspelo",
"ToastNotificationFailedMaximum": "Največje število neuspelih poskusov mora biti >= 0",
"ToastNotificationQueueMaximum": "Največja čakalna vrsta obvestil mora biti >= 0",
"ToastNotificationSettingsUpdateFailed": "Nastavitev obvestil ni bilo mogoče posodobiti",
"ToastNotificationSettingsUpdateSuccess": "Nastavitve obvestil so bile posodobljene",
"ToastNotificationTestTriggerFailed": "Sprožitev testnega obvestila ni uspela",
"ToastNotificationTestTriggerSuccess": "Sproženo testno obvestilo",
"ToastNotificationUpdateFailed": "Obvestila ni bilo mogoče posodobiti",
"ToastNotificationUpdateSuccess": "Obvestilo posodobljeno",
"ToastPlaylistCreateFailed": "Seznama predvajanja ni bilo mogoče ustvariti",
"ToastPlaylistCreateSuccess": "Seznam predvajanja je bil ustvarjen",
"ToastPlaylistRemoveSuccess": "Seznam predvajanja odstranjen",
"ToastPlaylistUpdateFailed": "Seznama predvajanja ni bilo mogoče posodobiti",
"ToastPlaylistUpdateSuccess": "Seznam predvajanja je bil posodobljen",
"ToastPodcastCreateFailed": "Podcasta ni bilo mogoče ustvariti",
"ToastPodcastCreateSuccess": "Podcast je bil uspešno ustvarjen",
"ToastPodcastGetFeedFailed": "Vira podcasta ni bilo mogoče pridobiti",
"ToastPodcastNoEpisodesInFeed": "V viru RSS ni bilo mogoče najti nobene epizode",
"ToastPodcastNoRssFeed": "Podcast nima vira RSS",
"ToastProviderCreatedFailed": "Ponudnika ni bilo mogoče dodati",
"ToastProviderCreatedSuccess": "Dodan je bil nov ponudnik",
"ToastProviderNameAndUrlRequired": "Obvezen podatek sta ime in URL",
"ToastProviderRemoveSuccess": "Ponudnik je bil odstranjen",
"ToastRSSFeedCloseFailed": "Vira RSS ni bilo mogoče zapreti",
"ToastRSSFeedCloseSuccess": "Vir RSS je bil zaprt",
"ToastRemoveFailed": "Odstranitev ni uspela",
"ToastRemoveItemFromCollectionFailed": "Elementa ni bilo mogoče odstraniti iz zbirke",
"ToastRemoveItemFromCollectionSuccess": "Element je bil odstranjen iz zbirke",
"ToastRemoveItemsWithIssuesFailed": "Elementov knjižnice s težavami ni bilo mogoče odstraniti",
"ToastRemoveItemsWithIssuesSuccess": "Odstranjeni so bili elementi knjižnice s težavami",
"ToastRenameFailed": "Preimenovanje ni uspelo",
"ToastRescanFailed": "Ponovni pregled ni uspel za {0}",
"ToastRescanRemoved": "Ponovni pregled celotnega elementa je bil odstranjen",
"ToastRescanUpToDate": "Ponovni pregled celotnega elementa je bil ažuren",
"ToastRescanUpdated": "Ponovni pregled celotnega elementa je bil posodobljen",
"ToastScanFailed": "Pregled elementa knjižnice ni uspel",
"ToastSelectAtLeastOneUser": "Izberite vsaj enega uporabnika",
"ToastSendEbookToDeviceFailed": "E-knjige ni bilo mogoče poslati v napravo",
"ToastSendEbookToDeviceSuccess": "E-knjiga je bila poslana v napravo \"{0}\"",
"ToastSeriesUpdateFailed": "Posodobitev serije ni uspela",
"ToastSeriesUpdateSuccess": "Uspešna posodobitev serije",
"ToastServerSettingsUpdateFailed": "Nastavitev strežnika ni bilo mogoče posodobiti",
"ToastServerSettingsUpdateSuccess": "Nastavitve strežnika so bile posodobljene",
"ToastSessionCloseFailed": "Seje ni bilo mogoče zapreti",
"ToastSessionDeleteFailed": "Brisanje seje ni uspelo",
"ToastSessionDeleteSuccess": "Seja je bila izbrisana",
"ToastSlugMustChange": "Slug vsebuje neveljavne znake",
"ToastSlugRequired": "Slug je obvezen podatek",
"ToastSocketConnected": "Omrežna povezava je priklopljena",
"ToastSocketDisconnected": "Omrežna povezava je odklopljena",
"ToastSocketFailedToConnect": "Omrežna povezava ni uspela vzpostaviti priklopa",
"ToastSortingPrefixesEmptyError": "Imeti mora vsaj 1 predpono za razvrščanje",
"ToastSortingPrefixesUpdateFailed": "Posodobitev predpon za razvrščanje ni uspela",
"ToastSortingPrefixesUpdateSuccess": "Predpone za razvrščanje so bile posodobljene ({0} elementov)",
"ToastTitleRequired": "Naslov je obvezen",
"ToastUnknownError": "Neznana napaka",
"ToastUnlinkOpenIdFailed": "Prekinitev povezave uporabnika z OpenID ni uspela",
"ToastUnlinkOpenIdSuccess": "Uporabnik je prekinil povezavo z OpenID",
"ToastUserDeleteFailed": "Brisanje uporabnika ni uspelo",
"ToastUserDeleteSuccess": "Uporabnik je bil izbrisan",
"ToastUserPasswordChangeSuccess": "Geslo je bilo uspešno spremenjeno",
"ToastUserPasswordMismatch": "Gesli se ne ujemata",
"ToastUserPasswordMustChange": "Novo geslo se ne sme ujemati s starim geslom",
"ToastUserRootRequireName": "Vnesti morate korensko uporabniško ime"
} }
+121 -7
View File
@@ -19,6 +19,7 @@
"ButtonChooseFiles": "选择文件", "ButtonChooseFiles": "选择文件",
"ButtonClearFilter": "清除过滤器", "ButtonClearFilter": "清除过滤器",
"ButtonCloseFeed": "关闭源", "ButtonCloseFeed": "关闭源",
"ButtonCloseSession": "关闭开放会话",
"ButtonCollections": "收藏", "ButtonCollections": "收藏",
"ButtonConfigureScanner": "配置扫描", "ButtonConfigureScanner": "配置扫描",
"ButtonCreate": "创建", "ButtonCreate": "创建",
@@ -28,6 +29,9 @@
"ButtonEdit": "编辑", "ButtonEdit": "编辑",
"ButtonEditChapters": "编辑章节", "ButtonEditChapters": "编辑章节",
"ButtonEditPodcast": "编辑播客", "ButtonEditPodcast": "编辑播客",
"ButtonEnable": "启用",
"ButtonFireAndFail": "故障和失败",
"ButtonFireOnTest": "测试事件触发",
"ButtonForceReScan": "强制重新扫描", "ButtonForceReScan": "强制重新扫描",
"ButtonFullPath": "完整路径", "ButtonFullPath": "完整路径",
"ButtonHide": "隐藏", "ButtonHide": "隐藏",
@@ -46,6 +50,7 @@
"ButtonNevermind": "没有关系", "ButtonNevermind": "没有关系",
"ButtonNext": "下一个", "ButtonNext": "下一个",
"ButtonNextChapter": "下一章节", "ButtonNextChapter": "下一章节",
"ButtonNextItemInQueue": "队列中的下一个项目",
"ButtonOk": "确定", "ButtonOk": "确定",
"ButtonOpenFeed": "打开源", "ButtonOpenFeed": "打开源",
"ButtonOpenManager": "打开管理器", "ButtonOpenManager": "打开管理器",
@@ -55,6 +60,7 @@
"ButtonPlaylists": "播放列表", "ButtonPlaylists": "播放列表",
"ButtonPrevious": "上一个", "ButtonPrevious": "上一个",
"ButtonPreviousChapter": "上一章节", "ButtonPreviousChapter": "上一章节",
"ButtonProbeAudioFile": "探测音频文件",
"ButtonPurgeAllCache": "清理所有缓存", "ButtonPurgeAllCache": "清理所有缓存",
"ButtonPurgeItemsCache": "清理项目缓存", "ButtonPurgeItemsCache": "清理项目缓存",
"ButtonQueueAddItem": "添加到队列", "ButtonQueueAddItem": "添加到队列",
@@ -92,6 +98,7 @@
"ButtonStats": "统计数据", "ButtonStats": "统计数据",
"ButtonSubmit": "提交", "ButtonSubmit": "提交",
"ButtonTest": "测试", "ButtonTest": "测试",
"ButtonUnlinkOpenId": "取消 OpenID 链接",
"ButtonUpload": "上传", "ButtonUpload": "上传",
"ButtonUploadBackup": "上传备份", "ButtonUploadBackup": "上传备份",
"ButtonUploadCover": "上传封面", "ButtonUploadCover": "上传封面",
@@ -104,6 +111,7 @@
"ErrorUploadFetchMetadataNoResults": "无法获取元数据 - 尝试更新标题和/或作者", "ErrorUploadFetchMetadataNoResults": "无法获取元数据 - 尝试更新标题和/或作者",
"ErrorUploadLacksTitle": "必须有标题", "ErrorUploadLacksTitle": "必须有标题",
"HeaderAccount": "帐户", "HeaderAccount": "帐户",
"HeaderAddCustomMetadataProvider": "添加自定义元数据提供商",
"HeaderAdvanced": "高级", "HeaderAdvanced": "高级",
"HeaderAppriseNotificationSettings": "测试通知设置", "HeaderAppriseNotificationSettings": "测试通知设置",
"HeaderAudioTracks": "音轨", "HeaderAudioTracks": "音轨",
@@ -118,7 +126,7 @@
"HeaderCover": "封面", "HeaderCover": "封面",
"HeaderCurrentDownloads": "当前下载", "HeaderCurrentDownloads": "当前下载",
"HeaderCustomMessageOnLogin": "登录时的自定义消息", "HeaderCustomMessageOnLogin": "登录时的自定义消息",
"HeaderCustomMetadataProviders": "自定义元数据提供", "HeaderCustomMetadataProviders": "自定义元数据提供",
"HeaderDetails": "详情", "HeaderDetails": "详情",
"HeaderDownloadQueue": "下载队列", "HeaderDownloadQueue": "下载队列",
"HeaderEbookFiles": "电子书文件", "HeaderEbookFiles": "电子书文件",
@@ -149,6 +157,8 @@
"HeaderMetadataToEmbed": "嵌入元数据", "HeaderMetadataToEmbed": "嵌入元数据",
"HeaderNewAccount": "新建帐户", "HeaderNewAccount": "新建帐户",
"HeaderNewLibrary": "新建媒体库", "HeaderNewLibrary": "新建媒体库",
"HeaderNotificationCreate": "创建通知",
"HeaderNotificationUpdate": "更新通知",
"HeaderNotifications": "通知", "HeaderNotifications": "通知",
"HeaderOpenIDConnectAuthentication": "OpenID 连接身份验证", "HeaderOpenIDConnectAuthentication": "OpenID 连接身份验证",
"HeaderOpenRSSFeed": "打开 RSS 源", "HeaderOpenRSSFeed": "打开 RSS 源",
@@ -206,6 +216,7 @@
"LabelAddToPlaylist": "添加到播放列表", "LabelAddToPlaylist": "添加到播放列表",
"LabelAddToPlaylistBatch": "添加 {0} 个项目到播放列表", "LabelAddToPlaylistBatch": "添加 {0} 个项目到播放列表",
"LabelAddedAt": "添加于", "LabelAddedAt": "添加于",
"LabelAddedDate": "添加 {0}",
"LabelAdminUsersOnly": "仅限管理员用户", "LabelAdminUsersOnly": "仅限管理员用户",
"LabelAll": "全部", "LabelAll": "全部",
"LabelAllUsers": "所有用户", "LabelAllUsers": "所有用户",
@@ -235,6 +246,7 @@
"LabelBitrate": "比特率", "LabelBitrate": "比特率",
"LabelBooks": "图书", "LabelBooks": "图书",
"LabelButtonText": "按钮文本", "LabelButtonText": "按钮文本",
"LabelByAuthor": "由 {0}",
"LabelChangePassword": "修改密码", "LabelChangePassword": "修改密码",
"LabelChannels": "声道", "LabelChannels": "声道",
"LabelChapterTitle": "章节标题", "LabelChapterTitle": "章节标题",
@@ -244,6 +256,7 @@
"LabelClosePlayer": "关闭播放器", "LabelClosePlayer": "关闭播放器",
"LabelCodec": "编解码", "LabelCodec": "编解码",
"LabelCollapseSeries": "折叠系列", "LabelCollapseSeries": "折叠系列",
"LabelCollapseSubSeries": "折叠子系列",
"LabelCollection": "收藏", "LabelCollection": "收藏",
"LabelCollections": "收藏", "LabelCollections": "收藏",
"LabelComplete": "已完成", "LabelComplete": "已完成",
@@ -294,8 +307,10 @@
"LabelEpisode": "剧集", "LabelEpisode": "剧集",
"LabelEpisodeTitle": "剧集标题", "LabelEpisodeTitle": "剧集标题",
"LabelEpisodeType": "剧集类型", "LabelEpisodeType": "剧集类型",
"LabelEpisodes": "剧集",
"LabelExample": "示例", "LabelExample": "示例",
"LabelExpandSeries": "展开系列", "LabelExpandSeries": "展开系列",
"LabelExpandSubSeries": "展开子系列",
"LabelExplicit": "信息准确", "LabelExplicit": "信息准确",
"LabelExplicitChecked": "明确(已选中)", "LabelExplicitChecked": "明确(已选中)",
"LabelExplicitUnchecked": "不明确 (未选中)", "LabelExplicitUnchecked": "不明确 (未选中)",
@@ -304,7 +319,9 @@
"LabelFetchingMetadata": "正在获取元数据", "LabelFetchingMetadata": "正在获取元数据",
"LabelFile": "文件", "LabelFile": "文件",
"LabelFileBirthtime": "文件创建时间", "LabelFileBirthtime": "文件创建时间",
"LabelFileBornDate": "生于 {0}",
"LabelFileModified": "文件修改时间", "LabelFileModified": "文件修改时间",
"LabelFileModifiedDate": "已修改 {0}",
"LabelFilename": "文件名", "LabelFilename": "文件名",
"LabelFilterByUser": "按用户筛选", "LabelFilterByUser": "按用户筛选",
"LabelFindEpisodes": "查找剧集", "LabelFindEpisodes": "查找剧集",
@@ -360,6 +377,7 @@
"LabelLess": "较少", "LabelLess": "较少",
"LabelLibrariesAccessibleToUser": "用户可访问的媒体库", "LabelLibrariesAccessibleToUser": "用户可访问的媒体库",
"LabelLibrary": "媒体库", "LabelLibrary": "媒体库",
"LabelLibraryFilterSublistEmpty": "没有 {0}",
"LabelLibraryItem": "媒体库项目", "LabelLibraryItem": "媒体库项目",
"LabelLibraryName": "媒体库名称", "LabelLibraryName": "媒体库名称",
"LabelLimit": "限制", "LabelLimit": "限制",
@@ -371,13 +389,13 @@
"LabelLookForNewEpisodesAfterDate": "在此日期后查找新剧集", "LabelLookForNewEpisodesAfterDate": "在此日期后查找新剧集",
"LabelLowestPriority": "最低优先级", "LabelLowestPriority": "最低优先级",
"LabelMatchExistingUsersBy": "匹配现有用户", "LabelMatchExistingUsersBy": "匹配现有用户",
"LabelMatchExistingUsersByDescription": "用于连接现有用户. 连接后, 用户将通过SSO提供商提供的唯一 id 进行匹配", "LabelMatchExistingUsersByDescription": "用于连接现有用户. 连接后, 用户将通过 SSO 提供商提供的唯一 id 进行匹配",
"LabelMediaPlayer": "媒体播放器", "LabelMediaPlayer": "媒体播放器",
"LabelMediaType": "媒体类型", "LabelMediaType": "媒体类型",
"LabelMetaTag": "元数据标签", "LabelMetaTag": "元数据标签",
"LabelMetaTags": "元标签", "LabelMetaTags": "元标签",
"LabelMetadataOrderOfPrecedenceDescription": "较高优先级的元数据源将覆盖较低优先级的元数据源", "LabelMetadataOrderOfPrecedenceDescription": "较高优先级的元数据源将覆盖较低优先级的元数据源",
"LabelMetadataProvider": "元数据提供", "LabelMetadataProvider": "元数据提供",
"LabelMinute": "分钟", "LabelMinute": "分钟",
"LabelMinutes": "分钟", "LabelMinutes": "分钟",
"LabelMissing": "丢失", "LabelMissing": "丢失",
@@ -396,7 +414,7 @@
"LabelNewestEpisodes": "最新剧集", "LabelNewestEpisodes": "最新剧集",
"LabelNextBackupDate": "下次备份日期", "LabelNextBackupDate": "下次备份日期",
"LabelNextScheduledRun": "下次任务运行", "LabelNextScheduledRun": "下次任务运行",
"LabelNoCustomMetadataProviders": "没有自定义元数据提供程序", "LabelNoCustomMetadataProviders": "没有自定义元数据提供",
"LabelNoEpisodesSelected": "未选择任何剧集", "LabelNoEpisodesSelected": "未选择任何剧集",
"LabelNotFinished": "未听完", "LabelNotFinished": "未听完",
"LabelNotStarted": "未开始", "LabelNotStarted": "未开始",
@@ -412,7 +430,7 @@
"LabelNotificationsMaxQueueSizeHelp": "通知事件被限制为每秒触发 1 个. 如果队列处于最大大小, 则将忽略事件. 这可以防止通知垃圾邮件.", "LabelNotificationsMaxQueueSizeHelp": "通知事件被限制为每秒触发 1 个. 如果队列处于最大大小, 则将忽略事件. 这可以防止通知垃圾邮件.",
"LabelNumberOfBooks": "图书数量", "LabelNumberOfBooks": "图书数量",
"LabelNumberOfEpisodes": "# 集", "LabelNumberOfEpisodes": "# 集",
"LabelOpenIDAdvancedPermsClaimDescription": "OpenID 声明的名称, 该声明包含应用程序内用户操作的高级权限, 该权限将应用于非管理员角色(<b>如果已配置</b>). 如果响应中缺少声明, 获取 ABS 的权限将被拒绝. 如果缺少单个选项, 它将被视为 <code>禁用</code>. 确保身份提供的声明与预期结构匹配:", "LabelOpenIDAdvancedPermsClaimDescription": "OpenID 声明的名称, 该声明包含应用程序内用户操作的高级权限, 该权限将应用于非管理员角色(<b>如果已配置</b>). 如果响应中缺少声明, 获取 ABS 的权限将被拒绝. 如果缺少单个选项, 它将被视为 <code>禁用</code>. 确保身份提供的声明与预期结构匹配:",
"LabelOpenIDClaims": "将以下选项留空以禁用高级组和权限分配, 然后自动分配 'User' 组.", "LabelOpenIDClaims": "将以下选项留空以禁用高级组和权限分配, 然后自动分配 'User' 组.",
"LabelOpenIDGroupClaimDescription": "OpenID 声明的名称, 该声明包含用户组的列表. 通常称为<code>组</code><b>如果已配置</b>, 应用程序将根据用户的组成员身份自动分配角色, 前提是这些组在声明中以不区分大小写的方式命名为 'Admin', 'User' 或 'Guest'. 声明应包含一个列表, 如果用户属于多个组, 则应用程序将分配与最高访问级别相对应的角色. 如果没有组匹配, 访问将被拒绝.", "LabelOpenIDGroupClaimDescription": "OpenID 声明的名称, 该声明包含用户组的列表. 通常称为<code>组</code><b>如果已配置</b>, 应用程序将根据用户的组成员身份自动分配角色, 前提是这些组在声明中以不区分大小写的方式命名为 'Admin', 'User' 或 'Guest'. 声明应包含一个列表, 如果用户属于多个组, 则应用程序将分配与最高访问级别相对应的角色. 如果没有组匹配, 访问将被拒绝.",
"LabelOpenRSSFeed": "打开 RSS 源", "LabelOpenRSSFeed": "打开 RSS 源",
@@ -430,6 +448,7 @@
"LabelPersonalYearReview": "你的年度回顾 ({0})", "LabelPersonalYearReview": "你的年度回顾 ({0})",
"LabelPhotoPathURL": "图片路径或 URL", "LabelPhotoPathURL": "图片路径或 URL",
"LabelPlayMethod": "播放方法", "LabelPlayMethod": "播放方法",
"LabelPlayerChapterNumberMarker": "{0} 于 {1}",
"LabelPlaylists": "播放列表", "LabelPlaylists": "播放列表",
"LabelPodcast": "播客", "LabelPodcast": "播客",
"LabelPodcastSearchRegion": "播客搜索地区", "LabelPodcastSearchRegion": "播客搜索地区",
@@ -440,9 +459,11 @@
"LabelPreventIndexing": "防止 iTunes 和 Google 播客目录对你的源进行索引", "LabelPreventIndexing": "防止 iTunes 和 Google 播客目录对你的源进行索引",
"LabelPrimaryEbook": "主电子书", "LabelPrimaryEbook": "主电子书",
"LabelProgress": "进度", "LabelProgress": "进度",
"LabelProvider": "供商", "LabelProvider": "供商",
"LabelProviderAuthorizationValue": "授权标头值",
"LabelPubDate": "出版日期", "LabelPubDate": "出版日期",
"LabelPublishYear": "发布年份", "LabelPublishYear": "发布年份",
"LabelPublishedDate": "已发布 {0}",
"LabelPublisher": "出版商", "LabelPublisher": "出版商",
"LabelPublishers": "出版商", "LabelPublishers": "出版商",
"LabelRSSFeedCustomOwnerEmail": "自定义所有者电子邮件", "LabelRSSFeedCustomOwnerEmail": "自定义所有者电子邮件",
@@ -526,6 +547,7 @@
"LabelShowSubtitles": "显示标题", "LabelShowSubtitles": "显示标题",
"LabelSize": "文件大小", "LabelSize": "文件大小",
"LabelSleepTimer": "睡眠定时", "LabelSleepTimer": "睡眠定时",
"LabelSlug": "Slug",
"LabelStart": "开始", "LabelStart": "开始",
"LabelStartTime": "开始时间", "LabelStartTime": "开始时间",
"LabelStarted": "开始于", "LabelStarted": "开始于",
@@ -587,6 +609,7 @@
"LabelUnabridged": "未删节", "LabelUnabridged": "未删节",
"LabelUndo": "撤消", "LabelUndo": "撤消",
"LabelUnknown": "未知", "LabelUnknown": "未知",
"LabelUnknownPublishDate": "未知发布日期",
"LabelUpdateCover": "更新封面", "LabelUpdateCover": "更新封面",
"LabelUpdateCoverHelp": "找到匹配项时允许覆盖所选书籍存在的封面", "LabelUpdateCoverHelp": "找到匹配项时允许覆盖所选书籍存在的封面",
"LabelUpdateDetails": "更新详细信息", "LabelUpdateDetails": "更新详细信息",
@@ -635,16 +658,22 @@
"MessageCheckingCron": "检查计划任务...", "MessageCheckingCron": "检查计划任务...",
"MessageConfirmCloseFeed": "你确定要关闭此订阅源吗?", "MessageConfirmCloseFeed": "你确定要关闭此订阅源吗?",
"MessageConfirmDeleteBackup": "你确定要删除备份 {0}?", "MessageConfirmDeleteBackup": "你确定要删除备份 {0}?",
"MessageConfirmDeleteDevice": "您确定要删除电子阅读器设备 \"{0}\" 吗?",
"MessageConfirmDeleteFile": "这将从文件系统中删除该文件. 你确定吗?", "MessageConfirmDeleteFile": "这将从文件系统中删除该文件. 你确定吗?",
"MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?", "MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "这将从数据库和文件系统中删除库项目. 你确定吗?", "MessageConfirmDeleteLibraryItem": "这将从数据库和文件系统中删除库项目. 你确定吗?",
"MessageConfirmDeleteLibraryItems": "这将从数据库和文件系统中删除 {0} 个库项目. 你确定吗?", "MessageConfirmDeleteLibraryItems": "这将从数据库和文件系统中删除 {0} 个库项目. 你确定吗?",
"MessageConfirmDeleteMetadataProvider": "是否确实要删除自定义元数据提供商 \"{0}\" ?",
"MessageConfirmDeleteNotification": "您确定要删除此通知吗?",
"MessageConfirmDeleteSession": "你确定要删除此会话吗?", "MessageConfirmDeleteSession": "你确定要删除此会话吗?",
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?", "MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
"MessageConfirmMarkAllEpisodesFinished": "你确定要将所有剧集都标记为已完成吗?", "MessageConfirmMarkAllEpisodesFinished": "你确定要将所有剧集都标记为已完成吗?",
"MessageConfirmMarkAllEpisodesNotFinished": "你确定要将所有剧集都标记为未完成吗?", "MessageConfirmMarkAllEpisodesNotFinished": "你确定要将所有剧集都标记为未完成吗?",
"MessageConfirmMarkItemFinished": "您确定要将 \"{0}\" 标记为已完成吗?",
"MessageConfirmMarkItemNotFinished": "您确定要将 \"{0}\" 标记为未完成吗?",
"MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?", "MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?",
"MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?", "MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?",
"MessageConfirmNotificationTestTrigger": "使用测试数据触发此通知吗?",
"MessageConfirmPurgeCache": "清除缓存将删除 <code>/metadata/cache</code> 整个目录. <br /><br />你确定要删除缓存目录吗?", "MessageConfirmPurgeCache": "清除缓存将删除 <code>/metadata/cache</code> 整个目录. <br /><br />你确定要删除缓存目录吗?",
"MessageConfirmPurgeItemsCache": "清除项目缓存将删除 <code>/metadata/cache/items</code> 整个目录.<br />你确定吗?", "MessageConfirmPurgeItemsCache": "清除项目缓存将删除 <code>/metadata/cache/items</code> 整个目录.<br />你确定吗?",
"MessageConfirmQuickEmbed": "警告! 快速嵌入不会备份你的音频文件. 确保你有音频文件的备份. <br><br>你是否想继续吗?", "MessageConfirmQuickEmbed": "警告! 快速嵌入不会备份你的音频文件. 确保你有音频文件的备份. <br><br>你是否想继续吗?",
@@ -663,7 +692,9 @@
"MessageConfirmRenameTag": "你确定要将所有项目标签 \"{0}\" 重命名到 \"{1}\"?", "MessageConfirmRenameTag": "你确定要将所有项目标签 \"{0}\" 重命名到 \"{1}\"?",
"MessageConfirmRenameTagMergeNote": "注意: 该标签已经存在, 因此它们将被合并.", "MessageConfirmRenameTagMergeNote": "注意: 该标签已经存在, 因此它们将被合并.",
"MessageConfirmRenameTagWarning": "警告! 已经存在有大小写不同的类似标签 \"{0}\".", "MessageConfirmRenameTagWarning": "警告! 已经存在有大小写不同的类似标签 \"{0}\".",
"MessageConfirmResetProgress": "你确定要重置进度吗?",
"MessageConfirmSendEbookToDevice": "你确定要发送 {0} 电子书 \"{1}\" 到设备 \"{2}\"?", "MessageConfirmSendEbookToDevice": "你确定要发送 {0} 电子书 \"{1}\" 到设备 \"{2}\"?",
"MessageConfirmUnlinkOpenId": "您确定要取消该用户与 OpenID 的链接吗?",
"MessageDownloadingEpisode": "正在下载剧集", "MessageDownloadingEpisode": "正在下载剧集",
"MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序", "MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序",
"MessageEmbedFailed": "嵌入失败!", "MessageEmbedFailed": "嵌入失败!",
@@ -698,6 +729,7 @@
"MessageNoCollections": "没有收藏", "MessageNoCollections": "没有收藏",
"MessageNoCoversFound": "没有找到封面", "MessageNoCoversFound": "没有找到封面",
"MessageNoDescription": "没有描述", "MessageNoDescription": "没有描述",
"MessageNoDevices": "没有设备",
"MessageNoDownloadsInProgress": "当前没有正在进行的下载", "MessageNoDownloadsInProgress": "当前没有正在进行的下载",
"MessageNoDownloadsQueued": "下载队列无任务", "MessageNoDownloadsQueued": "下载队列无任务",
"MessageNoEpisodeMatchesFound": "没有找到任何剧集匹配项", "MessageNoEpisodeMatchesFound": "没有找到任何剧集匹配项",
@@ -725,6 +757,7 @@
"MessagePauseChapter": "暂停章节播放", "MessagePauseChapter": "暂停章节播放",
"MessagePlayChapter": "开始章节播放", "MessagePlayChapter": "开始章节播放",
"MessagePlaylistCreateFromCollection": "从收藏中创建播放列表", "MessagePlaylistCreateFromCollection": "从收藏中创建播放列表",
"MessagePleaseWait": "请稍等...",
"MessagePodcastHasNoRSSFeedForMatching": "播客没有可用于匹配 RSS 源的 url", "MessagePodcastHasNoRSSFeedForMatching": "播客没有可用于匹配 RSS 源的 url",
"MessageQuickMatchDescription": "使用来自 '{0}' 的第一个匹配结果填充空白详细信息和封面. 除非启用 '首选匹配元数据' 服务器设置, 否则不会覆盖详细信息.", "MessageQuickMatchDescription": "使用来自 '{0}' 的第一个匹配结果填充空白详细信息和封面. 除非启用 '首选匹配元数据' 服务器设置, 否则不会覆盖详细信息.",
"MessageRemoveChapter": "移除章节", "MessageRemoveChapter": "移除章节",
@@ -785,18 +818,28 @@
"StatsYearInReview": "年度回顾", "StatsYearInReview": "年度回顾",
"ToastAccountUpdateFailed": "账户更新失败", "ToastAccountUpdateFailed": "账户更新失败",
"ToastAccountUpdateSuccess": "帐户已更新", "ToastAccountUpdateSuccess": "帐户已更新",
"ToastAppriseUrlRequired": "必须输入 Apprise URL",
"ToastAuthorImageRemoveSuccess": "作者图像已删除", "ToastAuthorImageRemoveSuccess": "作者图像已删除",
"ToastAuthorNotFound": "未找到作者 \"{0}\"",
"ToastAuthorRemoveSuccess": "作者已删除",
"ToastAuthorSearchNotFound": "未找到作者",
"ToastAuthorUpdateFailed": "作者更新失败", "ToastAuthorUpdateFailed": "作者更新失败",
"ToastAuthorUpdateMerged": "作者已合并", "ToastAuthorUpdateMerged": "作者已合并",
"ToastAuthorUpdateSuccess": "作者已更新", "ToastAuthorUpdateSuccess": "作者已更新",
"ToastAuthorUpdateSuccessNoImageFound": "作者已更新 (未找到图像)", "ToastAuthorUpdateSuccessNoImageFound": "作者已更新 (未找到图像)",
"ToastBackupAppliedSuccess": "已应用备份",
"ToastBackupCreateFailed": "备份创建失败", "ToastBackupCreateFailed": "备份创建失败",
"ToastBackupCreateSuccess": "备份已创建", "ToastBackupCreateSuccess": "备份已创建",
"ToastBackupDeleteFailed": "备份删除失败", "ToastBackupDeleteFailed": "备份删除失败",
"ToastBackupDeleteSuccess": "备份已删除", "ToastBackupDeleteSuccess": "备份已删除",
"ToastBackupInvalidMaxKeep": "要保留的备份数无效",
"ToastBackupInvalidMaxSize": "最大备份大小无效",
"ToastBackupPathUpdateFailed": "无法更新备份路径",
"ToastBackupRestoreFailed": "备份还原失败", "ToastBackupRestoreFailed": "备份还原失败",
"ToastBackupUploadFailed": "上传备份失败", "ToastBackupUploadFailed": "上传备份失败",
"ToastBackupUploadSuccess": "备份已上传", "ToastBackupUploadSuccess": "备份已上传",
"ToastBatchDeleteFailed": "批量删除失败",
"ToastBatchDeleteSuccess": "批量删除成功",
"ToastBatchUpdateFailed": "批量更新失败", "ToastBatchUpdateFailed": "批量更新失败",
"ToastBatchUpdateSuccess": "批量更新成功", "ToastBatchUpdateSuccess": "批量更新成功",
"ToastBookmarkCreateFailed": "创建书签失败", "ToastBookmarkCreateFailed": "创建书签失败",
@@ -808,22 +851,46 @@
"ToastCachePurgeSuccess": "缓存清除成功", "ToastCachePurgeSuccess": "缓存清除成功",
"ToastChaptersHaveErrors": "章节有错误", "ToastChaptersHaveErrors": "章节有错误",
"ToastChaptersMustHaveTitles": "章节必须有标题", "ToastChaptersMustHaveTitles": "章节必须有标题",
"ToastChaptersRemoved": "已删除章节",
"ToastCollectionItemsAddFailed": "项目添加到收藏夹失败",
"ToastCollectionItemsAddSuccess": "项目添加到收藏夹成功",
"ToastCollectionItemsRemoveSuccess": "项目从收藏夹移除", "ToastCollectionItemsRemoveSuccess": "项目从收藏夹移除",
"ToastCollectionRemoveSuccess": "收藏夹已删除", "ToastCollectionRemoveSuccess": "收藏夹已删除",
"ToastCollectionUpdateFailed": "更新收藏夹失败", "ToastCollectionUpdateFailed": "更新收藏夹失败",
"ToastCollectionUpdateSuccess": "收藏夹已更新", "ToastCollectionUpdateSuccess": "收藏夹已更新",
"ToastCoverUpdateFailed": "封面更新失败",
"ToastDeleteFileFailed": "删除文件失败", "ToastDeleteFileFailed": "删除文件失败",
"ToastDeleteFileSuccess": "文件已删除", "ToastDeleteFileSuccess": "文件已删除",
"ToastDeviceAddFailed": "添加设备失败",
"ToastDeviceNameAlreadyExists": "同名的电子阅读器设备已存在",
"ToastDeviceTestEmailFailed": "无法发送测试电子邮件",
"ToastDeviceTestEmailSuccess": "测试邮件已发送",
"ToastDeviceUpdateFailed": "无法更新设备",
"ToastEmailSettingsUpdateFailed": "无法更新电子邮件设置",
"ToastEmailSettingsUpdateSuccess": "电子邮件设置已更新",
"ToastEncodeCancelFailed": "取消编码失败",
"ToastEncodeCancelSucces": "编码已取消",
"ToastEpisodeDownloadQueueClearFailed": "无法清除队列",
"ToastEpisodeDownloadQueueClearSuccess": "剧集下载队列已清空",
"ToastErrorCannotShare": "无法在此设备上本地共享", "ToastErrorCannotShare": "无法在此设备上本地共享",
"ToastFailedToLoadData": "加载数据失败", "ToastFailedToLoadData": "加载数据失败",
"ToastFailedToShare": "分享失败",
"ToastFailedToUpdateAccount": "无法更新账户",
"ToastFailedToUpdateUser": "无法更新用户",
"ToastInvalidImageUrl": "图片网址无效",
"ToastInvalidUrl": "网址无效",
"ToastItemCoverUpdateFailed": "更新项目封面失败", "ToastItemCoverUpdateFailed": "更新项目封面失败",
"ToastItemCoverUpdateSuccess": "项目封面已更新", "ToastItemCoverUpdateSuccess": "项目封面已更新",
"ToastItemDeletedFailed": "删除项目失败",
"ToastItemDeletedSuccess": "已删除项目",
"ToastItemDetailsUpdateFailed": "更新项目详细信息失败", "ToastItemDetailsUpdateFailed": "更新项目详细信息失败",
"ToastItemDetailsUpdateSuccess": "项目详细信息已更新", "ToastItemDetailsUpdateSuccess": "项目详细信息已更新",
"ToastItemMarkedAsFinishedFailed": "无法标记为已听完", "ToastItemMarkedAsFinishedFailed": "无法标记为已听完",
"ToastItemMarkedAsFinishedSuccess": "标记为已听完的项目", "ToastItemMarkedAsFinishedSuccess": "标记为已听完的项目",
"ToastItemMarkedAsNotFinishedFailed": "无法标记为未听完", "ToastItemMarkedAsNotFinishedFailed": "无法标记为未听完",
"ToastItemMarkedAsNotFinishedSuccess": "标记为未听完的项目", "ToastItemMarkedAsNotFinishedSuccess": "标记为未听完的项目",
"ToastItemUpdateFailed": "更新项目失败",
"ToastItemUpdateSuccess": "项目已更新",
"ToastLibraryCreateFailed": "创建媒体库失败", "ToastLibraryCreateFailed": "创建媒体库失败",
"ToastLibraryCreateSuccess": "媒体库 \"{0}\" 创建成功", "ToastLibraryCreateSuccess": "媒体库 \"{0}\" 创建成功",
"ToastLibraryDeleteFailed": "删除媒体库失败", "ToastLibraryDeleteFailed": "删除媒体库失败",
@@ -832,6 +899,25 @@
"ToastLibraryScanStarted": "媒体库扫描已启动", "ToastLibraryScanStarted": "媒体库扫描已启动",
"ToastLibraryUpdateFailed": "更新图书库失败", "ToastLibraryUpdateFailed": "更新图书库失败",
"ToastLibraryUpdateSuccess": "媒体库 \"{0}\" 已更新", "ToastLibraryUpdateSuccess": "媒体库 \"{0}\" 已更新",
"ToastNameEmailRequired": "姓名和电子邮件为必填项",
"ToastNameRequired": "姓名为必填项",
"ToastNewUserCreatedFailed": "无法创建帐户: \"{0}\"",
"ToastNewUserCreatedSuccess": "已创建新帐户",
"ToastNewUserLibraryError": "必须至少选择一个图书馆",
"ToastNewUserPasswordError": "必须有密码, 只有root用户可以有空密码",
"ToastNewUserTagError": "必须至少选择一个标签",
"ToastNewUserUsernameError": "输入用户名",
"ToastNoUpdatesNecessary": "无需更新",
"ToastNotificationCreateFailed": "无法创建通知",
"ToastNotificationDeleteFailed": "删除通知失败",
"ToastNotificationFailedMaximum": "最大失败尝试次数必须 >= 0",
"ToastNotificationQueueMaximum": "最大通知队列必须 >= 0",
"ToastNotificationSettingsUpdateFailed": "无法更新通知设置",
"ToastNotificationSettingsUpdateSuccess": "通知设置已更新",
"ToastNotificationTestTriggerFailed": "无法触发测试通知",
"ToastNotificationTestTriggerSuccess": "触发测试通知",
"ToastNotificationUpdateFailed": "更新通知失败",
"ToastNotificationUpdateSuccess": "通知已更新",
"ToastPlaylistCreateFailed": "创建播放列表失败", "ToastPlaylistCreateFailed": "创建播放列表失败",
"ToastPlaylistCreateSuccess": "已成功创建播放列表", "ToastPlaylistCreateSuccess": "已成功创建播放列表",
"ToastPlaylistRemoveSuccess": "播放列表已删除", "ToastPlaylistRemoveSuccess": "播放列表已删除",
@@ -839,24 +925,52 @@
"ToastPlaylistUpdateSuccess": "播放列表已更新", "ToastPlaylistUpdateSuccess": "播放列表已更新",
"ToastPodcastCreateFailed": "创建播客失败", "ToastPodcastCreateFailed": "创建播客失败",
"ToastPodcastCreateSuccess": "已成功创建播客", "ToastPodcastCreateSuccess": "已成功创建播客",
"ToastPodcastGetFeedFailed": "无法获取播客信息",
"ToastPodcastNoEpisodesInFeed": "RSS 订阅中未找到任何剧集",
"ToastPodcastNoRssFeed": "播客没有 RSS 源",
"ToastProviderCreatedFailed": "无法添加提供商",
"ToastProviderCreatedSuccess": "已添加新提供商",
"ToastProviderNameAndUrlRequired": "名称和网址必需填写",
"ToastProviderRemoveSuccess": "提供商已移除",
"ToastRSSFeedCloseFailed": "关闭 RSS 源失败", "ToastRSSFeedCloseFailed": "关闭 RSS 源失败",
"ToastRSSFeedCloseSuccess": "RSS 源已关闭", "ToastRSSFeedCloseSuccess": "RSS 源已关闭",
"ToastRemoveFailed": "删除失败",
"ToastRemoveItemFromCollectionFailed": "从收藏中删除项目失败", "ToastRemoveItemFromCollectionFailed": "从收藏中删除项目失败",
"ToastRemoveItemFromCollectionSuccess": "项目已从收藏中删除", "ToastRemoveItemFromCollectionSuccess": "项目已从收藏中删除",
"ToastRemoveItemsWithIssuesFailed": "无法删除有问题的库项目",
"ToastRemoveItemsWithIssuesSuccess": "已删除有问题的库项目",
"ToastRenameFailed": "重命名失败",
"ToastRescanFailed": "{0} 重新扫描失败",
"ToastRescanRemoved": "重新扫描完成项目已删除",
"ToastRescanUpToDate": "重新扫描完成项目已更新",
"ToastRescanUpdated": "重新扫描完成项目已更新",
"ToastScanFailed": "扫描库项目失败",
"ToastSelectAtLeastOneUser": "至少选择一位用户",
"ToastSendEbookToDeviceFailed": "发送电子书到设备失败", "ToastSendEbookToDeviceFailed": "发送电子书到设备失败",
"ToastSendEbookToDeviceSuccess": "电子书已经发送到设备 \"{0}\"", "ToastSendEbookToDeviceSuccess": "电子书已经发送到设备 \"{0}\"",
"ToastSeriesUpdateFailed": "更新系列失败", "ToastSeriesUpdateFailed": "更新系列失败",
"ToastSeriesUpdateSuccess": "系列已更新", "ToastSeriesUpdateSuccess": "系列已更新",
"ToastServerSettingsUpdateFailed": "无法更新服务器设置", "ToastServerSettingsUpdateFailed": "无法更新服务器设置",
"ToastServerSettingsUpdateSuccess": "服务器设置已更新", "ToastServerSettingsUpdateSuccess": "服务器设置已更新",
"ToastSessionCloseFailed": "关闭会话失败",
"ToastSessionDeleteFailed": "删除会话失败", "ToastSessionDeleteFailed": "删除会话失败",
"ToastSessionDeleteSuccess": "会话已删除", "ToastSessionDeleteSuccess": "会话已删除",
"ToastSlugMustChange": "Slug 包含无效字符",
"ToastSlugRequired": "Slug 是必填项",
"ToastSocketConnected": "网络已连接", "ToastSocketConnected": "网络已连接",
"ToastSocketDisconnected": "网络已断开", "ToastSocketDisconnected": "网络已断开",
"ToastSocketFailedToConnect": "网络连接失败", "ToastSocketFailedToConnect": "网络连接失败",
"ToastSortingPrefixesEmptyError": "必须至少有 1 个排序前缀", "ToastSortingPrefixesEmptyError": "必须至少有 1 个排序前缀",
"ToastSortingPrefixesUpdateFailed": "无法更新排序前缀", "ToastSortingPrefixesUpdateFailed": "无法更新排序前缀",
"ToastSortingPrefixesUpdateSuccess": "排序前缀已更新 ({0} 项)", "ToastSortingPrefixesUpdateSuccess": "排序前缀已更新 ({0} 项)",
"ToastTitleRequired": "标题为必填项",
"ToastUnknownError": "未知错误",
"ToastUnlinkOpenIdFailed": "无法取消用户与 OpenID 的关联",
"ToastUnlinkOpenIdSuccess": "用户已取消与 OpenID 的关联",
"ToastUserDeleteFailed": "删除用户失败", "ToastUserDeleteFailed": "删除用户失败",
"ToastUserDeleteSuccess": "用户已删除" "ToastUserDeleteSuccess": "用户已删除",
"ToastUserPasswordChangeSuccess": "密码修改成功",
"ToastUserPasswordMismatch": "密码不匹配",
"ToastUserPasswordMustChange": "新密码不能与旧密码相同",
"ToastUserRootRequireName": "必须输入 root 用户名"
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.13.2", "version": "2.13.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.13.2", "version": "2.13.4",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"axios": "^0.27.2", "axios": "^0.27.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.13.2", "version": "2.13.4",
"buildNumber": 1, "buildNumber": 1,
"description": "Self-hosted audiobook and podcast server", "description": "Self-hosted audiobook and podcast server",
"main": "index.js", "main": "index.js",
+1 -1
View File
@@ -384,7 +384,7 @@ class LibraryItemController {
* @param {Response} res * @param {Response} res
*/ */
startPlaybackSession(req, res) { startPlaybackSession(req, res) {
if (!req.libraryItem.media.numTracks && req.libraryItem.mediaType !== 'video') { if (!req.libraryItem.media.numTracks) {
Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`) Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`)
return res.sendStatus(404) return res.sendStatus(404)
} }
-1
View File
@@ -3,7 +3,6 @@ const Logger = require('../Logger')
const BookFinder = require('../finders/BookFinder') const BookFinder = require('../finders/BookFinder')
const PodcastFinder = require('../finders/PodcastFinder') const PodcastFinder = require('../finders/PodcastFinder')
const AuthorFinder = require('../finders/AuthorFinder') const AuthorFinder = require('../finders/AuthorFinder')
const MusicFinder = require('../finders/MusicFinder')
const Database = require('../Database') const Database = require('../Database')
const { isValidASIN } = require('../utils') const { isValidASIN } = require('../utils')
+7 -2
View File
@@ -205,9 +205,12 @@ class UserController {
async update(req, res) { async update(req, res) {
const user = req.reqUser const user = req.reqUser
if (user.type === 'root' && !req.user.isRoot) { if (user.isRoot && !req.user.isRoot) {
Logger.error(`[UserController] Admin user "${req.user.username}" attempted to update root user`) Logger.error(`[UserController] Admin user "${req.user.username}" attempted to update root user`)
return res.sendStatus(403) return res.sendStatus(403)
} else if (user.isRoot) {
// Root user cannot update type
delete req.body.type
} }
const updatePayload = req.body const updatePayload = req.body
@@ -270,8 +273,10 @@ class UserController {
const permissions = { const permissions = {
...user.permissions ...user.permissions
} }
const defaultPermissions = Database.userModel.getDefaultPermissionsForUserType(updatePayload.type || user.type || 'user')
for (const key in updatePayload.permissions) { for (const key in updatePayload.permissions) {
if (permissions[key] !== undefined) { // Check that the key is a valid permission key or is included in the default permissions
if (permissions[key] !== undefined || defaultPermissions[key] !== undefined) {
if (typeof updatePayload.permissions[key] !== 'boolean') { if (typeof updatePayload.permissions[key] !== 'boolean') {
Logger.warn(`[UserController] update: Invalid permission value for key ${key}. Should be boolean`) Logger.warn(`[UserController] update: Invalid permission value for key ${key}. Should be boolean`)
} else if (permissions[key] !== updatePayload.permissions[key]) { } else if (permissions[key] !== updatePayload.permissions[key]) {
+8 -4
View File
@@ -202,10 +202,14 @@ class BookFinder {
* @returns {Promise<Object[]>} * @returns {Promise<Object[]>}
*/ */
async getCustomProviderResults(title, author, isbn, providerSlug) { async getCustomProviderResults(title, author, isbn, providerSlug) {
const books = await this.customProviderAdapter.search(title, author, isbn, providerSlug, 'book', this.#providerResponseTimeout) try {
if (this.verbose) Logger.debug(`Custom provider '${providerSlug}' Search Results: ${books.length || 0}`) const books = await this.customProviderAdapter.search(title, author, isbn, providerSlug, 'book', this.#providerResponseTimeout)
if (this.verbose) Logger.debug(`Custom provider '${providerSlug}' Search Results: ${books.length || 0}`)
return books return books
} catch (error) {
Logger.error(`Error searching Custom provider '${providerSlug}':`, error)
return []
}
} }
static TitleCandidates = class { static TitleCandidates = class {
-12
View File
@@ -1,12 +0,0 @@
const MusicBrainz = require('../providers/MusicBrainz')
class MusicFinder {
constructor() {
this.musicBrainz = new MusicBrainz()
}
searchTrack(options) {
return this.musicBrainz.searchTrack(options)
}
}
module.exports = new MusicFinder()
+17 -27
View File
@@ -293,37 +293,27 @@ class PlaybackSessionManager {
const newPlaybackSession = new PlaybackSession() const newPlaybackSession = new PlaybackSession()
newPlaybackSession.setData(libraryItem, user.id, mediaPlayer, deviceInfo, userStartTime, episodeId) newPlaybackSession.setData(libraryItem, user.id, mediaPlayer, deviceInfo, userStartTime, episodeId)
if (libraryItem.mediaType === 'video') { let audioTracks = []
if (shouldDirectPlay) { if (shouldDirectPlay) {
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}" with id ${newPlaybackSession.id}`) Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}" with id ${newPlaybackSession.id} (Device: ${newPlaybackSession.deviceDescription})`)
newPlaybackSession.videoTrack = libraryItem.media.getVideoTrack() audioTracks = libraryItem.getDirectPlayTracklist(episodeId)
newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
} else {
// HLS not supported for video yet
}
} else { } else {
let audioTracks = [] Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}" (Device: ${newPlaybackSession.deviceDescription})`)
if (shouldDirectPlay) { const stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, episodeId, userStartTime)
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}" with id ${newPlaybackSession.id} (Device: ${newPlaybackSession.deviceDescription})`) await stream.generatePlaylist()
audioTracks = libraryItem.getDirectPlayTracklist(episodeId) stream.start() // Start transcode
newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
} else {
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}" (Device: ${newPlaybackSession.deviceDescription})`)
const stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, episodeId, userStartTime)
await stream.generatePlaylist()
stream.start() // Start transcode
audioTracks = [stream.getAudioTrack()] audioTracks = [stream.getAudioTrack()]
newPlaybackSession.stream = stream newPlaybackSession.stream = stream
newPlaybackSession.playMethod = PlayMethod.TRANSCODE newPlaybackSession.playMethod = PlayMethod.TRANSCODE
stream.on('closed', () => { stream.on('closed', () => {
Logger.debug(`[PlaybackSessionManager] Stream closed for session "${newPlaybackSession.id}" (Device: ${newPlaybackSession.deviceDescription})`) Logger.debug(`[PlaybackSessionManager] Stream closed for session "${newPlaybackSession.id}" (Device: ${newPlaybackSession.deviceDescription})`)
newPlaybackSession.stream = null newPlaybackSession.stream = null
}) })
}
newPlaybackSession.audioTracks = audioTracks
} }
newPlaybackSession.audioTracks = audioTracks
this.sessions.push(newPlaybackSession) this.sessions.push(newPlaybackSession)
SocketAuthority.adminEmitter('user_stream_update', user.toJSONForPublic(this.sessions)) SocketAuthority.adminEmitter('user_stream_update', user.toJSONForPublic(this.sessions))
+104 -103
View File
@@ -38,7 +38,7 @@ class Collection extends Model {
// Optionally include rssfeed for collection // Optionally include rssfeed for collection
const collectionIncludes = [] const collectionIncludes = []
if (include.includes('rssfeed')) { if (include?.includes('rssfeed')) {
collectionIncludes.push({ collectionIncludes.push({
model: this.sequelize.models.feed model: this.sequelize.models.feed
}) })
@@ -115,78 +115,6 @@ class Collection extends Model {
.filter((c) => c) .filter((c) => c)
} }
/**
* Get old collection toJSONExpanded, items filtered for user permissions
*
* @param {import('./User')|null} user
* @param {string[]} [include]
* @returns {Promise<oldCollection>} oldCollection.toJSONExpanded
*/
async getOldJsonExpanded(user, include) {
this.books =
(await this.getBooks({
include: [
{
model: this.sequelize.models.libraryItem
},
{
model: this.sequelize.models.author,
through: {
attributes: []
}
},
{
model: this.sequelize.models.series,
through: {
attributes: ['sequence']
}
}
],
order: [Sequelize.literal('`collectionBook.order` ASC')]
})) || []
const oldCollection = this.sequelize.models.collection.getOldCollection(this)
// Filter books using user permissions
// TODO: Handle user permission restrictions on initial query
const books =
this.books?.filter((b) => {
if (user) {
if (b.tags?.length && !user.checkCanAccessLibraryItemWithTags(b.tags)) {
return false
}
if (b.explicit === true && !user.canAccessExplicitContent) {
return false
}
}
return true
}) || []
// Map to library items
const libraryItems = books.map((b) => {
const libraryItem = b.libraryItem
delete b.libraryItem
libraryItem.media = b
return this.sequelize.models.libraryItem.getOldLibraryItem(libraryItem)
})
// Users with restricted permissions will not see this collection
if (!books.length && oldCollection.books.length) {
return null
}
const collectionExpanded = oldCollection.toJSONExpanded(libraryItems)
if (include?.includes('rssfeed')) {
const feeds = await this.getFeeds()
if (feeds?.length) {
collectionExpanded.rssFeed = this.sequelize.models.feed.getOldFeed(feeds[0])
}
}
return collectionExpanded
}
/** /**
* Get old collection from Collection * Get old collection from Collection
* @param {Collection} collectionExpanded * @param {Collection} collectionExpanded
@@ -250,36 +178,6 @@ class Collection extends Model {
return this.getOldCollection(collection) return this.getOldCollection(collection)
} }
/**
* Get old collection from current
* @returns {Promise<oldCollection>}
*/
async getOld() {
this.books =
(await this.getBooks({
include: [
{
model: this.sequelize.models.libraryItem
},
{
model: this.sequelize.models.author,
through: {
attributes: []
}
},
{
model: this.sequelize.models.series,
through: {
attributes: ['sequence']
}
}
],
order: [Sequelize.literal('`collectionBook.order` ASC')]
})) || []
return this.sequelize.models.collection.getOldCollection(this)
}
/** /**
* Remove all collections belonging to library * Remove all collections belonging to library
* @param {string} libraryId * @param {string} libraryId
@@ -320,6 +218,109 @@ class Collection extends Model {
library.hasMany(Collection) library.hasMany(Collection)
Collection.belongsTo(library) Collection.belongsTo(library)
} }
/**
* Get old collection toJSONExpanded, items filtered for user permissions
*
* @param {import('./User')|null} user
* @param {string[]} [include]
* @returns {Promise<oldCollection>} oldCollection.toJSONExpanded
*/
async getOldJsonExpanded(user, include) {
this.books =
(await this.getBooks({
include: [
{
model: this.sequelize.models.libraryItem
},
{
model: this.sequelize.models.author,
through: {
attributes: []
}
},
{
model: this.sequelize.models.series,
through: {
attributes: ['sequence']
}
}
],
order: [Sequelize.literal('`collectionBook.order` ASC')]
})) || []
// Filter books using user permissions
// TODO: Handle user permission restrictions on initial query
const books =
this.books?.filter((b) => {
if (user) {
if (b.tags?.length && !user.checkCanAccessLibraryItemWithTags(b.tags)) {
return false
}
if (b.explicit === true && !user.canAccessExplicitContent) {
return false
}
}
return true
}) || []
// Map to library items
const libraryItems = books.map((b) => {
const libraryItem = b.libraryItem
delete b.libraryItem
libraryItem.media = b
return this.sequelize.models.libraryItem.getOldLibraryItem(libraryItem)
})
// Users with restricted permissions will not see this collection
if (!books.length && this.books.length) {
return null
}
const collectionExpanded = this.toOldJSONExpanded(libraryItems)
if (include?.includes('rssfeed')) {
const feeds = await this.getFeeds()
if (feeds?.length) {
collectionExpanded.rssFeed = this.sequelize.models.feed.getOldFeed(feeds[0])
}
}
return collectionExpanded
}
/**
*
* @param {string[]} libraryItemIds
* @returns
*/
toOldJSON(libraryItemIds) {
return {
id: this.id,
libraryId: this.libraryId,
name: this.name,
description: this.description,
books: [...libraryItemIds],
lastUpdate: this.updatedAt.valueOf(),
createdAt: this.createdAt.valueOf()
}
}
/**
*
* @param {import('../objects/LibraryItem')} oldLibraryItems
* @returns
*/
toOldJSONExpanded(oldLibraryItems) {
const json = this.toOldJSON(oldLibraryItems.map((li) => li.id))
json.books = json.books
.map((libraryItemId) => {
const book = oldLibraryItems.find((li) => li.id === libraryItemId)
return book ? book.toJSONExpanded() : null
})
.filter((b) => !!b)
return json
}
} }
module.exports = Collection module.exports = Collection
+17 -1
View File
@@ -365,7 +365,23 @@ class LibraryItem extends Model {
if (existingValue instanceof Date) existingValue = existingValue.valueOf() if (existingValue instanceof Date) existingValue = existingValue.valueOf()
if (!areEquivalent(updatedMedia[key], existingValue, true)) { if (!areEquivalent(updatedMedia[key], existingValue, true)) {
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" ${libraryItemExpanded.mediaType}.${key} updated from ${existingValue} to ${updatedMedia[key]}`) if (key === 'chapters') {
// Handle logging of chapters separately because the object is large
const chaptersRemoved = libraryItemExpanded.media.chapters.filter((ch) => !updatedMedia.chapters.some((uch) => uch.id === ch.id))
if (chaptersRemoved.length) {
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" chapters removed: ${chaptersRemoved.map((ch) => ch.title).join(', ')}`)
}
const chaptersAdded = updatedMedia.chapters.filter((uch) => !libraryItemExpanded.media.chapters.some((ch) => ch.id === uch.id))
if (chaptersAdded.length) {
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" chapters added: ${chaptersAdded.map((ch) => ch.title).join(', ')}`)
}
if (!chaptersRemoved.length && !chaptersAdded.length) {
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" chapters updated`)
}
} else {
Logger.debug(util.format(`[LibraryItem] "${libraryItemExpanded.media.title}" ${libraryItemExpanded.mediaType}.${key} updated from %j to %j`, existingValue, updatedMedia[key]))
}
hasMediaUpdates = true hasMediaUpdates = true
} }
} }
+1
View File
@@ -108,6 +108,7 @@ class User extends Model {
accessAllLibraries: true, accessAllLibraries: true,
accessAllTags: true, accessAllTags: true,
accessExplicitContent: true, accessExplicitContent: true,
selectedTagsNotAccessible: false,
librariesAccessible: [], librariesAccessible: [],
itemTagsSelected: [] itemTagsSelected: []
} }
+54 -55
View File
@@ -1,12 +1,10 @@
const uuidv4 = require("uuid").v4 const uuidv4 = require('uuid').v4
const fs = require('../libs/fsExtra') const fs = require('../libs/fsExtra')
const Path = require('path') const Path = require('path')
const Logger = require('../Logger') const Logger = require('../Logger')
const LibraryFile = require('./files/LibraryFile') const LibraryFile = require('./files/LibraryFile')
const Book = require('./mediaTypes/Book') const Book = require('./mediaTypes/Book')
const Podcast = require('./mediaTypes/Podcast') const Podcast = require('./mediaTypes/Podcast')
const Video = require('./mediaTypes/Video')
const Music = require('./mediaTypes/Music')
const { areEquivalent, copyValue } = require('../utils/index') const { areEquivalent, copyValue } = require('../utils/index')
const { filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils') const { filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils')
@@ -74,14 +72,10 @@ class LibraryItem {
this.media = new Book(libraryItem.media) this.media = new Book(libraryItem.media)
} else if (this.mediaType === 'podcast') { } else if (this.mediaType === 'podcast') {
this.media = new Podcast(libraryItem.media) this.media = new Podcast(libraryItem.media)
} else if (this.mediaType === 'video') {
this.media = new Video(libraryItem.media)
} else if (this.mediaType === 'music') {
this.media = new Music(libraryItem.media)
} }
this.media.libraryItemId = this.id this.media.libraryItemId = this.id
this.libraryFiles = libraryItem.libraryFiles.map(f => new LibraryFile(f)) this.libraryFiles = libraryItem.libraryFiles.map((f) => new LibraryFile(f))
// Migration for v2.2.23 to set ebook library files as supplementary // Migration for v2.2.23 to set ebook library files as supplementary
if (this.isBook && this.media.ebookFile) { if (this.isBook && this.media.ebookFile) {
@@ -91,7 +85,6 @@ class LibraryItem {
} }
} }
} }
} }
toJSON() { toJSON() {
@@ -115,7 +108,7 @@ class LibraryItem {
isInvalid: !!this.isInvalid, isInvalid: !!this.isInvalid,
mediaType: this.mediaType, mediaType: this.mediaType,
media: this.media.toJSON(), media: this.media.toJSON(),
libraryFiles: this.libraryFiles.map(f => f.toJSON()) libraryFiles: this.libraryFiles.map((f) => f.toJSON())
} }
} }
@@ -165,21 +158,24 @@ class LibraryItem {
isInvalid: !!this.isInvalid, isInvalid: !!this.isInvalid,
mediaType: this.mediaType, mediaType: this.mediaType,
media: this.media.toJSONExpanded(), media: this.media.toJSONExpanded(),
libraryFiles: this.libraryFiles.map(f => f.toJSON()), libraryFiles: this.libraryFiles.map((f) => f.toJSON()),
size: this.size size: this.size
} }
} }
get isPodcast() { return this.mediaType === 'podcast' } get isPodcast() {
get isBook() { return this.mediaType === 'book' } return this.mediaType === 'podcast'
get isMusic() { return this.mediaType === 'music' } }
get isBook() {
return this.mediaType === 'book'
}
get size() { get size() {
let total = 0 let total = 0
this.libraryFiles.forEach((lf) => total += lf.metadata.size) this.libraryFiles.forEach((lf) => (total += lf.metadata.size))
return total return total
} }
get hasAudioFiles() { get hasAudioFiles() {
return this.libraryFiles.some(lf => lf.fileType === 'audio') return this.libraryFiles.some((lf) => lf.fileType === 'audio')
} }
get hasMediaEntities() { get hasMediaEntities() {
return this.media.hasMediaEntities return this.media.hasMediaEntities
@@ -201,17 +197,16 @@ class LibraryItem {
for (const key in payload) { for (const key in payload) {
if (key === 'libraryFiles') { if (key === 'libraryFiles') {
this.libraryFiles = payload.libraryFiles.map(lf => lf.clone()) this.libraryFiles = payload.libraryFiles.map((lf) => lf.clone())
// Set cover image // Set cover image
const imageFiles = this.libraryFiles.filter(lf => lf.fileType === 'image') const imageFiles = this.libraryFiles.filter((lf) => lf.fileType === 'image')
const coverMatch = imageFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path)) const coverMatch = imageFiles.find((iFile) => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
if (coverMatch) { if (coverMatch) {
this.media.coverPath = coverMatch.metadata.path this.media.coverPath = coverMatch.metadata.path
} else if (imageFiles.length) { } else if (imageFiles.length) {
this.media.coverPath = imageFiles[0].metadata.path this.media.coverPath = imageFiles[0].metadata.path
} }
} else if (this[key] !== undefined && key !== 'media') { } else if (this[key] !== undefined && key !== 'media') {
this[key] = payload[key] this[key] = payload[key]
} }
@@ -283,46 +278,50 @@ class LibraryItem {
const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`) const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`)
return fs.writeFile(metadataFilePath, JSON.stringify(this.media.toJSONForMetadataFile(), null, 2)).then(async () => { return fs
// Add metadata.json to libraryFiles array if it is new .writeFile(metadataFilePath, JSON.stringify(this.media.toJSONForMetadataFile(), null, 2))
let metadataLibraryFile = this.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath)) .then(async () => {
if (storeMetadataWithItem) { // Add metadata.json to libraryFiles array if it is new
if (!metadataLibraryFile) { let metadataLibraryFile = this.libraryFiles.find((lf) => lf.metadata.path === filePathToPOSIX(metadataFilePath))
metadataLibraryFile = new LibraryFile() if (storeMetadataWithItem) {
await metadataLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`) if (!metadataLibraryFile) {
this.libraryFiles.push(metadataLibraryFile) metadataLibraryFile = new LibraryFile()
} else { await metadataLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath) this.libraryFiles.push(metadataLibraryFile)
if (fileTimestamps) { } else {
metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs if (fileTimestamps) {
metadataLibraryFile.metadata.size = fileTimestamps.size metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
metadataLibraryFile.ino = fileTimestamps.ino metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs
metadataLibraryFile.metadata.size = fileTimestamps.size
metadataLibraryFile.ino = fileTimestamps.ino
}
}
const libraryItemDirTimestamps = await getFileTimestampsWithIno(this.path)
if (libraryItemDirTimestamps) {
this.mtimeMs = libraryItemDirTimestamps.mtimeMs
this.ctimeMs = libraryItemDirTimestamps.ctimeMs
} }
} }
const libraryItemDirTimestamps = await getFileTimestampsWithIno(this.path)
if (libraryItemDirTimestamps) {
this.mtimeMs = libraryItemDirTimestamps.mtimeMs
this.ctimeMs = libraryItemDirTimestamps.ctimeMs
}
}
Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataFilePath}"`) Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataFilePath}"`)
return metadataLibraryFile return metadataLibraryFile
}).catch((error) => { })
Logger.error(`[LibraryItem] Failed to save json file at "${metadataFilePath}"`, error) .catch((error) => {
return null Logger.error(`[LibraryItem] Failed to save json file at "${metadataFilePath}"`, error)
}).finally(() => { return null
this.isSavingMetadata = false })
}) .finally(() => {
this.isSavingMetadata = false
})
} }
removeLibraryFile(ino) { removeLibraryFile(ino) {
if (!ino) return false if (!ino) return false
const libraryFile = this.libraryFiles.find(lf => lf.ino === ino) const libraryFile = this.libraryFiles.find((lf) => lf.ino === ino)
if (libraryFile) { if (libraryFile) {
this.libraryFiles = this.libraryFiles.filter(lf => lf.ino !== ino) this.libraryFiles = this.libraryFiles.filter((lf) => lf.ino !== ino)
this.updatedAt = Date.now() this.updatedAt = Date.now()
return true return true
} }
@@ -333,15 +332,15 @@ class LibraryItem {
* Set the EBookFile from a LibraryFile * Set the EBookFile from a LibraryFile
* If null then ebookFile will be removed from the book * If null then ebookFile will be removed from the book
* all ebook library files that are not primary are marked as supplementary * all ebook library files that are not primary are marked as supplementary
* *
* @param {LibraryFile} [libraryFile] * @param {LibraryFile} [libraryFile]
*/ */
setPrimaryEbook(ebookLibraryFile = null) { setPrimaryEbook(ebookLibraryFile = null) {
const ebookLibraryFiles = this.libraryFiles.filter(lf => lf.isEBookFile) const ebookLibraryFiles = this.libraryFiles.filter((lf) => lf.isEBookFile)
for (const libraryFile of ebookLibraryFiles) { for (const libraryFile of ebookLibraryFiles) {
libraryFile.isSupplementary = ebookLibraryFile?.ino !== libraryFile.ino libraryFile.isSupplementary = ebookLibraryFile?.ino !== libraryFile.ino
} }
this.media.setEbookFile(ebookLibraryFile) this.media.setEbookFile(ebookLibraryFile)
} }
} }
module.exports = LibraryItem module.exports = LibraryItem
-5
View File
@@ -4,7 +4,6 @@ const serverVersion = require('../../package.json').version
const BookMetadata = require('./metadata/BookMetadata') const BookMetadata = require('./metadata/BookMetadata')
const PodcastMetadata = require('./metadata/PodcastMetadata') const PodcastMetadata = require('./metadata/PodcastMetadata')
const DeviceInfo = require('./DeviceInfo') const DeviceInfo = require('./DeviceInfo')
const VideoMetadata = require('./metadata/VideoMetadata')
class PlaybackSession { class PlaybackSession {
constructor(session) { constructor(session) {
@@ -41,7 +40,6 @@ class PlaybackSession {
// Not saved in DB // Not saved in DB
this.lastSave = 0 this.lastSave = 0
this.audioTracks = [] this.audioTracks = []
this.videoTrack = null
this.stream = null this.stream = null
// Used for share sessions // Used for share sessions
this.shareSessionId = null this.shareSessionId = null
@@ -114,7 +112,6 @@ class PlaybackSession {
startedAt: this.startedAt, startedAt: this.startedAt,
updatedAt: this.updatedAt, updatedAt: this.updatedAt,
audioTracks: this.audioTracks.map((at) => at.toJSON?.() || { ...at }), audioTracks: this.audioTracks.map((at) => at.toJSON?.() || { ...at }),
videoTrack: this.videoTrack?.toJSON() || null,
libraryItem: libraryItem?.toJSONExpanded() || null libraryItem: libraryItem?.toJSONExpanded() || null
} }
} }
@@ -157,8 +154,6 @@ class PlaybackSession {
this.mediaMetadata = new BookMetadata(session.mediaMetadata) this.mediaMetadata = new BookMetadata(session.mediaMetadata)
} else if (this.mediaType === 'podcast') { } else if (this.mediaType === 'podcast') {
this.mediaMetadata = new PodcastMetadata(session.mediaMetadata) this.mediaMetadata = new PodcastMetadata(session.mediaMetadata)
} else if (this.mediaType === 'video') {
this.mediaMetadata = new VideoMetadata(session.mediaMetadata)
} }
} }
this.displayTitle = session.displayTitle || '' this.displayTitle = session.displayTitle || ''
+2 -3
View File
@@ -43,14 +43,13 @@ class LibraryFile {
if (globals.SupportedImageTypes.includes(this.metadata.format)) return 'image' if (globals.SupportedImageTypes.includes(this.metadata.format)) return 'image'
if (globals.SupportedAudioTypes.includes(this.metadata.format)) return 'audio' if (globals.SupportedAudioTypes.includes(this.metadata.format)) return 'audio'
if (globals.SupportedEbookTypes.includes(this.metadata.format)) return 'ebook' if (globals.SupportedEbookTypes.includes(this.metadata.format)) return 'ebook'
if (globals.SupportedVideoTypes.includes(this.metadata.format)) return 'video'
if (globals.TextFileTypes.includes(this.metadata.format)) return 'text' if (globals.TextFileTypes.includes(this.metadata.format)) return 'text'
if (globals.MetadataFileTypes.includes(this.metadata.format)) return 'metadata' if (globals.MetadataFileTypes.includes(this.metadata.format)) return 'metadata'
return 'unknown' return 'unknown'
} }
get isMediaFile() { get isMediaFile() {
return this.fileType === 'audio' || this.fileType === 'ebook' || this.fileType === 'video' return this.fileType === 'audio' || this.fileType === 'ebook'
} }
get isEBookFile() { get isEBookFile() {
@@ -75,4 +74,4 @@ class LibraryFile {
this.updatedAt = Date.now() this.updatedAt = Date.now()
} }
} }
module.exports = LibraryFile module.exports = LibraryFile
-109
View File
@@ -1,109 +0,0 @@
const { VideoMimeType } = require('../../utils/constants')
const FileMetadata = require('../metadata/FileMetadata')
class VideoFile {
constructor(data) {
this.index = null
this.ino = null
this.metadata = null
this.addedAt = null
this.updatedAt = null
this.format = null
this.duration = null
this.bitRate = null
this.language = null
this.codec = null
this.timeBase = null
this.frameRate = null
this.width = null
this.height = null
this.embeddedCoverArt = null
this.invalid = false
this.error = null
if (data) {
this.construct(data)
}
}
toJSON() {
return {
index: this.index,
ino: this.ino,
metadata: this.metadata.toJSON(),
addedAt: this.addedAt,
updatedAt: this.updatedAt,
invalid: !!this.invalid,
error: this.error || null,
format: this.format,
duration: this.duration,
bitRate: this.bitRate,
language: this.language,
codec: this.codec,
timeBase: this.timeBase,
frameRate: this.frameRate,
width: this.width,
height: this.height,
embeddedCoverArt: this.embeddedCoverArt,
mimeType: this.mimeType
}
}
construct(data) {
this.index = data.index
this.ino = data.ino
this.metadata = new FileMetadata(data.metadata || {})
this.addedAt = data.addedAt
this.updatedAt = data.updatedAt
this.invalid = !!data.invalid
this.error = data.error || null
this.format = data.format
this.duration = data.duration
this.bitRate = data.bitRate
this.language = data.language
this.codec = data.codec || null
this.timeBase = data.timeBase
this.frameRate = data.frameRate
this.width = data.width
this.height = data.height
this.embeddedCoverArt = data.embeddedCoverArt || null
}
get mimeType() {
var format = this.metadata.format.toUpperCase()
if (VideoMimeType[format]) {
return VideoMimeType[format]
} else {
return VideoMimeType.MP4
}
}
clone() {
return new VideoFile(this.toJSON())
}
setDataFromProbe(libraryFile, probeData) {
this.ino = libraryFile.ino || null
this.metadata = libraryFile.metadata.clone()
this.addedAt = Date.now()
this.updatedAt = Date.now()
const videoStream = probeData.videoStream
this.format = probeData.format
this.duration = probeData.duration
this.bitRate = videoStream.bit_rate || probeData.bitRate || null
this.language = probeData.language
this.codec = videoStream.codec || null
this.timeBase = videoStream.time_base
this.frameRate = videoStream.frame_rate || null
this.width = videoStream.width || null
this.height = videoStream.height || null
this.embeddedCoverArt = probeData.embeddedCoverArt
}
}
module.exports = VideoFile
-45
View File
@@ -1,45 +0,0 @@
const Path = require('path')
const { encodeUriPath } = require('../../utils/fileUtils')
class VideoTrack {
constructor() {
this.index = null
this.duration = null
this.title = null
this.contentUrl = null
this.mimeType = null
this.codec = null
this.metadata = null
}
toJSON() {
return {
index: this.index,
duration: this.duration,
title: this.title,
contentUrl: this.contentUrl,
mimeType: this.mimeType,
codec: this.codec,
metadata: this.metadata ? this.metadata.toJSON() : null
}
}
setData(itemId, videoFile) {
this.index = videoFile.index
this.duration = videoFile.duration
this.title = videoFile.metadata.filename || ''
this.contentUrl = Path.join(`${global.RouterBasePath}/api/items/${itemId}/file/${videoFile.ino}`, encodeUriPath(videoFile.metadata.relPath))
this.mimeType = videoFile.mimeType
this.codec = videoFile.codec
this.metadata = videoFile.metadata.clone()
}
setFromStream(title, duration, contentUrl) {
this.index = 1
this.duration = duration
this.title = title
this.contentUrl = contentUrl
this.mimeType = 'application/vnd.apple.mpegurl'
}
}
module.exports = VideoTrack
-145
View File
@@ -1,145 +0,0 @@
const Logger = require('../../Logger')
const AudioFile = require('../files/AudioFile')
const AudioTrack = require('../files/AudioTrack')
const MusicMetadata = require('../metadata/MusicMetadata')
const { areEquivalent, copyValue } = require('../../utils/index')
const { filePathToPOSIX } = require('../../utils/fileUtils')
class Music {
constructor(music) {
this.libraryItemId = null
this.metadata = null
this.coverPath = null
this.tags = []
this.audioFile = null
if (music) {
this.construct(music)
}
}
construct(music) {
this.libraryItemId = music.libraryItemId
this.metadata = new MusicMetadata(music.metadata)
this.coverPath = music.coverPath
this.tags = [...music.tags]
this.audioFile = new AudioFile(music.audioFile)
}
toJSON() {
return {
libraryItemId: this.libraryItemId,
metadata: this.metadata.toJSON(),
coverPath: this.coverPath,
tags: [...this.tags],
audioFile: this.audioFile.toJSON(),
}
}
toJSONMinified() {
return {
metadata: this.metadata.toJSONMinified(),
coverPath: this.coverPath,
tags: [...this.tags],
audioFile: this.audioFile.toJSON(),
duration: this.duration,
size: this.size
}
}
toJSONExpanded() {
return {
libraryItemId: this.libraryItemId,
metadata: this.metadata.toJSONExpanded(),
coverPath: this.coverPath,
tags: [...this.tags],
audioFile: this.audioFile.toJSON(),
duration: this.duration,
size: this.size
}
}
get size() {
return this.audioFile.metadata.size
}
get hasMediaEntities() {
return !!this.audioFile
}
get duration() {
return this.audioFile.duration || 0
}
get audioTrack() {
const audioTrack = new AudioTrack()
audioTrack.setData(this.libraryItemId, this.audioFile, 0)
return audioTrack
}
get numTracks() {
return 1
}
update(payload) {
const json = this.toJSON()
delete json.episodes // do not update media entities here
let hasUpdates = false
for (const key in json) {
if (payload[key] !== undefined) {
if (key === 'metadata') {
if (this.metadata.update(payload.metadata)) {
hasUpdates = true
}
} else if (!areEquivalent(payload[key], json[key])) {
this[key] = copyValue(payload[key])
Logger.debug('[Podcast] Key updated', key, this[key])
hasUpdates = true
}
}
}
return hasUpdates
}
updateCover(coverPath) {
coverPath = filePathToPOSIX(coverPath)
if (this.coverPath === coverPath) return false
this.coverPath = coverPath
return true
}
removeFileWithInode(inode) {
return false
}
findFileWithInode(inode) {
return (this.audioFile && this.audioFile.ino === inode) ? this.audioFile : null
}
setData(mediaData) {
this.metadata = new MusicMetadata()
if (mediaData.metadata) {
this.metadata.setData(mediaData.metadata)
}
this.coverPath = mediaData.coverPath || null
}
setAudioFile(audioFile) {
this.audioFile = audioFile
}
// Only checks container format
checkCanDirectPlay(payload) {
return true
}
getDirectPlayTracklist() {
return [this.audioTrack]
}
getPlaybackTitle() {
return this.metadata.title
}
getPlaybackAuthor() {
return this.metadata.artist
}
}
module.exports = Music
-137
View File
@@ -1,137 +0,0 @@
const Logger = require('../../Logger')
const VideoFile = require('../files/VideoFile')
const VideoTrack = require('../files/VideoTrack')
const VideoMetadata = require('../metadata/VideoMetadata')
const { areEquivalent, copyValue } = require('../../utils/index')
const { filePathToPOSIX } = require('../../utils/fileUtils')
class Video {
constructor(video) {
this.libraryItemId = null
this.metadata = null
this.coverPath = null
this.tags = []
this.episodes = []
this.autoDownloadEpisodes = false
this.lastEpisodeCheck = 0
this.lastCoverSearch = null
this.lastCoverSearchQuery = null
if (video) {
this.construct(video)
}
}
construct(video) {
this.libraryItemId = video.libraryItemId
this.metadata = new VideoMetadata(video.metadata)
this.coverPath = video.coverPath
this.tags = [...video.tags]
this.videoFile = new VideoFile(video.videoFile)
}
toJSON() {
return {
libraryItemId: this.libraryItemId,
metadata: this.metadata.toJSONExpanded(),
coverPath: this.coverPath,
tags: [...this.tags],
videoFile: this.videoFile.toJSON()
}
}
toJSONMinified() {
return {
metadata: this.metadata.toJSONMinified(),
coverPath: this.coverPath,
tags: [...this.tags],
videoFile: this.videoFile.toJSON(),
size: this.size
}
}
toJSONExpanded() {
return {
libraryItemId: this.libraryItemId,
metadata: this.metadata.toJSONExpanded(),
coverPath: this.coverPath,
tags: [...this.tags],
videoFile: this.videoFile.toJSON(),
size: this.size
}
}
get size() {
return this.videoFile.metadata.size
}
get hasMediaEntities() {
return true
}
get duration() {
return 0
}
update(payload) {
var json = this.toJSON()
var hasUpdates = false
for (const key in json) {
if (payload[key] !== undefined) {
if (key === 'metadata') {
if (this.metadata.update(payload.metadata)) {
hasUpdates = true
}
} else if (!areEquivalent(payload[key], json[key])) {
this[key] = copyValue(payload[key])
Logger.debug('[Video] Key updated', key, this[key])
hasUpdates = true
}
}
}
return hasUpdates
}
updateCover(coverPath) {
coverPath = filePathToPOSIX(coverPath)
if (this.coverPath === coverPath) return false
this.coverPath = coverPath
return true
}
removeFileWithInode(inode) {
}
findFileWithInode(inode) {
return null
}
setVideoFile(videoFile) {
this.videoFile = videoFile
}
setData(mediaMetadata) {
this.metadata = new VideoMetadata()
if (mediaMetadata.metadata) {
this.metadata.setData(mediaMetadata.metadata)
}
this.coverPath = mediaMetadata.coverPath || null
}
getPlaybackTitle() {
return this.metadata.title
}
getPlaybackAuthor() {
return ''
}
getVideoTrack() {
var track = new VideoTrack()
track.setData(this.libraryItemId, this.videoFile)
return track
}
}
module.exports = Video
-307
View File
@@ -1,307 +0,0 @@
const Logger = require('../../Logger')
const { areEquivalent, copyValue, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index')
class MusicMetadata {
constructor(metadata) {
this.title = null
this.artists = [] // Array of strings
this.album = null
this.albumArtist = null
this.genres = [] // Array of strings
this.composer = null
this.originalYear = null
this.releaseDate = null
this.releaseCountry = null
this.releaseType = null
this.releaseStatus = null
this.recordLabel = null
this.language = null
this.explicit = false
this.discNumber = null
this.discTotal = null
this.trackNumber = null
this.trackTotal = null
this.isrc = null
this.musicBrainzTrackId = null
this.musicBrainzAlbumId = null
this.musicBrainzAlbumArtistId = null
this.musicBrainzArtistId = null
if (metadata) {
this.construct(metadata)
}
}
construct(metadata) {
this.title = metadata.title
this.artists = metadata.artists ? [...metadata.artists] : []
this.album = metadata.album
this.albumArtist = metadata.albumArtist
this.genres = metadata.genres ? [...metadata.genres] : []
this.composer = metadata.composer || null
this.originalYear = metadata.originalYear || null
this.releaseDate = metadata.releaseDate || null
this.releaseCountry = metadata.releaseCountry || null
this.releaseType = metadata.releaseType || null
this.releaseStatus = metadata.releaseStatus || null
this.recordLabel = metadata.recordLabel || null
this.language = metadata.language || null
this.explicit = !!metadata.explicit
this.discNumber = metadata.discNumber || null
this.discTotal = metadata.discTotal || null
this.trackNumber = metadata.trackNumber || null
this.trackTotal = metadata.trackTotal || null
this.isrc = metadata.isrc || null
this.musicBrainzTrackId = metadata.musicBrainzTrackId || null
this.musicBrainzAlbumId = metadata.musicBrainzAlbumId || null
this.musicBrainzAlbumArtistId = metadata.musicBrainzAlbumArtistId || null
this.musicBrainzArtistId = metadata.musicBrainzArtistId || null
}
toJSON() {
return {
title: this.title,
artists: [...this.artists],
album: this.album,
albumArtist: this.albumArtist,
genres: [...this.genres],
composer: this.composer,
originalYear: this.originalYear,
releaseDate: this.releaseDate,
releaseCountry: this.releaseCountry,
releaseType: this.releaseType,
releaseStatus: this.releaseStatus,
recordLabel: this.recordLabel,
language: this.language,
explicit: this.explicit,
discNumber: this.discNumber,
discTotal: this.discTotal,
trackNumber: this.trackNumber,
trackTotal: this.trackTotal,
isrc: this.isrc,
musicBrainzTrackId: this.musicBrainzTrackId,
musicBrainzAlbumId: this.musicBrainzAlbumId,
musicBrainzAlbumArtistId: this.musicBrainzAlbumArtistId,
musicBrainzArtistId: this.musicBrainzArtistId
}
}
toJSONMinified() {
return {
title: this.title,
titleIgnorePrefix: this.titlePrefixAtEnd,
artists: [...this.artists],
album: this.album,
albumArtist: this.albumArtist,
genres: [...this.genres],
composer: this.composer,
originalYear: this.originalYear,
releaseDate: this.releaseDate,
releaseCountry: this.releaseCountry,
releaseType: this.releaseType,
releaseStatus: this.releaseStatus,
recordLabel: this.recordLabel,
language: this.language,
explicit: this.explicit,
discNumber: this.discNumber,
discTotal: this.discTotal,
trackNumber: this.trackNumber,
trackTotal: this.trackTotal,
isrc: this.isrc,
musicBrainzTrackId: this.musicBrainzTrackId,
musicBrainzAlbumId: this.musicBrainzAlbumId,
musicBrainzAlbumArtistId: this.musicBrainzAlbumArtistId,
musicBrainzArtistId: this.musicBrainzArtistId
}
}
toJSONExpanded() {
return this.toJSONMinified()
}
clone() {
return new MusicMetadata(this.toJSON())
}
get titleIgnorePrefix() {
return getTitleIgnorePrefix(this.title)
}
get titlePrefixAtEnd() {
return getTitlePrefixAtEnd(this.title)
}
setData(mediaMetadata = {}) {
this.title = mediaMetadata.title || null
this.artist = mediaMetadata.artist || null
this.album = mediaMetadata.album || null
}
update(payload) {
const json = this.toJSON()
let hasUpdates = false
for (const key in json) {
if (payload[key] !== undefined) {
if (!areEquivalent(payload[key], json[key])) {
this[key] = copyValue(payload[key])
Logger.debug('[MusicMetadata] Key updated', key, this[key])
hasUpdates = true
}
}
}
return hasUpdates
}
parseArtistsTag(artistsTag) {
if (!artistsTag || !artistsTag.length) return []
const separators = ['/', '//', ';']
for (let i = 0; i < separators.length; i++) {
if (artistsTag.includes(separators[i])) {
return artistsTag.split(separators[i]).map(artist => artist.trim()).filter(a => !!a)
}
}
return [artistsTag]
}
parseGenresTag(genreTag) {
if (!genreTag || !genreTag.length) return []
const separators = ['/', '//', ';']
for (let i = 0; i < separators.length; i++) {
if (genreTag.includes(separators[i])) {
return genreTag.split(separators[i]).map(genre => genre.trim()).filter(g => !!g)
}
}
return [genreTag]
}
setDataFromAudioMetaTags(audioFileMetaTags, overrideExistingDetails = false) {
const MetadataMapArray = [
{
tag: 'tagTitle',
key: 'title',
},
{
tag: 'tagArtist',
key: 'artists'
},
{
tag: 'tagAlbumArtist',
key: 'albumArtist'
},
{
tag: 'tagAlbum',
key: 'album',
},
{
tag: 'tagPublisher',
key: 'recordLabel'
},
{
tag: 'tagComposer',
key: 'composer'
},
{
tag: 'tagDate',
key: 'releaseDate'
},
{
tag: 'tagReleaseCountry',
key: 'releaseCountry'
},
{
tag: 'tagReleaseType',
key: 'releaseType'
},
{
tag: 'tagReleaseStatus',
key: 'releaseStatus'
},
{
tag: 'tagOriginalYear',
key: 'originalYear'
},
{
tag: 'tagGenre',
key: 'genres'
},
{
tag: 'tagLanguage',
key: 'language'
},
{
tag: 'tagLanguage',
key: 'language'
},
{
tag: 'tagISRC',
key: 'isrc'
},
{
tag: 'tagMusicBrainzTrackId',
key: 'musicBrainzTrackId'
},
{
tag: 'tagMusicBrainzAlbumId',
key: 'musicBrainzAlbumId'
},
{
tag: 'tagMusicBrainzAlbumArtistId',
key: 'musicBrainzAlbumArtistId'
},
{
tag: 'tagMusicBrainzArtistId',
key: 'musicBrainzArtistId'
},
{
tag: 'trackNumber',
key: 'trackNumber'
},
{
tag: 'trackTotal',
key: 'trackTotal'
},
{
tag: 'discNumber',
key: 'discNumber'
},
{
tag: 'discTotal',
key: 'discTotal'
}
]
const updatePayload = {}
// Metadata is only mapped to the music track if it is empty
MetadataMapArray.forEach((mapping) => {
let value = audioFileMetaTags[mapping.tag]
// let tagToUse = mapping.tag
if (!value && mapping.altTag) {
value = audioFileMetaTags[mapping.altTag]
// tagToUse = mapping.altTag
}
if (value && (typeof value === 'string' || typeof value === 'number')) {
value = value.toString().trim() // Trim whitespace
if (mapping.key === 'artists' && (!this.artists.length || overrideExistingDetails)) {
updatePayload.artists = this.parseArtistsTag(value)
} else if (mapping.key === 'genres' && (!this.genres.length || overrideExistingDetails)) {
updatePayload.genres = this.parseGenresTag(value)
} else if (!this[mapping.key] || overrideExistingDetails) {
updatePayload[mapping.key] = value
// Logger.debug(`[Book] Mapping metadata to key ${tagToUse} => ${mapping.key}: ${updatePayload[mapping.key]}`)
}
}
})
if (Object.keys(updatePayload).length) {
return this.update(updatePayload)
}
return false
}
}
module.exports = MusicMetadata
-80
View File
@@ -1,80 +0,0 @@
const Logger = require('../../Logger')
const { areEquivalent, copyValue, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index')
class VideoMetadata {
constructor(metadata) {
this.title = null
this.description = null
this.explicit = false
this.language = null
if (metadata) {
this.construct(metadata)
}
}
construct(metadata) {
this.title = metadata.title
this.description = metadata.description
this.explicit = metadata.explicit
this.language = metadata.language || null
}
toJSON() {
return {
title: this.title,
description: this.description,
explicit: this.explicit,
language: this.language
}
}
toJSONMinified() {
return {
title: this.title,
titleIgnorePrefix: this.titlePrefixAtEnd,
description: this.description,
explicit: this.explicit,
language: this.language
}
}
toJSONExpanded() {
return this.toJSONMinified()
}
clone() {
return new VideoMetadata(this.toJSON())
}
get titleIgnorePrefix() {
return getTitleIgnorePrefix(this.title)
}
get titlePrefixAtEnd() {
return getTitlePrefixAtEnd(this.title)
}
setData(mediaMetadata = {}) {
this.title = mediaMetadata.title || null
this.description = mediaMetadata.description || null
this.explicit = !!mediaMetadata.explicit
this.language = mediaMetadata.language || null
}
update(payload) {
var json = this.toJSON()
var hasUpdates = false
for (const key in json) {
if (payload[key] !== undefined) {
if (!areEquivalent(payload[key], json[key])) {
this[key] = copyValue(payload[key])
Logger.debug('[VideoMetadata] Key updated', key, this[key])
hasUpdates = true
}
}
}
return hasUpdates
}
}
module.exports = VideoMetadata
+2 -1
View File
@@ -1,6 +1,7 @@
const Logger = require('../Logger') const Logger = require('../Logger')
const SocketAuthority = require('../SocketAuthority') const SocketAuthority = require('../SocketAuthority')
const Database = require('../Database') const Database = require('../Database')
const { getTitleIgnorePrefix } = require('../utils/index')
// Utils // Utils
const { findMatchingEpisodesInFeed, getPodcastFeed } = require('../utils/podcastUtils') const { findMatchingEpisodesInFeed, getPodcastFeed } = require('../utils/podcastUtils')
@@ -230,7 +231,7 @@ class Scanner {
seriesItem = await Database.seriesModel.create({ seriesItem = await Database.seriesModel.create({
name: seriesMatchItem.series, name: seriesMatchItem.series,
nameIgnorePrefix: getTitleIgnorePrefix(seriesMatchItem.series), nameIgnorePrefix: getTitleIgnorePrefix(seriesMatchItem.series),
libraryId libraryId: libraryItem.libraryId
}) })
// Update filter data // Update filter data
Database.addSeriesToFilterData(libraryItem.libraryId, seriesItem.name, seriesItem.id) Database.addSeriesToFilterData(libraryItem.libraryId, seriesItem.name, seriesItem.id)
-4
View File
@@ -51,7 +51,3 @@ module.exports.AudioMimeType = {
AWB: 'audio/amr-wb', AWB: 'audio/amr-wb',
CAF: 'audio/x-caf' CAF: 'audio/x-caf'
} }
module.exports.VideoMimeType = {
MP4: 'video/mp4'
}
+6
View File
@@ -299,6 +299,12 @@ async function addCoverAndMetadataToFile(audioFilePath, coverFilePath, metadataF
'-metadata:s:v', '-metadata:s:v',
'comment=Cover' // add comment metadata to cover image stream 'comment=Cover' // add comment metadata to cover image stream
]) ])
const ext = Path.extname(coverFilePath).toLowerCase()
if (ext === '.webp') {
ffmpeg.outputOptions([
'-c:v mjpeg' // convert webp images to jpeg
])
}
} else { } else {
ffmpeg.outputOptions([ ffmpeg.outputOptions([
'-map 0:v?' // retain video stream from input file if exists '-map 0:v?' // retain video stream from input file if exists
-13
View File
@@ -131,19 +131,6 @@ async function readTextFile(path) {
} }
module.exports.readTextFile = readTextFile module.exports.readTextFile = readTextFile
function bytesPretty(bytes, decimals = 0) {
if (bytes === 0) {
return '0 Bytes'
}
const k = 1000
var dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
if (i > 2 && dm === 0) dm = 1
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
}
module.exports.bytesPretty = bytesPretty
/** /**
* Get array of files inside dir * Get array of files inside dir
* @param {string} path * @param {string} path
-1
View File
@@ -2,7 +2,6 @@ const globals = {
SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'], SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'],
SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma', 'mka', 'awb', 'caf'], SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma', 'mka', 'awb', 'caf'],
SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'], SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
SupportedVideoTypes: ['mp4'],
TextFileTypes: ['txt', 'nfo'], TextFileTypes: ['txt', 'nfo'],
MetadataFileTypes: ['opf', 'abs', 'xml', 'json'] MetadataFileTypes: ['opf', 'abs', 'xml', 'json']
} }
+83 -76
View File
@@ -19,8 +19,7 @@ const parseNameString = require('./parsers/parseNameString')
function isMediaFile(mediaType, ext, audiobooksOnly = false) { function isMediaFile(mediaType, ext, audiobooksOnly = false) {
if (!ext) return false if (!ext) return false
const extclean = ext.slice(1).toLowerCase() const extclean = ext.slice(1).toLowerCase()
if (mediaType === 'podcast' || mediaType === 'music') return globals.SupportedAudioTypes.includes(extclean) if (mediaType === 'podcast') return globals.SupportedAudioTypes.includes(extclean)
else if (mediaType === 'video') return globals.SupportedVideoTypes.includes(extclean)
else if (audiobooksOnly) return globals.SupportedAudioTypes.includes(extclean) else if (audiobooksOnly) return globals.SupportedAudioTypes.includes(extclean)
return globals.SupportedAudioTypes.includes(extclean) || globals.SupportedEbookTypes.includes(extclean) return globals.SupportedAudioTypes.includes(extclean) || globals.SupportedEbookTypes.includes(extclean)
} }
@@ -35,29 +34,33 @@ module.exports.checkFilepathIsAudioFile = checkFilepathIsAudioFile
/** /**
* TODO: Function needs to be re-done * TODO: Function needs to be re-done
* @param {string} mediaType * @param {string} mediaType
* @param {string[]} paths array of relative file paths * @param {string[]} paths array of relative file paths
* @returns {Record<string,string[]>} map of files grouped into potential libarary item dirs * @returns {Record<string,string[]>} map of files grouped into potential libarary item dirs
*/ */
function groupFilesIntoLibraryItemPaths(mediaType, paths) { function groupFilesIntoLibraryItemPaths(mediaType, paths) {
// Step 1: Clean path, Remove leading "/", Filter out non-media files in root dir // Step 1: Clean path, Remove leading "/", Filter out non-media files in root dir
var nonMediaFilePaths = [] var nonMediaFilePaths = []
var pathsFiltered = paths.map(path => { var pathsFiltered = paths
return path.startsWith('/') ? path.slice(1) : path .map((path) => {
}).filter(path => { return path.startsWith('/') ? path.slice(1) : path
let parsedPath = Path.parse(path) })
// Is not in root dir OR is a book media file .filter((path) => {
if (parsedPath.dir) { let parsedPath = Path.parse(path)
if (!isMediaFile(mediaType, parsedPath.ext, false)) { // Seperate out non-media files // Is not in root dir OR is a book media file
nonMediaFilePaths.push(path) if (parsedPath.dir) {
return false if (!isMediaFile(mediaType, parsedPath.ext, false)) {
// Seperate out non-media files
nonMediaFilePaths.push(path)
return false
}
return true
} else if (mediaType === 'book' && isMediaFile(mediaType, parsedPath.ext, false)) {
// (book media type supports single file audiobooks/ebooks in root dir)
return true
} }
return true return false
} else if (mediaType === 'book' && isMediaFile(mediaType, parsedPath.ext, false)) { // (book media type supports single file audiobooks/ebooks in root dir) })
return true
}
return false
})
// Step 2: Sort by least number of directories // Step 2: Sort by least number of directories
pathsFiltered.sort((a, b) => { pathsFiltered.sort((a, b) => {
@@ -69,7 +72,9 @@ function groupFilesIntoLibraryItemPaths(mediaType, paths) {
// Step 3: Group files in dirs // Step 3: Group files in dirs
var itemGroup = {} var itemGroup = {}
pathsFiltered.forEach((path) => { pathsFiltered.forEach((path) => {
var dirparts = Path.dirname(path).split('/').filter(p => !!p && p !== '.') // dirname returns . if no directory var dirparts = Path.dirname(path)
.split('/')
.filter((p) => !!p && p !== '.') // dirname returns . if no directory
var numparts = dirparts.length var numparts = dirparts.length
var _path = '' var _path = ''
@@ -82,14 +87,17 @@ function groupFilesIntoLibraryItemPaths(mediaType, paths) {
var dirpart = dirparts.shift() var dirpart = dirparts.shift()
_path = Path.posix.join(_path, dirpart) _path = Path.posix.join(_path, dirpart)
if (itemGroup[_path]) { // Directory already has files, add file if (itemGroup[_path]) {
// Directory already has files, add file
var relpath = Path.posix.join(dirparts.join('/'), Path.basename(path)) var relpath = Path.posix.join(dirparts.join('/'), Path.basename(path))
itemGroup[_path].push(relpath) itemGroup[_path].push(relpath)
return return
} else if (!dirparts.length) { // This is the last directory, create group } else if (!dirparts.length) {
// This is the last directory, create group
itemGroup[_path] = [Path.basename(path)] itemGroup[_path] = [Path.basename(path)]
return return
} else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) { // Next directory is the last and is a CD dir, create group } else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) {
// Next directory is the last and is a CD dir, create group
itemGroup[_path] = [Path.posix.join(dirparts[0], Path.basename(path))] itemGroup[_path] = [Path.posix.join(dirparts[0], Path.basename(path))]
return return
} }
@@ -99,7 +107,6 @@ function groupFilesIntoLibraryItemPaths(mediaType, paths) {
// Step 4: Add in non-media files if they fit into item group // Step 4: Add in non-media files if they fit into item group
if (nonMediaFilePaths.length) { if (nonMediaFilePaths.length) {
for (const nonMediaFilePath of nonMediaFilePaths) { for (const nonMediaFilePath of nonMediaFilePaths) {
const pathDir = Path.dirname(nonMediaFilePath) const pathDir = Path.dirname(nonMediaFilePath)
const filename = Path.basename(nonMediaFilePath) const filename = Path.basename(nonMediaFilePath)
@@ -111,7 +118,8 @@ function groupFilesIntoLibraryItemPaths(mediaType, paths) {
for (let i = 0; i < numparts; i++) { for (let i = 0; i < numparts; i++) {
const dirpart = dirparts.shift() const dirpart = dirparts.shift()
_path = Path.posix.join(_path, dirpart) _path = Path.posix.join(_path, dirpart)
if (itemGroup[_path]) { // Directory is a group if (itemGroup[_path]) {
// Directory is a group
const relpath = Path.posix.join(dirparts.join('/'), filename) const relpath = Path.posix.join(dirparts.join('/'), filename)
itemGroup[_path].push(relpath) itemGroup[_path].push(relpath)
} else if (!dirparts.length) { } else if (!dirparts.length) {
@@ -126,31 +134,22 @@ function groupFilesIntoLibraryItemPaths(mediaType, paths) {
module.exports.groupFilesIntoLibraryItemPaths = groupFilesIntoLibraryItemPaths module.exports.groupFilesIntoLibraryItemPaths = groupFilesIntoLibraryItemPaths
/** /**
* @param {string} mediaType * @param {string} mediaType
* @param {{name:string, path:string, dirpath:string, reldirpath:string, fullpath:string, extension:string, deep:number}[]} fileItems (see recurseFiles) * @param {{name:string, path:string, dirpath:string, reldirpath:string, fullpath:string, extension:string, deep:number}[]} fileItems (see recurseFiles)
* @param {boolean} [audiobooksOnly=false] * @param {boolean} [audiobooksOnly=false]
* @returns {Record<string,string[]>} map of files grouped into potential libarary item dirs * @returns {Record<string,string[]>} map of files grouped into potential libarary item dirs
*/ */
function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems, audiobooksOnly = false) { function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems, audiobooksOnly = false) {
// Handle music where every audio file is a library item
if (mediaType === 'music') {
const audioFileGroup = {}
fileItems.filter(i => isMediaFile(mediaType, i.extension, audiobooksOnly)).forEach((item) => {
audioFileGroup[item.path] = item.path
})
return audioFileGroup
}
// Step 1: Filter out non-book-media files in root dir (with depth of 0) // Step 1: Filter out non-book-media files in root dir (with depth of 0)
const itemsFiltered = fileItems.filter(i => { const itemsFiltered = fileItems.filter((i) => {
return i.deep > 0 || ((mediaType === 'book' || mediaType === 'video' || mediaType === 'music') && isMediaFile(mediaType, i.extension, audiobooksOnly)) return i.deep > 0 || (mediaType === 'book' && isMediaFile(mediaType, i.extension, audiobooksOnly))
}) })
// Step 2: Seperate media files and other files // Step 2: Seperate media files and other files
// - Directories without a media file will not be included // - Directories without a media file will not be included
const mediaFileItems = [] const mediaFileItems = []
const otherFileItems = [] const otherFileItems = []
itemsFiltered.forEach(item => { itemsFiltered.forEach((item) => {
if (isMediaFile(mediaType, item.extension, audiobooksOnly)) mediaFileItems.push(item) if (isMediaFile(mediaType, item.extension, audiobooksOnly)) mediaFileItems.push(item)
else otherFileItems.push(item) else otherFileItems.push(item)
}) })
@@ -158,7 +157,7 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems, audiobooksOnly
// Step 3: Group audio files in library items // Step 3: Group audio files in library items
const libraryItemGroup = {} const libraryItemGroup = {}
mediaFileItems.forEach((item) => { mediaFileItems.forEach((item) => {
const dirparts = item.reldirpath.split('/').filter(p => !!p) const dirparts = item.reldirpath.split('/').filter((p) => !!p)
const numparts = dirparts.length const numparts = dirparts.length
let _path = '' let _path = ''
@@ -171,14 +170,17 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems, audiobooksOnly
const dirpart = dirparts.shift() const dirpart = dirparts.shift()
_path = Path.posix.join(_path, dirpart) _path = Path.posix.join(_path, dirpart)
if (libraryItemGroup[_path]) { // Directory already has files, add file if (libraryItemGroup[_path]) {
// Directory already has files, add file
const relpath = Path.posix.join(dirparts.join('/'), item.name) const relpath = Path.posix.join(dirparts.join('/'), item.name)
libraryItemGroup[_path].push(relpath) libraryItemGroup[_path].push(relpath)
return return
} else if (!dirparts.length) { // This is the last directory, create group } else if (!dirparts.length) {
// This is the last directory, create group
libraryItemGroup[_path] = [item.name] libraryItemGroup[_path] = [item.name]
return return
} else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) { // Next directory is the last and is a CD dir, create group } else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) {
// Next directory is the last and is a CD dir, create group
libraryItemGroup[_path] = [Path.posix.join(dirparts[0], item.name)] libraryItemGroup[_path] = [Path.posix.join(dirparts[0], item.name)]
return return
} }
@@ -196,7 +198,8 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems, audiobooksOnly
for (let i = 0; i < numparts; i++) { for (let i = 0; i < numparts; i++) {
const dirpart = dirparts.shift() const dirpart = dirparts.shift()
_path = Path.posix.join(_path, dirpart) _path = Path.posix.join(_path, dirpart)
if (libraryItemGroup[_path]) { // Directory is audiobook group if (libraryItemGroup[_path]) {
// Directory is audiobook group
const relpath = Path.posix.join(dirparts.join('/'), item.name) const relpath = Path.posix.join(dirparts.join('/'), item.name)
libraryItemGroup[_path].push(relpath) libraryItemGroup[_path].push(relpath)
return return
@@ -209,33 +212,35 @@ module.exports.groupFileItemsIntoLibraryItemDirs = groupFileItemsIntoLibraryItem
/** /**
* Get LibraryFile from filepath * Get LibraryFile from filepath
* @param {string} libraryItemPath * @param {string} libraryItemPath
* @param {string[]} files * @param {string[]} files
* @returns {import('../objects/files/LibraryFile')} * @returns {import('../objects/files/LibraryFile')}
*/ */
function buildLibraryFile(libraryItemPath, files) { function buildLibraryFile(libraryItemPath, files) {
return Promise.all(files.map(async (file) => { return Promise.all(
const filePath = Path.posix.join(libraryItemPath, file) files.map(async (file) => {
const newLibraryFile = new LibraryFile() const filePath = Path.posix.join(libraryItemPath, file)
await newLibraryFile.setDataFromPath(filePath, file) const newLibraryFile = new LibraryFile()
return newLibraryFile await newLibraryFile.setDataFromPath(filePath, file)
})) return newLibraryFile
})
)
} }
module.exports.buildLibraryFile = buildLibraryFile module.exports.buildLibraryFile = buildLibraryFile
/** /**
* Get details parsed from filenames * Get details parsed from filenames
* *
* @param {string} relPath * @param {string} relPath
* @param {boolean} parseSubtitle * @param {boolean} parseSubtitle
* @returns {LibraryItemFilenameMetadata} * @returns {LibraryItemFilenameMetadata}
*/ */
function getBookDataFromDir(relPath, parseSubtitle = false) { function getBookDataFromDir(relPath, parseSubtitle = false) {
const splitDir = relPath.split('/') const splitDir = relPath.split('/')
var folder = splitDir.pop() // Audio files will always be in the directory named for the title var folder = splitDir.pop() // Audio files will always be in the directory named for the title
series = (splitDir.length > 1) ? splitDir.pop() : null // If there are at least 2 more directories, next furthest will be the series series = splitDir.length > 1 ? splitDir.pop() : null // If there are at least 2 more directories, next furthest will be the series
author = (splitDir.length > 0) ? splitDir.pop() : null // There could be many more directories, but only the top 3 are used for naming /author/series/title/ author = splitDir.length > 0 ? splitDir.pop() : null // There could be many more directories, but only the top 3 are used for naming /author/series/title/
// The may contain various other pieces of metadata, these functions extract it. // The may contain various other pieces of metadata, these functions extract it.
var [folder, asin] = getASIN(folder) var [folder, asin] = getASIN(folder)
@@ -244,7 +249,6 @@ function getBookDataFromDir(relPath, parseSubtitle = false) {
var [folder, publishedYear] = getPublishedYear(folder) var [folder, publishedYear] = getPublishedYear(folder)
var [title, subtitle] = parseSubtitle ? getSubtitle(folder) : [folder, null] var [title, subtitle] = parseSubtitle ? getSubtitle(folder) : [folder, null]
return { return {
title, title,
subtitle, subtitle,
@@ -260,8 +264,8 @@ module.exports.getBookDataFromDir = getBookDataFromDir
/** /**
* Extract narrator from folder name * Extract narrator from folder name
* *
* @param {string} folder * @param {string} folder
* @returns {[string, string]} [folder, narrator] * @returns {[string, string]} [folder, narrator]
*/ */
function getNarrator(folder) { function getNarrator(folder) {
@@ -272,7 +276,7 @@ function getNarrator(folder) {
/** /**
* Extract series sequence from folder name * Extract series sequence from folder name
* *
* @example * @example
* 'Book 2 - Title - Subtitle' * 'Book 2 - Title - Subtitle'
* 'Title - Subtitle - Vol 12' * 'Title - Subtitle - Vol 12'
@@ -283,8 +287,8 @@ function getNarrator(folder) {
* '100 - Book Title' * '100 - Book Title'
* '6. Title' * '6. Title'
* '0.5 - Book Title' * '0.5 - Book Title'
* *
* @param {string} folder * @param {string} folder
* @returns {[string, string]} [folder, sequence] * @returns {[string, string]} [folder, sequence]
*/ */
function getSequence(folder) { function getSequence(folder) {
@@ -299,7 +303,9 @@ function getSequence(folder) {
if (match && !(match.groups.suffix && !(match.groups.volumeLabel || match.groups.trailingDot))) { if (match && !(match.groups.suffix && !(match.groups.volumeLabel || match.groups.trailingDot))) {
volumeNumber = isNaN(match.groups.sequence) ? match.groups.sequence : Number(match.groups.sequence).toString() volumeNumber = isNaN(match.groups.sequence) ? match.groups.sequence : Number(match.groups.sequence).toString()
parts[i] = match.groups.suffix parts[i] = match.groups.suffix
if (!parts[i]) { parts.splice(i, 1) } if (!parts[i]) {
parts.splice(i, 1)
}
break break
} }
} }
@@ -310,8 +316,8 @@ function getSequence(folder) {
/** /**
* Extract published year from folder name * Extract published year from folder name
* *
* @param {string} folder * @param {string} folder
* @returns {[string, string]} [folder, publishedYear] * @returns {[string, string]} [folder, publishedYear]
*/ */
function getPublishedYear(folder) { function getPublishedYear(folder) {
@@ -329,8 +335,8 @@ function getPublishedYear(folder) {
/** /**
* Extract subtitle from folder name * Extract subtitle from folder name
* *
* @param {string} folder * @param {string} folder
* @returns {[string, string]} [folder, subtitle] * @returns {[string, string]} [folder, subtitle]
*/ */
function getSubtitle(folder) { function getSubtitle(folder) {
@@ -341,8 +347,8 @@ function getSubtitle(folder) {
/** /**
* Extract asin from folder name * Extract asin from folder name
* *
* @param {string} folder * @param {string} folder
* @returns {[string, string]} [folder, asin] * @returns {[string, string]} [folder, asin]
*/ */
function getASIN(folder) { function getASIN(folder) {
@@ -358,8 +364,8 @@ function getASIN(folder) {
} }
/** /**
* *
* @param {string} relPath * @param {string} relPath
* @returns {LibraryItemFilenameMetadata} * @returns {LibraryItemFilenameMetadata}
*/ */
function getPodcastDataFromDir(relPath) { function getPodcastDataFromDir(relPath) {
@@ -373,10 +379,10 @@ function getPodcastDataFromDir(relPath) {
} }
/** /**
* *
* @param {string} libraryMediaType * @param {string} libraryMediaType
* @param {string} folderPath * @param {string} folderPath
* @param {string} relPath * @param {string} relPath
* @returns {{ mediaMetadata: LibraryItemFilenameMetadata, relPath: string, path: string}} * @returns {{ mediaMetadata: LibraryItemFilenameMetadata, relPath: string, path: string}}
*/ */
function getDataFromMediaDir(libraryMediaType, folderPath, relPath) { function getDataFromMediaDir(libraryMediaType, folderPath, relPath) {
@@ -386,7 +392,8 @@ function getDataFromMediaDir(libraryMediaType, folderPath, relPath) {
if (libraryMediaType === 'podcast') { if (libraryMediaType === 'podcast') {
mediaMetadata = getPodcastDataFromDir(relPath) mediaMetadata = getPodcastDataFromDir(relPath)
} else { // book } else {
// book
mediaMetadata = getBookDataFromDir(relPath, !!global.ServerSettings.scannerParseSubtitle) mediaMetadata = getBookDataFromDir(relPath, !!global.ServerSettings.scannerParseSubtitle)
} }