mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-11-07 08:34:10 +01:00
Merge branch 'master' into openid_signing_algorithm
This commit is contained in:
commit
af856ce1ec
5
.github/workflows/i18n-integration.yml
vendored
5
.github/workflows/i18n-integration.yml
vendored
@ -1,6 +1,9 @@
|
||||
name: Verify all i18n files are alphabetized
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- client/strings/** # Should only check if any strings changed
|
||||
push:
|
||||
paths:
|
||||
- client/strings/** # Should only check if any strings changed
|
||||
@ -22,6 +25,6 @@ jobs:
|
||||
# The only argument is the `directory`, which is where the i18n files are
|
||||
# stored.
|
||||
- name: Run Update JSON Files action
|
||||
uses: audiobookshelf/audiobookshelf-i18n-updater@v1.1.1
|
||||
uses: audiobookshelf/audiobookshelf-i18n-updater@v1.2.0
|
||||
with:
|
||||
directory: "client/strings/" # Adjust the directory path as needed
|
||||
|
@ -30,8 +30,7 @@
|
||||
}
|
||||
|
||||
.bookshelf-row {
|
||||
/* Sidebar width + scrollbar width */
|
||||
width: calc(100vw - 88px);
|
||||
width: calc(100vw - (100vw - 100%));
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="w-full h-full pt-6">
|
||||
<div v-if="shelf.type === 'book' || shelf.type === 'podcast'" class="flex items-center">
|
||||
<template v-for="(entity, index) in shelf.entities">
|
||||
<cards-lazy-book-card :key="entity.id" :ref="`shelf-book-${entity.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" :continue-listening-shelf="continueListeningShelf" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @edit="editItem" />
|
||||
<cards-lazy-book-card :key="`${entity.id}-${index}`" :ref="`shelf-book-${entity.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" :continue-listening-shelf="continueListeningShelf" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @edit="editItem" />
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="shelf.type === 'episode'" class="flex items-center">
|
||||
|
@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div v-if="streamLibraryItem" id="mediaPlayerContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 md:h-40 z-50 bg-primary px-2 md:px-4 pb-1 md:pb-4 pt-2">
|
||||
<div 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 md: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" />
|
||||
</div>
|
||||
<div class="flex items-start mb-6 md: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="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
|
||||
<div class="min-w-0">
|
||||
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg block truncate">
|
||||
{{ title }}
|
||||
@ -29,7 +29,7 @@
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<ui-tooltip direction="top" :text="$strings.LabelClosePlayer">
|
||||
<button :aria-label="$strings.LabelClosePlayer" class="material-icons sm:px-2 py-1 md:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</button>
|
||||
<button :aria-label="$strings.LabelClosePlayer" class="material-icons sm:px-2 py-1 lg:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</button>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<player-ui
|
||||
|
@ -89,6 +89,14 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="language" 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">{{ $strings.LabelLanguage }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=languages.${$encode(language)}`" class="hover:underline">{{ language }}</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" 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">{{ $strings.LabelDuration }}</span>
|
||||
@ -182,6 +190,9 @@ export default {
|
||||
narrators() {
|
||||
return this.mediaMetadata.narrators || []
|
||||
},
|
||||
language() {
|
||||
return this.mediaMetadata.language || null
|
||||
},
|
||||
durationPretty() {
|
||||
if (this.isPodcast) return this.$elapsedPrettyExtended(this.totalPodcastDuration)
|
||||
|
||||
|
@ -235,6 +235,11 @@ export default {
|
||||
value: 'tags',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelLanguage,
|
||||
value: 'languages',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.ButtonIssues,
|
||||
value: 'issues',
|
||||
|
@ -88,10 +88,11 @@
|
||||
<p class="mb-1">{{ _session.mediaPlayer }}</p>
|
||||
|
||||
<p v-if="hasDeviceInfo" class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">{{ $strings.LabelDevice }}</p>
|
||||
<p v-if="clientDisplayName" class="mb-1">{{ clientDisplayName }}</p>
|
||||
<p v-if="deviceInfo.ipAddress" class="mb-1">{{ deviceInfo.ipAddress }}</p>
|
||||
<p v-if="osDisplayName" class="mb-1">{{ osDisplayName }}</p>
|
||||
<p v-if="deviceInfo.browserName" class="mb-1">{{ deviceInfo.browserName }}</p>
|
||||
<p v-if="clientDisplayName" class="mb-1">{{ clientDisplayName }}</p>
|
||||
<p v-if="deviceDisplayName" class="mb-1">{{ deviceDisplayName }}</p>
|
||||
<p v-if="deviceInfo.sdkVersion" class="mb-1">SDK {{ $strings.LabelVersion }}: {{ deviceInfo.sdkVersion }}</p>
|
||||
<p v-if="deviceInfo.deviceType" class="mb-1">{{ $strings.LabelType }}: {{ deviceInfo.deviceType }}</p>
|
||||
</div>
|
||||
@ -141,10 +142,14 @@ export default {
|
||||
if (!this.deviceInfo.osName) return null
|
||||
return `${this.deviceInfo.osName} ${this.deviceInfo.osVersion}`
|
||||
},
|
||||
clientDisplayName() {
|
||||
deviceDisplayName() {
|
||||
if (!this.deviceInfo.manufacturer || !this.deviceInfo.model) return null
|
||||
return `${this.deviceInfo.manufacturer} ${this.deviceInfo.model}`
|
||||
},
|
||||
clientDisplayName() {
|
||||
if (!this.deviceInfo.clientName) return null
|
||||
return `${this.deviceInfo.clientName} ${this.deviceInfo.clientVersion || ''}`
|
||||
},
|
||||
playMethodName() {
|
||||
const playMethod = this._session.playMethod
|
||||
if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play'
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative">
|
||||
<div class="flex flex-wrap mb-4">
|
||||
<div class="relative">
|
||||
<div class="flex flex-col sm:flex-row mb-4">
|
||||
<div class="relative self-center">
|
||||
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, libraryItemUpdatedAt, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
|
||||
<!-- book cover overlay -->
|
||||
@ -14,7 +14,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-2 md:mt-0">
|
||||
<div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-6 md:mt-0">
|
||||
<div class="flex items-center">
|
||||
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 md:min-w-32">
|
||||
<ui-file-input ref="fileInput" @change="fileUploadSelected">
|
||||
@ -49,20 +49,20 @@
|
||||
</div>
|
||||
</div>
|
||||
<form @submit.prevent="submitSearchForm">
|
||||
<div class="flex items-center justify-start -mx-1 h-20">
|
||||
<div class="w-48 px-1">
|
||||
<div class="flex flex-wrap sm:flex-nowrap items-center justify-start -mx-1">
|
||||
<div class="w-48 flex-grow p-1">
|
||||
<ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small />
|
||||
</div>
|
||||
<div class="w-72 px-1">
|
||||
<div class="w-72 flex-grow p-1">
|
||||
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" />
|
||||
</div>
|
||||
<div v-show="provider != 'itunes' && provider != 'audiobookcovers'" class="w-72 px-1">
|
||||
<div v-show="provider != 'itunes' && provider != 'audiobookcovers'" class="w-72 flex-grow p-1">
|
||||
<ui-text-input-with-label v-model="searchAuthor" :label="$strings.LabelAuthor" />
|
||||
</div>
|
||||
<ui-btn class="mt-5 ml-1" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
|
||||
<ui-btn class="mt-5 ml-1 md:min-w-24" :padding-x="4" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
|
||||
</div>
|
||||
</form>
|
||||
<div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-80 overflow-y-scroll mt-2 max-w-full">
|
||||
<div v-if="hasSearched" class="flex items-center flex-wrap justify-center sm:max-h-80 sm:overflow-y-scroll mt-2 max-w-full">
|
||||
<p v-if="!coversFound.length">{{ $strings.MessageNoCoversFound }}</p>
|
||||
<template v-for="cover in coversFound">
|
||||
<div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === coverPath ? 'border-yellow-300' : ''" @click="updateCover(cover)">
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="flex items-center pt-4 pb-2 md:pt-0 md:pb-2">
|
||||
<div class="flex items-center pt-4 pb-2 lg:pt-0 lg:pb-2">
|
||||
<div class="flex-grow" />
|
||||
<template v-if="!loading">
|
||||
<ui-tooltip direction="top" :text="$strings.ButtonPreviousChapter" class="mr-4 md:mr-8">
|
||||
<ui-tooltip direction="top" :text="$strings.ButtonPreviousChapter" class="mr-4 lg:mr-8">
|
||||
<button :aria-label="$strings.ButtonPreviousChapter" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
|
||||
<span class="material-icons text-2xl sm:text-3xl">first_page</span>
|
||||
</button>
|
||||
@ -12,7 +12,7 @@
|
||||
<span class="material-icons text-2xl sm:text-3xl">replay_10</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 md:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
||||
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 lg:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
||||
<span class="material-icons text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
||||
</button>
|
||||
<ui-tooltip direction="top" :text="$strings.ButtonJumpForward">
|
||||
@ -20,7 +20,7 @@
|
||||
<span class="material-icons text-2xl sm:text-3xl">forward_10</span>
|
||||
</button>
|
||||
</ui-tooltip>
|
||||
<ui-tooltip direction="top" :text="$strings.ButtonNextChapter" class="ml-4 md:ml-8">
|
||||
<ui-tooltip direction="top" :text="$strings.ButtonNextChapter" class="ml-4 lg:ml-8">
|
||||
<button :aria-label="$strings.ButtonNextChapter" :disabled="!hasNextChapter" :class="hasNextChapter ? 'text-gray-300' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter">
|
||||
<span class="material-icons text-2xl sm:text-3xl">last_page</span>
|
||||
</button>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="w-full -mt-6">
|
||||
<div class="w-full relative mb-1">
|
||||
<div class="absolute -top-10 md:top-0 right-0 lg:right-2 flex items-center h-full">
|
||||
<div class="absolute -top-10 lg:top-0 right-0 lg:right-2 flex items-center h-full">
|
||||
<!-- <span class="material-icons text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> -->
|
||||
|
||||
<ui-tooltip direction="top" :text="$strings.LabelVolume">
|
||||
|
@ -1,45 +1,45 @@
|
||||
<template>
|
||||
<div class="flex flex-wrap justify-center mt-6">
|
||||
<div class="flex px-2">
|
||||
<svg class="h-14 w-14 md:h-18 md:w-18" viewBox="0 0 24 24">
|
||||
<div class="flex p-2">
|
||||
<svg class="h-14 w-14" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z" />
|
||||
</svg>
|
||||
<div class="px-2">
|
||||
<p class="text-4xl md:text-5xl font-bold">{{ totalItems }}</p>
|
||||
<div class="px-1">
|
||||
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalItems) }}</p>
|
||||
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsItemsInLibrary }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex px-4">
|
||||
<span class="material-icons text-7xl">show_chart</span>
|
||||
<div class="flex p-2">
|
||||
<span class="material-icons text-5xl py-1">show_chart</span>
|
||||
<div class="px-1">
|
||||
<p class="text-4xl md:text-5xl font-bold">{{ totalTime }}</p>
|
||||
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalTime) }}</p>
|
||||
<p class="text-xs md:text-sm text-white text-opacity-80">{{ useOverallHours ? $strings.LabelStatsOverallHours : $strings.LabelStatsOverallDays }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isBookLibrary" class="flex px-4">
|
||||
<svg class="h-14 w-14 md:h-18 md:w-18" viewBox="0 0 24 24">
|
||||
<div v-if="isBookLibrary" class="flex p-2">
|
||||
<svg class="h-14 w-14" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" />
|
||||
</svg>
|
||||
<div class="px-1">
|
||||
<p class="text-4xl md:text-5xl font-bold">{{ totalAuthors }}</p>
|
||||
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalAuthors) }}</p>
|
||||
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAuthors }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex px-4">
|
||||
<span class="material-icons-outlined text-6xl pt-1">insert_drive_file</span>
|
||||
<div class="flex p-2">
|
||||
<span class="material-icons-outlined text-5xl pt-1">insert_drive_file</span>
|
||||
<div class="px-1">
|
||||
<p class="text-4xl md:text-5xl font-bold">{{ totalSizeNum }}</p>
|
||||
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalSizeNum) }}</p>
|
||||
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelSize }} ({{ totalSizeMod }})</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex px-4">
|
||||
<span class="material-icons-outlined text-6xl pt-1">audio_file</span>
|
||||
<div class="flex p-2">
|
||||
<span class="material-icons-outlined text-5xl pt-1">audio_file</span>
|
||||
<div class="px-1">
|
||||
<p class="text-4xl md:text-5xl font-bold">{{ numAudioTracks }}</p>
|
||||
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(numAudioTracks) }}</p>
|
||||
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAudioTracks }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<input ref="fileInput" type="file" :accept="accept" class="hidden" @change="inputChanged" />
|
||||
<ui-btn @click="clickUpload" color="primary" class="hidden md:block" type="text"><slot /></ui-btn>
|
||||
<ui-btn @click="clickUpload" color="primary" class="hidden md:block w-full" type="text"><slot /></ui-btn>
|
||||
<ui-icon-btn @click="clickUpload" icon="upload" class="block md:hidden" />
|
||||
</div>
|
||||
</template>
|
||||
|
@ -10,10 +10,10 @@
|
||||
<span class="material-icons text-2xl">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled">
|
||||
<div class="flex" :style="{ height: height + 'px' }">
|
||||
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll" style="scroll-behavior: smooth" @scroll="scrolled">
|
||||
<div class="flex space-x-4" :style="{ height: height + 'px' }">
|
||||
<template v-for="(item, index) in items">
|
||||
<cards-author-card :key="item.id" :ref="`slider-item-${item.id}`" :index="index" :author="item" :height="cardHeight" :width="cardWidth" class="relative mx-2" @edit="editAuthor" @hook:updated="setScrollVars" />
|
||||
<cards-author-card :key="item.id" :ref="`slider-item-${item.id}`" :index="index" :author="item" :height="cardHeight" :width="cardWidth" class="relative" @edit="editAuthor" @hook:updated="setScrollVars" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -10,8 +10,8 @@
|
||||
<span class="material-icons text-2xl">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled">
|
||||
<div class="flex" :style="{ height: height + 'px' }">
|
||||
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll" style="scroll-behavior: smooth" @scroll="scrolled">
|
||||
<div class="flex space-x-4" :style="{ height: height + 'px' }">
|
||||
<template v-for="(item, index) in items">
|
||||
<cards-lazy-book-card
|
||||
:key="item.recentEpisode.id"
|
||||
@ -23,7 +23,7 @@
|
||||
:book-cover-aspect-ratio="bookCoverAspectRatio"
|
||||
:bookshelf-view="bookshelfView"
|
||||
:continue-listening-shelf="continueListeningShelf"
|
||||
class="relative mx-2"
|
||||
class="relative"
|
||||
@edit="editEpisode"
|
||||
@editPodcast="editPodcast"
|
||||
@select="selectItem"
|
||||
|
@ -10,10 +10,24 @@
|
||||
<span class="material-icons text-2xl">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled">
|
||||
<div class="flex" :style="{ height: height + 'px' }">
|
||||
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll" style="scroll-behavior: smooth" @scroll="scrolled">
|
||||
<div class="flex space-x-4" :style="{ height: height + 'px' }">
|
||||
<template v-for="(item, index) in items">
|
||||
<cards-lazy-book-card :key="item.id + '-' + shelfId" :ref="`slider-item-${item.id}`" :index="index" :book-mount="item" :height="cardHeight" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" :bookshelf-view="bookshelfView" :continue-listening-shelf="continueListeningShelf" class="relative mx-2" @edit="editItem" @select="selectItem" @hook:updated="setScrollVars" />
|
||||
<cards-lazy-book-card
|
||||
:key="item.id + '-' + shelfId + '-' + index"
|
||||
:ref="`slider-item-${item.id}`"
|
||||
:index="index"
|
||||
:book-mount="item"
|
||||
:height="cardHeight"
|
||||
:width="cardWidth"
|
||||
:book-cover-aspect-ratio="bookCoverAspectRatio"
|
||||
:bookshelf-view="bookshelfView"
|
||||
:continue-listening-shelf="continueListeningShelf"
|
||||
class="relative"
|
||||
@edit="editItem"
|
||||
@select="selectItem"
|
||||
@hook:updated="setScrollVars"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -10,10 +10,10 @@
|
||||
<span class="material-icons text-2xl">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled">
|
||||
<div class="flex" :style="{ height: height + 'px' }">
|
||||
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll" style="scroll-behavior: smooth" @scroll="scrolled">
|
||||
<div class="flex space-x-4" :style="{ height: height + 'px' }">
|
||||
<template v-for="item in items">
|
||||
<cards-narrator-card :key="item.name" :ref="`slider-item-${item.name}`" :narrator="item" :height="cardHeight" :width="cardWidth" class="relative mx-2" @hook:updated="setScrollVars" />
|
||||
<cards-narrator-card :key="item.name" :ref="`slider-item-${item.name}`" :narrator="item" :height="cardHeight" :width="cardWidth" class="relative" @hook:updated="setScrollVars" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -10,10 +10,10 @@
|
||||
<span class="material-icons text-2xl">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled">
|
||||
<div class="flex" :style="{ height: height + 'px' }">
|
||||
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll" style="scroll-behavior: smooth" @scroll="scrolled">
|
||||
<div class="flex space-x-4" :style="{ height: height + 'px' }">
|
||||
<template v-for="(item, index) in items">
|
||||
<cards-lazy-series-card :key="item.id" :ref="`slider-item-${item.id}`" :index="index" :series-mount="item" :height="cardHeight" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" :bookshelf-view="$constants.BookshelfView.DETAIL" class="relative mx-2" @hook:updated="setScrollVars" />
|
||||
<cards-lazy-series-card :key="item.id" :ref="`slider-item-${item.id}`" :index="index" :series-mount="item" :height="cardHeight" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" :bookshelf-view="$constants.BookshelfView.DETAIL" class="relative" @hook:updated="setScrollVars" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -64,8 +64,8 @@
|
||||
<td class="hidden md:table-cell w-26 min-w-26">
|
||||
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
||||
</td>
|
||||
<td class="hidden sm:table-cell w-32 min-w-32">
|
||||
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||
<td class="hidden sm:table-cell max-w-32 min-w-32">
|
||||
<p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||
</td>
|
||||
<td class="text-center w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
||||
@ -127,8 +127,8 @@
|
||||
<td class="hidden md:table-cell">
|
||||
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
||||
</td>
|
||||
<td class="hidden sm:table-cell">
|
||||
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||
<td class="hidden sm:table-cell max-w-32 min-w-32">
|
||||
<p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
||||
@ -394,6 +394,7 @@ export default {
|
||||
getDeviceInfoString(deviceInfo) {
|
||||
if (!deviceInfo) return ''
|
||||
var lines = []
|
||||
if (deviceInfo.clientName) lines.push(`${deviceInfo.clientName} ${deviceInfo.clientVersion || ''}`)
|
||||
if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`)
|
||||
if (deviceInfo.browserName) lines.push(deviceInfo.browserName)
|
||||
|
||||
|
@ -36,8 +36,8 @@
|
||||
<td class="hidden md:table-cell">
|
||||
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
||||
</td>
|
||||
<td class="hidden sm:table-cell">
|
||||
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||
<td class="hidden sm:table-cell min-w-32 max-w-32">
|
||||
<p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
||||
@ -193,6 +193,7 @@ export default {
|
||||
getDeviceInfoString(deviceInfo) {
|
||||
if (!deviceInfo) return ''
|
||||
var lines = []
|
||||
if (deviceInfo.clientName) lines.push(`${deviceInfo.clientName} ${deviceInfo.clientVersion || ''}`)
|
||||
if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`)
|
||||
if (deviceInfo.browserName) lines.push(deviceInfo.browserName)
|
||||
|
||||
|
@ -34,7 +34,10 @@
|
||||
|
||||
<p v-if="bookSubtitle" class="text-gray-200 text-xl md:text-2xl">{{ bookSubtitle }}</p>
|
||||
|
||||
<nuxt-link v-for="_series in seriesList" :key="_series.id" :to="`/library/${libraryId}/series/${_series.id}`" class="hover:underline font-sans text-gray-300 text-lg leading-7"> {{ _series.text }}</nuxt-link>
|
||||
<template v-for="(_series, index) in seriesList">
|
||||
<nuxt-link :key="_series.id" :to="`/library/${libraryId}/series/${_series.id}`" class="hover:underline font-sans text-gray-300 text-lg leading-7">{{ _series.text }}</nuxt-link
|
||||
><span :key="index" v-if="index < seriesList.length - 1">, </span>
|
||||
</template>
|
||||
|
||||
<template v-if="!isVideo">
|
||||
<p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">by {{ podcastAuthor || 'Unknown' }}</p>
|
||||
|
@ -5,6 +5,7 @@ import { supplant } from './utils'
|
||||
const defaultCode = 'en-us'
|
||||
|
||||
const languageCodeMap = {
|
||||
'bn': { label: 'বাংলা', dateFnsLocale: 'bn' },
|
||||
'cs': { label: 'Čeština', dateFnsLocale: 'cs' },
|
||||
'da': { label: 'Dansk', dateFnsLocale: 'da' },
|
||||
'de': { label: 'Deutsch', dateFnsLocale: 'de' },
|
||||
@ -49,14 +50,22 @@ Vue.prototype.$podcastSearchRegionOptions = Object.keys(podcastSearchRegionMap).
|
||||
})
|
||||
|
||||
Vue.prototype.$languageCodes = {
|
||||
default: defaultCode,
|
||||
current: defaultCode,
|
||||
local: null,
|
||||
server: null
|
||||
default: defaultCode, // en-us
|
||||
current: defaultCode, // Current language code in use
|
||||
local: null, // Language code set at user level
|
||||
server: null // Language code set at server level
|
||||
}
|
||||
|
||||
// Currently loaded strings (default enUS)
|
||||
Vue.prototype.$strings = { ...enUsStrings }
|
||||
|
||||
/**
|
||||
* Get string and substitute
|
||||
*
|
||||
* @param {string} key
|
||||
* @param {string[]} subs
|
||||
* @returns {string}
|
||||
*/
|
||||
Vue.prototype.$getString = (key, subs) => {
|
||||
if (!Vue.prototype.$strings[key]) return ''
|
||||
if (subs?.length && Array.isArray(subs)) {
|
||||
@ -65,7 +74,11 @@ Vue.prototype.$getString = (key, subs) => {
|
||||
return Vue.prototype.$strings[key]
|
||||
}
|
||||
|
||||
var translations = {
|
||||
Vue.prototype.$formatNumber = (num) => {
|
||||
return Intl.NumberFormat(Vue.prototype.$languageCodes.current).format(num)
|
||||
}
|
||||
|
||||
const translations = {
|
||||
[defaultCode]: enUsStrings
|
||||
}
|
||||
|
||||
|
782
client/strings/bn.json
Normal file
782
client/strings/bn.json
Normal file
@ -0,0 +1,782 @@
|
||||
{
|
||||
"ButtonAdd": "যোগ করুন",
|
||||
"ButtonAddChapters": "অধ্যায় যোগ করুন",
|
||||
"ButtonAddDevice": "ডিভাইস যোগ করুন",
|
||||
"ButtonAddLibrary": "লাইব্রেরি যোগ করুন",
|
||||
"ButtonAddPodcasts": "পডকাস্ট যোগ করুন",
|
||||
"ButtonAddUser": "ব্যবহারকারী যোগ করুন",
|
||||
"ButtonAddYourFirstLibrary": "আপনার প্রথম লাইব্রেরি যোগ করুন",
|
||||
"ButtonApply": "প্রয়োগ করুন",
|
||||
"ButtonApplyChapters": "অধ্যায় প্রয়োগ করুন",
|
||||
"ButtonAuthors": "লেখক",
|
||||
"ButtonBrowseForFolder": "ফোল্ডারের জন্য ব্রাউজ করুন",
|
||||
"ButtonCancel": "বাতিল করুন",
|
||||
"ButtonCancelEncode": "এনকোড বাতিল করুন",
|
||||
"ButtonChangeRootPassword": "রুট পাসওয়ার্ড পরিবর্তন করুন",
|
||||
"ButtonCheckAndDownloadNewEpisodes": "নতুন পর্বগুলি পরীক্ষা এবং ডাউনলোড করুন",
|
||||
"ButtonChooseAFolder": "একটি ফোল্ডার চয়ন করুন",
|
||||
"ButtonChooseFiles": "ফাইল চয়ন করুন",
|
||||
"ButtonClearFilter": "ফিল্টার পরিষ্কার করুন",
|
||||
"ButtonCloseFeed": "ফিড বন্ধ করুন",
|
||||
"ButtonCollections": "সংগ্রহ",
|
||||
"ButtonConfigureScanner": "স্ক্যানার কনফিগার করুন",
|
||||
"ButtonCreate": "তৈরি করুন",
|
||||
"ButtonCreateBackup": "ব্যাকআপ তৈরি করুন",
|
||||
"ButtonDelete": "মুছুন",
|
||||
"ButtonDownloadQueue": "সারি",
|
||||
"ButtonEdit": "সম্পাদনা করুন",
|
||||
"ButtonEditChapters": "অধ্যায় সম্পাদনা করুন",
|
||||
"ButtonEditPodcast": "পডকাস্ট সম্পাদনা করুন",
|
||||
"ButtonForceReScan": "জোরপূর্বক পুনরায় স্ক্যান করুন",
|
||||
"ButtonFullPath": "সম্পূর্ণ পথ",
|
||||
"ButtonHide": "লুকান",
|
||||
"ButtonHome": "নীড়",
|
||||
"ButtonIssues": "ইস্যু",
|
||||
"ButtonJumpBackward": "পিছনে লাফ দিন",
|
||||
"ButtonJumpForward": "সামনে লাফ দিন",
|
||||
"ButtonLatest": "সর্বশেষ",
|
||||
"ButtonLibrary": "লাইব্রেরি",
|
||||
"ButtonLogout": "লগআউট",
|
||||
"ButtonLookup": "সন্ধান",
|
||||
"ButtonManageTracks": "ট্র্যাকগুলি পরিচালনা করুন",
|
||||
"ButtonMapChapterTitles": "অধ্যায়ের শিরোনাম ম্যাপ করুন",
|
||||
"ButtonMatchAllAuthors": "সমস্ত লেখকের সাথে মিল করুন",
|
||||
"ButtonMatchBooks": "বইগুলো মিল করুন",
|
||||
"ButtonNevermind": "কিছু মনে করবেন না",
|
||||
"ButtonNext": "পরবর্তী",
|
||||
"ButtonNextChapter": "পরবর্তী অধ্যায়",
|
||||
"ButtonOk": "ঠিক আছে",
|
||||
"ButtonOpenFeed": "ফিড খুলুন",
|
||||
"ButtonOpenManager": "ম্যানেজার খুলুন",
|
||||
"ButtonPause": "বিরতি",
|
||||
"ButtonPlay": "বাজান",
|
||||
"ButtonPlaying": "বাজছে",
|
||||
"ButtonPlaylists": "প্লেলিস্ট",
|
||||
"ButtonPrevious": "পূর্ববর্তী",
|
||||
"ButtonPreviousChapter": "আগের অধ্যায়",
|
||||
"ButtonPurgeAllCache": "সমস্ত ক্যাশে পরিষ্কার করুন",
|
||||
"ButtonPurgeItemsCache": "আইটেম ক্যাশে পরিষ্কার করুন",
|
||||
"ButtonPurgeMediaProgress": "মিডিয়া ক্যাশে পরিষ্কার করুন",
|
||||
"ButtonQueueAddItem": "সারিতে যোগ করুন",
|
||||
"ButtonQueueRemoveItem": "সারি থেকে মুছে ফেলুন",
|
||||
"ButtonQuickMatch": "দ্রুত ম্যাচ",
|
||||
"ButtonRead": "পড়ুন",
|
||||
"ButtonRefresh": "রিফ্রেশ",
|
||||
"ButtonRemove": "মুছে ফেলুন",
|
||||
"ButtonRemoveAll": "সব মুছে ফেলুন",
|
||||
"ButtonRemoveAllLibraryItems": "সমস্ত লাইব্রেরি আইটেম মুছে ফেলুন",
|
||||
"ButtonRemoveFromContinueListening": "শোনা চালিয়ে যাওয়া থেকে মুছে ফেলুন",
|
||||
"ButtonRemoveFromContinueReading": "পঠন চালিয়ে যান থেকে মুছে ফেলুন",
|
||||
"ButtonRemoveSeriesFromContinueSeries": "কন্টিনিউ সিরিজ থেকে সিরিজ মুছে ফেলুন",
|
||||
"ButtonReScan": "পুনরায় স্ক্যান",
|
||||
"ButtonReset": "রিসেট",
|
||||
"ButtonResetToDefault": "ডিফল্টে পুনরায় সেট করুন",
|
||||
"ButtonRestore": "পুনরুদ্ধার করুন",
|
||||
"ButtonSave": "সংরক্ষণ করুন",
|
||||
"ButtonSaveAndClose": "সংরক্ষণ এবং বন্ধ করুন",
|
||||
"ButtonSaveTracklist": "ট্র্যাকলিস্ট সংরক্ষণ করুন",
|
||||
"ButtonScan": "স্ক্যান",
|
||||
"ButtonScanLibrary": "স্ক্যান লাইব্রেরি",
|
||||
"ButtonSearch": "অনুসন্ধান",
|
||||
"ButtonSelectFolderPath": "ফোল্ডারের পথ নির্বাচন করুন",
|
||||
"ButtonSeries": "সিরিজ",
|
||||
"ButtonSetChaptersFromTracks": "ট্র্যাক থেকে অধ্যায় সেট করুন",
|
||||
"ButtonShare": "শেয়ার করুন",
|
||||
"ButtonShiftTimes": "সময় শিফট করুন",
|
||||
"ButtonShow": "দেখান",
|
||||
"ButtonStartM4BEncode": "M4B এনকোড শুরু করুন",
|
||||
"ButtonStartMetadataEmbed": "মেটাডেটা এম্বেড শুরু করুন",
|
||||
"ButtonSubmit": "জমা দিন",
|
||||
"ButtonTest": "পরীক্ষা",
|
||||
"ButtonUpload": "আপলোড",
|
||||
"ButtonUploadBackup": "আপলোড ব্যাকআপ",
|
||||
"ButtonUploadCover": "কভার আপলোড করুন",
|
||||
"ButtonUploadOPMLFile": "OPML ফাইল আপলোড করুন",
|
||||
"ButtonUserDelete": "ব্যবহারকারী {0} মুছুন",
|
||||
"ButtonUserEdit": "ব্যবহারকারী {0} সম্পাদনা করুন",
|
||||
"ButtonViewAll": "সমস্ত দেখুন",
|
||||
"ButtonYes": "হ্যাঁ",
|
||||
"ErrorUploadFetchMetadataAPI": "মেটাডেটা আনতে ত্রুটি হচ্ছে",
|
||||
"ErrorUploadFetchMetadataNoResults": "মেটাডেটা আনা যায়নি - শিরোনাম এবং/অথবা লেখক আপডেট করার চেষ্টা করুন",
|
||||
"ErrorUploadLacksTitle": "একটি শিরোনাম থাকতে হবে",
|
||||
"HeaderAccount": "অ্যাকাউন্ট",
|
||||
"HeaderAdvanced": "অ্যাডভান্সড",
|
||||
"HeaderAppriseNotificationSettings": "বিজ্ঞপ্তি সেটিংস অবহিত করুন",
|
||||
"HeaderAudiobookTools": "অডিওবই ফাইল ম্যানেজমেন্ট টুলস",
|
||||
"HeaderAudioTracks": "অডিও ট্র্যাকস",
|
||||
"HeaderAuthentication": "প্রমাণীকরণ",
|
||||
"HeaderBackups": "ব্যাকআপ",
|
||||
"HeaderChangePassword": "পাসওয়ার্ড পরিবর্তন করুন",
|
||||
"HeaderChapters": "অধ্যায়",
|
||||
"HeaderChooseAFolder": "একটি ফোল্ডার চয়ন করুন",
|
||||
"HeaderCollection": "সংগ্রহ",
|
||||
"HeaderCollectionItems": "সংগ্রহ আইটেম",
|
||||
"HeaderCover": "কভার",
|
||||
"HeaderCurrentDownloads": "বর্তমান ডাউনলোডগুলি",
|
||||
"HeaderCustomMetadataProviders": "কাস্টম মেটাডেটা প্রদানকারী",
|
||||
"HeaderDetails": "বিস্তারিত",
|
||||
"HeaderDownloadQueue": "ডাউনলোড সারি",
|
||||
"HeaderEbookFiles": "ই-বই ফাইল",
|
||||
"HeaderEmail": "ইমেইল",
|
||||
"HeaderEmailSettings": "ইমেল সেটিংস",
|
||||
"HeaderEpisodes": "পর্ব",
|
||||
"HeaderEreaderDevices": "ই-রিডার ডিভাইস",
|
||||
"HeaderEreaderSettings": "ই-রিডার সেটিংস",
|
||||
"HeaderFiles": "ফাইল",
|
||||
"HeaderFindChapters": "অধ্যায় খুঁজুন",
|
||||
"HeaderIgnoredFiles": "উপেক্ষিত ফাইল",
|
||||
"HeaderItemFiles": "আইটেম ফাইল",
|
||||
"HeaderItemMetadataUtils": "আইটেম মেটাডেটা ইউটিলস",
|
||||
"HeaderLastListeningSession": "শেষ শোনার অধিবেশন",
|
||||
"HeaderLatestEpisodes": "সর্বশেষ পর্ব",
|
||||
"HeaderLibraries": "লাইব্রেরি",
|
||||
"HeaderLibraryFiles": "লাইব্রেরি ফাইল",
|
||||
"HeaderLibraryStats": "লাইব্রেরি পরিসংখ্যান",
|
||||
"HeaderListeningSessions": "শোনার সেশন",
|
||||
"HeaderListeningStats": "শোনার পরিসংখ্যান",
|
||||
"HeaderLogin": "লগইন",
|
||||
"HeaderLogs": "লগস",
|
||||
"HeaderManageGenres": "ঘরানাগুলো পরিচালনা করুন",
|
||||
"HeaderManageTags": "ট্যাগগুলো পরিচালনা করুন",
|
||||
"HeaderMapDetails": "মানচিত্রের বিবরণ",
|
||||
"HeaderMatch": "ম্যাচ",
|
||||
"HeaderMetadataOrderOfPrecedence": "মেটাডেটা অগ্রাধিকারের ক্রম",
|
||||
"HeaderMetadataToEmbed": "এম্বেড করার জন্য মেটাডেটা",
|
||||
"HeaderNewAccount": "নতুন অ্যাকাউন্ট",
|
||||
"HeaderNewLibrary": "নতুন লাইব্রেরি",
|
||||
"HeaderNotifications": "বিজ্ঞপ্তি",
|
||||
"HeaderOpenIDConnectAuthentication": "ওপেনআইডি সংযোগ প্রমাণীকরণ",
|
||||
"HeaderOpenRSSFeed": "আরএসএস ফিড খুলুন",
|
||||
"HeaderOtherFiles": "অন্যান্য ফাইল",
|
||||
"HeaderPasswordAuthentication": "পাসওয়ার্ড প্রমাণীকরণ",
|
||||
"HeaderPermissions": "অনুমতি",
|
||||
"HeaderPlayerQueue": "প্লেয়ার সারি",
|
||||
"HeaderPlaylist": "প্লেলিস্ট",
|
||||
"HeaderPlaylistItems": "প্লেলিস্ট আইটেম",
|
||||
"HeaderPodcastsToAdd": "যোগ করার জন্য পডকাস্ট",
|
||||
"HeaderPreviewCover": "কভার ্দেখুন",
|
||||
"HeaderRemoveEpisode": "পর্বটি সরান",
|
||||
"HeaderRemoveEpisodes": "{0}টি পর্ব সরান",
|
||||
"HeaderRSSFeedGeneral": "আরএসএস বিবরণ",
|
||||
"HeaderRSSFeedIsOpen": "আরএসএস ফিড খোলা আছে",
|
||||
"HeaderRSSFeeds": "আরএসএস ফিড",
|
||||
"HeaderSavedMediaProgress": "মিডিয়া সংরক্ষণের অগ্রগতি",
|
||||
"HeaderSchedule": "সময়সূচী",
|
||||
"HeaderScheduleLibraryScans": "স্বয়ংক্রিয় লাইব্রেরি স্ক্যানের সময়সূচী",
|
||||
"HeaderSession": "সেশন",
|
||||
"HeaderSetBackupSchedule": "ব্যাকআপ সময়সূচী সেট করুন",
|
||||
"HeaderSettings": "সেটিংস",
|
||||
"HeaderSettingsDisplay": "প্রদর্শন",
|
||||
"HeaderSettingsExperimental": "পরীক্ষামূলক ফিচার",
|
||||
"HeaderSettingsGeneral": "সাধারণ",
|
||||
"HeaderSettingsScanner": "স্ক্যানার",
|
||||
"HeaderSleepTimer": "স্লিপ টাইমার",
|
||||
"HeaderStatsLargestItems": "সবচেয়ে বড় আইটেম",
|
||||
"HeaderStatsLongestItems": "দীর্ঘতম আইটেম (ঘন্টা)",
|
||||
"HeaderStatsMinutesListeningChart": "মিনিট শ্রবণ (গত ৭ দিন)",
|
||||
"HeaderStatsRecentSessions": "সাম্প্রতিক সেশন",
|
||||
"HeaderStatsTop10Authors": "শীর্ষ ১০ জন লেখক",
|
||||
"HeaderStatsTop5Genres": "শীর্ষ ৫ টি ঘরানা",
|
||||
"HeaderTableOfContents": "বিষয়বস্তুর সারণী",
|
||||
"HeaderTools": "টুলস",
|
||||
"HeaderUpdateAccount": "অ্যাকাউন্ট আপডেট করুন",
|
||||
"HeaderUpdateAuthor": "লেখক আপডেট করুন",
|
||||
"HeaderUpdateDetails": "বিশদ আপডেট করুন",
|
||||
"HeaderUpdateLibrary": "লাইব্রেরি আপডেট করুন",
|
||||
"HeaderUsers": "ব্যবহারকারীরা",
|
||||
"HeaderYearReview": "বাৎসরিক পর্যালোচনা {0}",
|
||||
"HeaderYourStats": "আপনার পরিসংখ্যান",
|
||||
"LabelAbridged": "সংক্ষিপ্ত",
|
||||
"LabelAccountType": "অ্যাকাউন্টের প্রকার",
|
||||
"LabelAccountTypeAdmin": "প্রশাসন",
|
||||
"LabelAccountTypeGuest": "অতিথি",
|
||||
"LabelAccountTypeUser": "ব্যবহারকারী",
|
||||
"LabelActivity": "ক্রিয়াকলাপ",
|
||||
"LabelAdded": "যোগ করা হয়েছে",
|
||||
"LabelAddedAt": "এতে যোগ করা হয়েছে",
|
||||
"LabelAddToCollection": "সংগ্রহে যোগ করুন",
|
||||
"LabelAddToCollectionBatch": "সংগ্রহে {0}টি বই যোগ করুন",
|
||||
"LabelAddToPlaylist": "প্লেলিস্টে যোগ করুন",
|
||||
"LabelAddToPlaylistBatch": "প্লেলিস্টে {0}টি আইটেম যোগ করুন",
|
||||
"LabelAdminUsersOnly": "শুধু অ্যাডমিন ব্যবহারকারী",
|
||||
"LabelAll": "সব",
|
||||
"LabelAllUsers": "সমস্ত ব্যবহারকারী",
|
||||
"LabelAllUsersExcludingGuests": "অতিথি ব্যতীত সকল ব্যবহারকারী",
|
||||
"LabelAllUsersIncludingGuests": "অতিথি সহ সকল ব্যবহারকারী",
|
||||
"LabelAlreadyInYourLibrary": "ইতিমধ্যেই আপনার লাইব্রেরিতে রয়েছে",
|
||||
"LabelAppend": "সংযোজন",
|
||||
"LabelAuthor": "লেখক",
|
||||
"LabelAuthorFirstLast": "লেখক (প্রথম শেষ)",
|
||||
"LabelAuthorLastFirst": "লেখক (শেষ, প্রথম)",
|
||||
"LabelAuthors": "লেখকগণ",
|
||||
"LabelAutoDownloadEpisodes": "স্বয়ংক্রিয় ডাউনলোড পর্ব",
|
||||
"LabelAutoFetchMetadata": "স্বয়ংক্রিয় ফেচ মেটাডেটা",
|
||||
"LabelAutoFetchMetadataHelp": "আপলোডিং স্ট্রিমলাইন করার জন্য শিরোনাম, লেখক এবং সিরিজের জন্য মেটাডেটা খুঁজুন। আপলোড করার পরে অতিরিক্ত মেটাডেটা মিলতে হতে পারে।",
|
||||
"LabelAutoLaunch": "স্বয়ংক্রিয় আরম্ভ",
|
||||
"LabelAutoLaunchDescription": "লগইন পৃষ্ঠায় নেভিগেট করার সময় স্বয়ংক্রিয়ভাবে অনুমোদন প্রদানকারীর কাছে পুনঃনির্দেশ করুন (হস্তকৃত ওভাররাইড পথ <code>/login?autoLaunch=0</code>)",
|
||||
"LabelAutoRegister": "স্বয়ংক্রিয় নিবন্ধন",
|
||||
"LabelAutoRegisterDescription": "লগ ইন করার পর স্বয়ংক্রিয়ভাবে নতুন ব্যবহারকারী তৈরি করুন",
|
||||
"LabelBackToUser": "ব্যবহারকারীর কাছে ফিরে যান",
|
||||
"LabelBackupLocation": "ব্যাকআপ অবস্থান",
|
||||
"LabelBackupsEnableAutomaticBackups": "স্বয়ংক্রিয় ব্যাকআপ সক্ষম করুন",
|
||||
"LabelBackupsEnableAutomaticBackupsHelp": "ব্যাকআপগুলি /মেটাডাটা/ব্যাকআপে সংরক্ষিত",
|
||||
"LabelBackupsMaxBackupSize": "সর্বোচ্চ ব্যাকআপ আকার (GB-তে)",
|
||||
"LabelBackupsMaxBackupSizeHelp": "ভুল কনফিগারেশনের বিরুদ্ধে সুরক্ষা হিসেবে ব্যাকআপগুলি ব্যর্থ হবে যদি তারা কনফিগার করা আকার অতিক্রম করে।",
|
||||
"LabelBackupsNumberToKeep": "ব্যাকআপের সংখ্যা রাখুন",
|
||||
"LabelBackupsNumberToKeepHelp": "এক সময়ে শুধুমাত্র ১ টি ব্যাকআপ সরানো হবে তাই যদি আপনার কাছে ইতিমধ্যে এর চেয়ে বেশি ব্যাকআপ থাকে তাহলে আপনাকে ম্যানুয়ালি সেগুলি সরিয়ে ফেলতে হবে।",
|
||||
"LabelBitrate": "বিটরেট",
|
||||
"LabelBooks": "বইগুলো",
|
||||
"LabelButtonText": "ঘর পাঠ্য",
|
||||
"LabelChangePassword": "পাসওয়ার্ড পরিবর্তন করুন",
|
||||
"LabelChannels": "চ্যানেল",
|
||||
"LabelChapters": "অধ্যায়",
|
||||
"LabelChaptersFound": "অধ্যায় পাওয়া গেছে",
|
||||
"LabelChapterTitle": "অধ্যায়ের শিরোনাম",
|
||||
"LabelClickForMoreInfo": "আরো তথ্যের জন্য ক্লিক করুন",
|
||||
"LabelClosePlayer": "প্লেয়ার বন্ধ করুন",
|
||||
"LabelCodec": "কোডেক",
|
||||
"LabelCollapseSeries": "সিরিজ সঙ্কুচিত করুন",
|
||||
"LabelCollection": "সংগ্রহ",
|
||||
"LabelCollections": "সংগ্রহ",
|
||||
"LabelComplete": "সম্পূর্ণ",
|
||||
"LabelConfirmPassword": "পাসওয়ার্ড নিশ্চিত করুন",
|
||||
"LabelContinueListening": "শোনা চালিয়ে যান",
|
||||
"LabelContinueReading": "পড়া চালিয়ে যান",
|
||||
"LabelContinueSeries": "সিরিজ চালিয়ে যান",
|
||||
"LabelCover": "কভার",
|
||||
"LabelCoverImageURL": "ছবির কভারের URL",
|
||||
"LabelCreatedAt": "তৈরি করা হয়েছে",
|
||||
"LabelCronExpression": "Cron এক্সপ্রেশন",
|
||||
"LabelCurrent": "বর্তমান",
|
||||
"LabelCurrently": "বর্তমানে:",
|
||||
"LabelCustomCronExpression": "কাস্টম Cron এক্সপ্রেশন:",
|
||||
"LabelDatetime": "তারিখ সময়",
|
||||
"LabelDeleteFromFileSystemCheckbox": "ফাইল সিস্টেম থেকে মুছে ফেলুন (শুধু ডাটাবেস থেকে সরাতে টিক চিহ্ন মুক্ত করুন)",
|
||||
"LabelDescription": "বিবরণ",
|
||||
"LabelDeselectAll": "সমস্ত অনির্বাচিত করুন",
|
||||
"LabelDevice": "ডিভাইস",
|
||||
"LabelDeviceInfo": "ডিভাইস তথ্য",
|
||||
"LabelDeviceIsAvailableTo": "ডিভাইস এর জন্য উপলব্ধ...",
|
||||
"LabelDirectory": "ডিরেক্টরি",
|
||||
"LabelDiscFromFilename": "ফাইলের নাম থেকে ডিস্ক",
|
||||
"LabelDiscFromMetadata": "মেটাডেটা থেকে ডিস্ক",
|
||||
"LabelDiscover": "আবিষ্কার",
|
||||
"LabelDownload": "ডাউনলোড করুন",
|
||||
"LabelDownloadNEpisodes": "{0}টি পর্ব ডাউনলোড করুন",
|
||||
"LabelDuration": "সময়কাল",
|
||||
"LabelDurationFound": "সময়কাল পাওয়া গেছে:",
|
||||
"LabelEbook": "ই-বই",
|
||||
"LabelEbooks": "ই-বইগুলো",
|
||||
"LabelEdit": "সম্পাদনা করুন",
|
||||
"LabelEmail": "ইমেইল",
|
||||
"LabelEmailSettingsFromAddress": "ঠিকানা থেকে",
|
||||
"LabelEmailSettingsSecure": "নিরাপদ",
|
||||
"LabelEmailSettingsSecureHelp": "যদি সত্য হয় সার্ভারের সাথে সংযোগ করার সময় সংযোগটি TLS ব্যবহার করবে। মিথ্যা হলে TLS ব্যবহার করা হবে যদি সার্ভার STARTTLS এক্সটেনশন সমর্থন করে। বেশিরভাগ ক্ষেত্রে এই মানটিকে সত্য হিসাবে সেট করুন যদি আপনি পোর্ট 465-এর সাথে সংযোগ করছেন। পোর্ট 587 বা পোর্টের জন্য 25 এটি মিথ্যা রাখুন। (nodemailer.com/smtp/#authentication থেকে)",
|
||||
"LabelEmailSettingsTestAddress": "পরীক্ষার ঠিকানা",
|
||||
"LabelEmbeddedCover": "এম্বেডেড কভার",
|
||||
"LabelEnable": "সক্ষম করুন",
|
||||
"LabelEnd": "সমাপ্ত",
|
||||
"LabelEpisode": "পর্ব",
|
||||
"LabelEpisodeTitle": "পর্বের শিরোনাম",
|
||||
"LabelEpisodeType": "পর্বের ধরন",
|
||||
"LabelExample": "উদাহরণ",
|
||||
"LabelExplicit": "বিশদ",
|
||||
"LabelFeedURL": "ফিড ইউআরএল",
|
||||
"LabelFetchingMetadata": "মেটাডেটা আনা হচ্ছে",
|
||||
"LabelFile": "ফাইল",
|
||||
"LabelFileBirthtime": "ফাইল জন্মের সময়",
|
||||
"LabelFileModified": "ফাইল পরিবর্তিত",
|
||||
"LabelFilename": "ফাইলের নাম",
|
||||
"LabelFilterByUser": "ব্যবহারকারী দ্বারা ফিল্টারকৃত",
|
||||
"LabelFindEpisodes": "পর্বগুলো খুঁজুন",
|
||||
"LabelFinished": "সমাপ্ত",
|
||||
"LabelFolder": "ফোল্ডার",
|
||||
"LabelFolders": "ফোল্ডারগুলো",
|
||||
"LabelFontBold": "বোল্ড",
|
||||
"LabelFontFamily": "ফন্ট পরিবার",
|
||||
"LabelFontItalic": "ইটালিক",
|
||||
"LabelFontScale": "ফন্ট স্কেল",
|
||||
"LabelFontStrikethrough": "অবচ্ছেদন রেখা",
|
||||
"LabelFormat": "ফরম্যাট",
|
||||
"LabelGenre": "ঘরানা",
|
||||
"LabelGenres": "ঘরানাগুলো",
|
||||
"LabelHardDeleteFile": "জোরপূর্বক ফাইল মুছে ফেলুন",
|
||||
"LabelHasEbook": "ই-বই আছে",
|
||||
"LabelHasSupplementaryEbook": "পরিপূরক ই-বই আছে",
|
||||
"LabelHighestPriority": "সর্বোচ্চ অগ্রাধিকার",
|
||||
"LabelHost": "নিমন্ত্রণকর্তা",
|
||||
"LabelHour": "ঘন্টা",
|
||||
"LabelIcon": "আইকন",
|
||||
"LabelImageURLFromTheWeb": "ওয়েব থেকে ছবির ইউআরএল",
|
||||
"LabelIncludeInTracklist": "ট্র্যাকলিস্টে অন্তর্ভুক্ত করুন",
|
||||
"LabelIncomplete": "অসম্পূর্ণ",
|
||||
"LabelInProgress": "প্রগতিতে আছে",
|
||||
"LabelInterval": "বিরতি",
|
||||
"LabelIntervalCustomDailyWeekly": "কাস্টম দৈনিক/সাপ্তাহিক",
|
||||
"LabelIntervalEvery12Hours": "প্রতি ১২ ঘন্টায়",
|
||||
"LabelIntervalEvery15Minutes": "প্রতি ১৫ মিনিটে",
|
||||
"LabelIntervalEvery2Hours": "প্রতি ২ ঘন্টায়",
|
||||
"LabelIntervalEvery30Minutes": "প্রতি ৩০ মিনিটে",
|
||||
"LabelIntervalEvery6Hours": "প্রতি ৬ ঘন্টায়",
|
||||
"LabelIntervalEveryDay": "প্রতিদিন",
|
||||
"LabelIntervalEveryHour": "প্রতি ঘন্টা",
|
||||
"LabelInvert": "উল্টানো",
|
||||
"LabelItem": "আইটেম",
|
||||
"LabelLanguage": "ভাষা",
|
||||
"LabelLanguageDefaultServer": "সার্ভারের ডিফল্ট ভাষা",
|
||||
"LabelLastBookAdded": "শেষ বই যোগ করা হয়েছে",
|
||||
"LabelLastBookUpdated": "শেষ বই আপডেট করা হয়েছে",
|
||||
"LabelLastSeen": "শেষ দেখা",
|
||||
"LabelLastTime": "শেষ বার",
|
||||
"LabelLastUpdate": "শেষ আপডেট",
|
||||
"LabelLayout": "লেআউট",
|
||||
"LabelLayoutSinglePage": "একক পৃষ্ঠা",
|
||||
"LabelLayoutSplitPage": "বিভক্ত পৃষ্ঠা",
|
||||
"LabelLess": "কম",
|
||||
"LabelLibrariesAccessibleToUser": "ব্যবহারকারীর কাছে অ্যাক্সেসযোগ্য লাইব্রেরি",
|
||||
"LabelLibrary": "লাইব্রেরি",
|
||||
"LabelLibraryItem": "লাইব্রেরি আইটেম",
|
||||
"LabelLibraryName": "লাইব্রেরির নাম",
|
||||
"LabelLimit": "সীমা",
|
||||
"LabelLineSpacing": "লাইন স্পেসিং",
|
||||
"LabelListenAgain": "আবার শুনুন",
|
||||
"LabelLogLevelDebug": "ডিবাগ",
|
||||
"LabelLogLevelInfo": "তথ্য",
|
||||
"LabelLogLevelWarn": "সতর্ক",
|
||||
"LabelLookForNewEpisodesAfterDate": "এই তারিখের পরে নতুন পর্বগুলি সন্ধান করুন",
|
||||
"LabelLowestPriority": "সর্বনিম্ন অগ্রাধিকার",
|
||||
"LabelMatchExistingUsersBy": "বিদ্যমান ব্যবহারকারীদের দ্বারা মিলিত করুন",
|
||||
"LabelMatchExistingUsersByDescription": "বিদ্যমান ব্যবহারকারীদের সংযোগ করার জন্য ব্যবহৃত হয়। একবার সংযুক্ত হলে, ব্যবহারকারীদের আপনার SSO প্রদানকারীর থেকে একটি অনন্য আইডি দ্বারা মিলিত হবে",
|
||||
"LabelMediaPlayer": "মিডিয়া প্লেয়ার",
|
||||
"LabelMediaType": "মিডিয়ার ধরন",
|
||||
"LabelMetadataOrderOfPrecedenceDescription": "উচ্চ অগ্রাধিকারের মেটাডেটার উৎসগুলো নিম্ন অগ্রাধিকারের মেটাডেটা উৎসগুলোকে ওভাররাইড করবে",
|
||||
"LabelMetadataProvider": "মেটাডেটা প্রদানকারী",
|
||||
"LabelMetaTag": "মেটা ট্যাগ",
|
||||
"LabelMetaTags": "মেটা ট্যাগগুলো",
|
||||
"LabelMinute": "মিনিট",
|
||||
"LabelMissing": "নিখোঁজ",
|
||||
"LabelMissingEbook": "কোনও ই-বই নেই",
|
||||
"LabelMissingSupplementaryEbook": "কোনও সম্পূরক ই-বই নেই",
|
||||
"LabelMobileRedirectURIs": "অনুমোদিত মোবাইল রিডাইরেক্ট URIs",
|
||||
"LabelMobileRedirectURIsDescription": "এটি মোবাইল অ্যাপের জন্য বৈধ পুনঃনির্দেশিত URI-এর একটি সাদা তালিকা। ডিফল্টটি হল <code>audiobookshelf://oauth</code>, যা আপনি তৃতীয় পক্ষের অ্যাপ ইন্টিগ্রেশনের জন্য অতিরিক্ত URI-এর সাথে সরাতে বা সম্পূরক করতে পারেন। একটি তারকাচিহ্ন (<code>*</code>) ব্যবহার করে একমাত্র এন্ট্রি যেকোন ইউআরআইকে অনুমতি দেয়।",
|
||||
"LabelMore": "আরো",
|
||||
"LabelMoreInfo": "আরো তথ্য",
|
||||
"LabelName": "নাম",
|
||||
"LabelNarrator": "কথক",
|
||||
"LabelNarrators": "কথক",
|
||||
"LabelNew": "নতুন",
|
||||
"LabelNewestAuthors": "নতুন লেখক",
|
||||
"LabelNewestEpisodes": "নতুনতম পর্ব",
|
||||
"LabelNewPassword": "নতুন পাসওয়ার্ড",
|
||||
"LabelNextBackupDate": "পরবর্তী ব্যাকআপ তারিখ",
|
||||
"LabelNextScheduledRun": "পরবর্তী নির্ধারিত দৌড়",
|
||||
"LabelNoEpisodesSelected": "কোন পর্ব নির্বাচন করা হয়নি",
|
||||
"LabelNotes": "নোটস",
|
||||
"LabelNotFinished": "সমাপ্ত হয়নি",
|
||||
"LabelNotificationAppriseURL": "অবহিত URL(গুলি)",
|
||||
"LabelNotificationAvailableVariables": "ব্যবহারযোগ্য ভেরিয়েবল",
|
||||
"LabelNotificationBodyTemplate": "বডি টেমপ্লেট",
|
||||
"LabelNotificationEvent": "ইভেন্ট বিজ্ঞপ্তি",
|
||||
"LabelNotificationsMaxFailedAttempts": "সর্বোচ্চ ব্যর্থ প্রচেষ্টা",
|
||||
"LabelNotificationsMaxFailedAttemptsHelp": "এটি বারবার পাঠাতে ব্যর্থ হলে বিজ্ঞপ্তি অক্ষম করা হবে",
|
||||
"LabelNotificationsMaxQueueSize": "বিজ্ঞপ্তি ইভেন্টের জন্য সর্বোচ্চ সারির আকার",
|
||||
"LabelNotificationsMaxQueueSizeHelp": "ইভেন্টগুলি প্রতি সেকেন্ডে ১ বার ইন্ধন করার মধ্যে সীমাবদ্ধ। সারি সর্বাধিক আকারে থাকলে ইভেন্টগুলি উপেক্ষা করা হবে। এটি বিজ্ঞপ্তি স্প্যামিং প্রতিরোধ করে।",
|
||||
"LabelNotificationTitleTemplate": "শিরোনাম টেমপ্লেট",
|
||||
"LabelNotStarted": "শুরু হয়নি",
|
||||
"LabelNumberOfBooks": "বইয়ের সংখ্যা",
|
||||
"LabelNumberOfEpisodes": "# টি পর্ব",
|
||||
"LabelOpenIDAdvancedPermsClaimDescription": "ওপেনআইডি দাবির নাম যাতে অ্যাপ্লিকেশনের মধ্যে ব্যবহারকারীর ক্রিয়াকলাপের জন্য উন্নত অনুমতি রয়েছে যা অ-প্রশাসক ভূমিকাগুলিতে প্রযোজ্য হবে (<b>যদি কনফিগার করা হয়</b>)। প্রতিক্রিয়া থেকে দাবিটি অনুপস্থিত থাকলে, অ্যাক্সেস করুন ABS-তে অস্বীকার করা হবে। যদি একটি একক বিকল্প অনুপস্থিত থাকে, তাহলে এটিকে <code>false</code> হিসাবে গণ্য করা হবে। নিশ্চিত করুন যে পরিচয় প্রদানকারীর দাবি প্রত্যাশিত কাঠামোর সাথে মেলে:",
|
||||
"LabelOpenIDClaims": "অ্যাডভান্সড গ্রুপ এবং পারমিশন অ্যাসাইনমেন্ট নিষ্ক্রিয় করতে নিম্নলিখিত বিকল্পগুলিকে খালি ছেড়ে দিন, তারপর স্বয়ংক্রিয়ভাবে 'ব্যবহারকারী' গ্রুপকে বরাদ্দ করা হবে।",
|
||||
"LabelOpenIDGroupClaimDescription": "ওপেনআইডি দাবির নাম যাতে ব্যবহারকারীর গোষ্ঠীর একটি তালিকা থাকে। সাধারণত <code>গ্রুপ</code> হিসাবে উল্লেখ করা হয়। <b>কনফিগার করা থাকলে</b>, অ্যাপ্লিকেশনটি স্বয়ংক্রিয়ভাবে এর উপর ভিত্তি করে ব্যবহারকারীর গোষ্ঠীর সদস্যপদ নির্ধারণ করবে, শর্ত এই যে এই গোষ্ঠীগুলি কেস-অসংবেদনশীলভাবে দাবিতে 'অ্যাডমিন', 'ব্যবহারকারী' বা 'অতিথি' নাম দেওয়া হয়৷ দাবিতে একটি তালিকা থাকা উচিত এবং যদি একজন ব্যবহারকারী একাধিক গোষ্ঠীর অন্তর্গত হয় তবে অ্যাপ্লিকেশনটি বরাদ্দ করবে সর্বোচ্চ স্তরের অ্যাক্সেসের সাথে সঙ্গতিপূর্ণ ভূমিকা৷ যদি কোনও গোষ্ঠীর সাথে মেলে না, তবে অ্যাক্সেস অস্বীকার করা হবে।",
|
||||
"LabelOpenRSSFeed": "আরএসএস ফিড খুলুন",
|
||||
"LabelOverwrite": "পুনঃলিখিত",
|
||||
"LabelPassword": "পাসওয়ার্ড",
|
||||
"LabelPath": "পথ",
|
||||
"LabelPermissionsAccessAllLibraries": "সমস্ত লাইব্রেরি অ্যাক্সেস করতে পারবে",
|
||||
"LabelPermissionsAccessAllTags": "সমস্ত ট্যাগ অ্যাক্সেস করতে পারবে",
|
||||
"LabelPermissionsAccessExplicitContent": "স্পষ্ট বিষয়বস্তু অ্যাক্সেস করতে পারে",
|
||||
"LabelPermissionsDelete": "মুছে দিতে পারবে",
|
||||
"LabelPermissionsDownload": "ডাউনলোড করতে পারবে",
|
||||
"LabelPermissionsUpdate": "আপডেট করতে পারবে",
|
||||
"LabelPermissionsUpload": "আপলোড করতে পারবে",
|
||||
"LabelPersonalYearReview": "আপনার বছরের পর্যালোচনা ({0})",
|
||||
"LabelPhotoPathURL": "ছবি পথ/ইউআরএল",
|
||||
"LabelPlaylists": "প্লেলিস্ট",
|
||||
"LabelPlayMethod": "প্লে পদ্ধতি",
|
||||
"LabelPodcast": "পডকাস্ট",
|
||||
"LabelPodcasts": "পডকাস্টগুলো",
|
||||
"LabelPodcastSearchRegion": "পডকাস্ট অনুসন্ধান অঞ্চল",
|
||||
"LabelPodcastType": "পডকাস্টের ধরন",
|
||||
"LabelPort": "পোর্ট",
|
||||
"LabelPrefixesToIgnore": "উপেক্ষা করার উপসর্গ (কেস সংবেদনশীল)",
|
||||
"LabelPreventIndexing": "আইটিউনস এবং গুগল পডকাস্ট ডিরেক্টরি দ্বারা আপনার ফিডকে ইন্ডেক্স করা থেকে বিরত রাখুন",
|
||||
"LabelPrimaryEbook": "প্রাথমিক ই-বই",
|
||||
"LabelProgress": "প্রগতি",
|
||||
"LabelProvider": "প্রদানকারী",
|
||||
"LabelPubDate": "প্রকাশের তারিখ",
|
||||
"LabelPublisher": "প্রকাশক",
|
||||
"LabelPublishYear": "প্রকাশের বছর",
|
||||
"LabelRead": "পড়ুন",
|
||||
"LabelReadAgain": "আবার পড়ুন",
|
||||
"LabelReadEbookWithoutProgress": "প্রগতি না রেখে ই-বই পড়ুন",
|
||||
"LabelRecentlyAdded": "সম্প্রতি যোগ করা হয়েছে",
|
||||
"LabelRecentSeries": "সাম্প্রতিক সিরিজ",
|
||||
"LabelRecommended": "সুপারিশকৃত",
|
||||
"LabelRedo": "পুনরায় করুন",
|
||||
"LabelRegion": "অঞ্চল",
|
||||
"LabelReleaseDate": "উন্মোচনের তারিখ",
|
||||
"LabelRemoveCover": "কভার সরান",
|
||||
"LabelRowsPerPage": "প্রতি পৃষ্ঠায় সারি",
|
||||
"LabelRSSFeedCustomOwnerEmail": "কাস্টম মালিকের ইমেইল",
|
||||
"LabelRSSFeedCustomOwnerName": "কাস্টম মালিকের নাম",
|
||||
"LabelRSSFeedOpen": "আরএসএস ফিড খুলুন",
|
||||
"LabelRSSFeedPreventIndexing": "সূচীকরণ প্রতিরোধ করুন",
|
||||
"LabelRSSFeedSlug": "আরএসএস ফিড স্লাগ",
|
||||
"LabelRSSFeedURL": "আরএসএস ফিড ইউআরএল",
|
||||
"LabelSearchTerm": "অনুসন্ধান শব্দ",
|
||||
"LabelSearchTitle": "অনুসন্ধান শিরোনাম",
|
||||
"LabelSearchTitleOrASIN": "অনুসন্ধান শিরোনাম বা ASIN",
|
||||
"LabelSeason": "সেশন",
|
||||
"LabelSelectAllEpisodes": "সমস্ত পর্ব নির্বাচন করুন",
|
||||
"LabelSelectEpisodesShowing": "দেখানো {0}টি পর্ব নির্বাচন করুন",
|
||||
"LabelSelectUsers": "ব্যবহারকারী নির্বাচন করুন",
|
||||
"LabelSendEbookToDevice": "ই-বই পাঠান...",
|
||||
"LabelSequence": "ক্রম",
|
||||
"LabelSeries": "সিরিজ",
|
||||
"LabelSeriesName": "সিরিজের নাম",
|
||||
"LabelSeriesProgress": "সিরিজের অগ্রগতি",
|
||||
"LabelServerYearReview": "সার্ভারের বাৎসরিক পর্যালোচনা ({0})",
|
||||
"LabelSetEbookAsPrimary": "প্রাথমিক হিসাবে সেট করুন",
|
||||
"LabelSetEbookAsSupplementary": "পরিপূরক হিসেবে সেট করুন",
|
||||
"LabelSettingsAudiobooksOnly": "শুধুমাত্র অডিও বই",
|
||||
"LabelSettingsAudiobooksOnlyHelp": "এই সেটিংটি সক্ষম করা ই-বই ফাইলগুলিকে উপেক্ষা করবে যদি না সেগুলি একটি অডিওবই ফোল্ডারের মধ্যে থাকে যে ক্ষেত্রে সেগুলিকে সম্পূরক ই-বই হিসাবে সেট করা হবে",
|
||||
"LabelSettingsBookshelfViewHelp": "কাঠের তাক সহ স্কুমরফিক ডিজাইন",
|
||||
"LabelSettingsChromecastSupport": "ক্রোমকাস্ট সমর্থন",
|
||||
"LabelSettingsDateFormat": "তারিখ বিন্যাস",
|
||||
"LabelSettingsDisableWatcher": "প্রহরী নিষ্ক্রিয় করুন",
|
||||
"LabelSettingsDisableWatcherForLibrary": "লাইব্রেরির জন্য ফোল্ডার প্রহরী নিষ্ক্রিয় করুন",
|
||||
"LabelSettingsDisableWatcherHelp": "ফাইলের পরিবর্তন শনাক্ত হলে স্বয়ংক্রিয়ভাবে আইটেম যোগ/আপডেট করা অক্ষম করবে। *সার্ভার পুনরায় চালু করতে হবে",
|
||||
"LabelSettingsEnableWatcher": "প্রহরী সক্ষম করুন",
|
||||
"LabelSettingsEnableWatcherForLibrary": "লাইব্রেরির জন্য ফোল্ডার প্রহরী সক্ষম করুন",
|
||||
"LabelSettingsEnableWatcherHelp": "ফাইলের পরিবর্তন শনাক্ত হলে আইটেমগুলির স্বয়ংক্রিয় যোগ/আপডেট সক্ষম করবে। *সার্ভার পুনরায় চালু করতে হবে",
|
||||
"LabelSettingsExperimentalFeatures": "পরীক্ষামূলক বৈশিষ্ট্য",
|
||||
"LabelSettingsExperimentalFeaturesHelp": "ফিচারের বৈশিষ্ট্য যা আপনার প্রতিক্রিয়া ব্যবহার করতে পারে এবং পরীক্ষায় সহায়তা করতে পারে। গিটহাব আলোচনা খুলতে ক্লিক করুন।",
|
||||
"LabelSettingsFindCovers": "কভার খুঁজুন",
|
||||
"LabelSettingsFindCoversHelp": "যদি আপনার অডিওবইয়ের ফোল্ডারের ভিতরে একটি এমবেডেড কভার বা কভার ইমেজ না থাকে, তাহলে স্ক্যানার একটি কভার খোঁজার চেষ্টা করবে৷<br>দ্রষ্টব্য: এটি স্ক্যানের সময় বাড়িয়ে দেবে",
|
||||
"LabelSettingsHideSingleBookSeries": "একক বই সিরিজ লুকান",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "যে সিরিজগুলোতে একটি বই আছে সেগুলো সিরিজের পাতা এবং নীড় পেজের তাক থেকে লুকিয়ে রাখা হবে।",
|
||||
"LabelSettingsHomePageBookshelfView": "নীড় পেজে বুকশেলফ ভিউ ব্যবহার করুন",
|
||||
"LabelSettingsLibraryBookshelfView": "লাইব্রেরি বুকশেলফ ভিউ ব্যবহার করুন",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "কন্টিনিউ সিরিজে আগের বইগুলো এড়িয়ে যান",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "কন্টিনিউ সিরিজের হোম পেজ শেল্ফ প্রথম বইটি দেখায় যেটি সিরিজে শুরু হয়নি যেটিতে অন্তত একটি বই শেষ হয়েছে এবং কোনো বই চলছে না। এই সেটিংটি সক্ষম করা হলে তা শুরু না হওয়া প্রথম বইটির পরিবর্তে সবচেয়ে দূরের সম্পূর্ণ বই থেকে সিরিজ চালিয়ে যাবে। ",
|
||||
"LabelSettingsParseSubtitles": "সাবটাইটেল পার্স করুন",
|
||||
"LabelSettingsParseSubtitlesHelp": "অডিওবুক ফোল্ডারের নাম থেকে সাবটাইটেল বের করুন৷<br>সাবটাইটেল অবশ্যই \" - \"<br>অর্থাৎ \"বুকের শিরোনাম - এখানে একটি সাবটাইটেল\" এর সাবটাইটেল আছে \"এখানে একটি সাবটাইটেল\"",
|
||||
"LabelSettingsPreferMatchedMetadata": "মিলিত মেটাডেটা পছন্দ করুন",
|
||||
"LabelSettingsPreferMatchedMetadataHelp": "দ্রুত ম্যাচ ব্যবহার করার সময় মিলে যাওয়া ডেটা আইটেমের বিবরণকে ওভাররাইড করবে। ডিফল্টরূপে দ্রুত ম্যাচ শুধুমাত্র অনুপস্থিত বিশদগুলি পূরণ করবে।",
|
||||
"LabelSettingsSkipMatchingBooksWithASIN": "এমন বইগুলি এড়িয়ে যান যেগুলির মধ্যে ইতিমধ্যে একটি ASIN আছে",
|
||||
"LabelSettingsSkipMatchingBooksWithISBN": "ইতিমধ্যে একটি ISBN আছে এমন মেলা বইগুলি এড়িয়ে যান",
|
||||
"LabelSettingsSortingIgnorePrefixes": "বাছাই করার সময় উপসর্গ উপেক্ষা করুন",
|
||||
"LabelSettingsSortingIgnorePrefixesHelp": "অর্থাৎ \"বইয়ের শিরোনাম\" বইয়ের শিরোনাম \"বইয়ের শিরোনাম, \" হিসাবে সাজানো হবে উপসর্গের জন্য",
|
||||
"LabelSettingsSquareBookCovers": "বর্গাকার বইয়ের কভার ব্যবহার করুন",
|
||||
"LabelSettingsSquareBookCoversHelp": "প্রমাণ ১.৬:১ বইয়ের কভারের চেয়ে বর্গাকার কভার ব্যবহার করতে পছন্দ করুন",
|
||||
"LabelSettingsStoreCoversWithItem": "আইটেম সহ কভার সংরক্ষণ",
|
||||
"LabelSettingsStoreCoversWithItemHelp": "ডিফল্টভাবে কভারগুলি /মেটাডাটা/আইটেমগুলিতে সংরক্ষণ করা হয়, এই সেটিংটি সক্ষম করলে আপনার লাইব্রেরি আইটেম ফোল্ডারে কভারগুলি সংরক্ষণ করা হবে৷ \"কভার\" নামে শুধুমাত্র একটি ফাইল রাখা হবে",
|
||||
"LabelSettingsStoreMetadataWithItem": "আইটেমের সাথে মেটাডেটা সংরক্ষণ করুন",
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "ডিফল্টরূপে মেটাডেটা ফাইলগুলি /মেটাডাটা/আইটেমগুলি -এ সংরক্ষণ করা হয়, এই সেটিংটি সক্ষম করলে মেটাডেটা ফাইলগুলি আপনার লাইব্রেরি আইটেম ফোল্ডারে সংরক্ষণ করা হবে",
|
||||
"LabelSettingsTimeFormat": "সময় বিন্যাস",
|
||||
"LabelShowAll": "সব দেখান",
|
||||
"LabelSize": "আকার",
|
||||
"LabelSleepTimer": "স্লিপ টাইমার",
|
||||
"LabelSlug": "স্লাগ",
|
||||
"LabelStart": "শুরু",
|
||||
"LabelStarted": "শুরু হয়েছে",
|
||||
"LabelStartedAt": "এতে শুরু হয়েছে",
|
||||
"LabelStartTime": "শুরু করার সময়",
|
||||
"LabelStatsAudioTracks": "অডিও ট্র্যাক",
|
||||
"LabelStatsAuthors": "লেখক",
|
||||
"LabelStatsBestDay": "সেরা দিন",
|
||||
"LabelStatsDailyAverage": "দৈনিক গড়",
|
||||
"LabelStatsDays": "দিন",
|
||||
"LabelStatsDaysListened": "যেদিন শোনা হয়েছে",
|
||||
"LabelStatsHours": "ঘন্টা",
|
||||
"LabelStatsInARow": "এক সারিতে",
|
||||
"LabelStatsItemsFinished": "আইটেম সমাপ্ত",
|
||||
"LabelStatsItemsInLibrary": "লাইব্রেরির আইটেম",
|
||||
"LabelStatsMinutes": "মিনিট",
|
||||
"LabelStatsMinutesListening": "মিনিট শুনছেন",
|
||||
"LabelStatsOverallDays": "সামগ্রিক দিন",
|
||||
"LabelStatsOverallHours": "সামগ্রিক ঘন্টা",
|
||||
"LabelStatsWeekListening": "সপ্তাহ শোনা",
|
||||
"LabelSubtitle": "সাবটাইটেল",
|
||||
"LabelSupportedFileTypes": "সমর্থিত ফাইল প্রকার",
|
||||
"LabelTag": "ট্যাগ",
|
||||
"LabelTags": "ট্যাগগুলো",
|
||||
"LabelTagsAccessibleToUser": "ব্যবহারকারীর কাছে অ্যাক্সেসযোগ্য ট্যাগ",
|
||||
"LabelTagsNotAccessibleToUser": "ট্যাগগুলি ব্যবহারকারীর কাছে অ্যাক্সেসযোগ্য নয়",
|
||||
"LabelTasks": "কাজ চলছে",
|
||||
"LabelTextEditorBulletedList": "বুলেটেড তালিকা",
|
||||
"LabelTextEditorLink": "লিঙ্ক",
|
||||
"LabelTextEditorNumberedList": "সংখ্যাযুক্ত তালিকা",
|
||||
"LabelTextEditorUnlink": "বিচ্ছিন্ন",
|
||||
"LabelTheme": "থিম",
|
||||
"LabelThemeDark": "অন্ধকার",
|
||||
"LabelThemeLight": "আলো",
|
||||
"LabelTimeBase": "সময় বেস",
|
||||
"LabelTimeListened": "সময় শোনা হয়েছে",
|
||||
"LabelTimeListenedToday": "আজ শোনার সময়",
|
||||
"LabelTimeRemaining": "{0}টি অবশিষ্ট",
|
||||
"LabelTimeToShift": "সেকেন্ডে স্থানান্তরের সময়",
|
||||
"LabelTitle": "শিরোনাম",
|
||||
"LabelToolsEmbedMetadata": "মেটাডেটা এম্বেড করুন",
|
||||
"LabelToolsEmbedMetadataDescription": "কভার ইমেজ এবং অধ্যায় সহ অডিও ফাইলগুলিতে মেটাডেটা এম্বেড করুন।",
|
||||
"LabelToolsMakeM4b": "M4B অডিওবুক ফাইল তৈরি করুন",
|
||||
"LabelToolsMakeM4bDescription": "এমবেডেড মেটাডেটা, কভার ইমেজ এবং অধ্যায় সহ একটি .M4B অডিওবুক ফাইল তৈরি করুন।",
|
||||
"LabelToolsSplitM4b": "M4B কে MP3 তে বিভক্ত করুন",
|
||||
"LabelToolsSplitM4bDescription": "এমবেডেড মেটাডেটা, কভার ইমেজ এবং অধ্যায় সহ অধ্যায় দ্বারা একটি M4B বিভক্ত থেকে MP3 তৈরি করুন।",
|
||||
"LabelTotalDuration": "মোট সময়কাল",
|
||||
"LabelTotalTimeListened": "মোট সময় শোনা",
|
||||
"LabelTrackFromFilename": "ফাইলের নাম থেকে ট্র্যাক করুন",
|
||||
"LabelTrackFromMetadata": "মেটাডেটা থেকে ট্র্যাক করুন",
|
||||
"LabelTracks": "ট্র্যাকস",
|
||||
"LabelTracksMultiTrack": "মাল্টি-ট্র্যাক",
|
||||
"LabelTracksNone": "কোন ট্র্যাক নেই",
|
||||
"LabelTracksSingleTrack": "একক-ট্র্যাক",
|
||||
"LabelType": "টাইপ",
|
||||
"LabelUnabridged": "অসংলগ্ন",
|
||||
"LabelUndo": "পূর্বাবস্থা",
|
||||
"LabelUnknown": "অজানা",
|
||||
"LabelUpdateCover": "কভার আপডেট করুন",
|
||||
"LabelUpdateCoverHelp": "একটি মিল থাকা অবস্থায় নির্বাচিত বইগুলির বিদ্যমান কভারগুলি ওভাররাইট করার অনুমতি দিন",
|
||||
"LabelUpdatedAt": "আপডেট করা হয়েছে",
|
||||
"LabelUpdateDetails": "বিশদ আপডেট করুন",
|
||||
"LabelUpdateDetailsHelp": "একটি মিল থাকা অবস্থায় নির্বাচিত বইগুলির বিদ্যমান বিবরণ ওভাররাইট করার অনুমতি দিন",
|
||||
"LabelUploaderDragAndDrop": "ফাইল বা ফোল্ডার টেনে আনুন এবং ফেলে দিন",
|
||||
"LabelUploaderDropFiles": "ফাইলগুলো ফেলে দিন",
|
||||
"LabelUploaderItemFetchMetadataHelp": "স্বয়ংক্রিয়ভাবে শিরোনাম, লেখক এবং সিরিজ আনুন",
|
||||
"LabelUseChapterTrack": "অধ্যায় ট্র্যাক ব্যবহার করুন",
|
||||
"LabelUseFullTrack": "সম্পূর্ণ ট্র্যাক ব্যবহার করুন",
|
||||
"LabelUser": "ব্যবহারকারী",
|
||||
"LabelUsername": "ব্যবহারকারীর নাম",
|
||||
"LabelValue": "মান",
|
||||
"LabelVersion": "সংস্করণ",
|
||||
"LabelViewBookmarks": "বুকমার্ক দেখুন",
|
||||
"LabelViewChapters": "অধ্যায় দেখুন",
|
||||
"LabelViewQueue": "প্লেয়ার সারি দেখুন",
|
||||
"LabelVolume": "ভলিউম",
|
||||
"LabelWeekdaysToRun": "চলতে হবে সপ্তাহের দিন",
|
||||
"LabelYearReviewHide": "পর্যালোচনার বছর লুকান",
|
||||
"LabelYearReviewShow": "পর্যালোচনার বছর দেখুন",
|
||||
"LabelYourAudiobookDuration": "আপনার অডিওবুকের সময়কাল",
|
||||
"LabelYourBookmarks": "আপনার বুকমার্কস",
|
||||
"LabelYourPlaylists": "আপনার প্লেলিস্ট",
|
||||
"LabelYourProgress": "আপনার অগ্রগতি",
|
||||
"MessageAddToPlayerQueue": "প্লেয়ার সারিতে যোগ করুন",
|
||||
"MessageAppriseDescription": "এই বৈশিষ্ট্যটি ব্যবহার করার জন্য আপনাকে <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</-এর একটি উদাহরণ থাকতে হবে a> চলমান বা একটি এপিআই যা সেই একই অনুরোধগুলি পরিচালনা করবে৷ <br /> বিজ্ঞপ্তি পাঠানোর জন্য Apprise API Url সম্পূর্ণ URL পাথ হওয়া উচিত, যেমন, যদি আপনার 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>।",
|
||||
"MessageBatchQuickMatchDescription": "কুইক ম্যাচ নির্বাচিত আইটেমগুলির জন্য অনুপস্থিত কভার এবং মেটাডেটা যোগ করার চেষ্টা করবে। বিদ্যমান কভার এবং/অথবা মেটাডেটা ওভাররাইট করার জন্য দ্রুত ম্যাচকে অনুমতি দিতে নীচের বিকল্পগুলি সক্ষম করুন।",
|
||||
"MessageBookshelfNoCollections": "আপনি এখনও কোনো সংগ্রহ করেননি",
|
||||
"MessageBookshelfNoResultsForFilter": "ফিল্টার \"{0}: {1}\" এর জন্য কোন ফলাফল নেই",
|
||||
"MessageBookshelfNoRSSFeeds": "কোনও RSS ফিড খোলা নেই",
|
||||
"MessageBookshelfNoSeries": "আপনার কোনো সিরিজ নেই",
|
||||
"MessageChapterEndIsAfter": "অধ্যায়ের সমাপ্তি আপনার অডিওবুকের শেষে",
|
||||
"MessageChapterErrorFirstNotZero": "প্রথম অধ্যায় 0 এ শুরু হতে হবে",
|
||||
"MessageChapterErrorStartGteDuration": "অবৈধ শুরুর সময় অবশ্যই অডিওবুকের সময়কালের কম হতে হবে",
|
||||
"MessageChapterErrorStartLtPrev": "অবৈধ শুরুর সময় অবশ্যই আগের অধ্যায় শুরুর সময়ের চেয়ে বেশি বা সমান হতে হবে",
|
||||
"MessageChapterStartIsAfter": "আপনার অডিওবুক শেষ হওয়ার পরে অধ্যায় শুরু হয়",
|
||||
"MessageCheckingCron": "ক্রন পরীক্ষা করা হচ্ছে...",
|
||||
"MessageConfirmCloseFeed": "আপনি কি নিশ্চিত যে আপনি এই ফিডটি বন্ধ করতে চান?",
|
||||
"MessageConfirmDeleteBackup": "আপনি কি নিশ্চিত যে আপনি {0} এর ব্যাকআপ মুছে ফেলতে চান?",
|
||||
"MessageConfirmDeleteFile": "এটি আপনার ফাইল সিস্টেম থেকে ফাইলটি মুছে দেবে। আপনি কি নিশ্চিত?",
|
||||
"MessageConfirmDeleteLibrary": "আপনি কি নিশ্চিত যে আপনি স্থায়ীভাবে লাইব্রেরি \"{0}\" মুছে ফেলতে চান?",
|
||||
"MessageConfirmDeleteLibraryItem": "এটি ডাটাবেস এবং আপনার ফাইল সিস্টেম থেকে লাইব্রেরি আইটেমটি মুছে ফেলবে। আপনি কি নিশ্চিত?",
|
||||
"MessageConfirmDeleteLibraryItems": "এটি ডাটাবেস এবং আপনার ফাইল সিস্টেম থেকে {0}টি লাইব্রেরি আইটেম মুছে ফেলবে। আপনি কি নিশ্চিত?",
|
||||
"MessageConfirmDeleteSession": "আপনি কি নিশ্চিত আপনি এই অধিবেশন মুছে দিতে চান?",
|
||||
"MessageConfirmForceReScan": "আপনি কি নিশ্চিত যে আপনি জোর করে পুনরায় স্ক্যান করতে চান?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "আপনি কি নিশ্চিত যে আপনি সমস্ত পর্ব সমাপ্ত হিসাবে চিহ্নিত করতে চান?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "আপনি কি নিশ্চিত যে আপনি সমস্ত পর্বকে শেষ হয়নি বলে চিহ্নিত করতে চান?",
|
||||
"MessageConfirmMarkSeriesFinished": "আপনি কি নিশ্চিত যে আপনি এই সিরিজের সমস্ত বইকে সমাপ্ত হিসাবে চিহ্নিত করতে চান?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "আপনি কি নিশ্চিত যে আপনি এই সিরিজের সমস্ত বইকে শেষ হয়নি বলে চিহ্নিত করতে চান?",
|
||||
"MessageConfirmQuickEmbed": "সতর্কতা! দ্রুত এম্বেড আপনার অডিও ফাইলের ব্যাকআপ করবে না। নিশ্চিত করুন যে আপনার অডিও ফাইলগুলির একটি ব্যাকআপ আছে। <br><br>আপনি কি চালিয়ে যেতে চান?",
|
||||
"MessageConfirmRemoveAllChapters": "আপনি কি নিশ্চিত যে আপনি সমস্ত অধ্যায় সরাতে চান?",
|
||||
"MessageConfirmRemoveAuthor": "আপনি কি নিশ্চিত যে আপনি লেখক \"{0}\" অপসারণ করতে চান?",
|
||||
"MessageConfirmRemoveCollection": "আপনি কি নিশ্চিত যে আপনি সংগ্রহ \"{0}\" সরাতে চান?",
|
||||
"MessageConfirmRemoveEpisode": "আপনি কি নিশ্চিত আপনি \"{0}\" পর্বটি সরাতে চান?",
|
||||
"MessageConfirmRemoveEpisodes": "আপনি কি নিশ্চিত যে আপনি {0}টি পর্ব সরাতে চান?",
|
||||
"MessageConfirmRemoveListeningSessions": "আপনি কি নিশ্চিত যে আপনি {0}টি শোনার সেশন সরাতে চান?",
|
||||
"MessageConfirmRemoveNarrator": "আপনি কি \"{0}\" বর্ণনাকারীকে সরানোর বিষয়ে নিশ্চিত?",
|
||||
"MessageConfirmRemovePlaylist": "আপনি কি নিশ্চিত যে আপনি আপনার প্লেলিস্ট \"{0}\" সরাতে চান?",
|
||||
"MessageConfirmRenameGenre": "আপনি কি নিশ্চিত যে আপনি সমস্ত আইটেমের জন্য \"{0}\" ধারার নাম পরিবর্তন করে \"{1}\" করতে চান?",
|
||||
"MessageConfirmRenameGenreMergeNote": "দ্রষ্টব্য: এই ধারাটি আগে থেকেই বিদ্যমান তাই সেগুলিকে একত্রিত করা হবে।",
|
||||
"MessageConfirmRenameGenreWarning": "সতর্কতা! একটি ভিন্ন কেসিং সহ একটি অনুরূপ ধারা ইতিমধ্যেই বিদ্যমান \"{0}\"।",
|
||||
"MessageConfirmRenameTag": "আপনি কি সব আইটেমের জন্য \"{0}\" ট্যাগের নাম পরিবর্তন করে \"{1}\" করার বিষয়ে নিশ্চিত?",
|
||||
"MessageConfirmRenameTagMergeNote": "দ্রষ্টব্য: এই ট্যাগটি আগে থেকেই বিদ্যমান তাই সেগুলিকে একত্র করা হবে।",
|
||||
"MessageConfirmRenameTagWarning": "সতর্কতা! একটি ভিন্ন কেসিং সহ একটি অনুরূপ ট্যাগ ইতিমধ্যেই বিদ্যমান \"{0}\"।",
|
||||
"MessageConfirmReScanLibraryItems": "আপনি কি নিশ্চিত যে আপনি {0}টি আইটেম পুনরায় স্ক্যান করতে চান?",
|
||||
"MessageConfirmSendEbookToDevice": "আপনি কি নিশ্চিত যে আপনি \"{2}\" ডিভাইসে {0} ইবুক \"{1}\" পাঠাতে চান?",
|
||||
"MessageDownloadingEpisode": "ডাউনলোডিং পর্ব",
|
||||
"MessageDragFilesIntoTrackOrder": "সঠিক ট্র্যাক অর্ডারে ফাইল টেনে আনুন",
|
||||
"MessageEmbedFinished": "এম্বেড করা শেষ!",
|
||||
"MessageEpisodesQueuedForDownload": "{0} পর্ব(গুলি) ডাউনলোডের জন্য সারিবদ্ধ",
|
||||
"MessageFeedURLWillBe": "ফিড URL হবে {0}",
|
||||
"MessageFetching": "আনয় হচ্ছে...",
|
||||
"MessageForceReScanDescription": "সকল ফাইল আবার নতুন স্ক্যানের মত স্ক্যান করবে। অডিও ফাইল ID3 ট্যাগ, OPF ফাইল, এবং টেক্সট ফাইলগুলি নতুন হিসাবে স্ক্যান করা হবে।",
|
||||
"MessageImportantNotice": "গুরুত্বপূর্ণ বিজ্ঞপ্তি!",
|
||||
"MessageInsertChapterBelow": "নীচে অধ্যায় ঢোকান",
|
||||
"MessageItemsSelected": "{0}টি আইটেম নির্বাচিত",
|
||||
"MessageItemsUpdated": "{0}টি আইটেম আপডেট করা হয়েছে",
|
||||
"MessageJoinUsOn": "আমাদের সাথে যোগ দিন",
|
||||
"MessageListeningSessionsInTheLastYear": "গত বছরে {0}টি শোনার সেশন",
|
||||
"MessageLoading": "লোড হচ্ছে...",
|
||||
"MessageLoadingFolders": "ফোল্ডার লোড হচ্ছে...",
|
||||
"MessageM4BFailed": "M4B ব্যর্থ!",
|
||||
"MessageM4BFinished": "M4B সমাপ্ত!",
|
||||
"MessageMapChapterTitles": "টাইমস্ট্যাম্প সামঞ্জস্য না করে আপনার বিদ্যমান অডিওবুক অধ্যায়গুলিতে অধ্যায়ের শিরোনাম ম্যাপ করুন",
|
||||
"MessageMarkAllEpisodesFinished": "সমস্ত পর্ব সমাপ্ত চিহ্নিত করুন",
|
||||
"MessageMarkAllEpisodesNotFinished": "সমস্ত পর্ব শেষ হয়নি চিহ্নিত করুন",
|
||||
"MessageMarkAsFinished": "সমাপ্ত হিসাবে চিহ্নিত করুন",
|
||||
"MessageMarkAsNotFinished": "সমাপ্ত হয়নি হিসাবে চিহ্নিত করুন",
|
||||
"MessageMatchBooksDescription": "নির্বাচিত অনুসন্ধান প্রদানকারীর একটি বইয়ের সাথে লাইব্রেরিতে বই মেলানোর চেষ্টা করবে এবং খালি বিবরণ এবং কভার আর্ট পূরণ করবে। বিস্তারিত ওভাররাইট করে না।",
|
||||
"MessageNoAudioTracks": "কোন অডিও ট্র্যাক নেই",
|
||||
"MessageNoAuthors": "কোন লেখক নেই",
|
||||
"MessageNoBackups": "কোন ব্যাকআপ নেই",
|
||||
"MessageNoBookmarks": "কোন বুকমার্ক নেই",
|
||||
"MessageNoChapters": "কোনও অধ্যায় নেই",
|
||||
"MessageNoCollections": "কোন সংগ্রহ নেই",
|
||||
"MessageNoCoversFound": "কোন কভার পাওয়া যায়নি",
|
||||
"MessageNoDescription": "কোন বর্ণনা নেই",
|
||||
"MessageNoDownloadsInProgress": "বর্তমানে কোনো ডাউনলোড চলছে না",
|
||||
"MessageNoDownloadsQueued": "কোনও ডাউনলোড সারি নেই",
|
||||
"MessageNoEpisodeMatchesFound": "কোন পর্বের মিল পাওয়া যায়নি",
|
||||
"MessageNoEpisodes": "কোন পর্ব নেই",
|
||||
"MessageNoFoldersAvailable": "কোন ফোল্ডার উপলব্ধ নেই",
|
||||
"MessageNoGenres": "কোন ধরন নেই",
|
||||
"MessageNoIssues": "কোন সমস্যা নেই",
|
||||
"MessageNoItems": "কোন আইটেম নেই",
|
||||
"MessageNoItemsFound": "কোন আইটেম পাওয়া যায়নি",
|
||||
"MessageNoListeningSessions": "কোনও শোনার সেশন নেই",
|
||||
"MessageNoLogs": "কোনও লগ নেই",
|
||||
"MessageNoMediaProgress": "মিডিয়া অগ্রগতি নেই",
|
||||
"MessageNoNotifications": "কোনো বিজ্ঞপ্তি নেই",
|
||||
"MessageNoPodcastsFound": "কোন পডকাস্ট পাওয়া যায়নি",
|
||||
"MessageNoResults": "কোন ফলাফল নেই",
|
||||
"MessageNoSearchResultsFor": "\"{0}\" এর জন্য কোন অনুসন্ধান ফলাফল নেই",
|
||||
"MessageNoSeries": "কোন সিরিজ নেই",
|
||||
"MessageNoTags": "কোন ট্যাগ নেই",
|
||||
"MessageNoTasksRunning": "কোন টাস্ক চলছে না",
|
||||
"MessageNotYetImplemented": "এখনও বাস্তবায়িত হয়নি",
|
||||
"MessageNoUpdateNecessary": "কোন আপডেটের প্রয়োজন নেই",
|
||||
"MessageNoUpdatesWereNecessary": "কোন আপডেটের প্রয়োজন ছিল না",
|
||||
"MessageNoUserPlaylists": "আপনার কোনো প্লেলিস্ট নেই",
|
||||
"MessageOr": "বা",
|
||||
"MessagePauseChapter": "পজ অধ্যায় প্লেব্যাক",
|
||||
"MessagePlayChapter": "অধ্যায়ের শুরুতে শুনুন",
|
||||
"MessagePlaylistCreateFromCollection": "সংগ্রহ থেকে প্লেলিস্ট তৈরি করুন",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "পডকাস্টের সাথে মিলের জন্য ব্যবহার করার জন্য কোন RSS ফিড ইউআরএল নেই",
|
||||
"MessageQuickMatchDescription": "খালি আইটেমের বিশদ বিবরণ এবং '{0}' থেকে প্রথম ম্যাচের ফলাফলের সাথে কভার করুন। সার্ভার সেটিং সক্ষম না থাকলে বিশদ ওভাররাইট করে না।",
|
||||
"MessageRemoveChapter": "অধ্যায় সরান",
|
||||
"MessageRemoveEpisodes": "{0}টি পর্ব(গুলি) সরান",
|
||||
"MessageRemoveFromPlayerQueue": "প্লেয়ার সারি থেকে সরান",
|
||||
"MessageRemoveUserWarning": "আপনি কি নিশ্চিত আপনি স্থায়ীভাবে ব্যবহারকারী \"{0}\" মুছে ফেলতে চান?",
|
||||
"MessageReportBugsAndContribute": "বাগ রিপোর্ট করুন, বৈশিষ্ট্যের অনুরোধ করুন এবং এতে অবদান রাখুন",
|
||||
"MessageResetChaptersConfirm": "আপনি কি নিশ্চিত যে আপনি অধ্যায়গুলি পুনরায় সেট করতে চান এবং আপনার করা পরিবর্তনগুলি পূর্বাবস্থায় ফেরাতে চান?",
|
||||
"MessageRestoreBackupConfirm": "আপনি কি নিশ্চিত যে আপনি তৈরি করা ব্যাকআপ পুনরুদ্ধার করতে চান",
|
||||
"MessageRestoreBackupWarning": "একটি ব্যাকআপ পুনরুদ্ধার করা হলে তা /config-এ অবস্থিত সমগ্র ডাটাবেস ওভাররাইট করবে এবং /metadata/items & /metadata/authors-এ থাকা ছবিগুলিকে কভার করবে৷<br /><br />ব্যাকআপগুলি আপনার লাইব্রেরি ফোল্ডারে কোনো ফাইল পরিবর্তন করে না৷ আপনি যদি আপনার লাইব্রেরি ফোল্ডারে কভার আর্ট এবং মেটাডেটা সংরক্ষণ করতে সার্ভার সেটিংস সক্ষম করে থাকেন তবে সেগুলি ব্যাক আপ বা ওভাররাইট করা হয় না৷<br /><br />আপনার সার্ভার ব্যবহারকারী সমস্ত ক্লায়েন্ট স্বয়ংক্রিয়ভাবে রিফ্রেশ হবে৷",
|
||||
"MessageSearchResultsFor": "এর জন্য অনুসন্ধান ফলাফল",
|
||||
"MessageSelected": "{0}টি নির্বাচিত",
|
||||
"MessageServerCouldNotBeReached": "সার্ভারে পৌঁছানো যায়নি",
|
||||
"MessageSetChaptersFromTracksDescription": "প্রতিটি অডিও ফাইলকে অধ্যায় হিসেবে ব্যবহার করে অধ্যায় সেট করুন এবং অডিও ফাইলের নাম হিসেবে অধ্যায়ের শিরোনাম করুন",
|
||||
"MessageStartPlaybackAtTime": "\"{0}\" এর জন্য {1} এ প্লেব্যাক শুরু করবেন?",
|
||||
"MessageThinking": "চিন্তা করছি...",
|
||||
"MessageUploaderItemFailed": "আপলোড করতে ব্যর্থ",
|
||||
"MessageUploaderItemSuccess": "সফলভাবে আপলোড হয়েছে!",
|
||||
"MessageUploading": "আপলোড হচ্ছে...",
|
||||
"MessageValidCronExpression": "বৈধ ক্রোন এক্সপ্রেশন",
|
||||
"MessageWatcherIsDisabledGlobally": "সার্ভার সেটিংসে বিশ্বব্যাপী প্রহরী অক্ষম করা হয়েছে",
|
||||
"MessageXLibraryIsEmpty": "{0} লাইব্রেরি খালি!",
|
||||
"MessageYourAudiobookDurationIsLonger": "আপনার অডিওবুকের সময়কাল পাওয়া সময়ের চেয়ে বেশি",
|
||||
"MessageYourAudiobookDurationIsShorter": "আপনার অডিওবুকের সময়কাল পাওয়া সময়ের চেয়ে কম",
|
||||
"NoteChangeRootPassword": "রুট ব্যবহারকারীই একমাত্র ব্যবহারকারী যার একটি খালি পাসওয়ার্ড থাকতে পারে",
|
||||
"NoteChapterEditorTimes": "দ্রষ্টব্য: প্রথম অধ্যায়ের শুরুর সময় অবশ্যই 0:00 এ থাকতে হবে এবং শেষ অধ্যায়ের শুরুর সময়টি এই অডিওবুকের সময়কাল অতিক্রম করতে পারবে না।",
|
||||
"NoteFolderPicker": "দ্রষ্টব্য: ইতিমধ্যে ম্যাপ করা ফোল্ডারগুলি দেখানো হবে না",
|
||||
"NoteRSSFeedPodcastAppsHttps": "সতর্কতা: বেশিরভাগ পডকাস্ট অ্যাপের জন্য প্রয়োজন হবে RSS ফিড URL যেটি HTTPS ব্যবহার করছে",
|
||||
"NoteRSSFeedPodcastAppsPubDate": "সতর্কতা: আপনার 1 বা তার বেশি পর্বের একটি পাব তারিখ নেই। কিছু পডকাস্ট অ্যাপের এটি প্রয়োজন।",
|
||||
"NoteUploaderFoldersWithMediaFiles": "মিডিয়া ফাইল সহ ফোল্ডারগুলি আলাদা লাইব্রেরি আইটেম হিসাবে পরিচালনা করা হবে।",
|
||||
"NoteUploaderOnlyAudioFiles": "যদি শুধুমাত্র অডিও ফাইল আপলোড করা হয় তবে প্রতিটি অডিও ফাইল একটি পৃথক অডিওবুক হিসাবে পরিচালনা করা হবে।",
|
||||
"NoteUploaderUnsupportedFiles": "অসমর্থিত ফাইলগুলি উপেক্ষা করা হয়। একটি ফোল্ডার বেছে নেওয়া বা ফেলে দেওয়ার সময়, আইটেম ফোল্ডারে নেই এমন অন্যান্য ফাইলগুলি উপেক্ষা করা হয়।",
|
||||
"PlaceholderNewCollection": "নতুন সংগ্রহের নাম",
|
||||
"PlaceholderNewFolderPath": "নতুন ফোল্ডার পথ",
|
||||
"PlaceholderNewPlaylist": "নতুন প্লেলিস্টের নাম",
|
||||
"PlaceholderSearch": "অনুসন্ধান..",
|
||||
"PlaceholderSearchEpisode": "অনুসন্ধান পর্ব..",
|
||||
"ToastAccountUpdateFailed": "অ্যাকাউন্ট আপডেট করতে ব্যর্থ",
|
||||
"ToastAccountUpdateSuccess": "অ্যাকাউন্ট আপডেট করা হয়েছে",
|
||||
"ToastAuthorImageRemoveFailed": "ছবি সরাতে ব্যর্থ",
|
||||
"ToastAuthorImageRemoveSuccess": "লেখকের ছবি সরানো হয়েছে",
|
||||
"ToastAuthorUpdateFailed": "লেখক আপডেট করতে ব্যর্থ",
|
||||
"ToastAuthorUpdateMerged": "লেখক একত্রিত হয়েছে",
|
||||
"ToastAuthorUpdateSuccess": "লেখক আপডেট করেছেন",
|
||||
"ToastAuthorUpdateSuccessNoImageFound": "লেখক আপডেট করেছেন (কোন ছবি পাওয়া যায়নি)",
|
||||
"ToastBackupCreateFailed": "ব্যাকআপ তৈরি করতে ব্যর্থ",
|
||||
"ToastBackupCreateSuccess": "ব্যাকআপ তৈরি করা হয়েছে",
|
||||
"ToastBackupDeleteFailed": "ব্যাকআপ মুছে ফেলতে ব্যর্থ",
|
||||
"ToastBackupDeleteSuccess": "ব্যাকআপ মুছে ফেলা হয়েছে",
|
||||
"ToastBackupRestoreFailed": "ব্যাকআপ পুনরুদ্ধার করতে ব্যর্থ",
|
||||
"ToastBackupUploadFailed": "ব্যাকআপ আপলোড করতে ব্যর্থ",
|
||||
"ToastBackupUploadSuccess": "ব্যাকআপ আপলোড হয়েছে",
|
||||
"ToastBatchUpdateFailed": "ব্যাচ আপডেট ব্যর্থ হয়েছে",
|
||||
"ToastBatchUpdateSuccess": "ব্যাচ আপডেট সাফল্য",
|
||||
"ToastBookmarkCreateFailed": "বুকমার্ক তৈরি করতে ব্যর্থ",
|
||||
"ToastBookmarkCreateSuccess": "বুকমার্ক যোগ করা হয়েছে",
|
||||
"ToastBookmarkRemoveFailed": "বুকমার্ক সরাতে ব্যর্থ",
|
||||
"ToastBookmarkRemoveSuccess": "বুকমার্ক সরানো হয়েছে",
|
||||
"ToastBookmarkUpdateFailed": "বুকমার্ক আপডেট করতে ব্যর্থ",
|
||||
"ToastBookmarkUpdateSuccess": "বুকমার্ক আপডেট করা হয়েছে",
|
||||
"ToastChaptersHaveErrors": "অধ্যায়ে ত্রুটি আছে",
|
||||
"ToastChaptersMustHaveTitles": "অধ্যায়ের শিরোনাম থাকতে হবে",
|
||||
"ToastCollectionItemsRemoveFailed": "সংগ্রহ থেকে আইটেম(গুলি) সরাতে ব্যর্থ",
|
||||
"ToastCollectionItemsRemoveSuccess": "আইটেম(গুলি) সংগ্রহ থেকে সরানো হয়েছে",
|
||||
"ToastCollectionRemoveFailed": "সংগ্রহ সরাতে ব্যর্থ",
|
||||
"ToastCollectionRemoveSuccess": "সংগ্রহ সরানো হয়েছে",
|
||||
"ToastCollectionUpdateFailed": "সংগ্রহ আপডেট করতে ব্যর্থ",
|
||||
"ToastCollectionUpdateSuccess": "সংগ্রহ আপডেট করা হয়েছে",
|
||||
"ToastItemCoverUpdateFailed": "আইটেম কভার আপডেট করতে ব্যর্থ হয়েছে",
|
||||
"ToastItemCoverUpdateSuccess": "আইটেম কভার আপডেট করা হয়েছে",
|
||||
"ToastItemDetailsUpdateFailed": "আইটেমের বিবরণ আপডেট করতে ব্যর্থ",
|
||||
"ToastItemDetailsUpdateSuccess": "আইটেমের বিবরণ আপডেট করা হয়েছে",
|
||||
"ToastItemDetailsUpdateUnneeded": "আইটেমের বিবরণের জন্য কোন আপডেটের প্রয়োজন নেই",
|
||||
"ToastItemMarkedAsFinishedFailed": "সমাপ্ত হিসাবে চিহ্নিত করতে ব্যর্থ",
|
||||
"ToastItemMarkedAsFinishedSuccess": "আইটেম সমাপ্ত হিসাবে চিহ্নিত",
|
||||
"ToastItemMarkedAsNotFinishedFailed": "সমাপ্ত হয়নি হিসাবে চিহ্নিত করতে ব্যর্থ",
|
||||
"ToastItemMarkedAsNotFinishedSuccess": "আইটেম সমাপ্ত হয়নি বলে চিহ্নিত",
|
||||
"ToastLibraryCreateFailed": "লাইব্রেরি তৈরি করতে ব্যর্থ",
|
||||
"ToastLibraryCreateSuccess": "লাইব্রেরি \"{0}\" তৈরি করা হয়েছে",
|
||||
"ToastLibraryDeleteFailed": "লাইব্রেরি মুছে ফেলতে ব্যর্থ",
|
||||
"ToastLibraryDeleteSuccess": "লাইব্রেরি মুছে ফেলা হয়েছে",
|
||||
"ToastLibraryScanFailedToStart": "স্ক্যান শুরু করতে ব্যর্থ",
|
||||
"ToastLibraryScanStarted": "লাইব্রেরি স্ক্যান শুরু হয়েছে",
|
||||
"ToastLibraryUpdateFailed": "লাইব্রেরি আপডেট করতে ব্যর্থ",
|
||||
"ToastLibraryUpdateSuccess": "লাইব্রেরি \"{0}\" আপডেট করা হয়েছে",
|
||||
"ToastPlaylistCreateFailed": "প্লেলিস্ট তৈরি করতে ব্যর্থ",
|
||||
"ToastPlaylistCreateSuccess": "প্লেলিস্ট তৈরি করা হয়েছে",
|
||||
"ToastPlaylistRemoveFailed": "প্লেলিস্ট সরাতে ব্যর্থ",
|
||||
"ToastPlaylistRemoveSuccess": "প্লেলিস্ট সরানো হয়েছে",
|
||||
"ToastPlaylistUpdateFailed": "প্লেলিস্ট আপডেট করতে ব্যর্থ",
|
||||
"ToastPlaylistUpdateSuccess": "প্লেলিস্ট আপডেট করা হয়েছে",
|
||||
"ToastPodcastCreateFailed": "পডকাস্ট তৈরি করতে ব্যর্থ",
|
||||
"ToastPodcastCreateSuccess": "পডকাস্ট সফলভাবে তৈরি করা হয়েছে",
|
||||
"ToastRemoveItemFromCollectionFailed": "সংগ্রহ থেকে আইটেম সরাতে ব্যর্থ",
|
||||
"ToastRemoveItemFromCollectionSuccess": "সংগ্রহ থেকে আইটেম সরানো হয়েছে",
|
||||
"ToastRSSFeedCloseFailed": "RSS ফিড বন্ধ করতে ব্যর্থ",
|
||||
"ToastRSSFeedCloseSuccess": "RSS ফিড বন্ধ",
|
||||
"ToastSendEbookToDeviceFailed": "ডিভাইসে ইবুক পাঠাতে ব্যর্থ",
|
||||
"ToastSendEbookToDeviceSuccess": "ইবুক \"{0}\" ডিভাইসে পাঠানো হয়েছে",
|
||||
"ToastSeriesUpdateFailed": "সিরিজ আপডেট ব্যর্থ হয়েছে",
|
||||
"ToastSeriesUpdateSuccess": "সিরিজ আপডেট সাফল্য",
|
||||
"ToastSessionDeleteFailed": "সেশন মুছে ফেলতে ব্যর্থ",
|
||||
"ToastSessionDeleteSuccess": "সেশন মুছে ফেলা হয়েছে",
|
||||
"ToastSocketConnected": "সকেট সংযুক্ত",
|
||||
"ToastSocketDisconnected": "সকেট সংযোগ বিচ্ছিন্ন",
|
||||
"ToastSocketFailedToConnect": "সকেট সংযোগ করতে ব্যর্থ হয়েছে",
|
||||
"ToastUserDeleteFailed": "ব্যবহারকারী মুছতে ব্যর্থ",
|
||||
"ToastUserDeleteSuccess": "ব্যবহারকারী মুছে ফেলা হয়েছে"
|
||||
}
|
@ -385,7 +385,7 @@
|
||||
"LabelNotStarted": "Não iniciado",
|
||||
"LabelNumberOfBooks": "Número de Livros",
|
||||
"LabelNumberOfEpisodes": "# de Episódios",
|
||||
"LabelOpenIDAdvancedPermsClaimDescription": "Nome do claim OpenID contendo as permissões avançadas para ações do usuário na aplicação para serem aplicadas aos perfis não-administradores (<b>se configurados</b>). Se o claim não estiver presente na resposta, acesso ao ABS será negado. Se apenas uma opção estiver ausente, ela será tratada como <code>false</code>. Garanta que o claim do provedor de identidade segue a estrutura esperada:",
|
||||
"LabelOpenIDAdvancedPermsClaimDescription": "Nome do claim OpenID contendo as permissões avançadas para ações do usuário na aplicação para serem aplicadas aos perfis não-administradores (<b>se configurados</b>). Se o claim não estiver presente na resposta, acesso ao ABS será negado. Se apenas uma opção estiver ausente, ela será tratada como <code>false</code>. Garanta que o claim do provedor de identidade segue a estrutura esperada:",
|
||||
"LabelOpenIDClaims": "Deixe as opções a seguir em branco para desativar a atribuição de grupos e permissões avançadas; nesse caso, o grupo 'Usuário' será atribuído automaticamente.",
|
||||
"LabelOpenIDGroupClaimDescription": "Nome do claim OpenID contendo a lista de grupos do usuário, normalmente chamada de <code>groups</code>. <b>Se configurada</b>, a aplicação atribuirá automaticamente os perfis com base na participação do usuário nos grupos, contanto que os nomes desses grupos no claim, sem distinção entre maiúsculas e minúsculas, sejam 'admin', 'user' ou 'guest'. O claim deve conter uma lista e, se o usuário pertencer a múltiplos grupos, a aplicação atribuirá o perfil correspondendo ao maior nível de acesso. Se não houver correspondência a qualquer grupo, o acesso será negado.",
|
||||
"LabelOpenRSSFeed": "Abrir Feed RSS",
|
||||
@ -779,4 +779,4 @@
|
||||
"ToastSocketFailedToConnect": "Falha na conexão do socket",
|
||||
"ToastUserDeleteFailed": "Falha ao apagar usuário",
|
||||
"ToastUserDeleteSuccess": "Usuário apagado"
|
||||
}
|
||||
}
|
@ -385,6 +385,9 @@
|
||||
"LabelNotStarted": "Не розпочато",
|
||||
"LabelNumberOfBooks": "Кількість книг",
|
||||
"LabelNumberOfEpisodes": "Кількість епізодів",
|
||||
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
|
||||
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
|
||||
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
|
||||
"LabelOpenRSSFeed": "Відкрити RSS-канал",
|
||||
"LabelOverwrite": "Перезаписати",
|
||||
"LabelPassword": "Пароль",
|
||||
|
@ -32,8 +32,8 @@
|
||||
"ButtonHide": "隐藏",
|
||||
"ButtonHome": "首页",
|
||||
"ButtonIssues": "问题",
|
||||
"ButtonJumpBackward": "Jump Backward",
|
||||
"ButtonJumpForward": "Jump Forward",
|
||||
"ButtonJumpBackward": "向后跳转",
|
||||
"ButtonJumpForward": "向前跳转",
|
||||
"ButtonLatest": "最新",
|
||||
"ButtonLibrary": "媒体库",
|
||||
"ButtonLogout": "注销",
|
||||
@ -43,8 +43,8 @@
|
||||
"ButtonMatchAllAuthors": "匹配所有作者",
|
||||
"ButtonMatchBooks": "匹配图书",
|
||||
"ButtonNevermind": "没有关系",
|
||||
"ButtonNext": "Next",
|
||||
"ButtonNextChapter": "Next Chapter",
|
||||
"ButtonNext": "下一个",
|
||||
"ButtonNextChapter": "下一章节",
|
||||
"ButtonOk": "确定",
|
||||
"ButtonOpenFeed": "打开源",
|
||||
"ButtonOpenManager": "打开管理器",
|
||||
@ -52,8 +52,8 @@
|
||||
"ButtonPlay": "播放",
|
||||
"ButtonPlaying": "正在播放",
|
||||
"ButtonPlaylists": "播放列表",
|
||||
"ButtonPrevious": "Previous",
|
||||
"ButtonPreviousChapter": "Previous Chapter",
|
||||
"ButtonPrevious": "上一个",
|
||||
"ButtonPreviousChapter": "上一章节",
|
||||
"ButtonPurgeAllCache": "清理所有缓存",
|
||||
"ButtonPurgeItemsCache": "清理项目缓存",
|
||||
"ButtonPurgeMediaProgress": "清理媒体进度",
|
||||
@ -61,7 +61,7 @@
|
||||
"ButtonQueueRemoveItem": "从队列中移除",
|
||||
"ButtonQuickMatch": "快速匹配",
|
||||
"ButtonRead": "读取",
|
||||
"ButtonRefresh": "Refresh",
|
||||
"ButtonRefresh": "刷新",
|
||||
"ButtonRemove": "移除",
|
||||
"ButtonRemoveAll": "移除所有",
|
||||
"ButtonRemoveAllLibraryItems": "移除所有媒体库项目",
|
||||
@ -113,7 +113,7 @@
|
||||
"HeaderCollectionItems": "收藏项目",
|
||||
"HeaderCover": "封面",
|
||||
"HeaderCurrentDownloads": "当前下载",
|
||||
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||
"HeaderCustomMetadataProviders": "自定义元数据提供者",
|
||||
"HeaderDetails": "详情",
|
||||
"HeaderDownloadQueue": "下载队列",
|
||||
"HeaderEbookFiles": "电子书文件",
|
||||
@ -184,7 +184,7 @@
|
||||
"HeaderUpdateDetails": "更新详情",
|
||||
"HeaderUpdateLibrary": "更新媒体库",
|
||||
"HeaderUsers": "用户",
|
||||
"HeaderYearReview": "Year {0} in Review",
|
||||
"HeaderYearReview": "{0} 年回顾",
|
||||
"HeaderYourStats": "你的统计数据",
|
||||
"LabelAbridged": "概要",
|
||||
"LabelAccountType": "帐户类型",
|
||||
@ -294,9 +294,9 @@
|
||||
"LabelFolders": "文件夹",
|
||||
"LabelFontBold": "Bold",
|
||||
"LabelFontFamily": "字体系列",
|
||||
"LabelFontItalic": "Italic",
|
||||
"LabelFontItalic": "斜体",
|
||||
"LabelFontScale": "字体比例",
|
||||
"LabelFontStrikethrough": "Strikethrough",
|
||||
"LabelFontStrikethrough": "删除线",
|
||||
"LabelFormat": "编码格式",
|
||||
"LabelGenre": "流派",
|
||||
"LabelGenres": "流派",
|
||||
@ -355,8 +355,8 @@
|
||||
"LabelMetaTags": "元标签",
|
||||
"LabelMinute": "分钟",
|
||||
"LabelMissing": "丢失",
|
||||
"LabelMissingEbook": "Has no ebook",
|
||||
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
|
||||
"LabelMissingEbook": "没有电子书",
|
||||
"LabelMissingSupplementaryEbook": "没有补充电子书",
|
||||
"LabelMobileRedirectURIs": "允许移动应用重定向 URI",
|
||||
"LabelMobileRedirectURIsDescription": "这是移动应用程序的有效重定向 URI 白名单. 默认值为 <code>audiobookshelf://oauth</code>,您可以删除它或添加其他 URI 以进行第三方应用集成. 使用星号 (<code>*</code>) 作为唯一条目允许任何 URI.",
|
||||
"LabelMore": "更多",
|
||||
@ -385,9 +385,9 @@
|
||||
"LabelNotStarted": "未开始",
|
||||
"LabelNumberOfBooks": "图书数量",
|
||||
"LabelNumberOfEpisodes": "# 集",
|
||||
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
|
||||
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
|
||||
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
|
||||
"LabelOpenIDAdvancedPermsClaimDescription": "OpenID 声明的名称, 该声明包含应用程序内用户操作的高级权限, 该权限将应用于非管理员角色(<b>如果已配置</b>). 如果响应中缺少声明, 获取 ABS 的权限将被拒绝. 如果缺少单个选项, 它将被视为 <code>禁用</code>. 确保身份提供者的声明与预期结构匹配:",
|
||||
"LabelOpenIDClaims": "将以下选项留空以禁用高级组和权限分配, 然后自动分配 'User' 组.",
|
||||
"LabelOpenIDGroupClaimDescription": "OpenID 声明的名称, 该声明包含用户组的列表. 通常称为<code>组</code><b>如果已配置</b>, 应用程序将根据用户的组成员身份自动分配角色, 前提是这些组在声明中以不区分大小写的方式命名为 'Admin', 'User' 或 'Guest'. 声明应包含一个列表, 如果用户属于多个组, 则应用程序将分配与最高访问级别相对应的角色. 如果没有组匹配, 访问将被拒绝.",
|
||||
"LabelOpenRSSFeed": "打开 RSS 源",
|
||||
"LabelOverwrite": "覆盖",
|
||||
"LabelPassword": "密码",
|
||||
@ -399,7 +399,7 @@
|
||||
"LabelPermissionsDownload": "可以下载",
|
||||
"LabelPermissionsUpdate": "可以更新",
|
||||
"LabelPermissionsUpload": "可以上传",
|
||||
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||
"LabelPersonalYearReview": "你的年度回顾 ({0})",
|
||||
"LabelPhotoPathURL": "图片路径或 URL",
|
||||
"LabelPlaylists": "播放列表",
|
||||
"LabelPlayMethod": "播放方法",
|
||||
@ -426,7 +426,7 @@
|
||||
"LabelRegion": "区域",
|
||||
"LabelReleaseDate": "发布日期",
|
||||
"LabelRemoveCover": "移除封面",
|
||||
"LabelRowsPerPage": "Rows per page",
|
||||
"LabelRowsPerPage": "每页行数",
|
||||
"LabelRSSFeedCustomOwnerEmail": "自定义所有者电子邮件",
|
||||
"LabelRSSFeedCustomOwnerName": "自定义所有者名称",
|
||||
"LabelRSSFeedOpen": "打开 RSS 源",
|
||||
@ -439,13 +439,13 @@
|
||||
"LabelSeason": "季",
|
||||
"LabelSelectAllEpisodes": "选择所有剧集",
|
||||
"LabelSelectEpisodesShowing": "选择正在播放的 {0} 剧集",
|
||||
"LabelSelectUsers": "Select users",
|
||||
"LabelSelectUsers": "选择用户",
|
||||
"LabelSendEbookToDevice": "发送电子书到...",
|
||||
"LabelSequence": "序列",
|
||||
"LabelSeries": "系列",
|
||||
"LabelSeriesName": "系列名称",
|
||||
"LabelSeriesProgress": "系列进度",
|
||||
"LabelServerYearReview": "Server Year in Review ({0})",
|
||||
"LabelServerYearReview": "服务器年度回顾 ({0})",
|
||||
"LabelSetEbookAsPrimary": "设置为主",
|
||||
"LabelSetEbookAsSupplementary": "设置为补充",
|
||||
"LabelSettingsAudiobooksOnly": "只有有声读物",
|
||||
@ -467,8 +467,8 @@
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "只有一本书的系列将从系列页面和主页书架中隐藏.",
|
||||
"LabelSettingsHomePageBookshelfView": "首页使用书架视图",
|
||||
"LabelSettingsLibraryBookshelfView": "媒体库使用书架视图",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "跳过继续系列中的早期书籍",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "继续系列主页书架显示系列中未开始的第一本书, 该系列至少有一本书已完成且没有正在进行的书. 启用此设置将从最远完成的书开始系列, 而不是从第一本书开始.",
|
||||
"LabelSettingsParseSubtitles": "解析副标题",
|
||||
"LabelSettingsParseSubtitlesHelp": "从有声读物文件夹中提取副标题.<br>副标题必须用 \" - \" 分隔.<br>例: \"书名 - 这里是副标题\" 则显示副标题 \"这里是副标题\"",
|
||||
"LabelSettingsPreferMatchedMetadata": "首选匹配的元数据",
|
||||
@ -514,10 +514,10 @@
|
||||
"LabelTagsAccessibleToUser": "用户可访问的标签",
|
||||
"LabelTagsNotAccessibleToUser": "用户无法访问标签",
|
||||
"LabelTasks": "正在运行的任务",
|
||||
"LabelTextEditorBulletedList": "Bulleted list",
|
||||
"LabelTextEditorLink": "Link",
|
||||
"LabelTextEditorNumberedList": "Numbered list",
|
||||
"LabelTextEditorUnlink": "Unlink",
|
||||
"LabelTextEditorBulletedList": "项目符号列表",
|
||||
"LabelTextEditorLink": "链接",
|
||||
"LabelTextEditorNumberedList": "编号列表",
|
||||
"LabelTextEditorUnlink": "取消链接",
|
||||
"LabelTheme": "主题",
|
||||
"LabelThemeDark": "黑暗",
|
||||
"LabelThemeLight": "明亮",
|
||||
@ -564,8 +564,8 @@
|
||||
"LabelViewQueue": "查看播放列表",
|
||||
"LabelVolume": "音量",
|
||||
"LabelWeekdaysToRun": "工作日运行",
|
||||
"LabelYearReviewHide": "Hide Year in Review",
|
||||
"LabelYearReviewShow": "See Year in Review",
|
||||
"LabelYearReviewHide": "隐藏年度回顾",
|
||||
"LabelYearReviewShow": "查看年度回顾",
|
||||
"LabelYourAudiobookDuration": "你的有声读物持续时间",
|
||||
"LabelYourBookmarks": "你的书签",
|
||||
"LabelYourPlaylists": "你的播放列表",
|
||||
@ -602,7 +602,7 @@
|
||||
"MessageConfirmRemoveCollection": "你确定要移除收藏 \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "你确定要移除剧集 \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?",
|
||||
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
|
||||
"MessageConfirmRemoveListeningSessions": "你确定要移除 {0} 收听会话吗?",
|
||||
"MessageConfirmRemoveNarrator": "你确定要删除演播者 \"{0}\"?",
|
||||
"MessageConfirmRemovePlaylist": "你确定要移除播放列表 \"{0}\"?",
|
||||
"MessageConfirmRenameGenre": "你确定要将所有项目流派 \"{0}\" 重命名到 \"{1}\"?",
|
||||
|
@ -86,7 +86,8 @@ module.exports = {
|
||||
fontSize: {
|
||||
xxs: '0.625rem',
|
||||
'1.5xl': '1.375rem',
|
||||
'2.5xl': '1.6875rem'
|
||||
'2.5xl': '1.6875rem',
|
||||
'4.5xl': '2.625rem'
|
||||
},
|
||||
zIndex: {
|
||||
'50': 50,
|
||||
|
30
docs/README.md
Normal file
30
docs/README.md
Normal file
@ -0,0 +1,30 @@
|
||||
# OpenAPI specification
|
||||
|
||||
This directory includes the OpenAPI spec for the ABS server.
|
||||
The spec is made up of a number of individual `yaml` files located here and in the subfolders, with `root.yaml` being the file that references all of the others.
|
||||
The files are organized to have the same hierarchy as the server source files.
|
||||
The full spec is bundled into one file in `openapi.json`.
|
||||
|
||||
The spec is linted and bundled by the [`vacuum` tool](https://quobix.com/vacuum/).
|
||||
The spec can also be tested with a real server using the [`wiretap` tool](https://pb33f.io/wiretap/).
|
||||
Both of these tools are created by [pb33f](https://pb33f.io/).
|
||||
|
||||
### Bundling the spec
|
||||
The command to bundle the spec into a `yaml` file is `vacuum bundle root.yaml openapi.yaml`.
|
||||
|
||||
The current version of `vacuum` cannot convert input `yaml` files to `json` files.
|
||||
To convert the spec to `json`, you can use the `yq` tool or another tool.
|
||||
|
||||
The command to convert the spec using `yq` is `yq -p yaml -o json openapi.yaml > openapi.json`.
|
||||
|
||||
### Viewing report
|
||||
To generate an HTML report, you can use `vacuum html-report [file]` to generate `report.html` and view the report in your browser.
|
||||
|
||||
### Putting it all together
|
||||
The full command that I run to bundle the spec and generate the report is:
|
||||
|
||||
```
|
||||
vacuum bundle root.yaml openapi.yaml && \
|
||||
yq -p yaml -o json openapi.yaml > openapi.json && \
|
||||
vacuum html-report openapi.json
|
||||
```
|
139
docs/controllers/AuthorController.yaml
Normal file
139
docs/controllers/AuthorController.yaml
Normal file
@ -0,0 +1,139 @@
|
||||
components:
|
||||
schemas:
|
||||
authorUpdated:
|
||||
description: Whether the author was updated without errors. Will not exist if author was merged.
|
||||
type: boolean
|
||||
nullable: true
|
||||
parameters:
|
||||
authorId:
|
||||
name: id
|
||||
in: path
|
||||
description: Author ID
|
||||
required: true
|
||||
schema:
|
||||
$ref: '../objects/entities/Author.yaml#/components/schemas/authorId'
|
||||
authorInclude:
|
||||
name: include
|
||||
in: query
|
||||
description: A comma separated list of what to include with the author. The options are `items` and `series`. `series` will only have an effect if `items` is included.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
example: "items"
|
||||
examples:
|
||||
empty:
|
||||
summary: Do not return library items
|
||||
value: ""
|
||||
itemOnly:
|
||||
summary: Only return library items
|
||||
value: "items"
|
||||
itemsAndSeries:
|
||||
summary: Return library items and series
|
||||
value: "items,series"
|
||||
authorLibraryId:
|
||||
name: library
|
||||
in: query
|
||||
description: The ID of the library to to include filter included items from.
|
||||
required: false
|
||||
schema:
|
||||
$ref: '../objects/Library.yaml#/components/schemas/libraryId'
|
||||
asin:
|
||||
name: asin
|
||||
in: query
|
||||
description: The Audible Identifier (ASIN).
|
||||
required: false
|
||||
schema:
|
||||
$ref: '../objects/entities/Author.yaml#/components/schemas/authorAsin'
|
||||
authorSearchName:
|
||||
name: q
|
||||
in: query
|
||||
description: The name of the author to use for searching.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
example: Terry Goodkind
|
||||
authorName:
|
||||
name: name
|
||||
in: query
|
||||
description: The new name of the author.
|
||||
required: false
|
||||
schema:
|
||||
$ref: '../objects/entities/Author.yaml#/components/schemas/authorName'
|
||||
authorDescription:
|
||||
name: description
|
||||
in: query
|
||||
description: The new description of the author.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
example: Terry Goodkind is a #1 New York Times Bestselling Author and creator of the critically acclaimed masterwork, ‘The Sword of Truth’. He has written 30+ major, bestselling novels, has been published in more than 20 languages world-wide, and has sold more than 26 Million books. ‘The Sword of Truth’ is a revered literary tour de force, comprised of 17 volumes, borne from over 25 years of dedicated writing.
|
||||
authorImagePath:
|
||||
name: imagePath
|
||||
in: query
|
||||
description: The new absolute path for the author image.
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
example: /metadata/authors/aut_z3leimgybl7uf3y4ab.jpg
|
||||
imageUrl:
|
||||
name: url
|
||||
in: query
|
||||
description: The URL of the image to add to the server
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uri
|
||||
example: https://images-na.ssl-images-amazon.com/images/I/51NoQTm33OL.__01_SX120_CR0,0,120,120__.jpg
|
||||
imageWidth:
|
||||
name: width
|
||||
in: query
|
||||
description: The requested width of image in pixels.
|
||||
schema:
|
||||
type: integer
|
||||
default: 400
|
||||
example: 400
|
||||
example: 400
|
||||
imageHeight:
|
||||
name: height
|
||||
in: query
|
||||
description: The requested height of image in pixels. If `null`, the height is scaled to maintain aspect ratio based on the requested width.
|
||||
schema:
|
||||
type: integer
|
||||
nullable: true
|
||||
default: null
|
||||
example: 600
|
||||
examples:
|
||||
scaleHeight:
|
||||
summary: Scale height with width
|
||||
value: null
|
||||
fixedHeight:
|
||||
summary: Force height of image
|
||||
value: 600
|
||||
imageFormat:
|
||||
name: format
|
||||
in: query
|
||||
description: The requested output format.
|
||||
schema:
|
||||
type: string
|
||||
default: jpeg
|
||||
example: webp
|
||||
imageRaw:
|
||||
name: raw
|
||||
in: query
|
||||
description: Return the raw image without scaling if true.
|
||||
schema:
|
||||
type: boolean
|
||||
default: false
|
||||
responses:
|
||||
author404:
|
||||
description: Author not found.
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
type: string
|
||||
example: Not found
|
||||
tags:
|
||||
- name: Authors
|
||||
description: Author endpoints
|
21
docs/objects/Folder.yaml
Normal file
21
docs/objects/Folder.yaml
Normal file
@ -0,0 +1,21 @@
|
||||
components:
|
||||
schemas:
|
||||
folderId:
|
||||
type: string
|
||||
description: The ID of the folder.
|
||||
format: uuid
|
||||
example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b
|
||||
folder:
|
||||
type: object
|
||||
description: Folder used in library
|
||||
properties:
|
||||
id:
|
||||
$ref: '#/components/schemas/folderId'
|
||||
fullPath:
|
||||
description: The path on the server for the folder. (Read Only)
|
||||
type: string
|
||||
example: /podcasts
|
||||
libraryId:
|
||||
$ref: './Library.yaml#/components/schemas/libraryId'
|
||||
addedAt:
|
||||
$ref: '../schemas.yaml#/components/schemas/addedAt'
|
12
docs/objects/Library.yaml
Normal file
12
docs/objects/Library.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
components:
|
||||
schemas:
|
||||
oldLibraryId:
|
||||
type: string
|
||||
description: The ID of the libraries created on server version 2.2.23 and before.
|
||||
format: "lib_[a-z0-9]{18}"
|
||||
example: lib_o78uaoeuh78h6aoeif
|
||||
libraryId:
|
||||
type: string
|
||||
description: The ID of the library.
|
||||
format: uuid
|
||||
example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b
|
66
docs/objects/LibraryItem.yaml
Normal file
66
docs/objects/LibraryItem.yaml
Normal file
@ -0,0 +1,66 @@
|
||||
components:
|
||||
schemas:
|
||||
oldLibraryItemId:
|
||||
description: The ID of library items on server version 2.2.23 and before.
|
||||
type: string
|
||||
nullable: true
|
||||
format: "li_[a-z0-9]{18}"
|
||||
example: li_o78uaoeuh78h6aoeif
|
||||
libraryItemId:
|
||||
type: string
|
||||
description: The ID of library items after 2.3.0.
|
||||
format: uuid
|
||||
example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b
|
||||
libraryItemBase:
|
||||
type: object
|
||||
description: Base library item schema
|
||||
properties:
|
||||
id:
|
||||
$ref: '#/components/schemas/libraryItemId'
|
||||
oldLibraryItemId:
|
||||
$ref: '#/components/schemas/oldLibraryItemId'
|
||||
ino:
|
||||
$ref: '../schemas.yaml#/components/schemas/inode'
|
||||
libraryId:
|
||||
$ref: './Library.yaml#/components/schemas/libraryId'
|
||||
folderId:
|
||||
$ref: './Folder.yaml#/components/schemas/folderId'
|
||||
path:
|
||||
description: The path of the library item on the server.
|
||||
type: string
|
||||
relPath:
|
||||
description: The path, relative to the library folder, of the library item.
|
||||
type: string
|
||||
isFile:
|
||||
description: Whether the library item is a single file in the root of the library folder.
|
||||
type: boolean
|
||||
mtimeMs:
|
||||
description: The time (in ms since POSIX epoch) when the library item was last modified on disk.
|
||||
type: integer
|
||||
ctimeMs:
|
||||
description: The time (in ms since POSIX epoch) when the library item status was changed on disk.
|
||||
type: integer
|
||||
birthtimeMs:
|
||||
description: The time (in ms since POSIX epoch) when the library item was created on disk. Will be 0 if unknown.
|
||||
type: integer
|
||||
addedAt:
|
||||
$ref: '../schemas.yaml#/components/schemas/addedAt'
|
||||
updatedAt:
|
||||
$ref: '../schemas.yaml#/components/schemas/updatedAt'
|
||||
isMissing:
|
||||
description: Whether the library item was scanned and no longer exists.
|
||||
type: boolean
|
||||
isInvalid:
|
||||
description: Whether the library item was scanned and no longer has media files.
|
||||
type: boolean
|
||||
mediaType:
|
||||
$ref: './mediaTypes/media.yaml#/components/schemas/mediaType'
|
||||
libraryItemMinified:
|
||||
type: object
|
||||
description: A single item on the server, like a book or podcast. Minified media format.
|
||||
allOf:
|
||||
- $ref : '#/components/schemas/libraryItemBase'
|
||||
- type: object
|
||||
properties:
|
||||
media:
|
||||
$ref: './mediaTypes/media.yaml#/components/schemas/mediaMinified'
|
104
docs/objects/entities/Author.yaml
Normal file
104
docs/objects/entities/Author.yaml
Normal file
@ -0,0 +1,104 @@
|
||||
components:
|
||||
schemas:
|
||||
authorId:
|
||||
type: string
|
||||
description: The ID of the author.
|
||||
format: uuid
|
||||
example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b
|
||||
authorAsin:
|
||||
type: string
|
||||
description: The Audible identifier (ASIN) of the author. Will be null if unknown. Not the Amazon identifier.
|
||||
nullable: true
|
||||
example: B000APZOQA
|
||||
authorName:
|
||||
description: The name of the author.
|
||||
type: string
|
||||
example: Terry Goodkind
|
||||
authorSeries:
|
||||
type: object
|
||||
description: Series and the included library items that an author has written.
|
||||
properties:
|
||||
id:
|
||||
$ref: './Series.yaml#/components/schemas/seriesId'
|
||||
name:
|
||||
$ref: './Series.yaml#/components/schemas/seriesName'
|
||||
items:
|
||||
description: The items in the series. Each library item's media's metadata will have a `series` attribute, a `Series Sequence`, which is the matching series.
|
||||
type: array
|
||||
items:
|
||||
ref: '../LibraryItem.yaml#/components/schemas/libraryItemMinified'
|
||||
author:
|
||||
description: An author object which includes a description and image path.
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
$ref: '#/components/schemas/authorId'
|
||||
asin:
|
||||
$ref: '#/components/schemas/authorAsin'
|
||||
name:
|
||||
$ref: '#/components/schemas/authorName'
|
||||
description:
|
||||
description: A description of the author. Will be null if there is none.
|
||||
type: string
|
||||
nullable: true
|
||||
example: |
|
||||
Terry Goodkind is a #1 New York Times Bestselling Author and creator of the critically acclaimed masterwork,
|
||||
‘The Sword of Truth’. He has written 30+ major, bestselling novels, has been published in more than 20
|
||||
languages world-wide, and has sold more than 26 Million books. ‘The Sword of Truth’ is a revered literary
|
||||
tour de force, comprised of 17 volumes, borne from over 25 years of dedicated writing. Terry Goodkind's
|
||||
brilliant books are character-driven stories, with a focus on the complexity of the human psyche. Goodkind
|
||||
has an uncanny grasp for crafting compelling stories about people like you and me, trapped in terrifying
|
||||
situations.
|
||||
imagePath:
|
||||
description: The absolute path for the author image located in the `metadata/` directory. Will be null if there is no image.
|
||||
type: string
|
||||
nullable: true
|
||||
example: /metadata/authors/aut_bxxbyjiptmgb56yzoz.jpg
|
||||
addedAt:
|
||||
$ref: '../../schemas.yaml#/components/schemas/addedAt'
|
||||
updatedAt:
|
||||
$ref: '../../schemas.yaml#/components/schemas/updatedAt'
|
||||
authorWithItems:
|
||||
type: object
|
||||
description: The author schema with an array of items they are associated with.
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/author'
|
||||
- type: object
|
||||
properties:
|
||||
libraryItems:
|
||||
description: The items associated with the author
|
||||
type: string
|
||||
type: array
|
||||
items:
|
||||
$ref: '../LibraryItem.yaml#/components/schemas/libraryItemMinified'
|
||||
authorWithSeries:
|
||||
type: object
|
||||
description: The author schema with an array of items and series they are associated with.
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/authorWithItems'
|
||||
- type: object
|
||||
properties:
|
||||
series:
|
||||
description: The series associated with the author
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/authorSeries'
|
||||
authorMinified:
|
||||
type: object
|
||||
description: Minified author object which only contains the author name and ID.
|
||||
properties:
|
||||
id:
|
||||
$ref: '#/components/schemas/authorId'
|
||||
name:
|
||||
$ref: '#/components/schemas/authorName'
|
||||
authorExpanded:
|
||||
type: object
|
||||
description: The author schema with the total number of books in the library.
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/author'
|
||||
- type: object
|
||||
properties:
|
||||
numBooks:
|
||||
description: The number of books associated with the author in the library.
|
||||
type: integer
|
||||
example: 1
|
11
docs/objects/entities/Series.yaml
Normal file
11
docs/objects/entities/Series.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
components:
|
||||
schemas:
|
||||
seriesId:
|
||||
type: string
|
||||
description: The ID of the series.
|
||||
format: uuid
|
||||
example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b
|
||||
seriesName:
|
||||
description: The name of the series.
|
||||
type: string
|
||||
example: Sword of Truth
|
94
docs/objects/files/AudioFile.yaml
Normal file
94
docs/objects/files/AudioFile.yaml
Normal file
@ -0,0 +1,94 @@
|
||||
components:
|
||||
schemas:
|
||||
audioFile:
|
||||
type: object
|
||||
description: An audio file for a book. Includes audio metadata and track numbers.
|
||||
properties:
|
||||
index:
|
||||
description: The index of the audio file.
|
||||
type: integer
|
||||
example: 1
|
||||
ino:
|
||||
$ref: '../../schemas.yaml#/components/schemas/inode'
|
||||
metadata:
|
||||
$ref: '../metadata/FileMetadata.yaml#/components/schemas/fileMetadata'
|
||||
addedAt:
|
||||
$ref: '../../schemas.yaml#/components/schemas/addedAt'
|
||||
updatedAt:
|
||||
$ref: '../../schemas.yaml#/components/schemas/updatedAt'
|
||||
trackNumFromMeta:
|
||||
description: The track number of the audio file as pulled from the file's metadata. Will be null if unknown.
|
||||
type: integer
|
||||
nullable: true
|
||||
example: 1
|
||||
discNumFromMeta:
|
||||
description: The disc number of the audio file as pulled from the file's metadata. Will be null if unknown.
|
||||
type: string
|
||||
nullable: true
|
||||
trackNumFromFilename:
|
||||
description: The track number of the audio file as determined from the file's name. Will be null if unknown.
|
||||
type: integer
|
||||
nullable: true
|
||||
example: 1
|
||||
discNumFromFilename:
|
||||
description: The disc number of the audio file as determined from the file's name. Will be null if unknown.
|
||||
type: string
|
||||
nullable: true
|
||||
manuallyVerified:
|
||||
description: Whether the audio file has been manually verified by a user.
|
||||
type: boolean
|
||||
invalid:
|
||||
description: Whether the audio file is missing from the server.
|
||||
type: boolean
|
||||
exclude:
|
||||
description: Whether the audio file has been marked for exclusion.
|
||||
type: boolean
|
||||
error:
|
||||
description: Any error with the audio file. Will be null if there is none.
|
||||
type: string
|
||||
nullable: true
|
||||
format:
|
||||
description: The format of the audio file.
|
||||
type: string
|
||||
example: MP2/3 (MPEG audio layer 2/3)
|
||||
duration:
|
||||
$ref: '#/components/schemas/durationSec'
|
||||
bitRate:
|
||||
description: The bit rate (in bit/s) of the audio file.
|
||||
type: integer
|
||||
example: 64000
|
||||
language:
|
||||
description: The language of the audio file.
|
||||
type: string
|
||||
nullable: true
|
||||
codec:
|
||||
description: The codec of the audio file.
|
||||
type: string
|
||||
example: mp3
|
||||
timeBase:
|
||||
description: The time base of the audio file.
|
||||
type: string
|
||||
example: 1/14112000
|
||||
channels:
|
||||
description: The number of channels the audio file has.
|
||||
type: integer
|
||||
example: 2
|
||||
channelLayout:
|
||||
description: The layout of the audio file's channels.
|
||||
type: string
|
||||
example: stereo
|
||||
chapters:
|
||||
description: If the audio file is part of an audiobook, the chapters the file contains.
|
||||
type: array
|
||||
items:
|
||||
$ref: '../metadata/BookMetadata.yaml#/components/schemas/bookChapter'
|
||||
embeddedCoverArt:
|
||||
description: The type of embedded cover art in the audio file. Will be null if none exists.
|
||||
type: string
|
||||
nullable: true
|
||||
metaTags:
|
||||
$ref: '../metadata/AudioMetaTags.yaml#/components/schemas/audioMetaTags'
|
||||
mimeType:
|
||||
description: The MIME type of the audio file.
|
||||
type: string
|
||||
example: audio/mpeg
|
70
docs/objects/mediaTypes/Book.yaml
Normal file
70
docs/objects/mediaTypes/Book.yaml
Normal file
@ -0,0 +1,70 @@
|
||||
components:
|
||||
schemas:
|
||||
bookCoverPath:
|
||||
description: The absolute path on the server of the cover file. Will be null if there is no cover.
|
||||
type: string
|
||||
nullable: true
|
||||
example: /audiobooks/Terry Goodkind/Sword of Truth/Wizards First Rule/cover.jpg
|
||||
bookBase:
|
||||
type: object
|
||||
description: Base book schema
|
||||
properties:
|
||||
libraryItemId:
|
||||
$ref: '../LibraryItem.yaml#/components/schemas/libraryItemId'
|
||||
coverPath:
|
||||
$ref: '#/components/schemas/bookCoverPath'
|
||||
tags:
|
||||
$ref: '../../schemas.yaml#/components/schemas/tags'
|
||||
audioFiles:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/audioFile'
|
||||
chapters:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/bookChapter'
|
||||
missingParts:
|
||||
description: Any parts missing from the book by track index.
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
ebookFile:
|
||||
$ref: '#/components/schemas/ebookFile'
|
||||
bookMinified:
|
||||
type: object
|
||||
description: Minified book schema. Does not depend on `bookBase` because there's pretty much no overlap.
|
||||
properties:
|
||||
metadata:
|
||||
$ref: '../metadata/BookMetadata.yaml#/components/schemas/bookMetadataMinified'
|
||||
coverPath:
|
||||
$ref: '#/components/schemas/bookCoverPath'
|
||||
tags:
|
||||
$ref: '../../schemas.yaml#/components/schemas/tags'
|
||||
numTracks:
|
||||
description: The number of tracks the book's audio files have.
|
||||
type: integer
|
||||
example: 1
|
||||
numAudioFiles:
|
||||
description: The number of audio files the book has.
|
||||
type: integer
|
||||
example: 1
|
||||
numChapters:
|
||||
description: The number of chapters the book has.
|
||||
type: integer
|
||||
example: 1
|
||||
numMissingParts:
|
||||
description: The total number of missing parts the book has.
|
||||
type: integer
|
||||
example: 0
|
||||
numInvalidAudioFiles:
|
||||
description: The number of invalid audio files the book has.
|
||||
type: integer
|
||||
example: 0
|
||||
duration:
|
||||
$ref: '../../schemas.yaml#/components/schemas/durationSec'
|
||||
size:
|
||||
$ref: '../../schemas.yaml#/components/schemas/size'
|
||||
ebookFormat:
|
||||
description: The format of ebook of the book. Will be null if the book is an audiobook.
|
||||
type: string
|
||||
nullable: true
|
10
docs/objects/mediaTypes/media.yaml
Normal file
10
docs/objects/mediaTypes/media.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
components:
|
||||
schemas:
|
||||
mediaType:
|
||||
type: string
|
||||
description: The type of media, will be book or podcast.
|
||||
enum: [book, podcast]
|
||||
mediaMinified:
|
||||
description: The minified media of the library item.
|
||||
oneOf:
|
||||
- $ref: './Book.yaml#/components/schemas/bookMinified'
|
103
docs/objects/metadata/AudioMetaTags.yaml
Normal file
103
docs/objects/metadata/AudioMetaTags.yaml
Normal file
@ -0,0 +1,103 @@
|
||||
components:
|
||||
schemas:
|
||||
audioMetaTags:
|
||||
description: ID3 metadata tags pulled from the audio file on import. Only non-null tags will be returned in requests.
|
||||
type: object
|
||||
properties:
|
||||
tagAlbum:
|
||||
type: string
|
||||
nullable: true
|
||||
example: SOT Bk01
|
||||
tagArtist:
|
||||
type: string
|
||||
nullable: true
|
||||
example: Terry Goodkind
|
||||
tagGenre:
|
||||
type: string
|
||||
nullable: true
|
||||
example: Audiobook Fantasy
|
||||
tagTitle:
|
||||
type: string
|
||||
nullable: true
|
||||
example: Wizards First Rule 01
|
||||
tagSeries:
|
||||
type: string
|
||||
nullable: true
|
||||
tagSeriesPart:
|
||||
type: string
|
||||
nullable: true
|
||||
tagTrack:
|
||||
type: string
|
||||
nullable: true
|
||||
example: 01/20
|
||||
tagDisc:
|
||||
type: string
|
||||
nullable: true
|
||||
tagSubtitle:
|
||||
type: string
|
||||
nullable: true
|
||||
tagAlbumArtist:
|
||||
type: string
|
||||
nullable: true
|
||||
example: Terry Goodkind
|
||||
tagDate:
|
||||
type: string
|
||||
nullable: true
|
||||
tagComposer:
|
||||
type: string
|
||||
nullable: true
|
||||
example: Terry Goodkind
|
||||
tagPublisher:
|
||||
type: string
|
||||
nullable: true
|
||||
tagComment:
|
||||
type: string
|
||||
nullable: true
|
||||
tagDescription:
|
||||
type: string
|
||||
nullable: true
|
||||
tagEncoder:
|
||||
type: string
|
||||
nullable: true
|
||||
tagEncodedBy:
|
||||
type: string
|
||||
nullable: true
|
||||
tagIsbn:
|
||||
type: string
|
||||
nullable: true
|
||||
tagLanguage:
|
||||
type: string
|
||||
nullable: true
|
||||
tagASIN:
|
||||
type: string
|
||||
nullable: true
|
||||
tagOverdriveMediaMarker:
|
||||
type: string
|
||||
nullable: true
|
||||
tagOriginalYear:
|
||||
type: string
|
||||
nullable: true
|
||||
tagReleaseCountry:
|
||||
type: string
|
||||
nullable: true
|
||||
tagReleaseType:
|
||||
type: string
|
||||
nullable: true
|
||||
tagReleaseStatus:
|
||||
type: string
|
||||
nullable: true
|
||||
tagISRC:
|
||||
type: string
|
||||
nullable: true
|
||||
tagMusicBrainzTrackId:
|
||||
type: string
|
||||
nullable: true
|
||||
tagMusicBrainzAlbumId:
|
||||
type: string
|
||||
nullable: true
|
||||
tagMusicBrainzAlbumArtistId:
|
||||
type: string
|
||||
nullable: true
|
||||
tagMusicBrainzArtistId:
|
||||
type: string
|
||||
nullable: true
|
126
docs/objects/metadata/BookMetadata.yaml
Normal file
126
docs/objects/metadata/BookMetadata.yaml
Normal file
@ -0,0 +1,126 @@
|
||||
components:
|
||||
schemas:
|
||||
narrators:
|
||||
description: The narrators of the audiobook.
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: Sam Tsoutsouvas
|
||||
bookMetadataBase:
|
||||
type: object
|
||||
description: The base book metadata object for minified, normal, and extended schemas to inherit from.
|
||||
properties:
|
||||
title:
|
||||
description: The title of the book. Will be null if unknown.
|
||||
type: string
|
||||
nullable: true
|
||||
example: Wizards First Rule
|
||||
subtitle:
|
||||
description: The subtitle of the book. Will be null if there is no subtitle.
|
||||
type: string
|
||||
nullable: true
|
||||
genres:
|
||||
description: The genres of the book.
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: ["Fantasy", "Sci-Fi", "Nonfiction: History"]
|
||||
publishedYear:
|
||||
description: The year the book was published. Will be null if unknown.
|
||||
type: string
|
||||
nullable: true
|
||||
example: '2008'
|
||||
publishedDate:
|
||||
description: The date the book was published. Will be null if unknown.
|
||||
type: string
|
||||
nullable: true
|
||||
publisher:
|
||||
description: The publisher of the book. Will be null if unknown.
|
||||
type: string
|
||||
nullable: true
|
||||
example: Brilliance Audio
|
||||
description:
|
||||
description: A description for the book. Will be null if empty.
|
||||
type: string
|
||||
nullable: true
|
||||
example: >-
|
||||
The masterpiece that started Terry Goodkind's New York Times bestselling
|
||||
epic Sword of Truth In the aftermath of the brutal murder of his father,
|
||||
a mysterious woman, Kahlan Amnell, appears in Richard Cypher's forest
|
||||
sanctuary seeking help...and more. His world, his very beliefs, are
|
||||
shattered when ancient debts come due with thundering violence. In a
|
||||
dark age it takes courage to live, and more than mere courage to
|
||||
challenge those who hold dominion, Richard and Kahlan must take up that
|
||||
challenge or become the next victims. Beyond awaits a bewitching land
|
||||
where even the best of their hearts could betray them. Yet, Richard
|
||||
fears nothing so much as what secrets his sword might reveal about his
|
||||
own soul. Falling in love would destroy them - for reasons Richard can't
|
||||
imagine and Kahlan dare not say. In their darkest hour, hunted
|
||||
relentlessly, tormented by treachery and loss, Kahlan calls upon Richard
|
||||
to reach beyond his sword - to invoke within himself something more
|
||||
noble. Neither knows that the rules of battle have just changed...or
|
||||
that their time has run out. Wizard's First Rule is the beginning. One
|
||||
book. One Rule. Witness the birth of a legend.
|
||||
isbn:
|
||||
description: The ISBN of the book. Will be null if unknown.
|
||||
type: string
|
||||
nullable: true
|
||||
asin:
|
||||
description: The ASIN of the book. Will be null if unknown.
|
||||
type: string
|
||||
nullable: true
|
||||
example: B002V0QK4C
|
||||
language:
|
||||
description: The language of the book. Will be null if unknown.
|
||||
type: string
|
||||
nullable: true
|
||||
explicit:
|
||||
description: Whether the book has been marked as explicit.
|
||||
type: boolean
|
||||
example: false
|
||||
bookMetadataMinified:
|
||||
type: object
|
||||
description: The minified metadata for a book in the database.
|
||||
allOf:
|
||||
- $ref : '#/components/schemas/bookMetadataBase'
|
||||
- type: object
|
||||
properties:
|
||||
titleIgnorePrefix:
|
||||
description: The title of the book with any prefix moved to the end.
|
||||
type: string
|
||||
authorName:
|
||||
description: The name of the book's author(s).
|
||||
type: string
|
||||
example: Terry Goodkind
|
||||
authorNameLF:
|
||||
description: The name of the book's author(s) with last names first.
|
||||
type: string
|
||||
example: Goodkind, Terry
|
||||
narratorName:
|
||||
description: The name of the audiobook's narrator(s).
|
||||
type: string
|
||||
example: Sam Tsoutsouvas
|
||||
seriesName:
|
||||
description: The name of the book's series.
|
||||
type: string
|
||||
example: Sword of Truth
|
||||
bookChapter:
|
||||
type: object
|
||||
description: A book chapter. Includes the title and timestamps.
|
||||
properties:
|
||||
id:
|
||||
description: The ID of the book chapter.
|
||||
type: integer
|
||||
example: 0
|
||||
start:
|
||||
description: When in the book (in seconds) the chapter starts.
|
||||
type: integer
|
||||
example: 0
|
||||
end:
|
||||
description: When in the book (in seconds) the chapter ends.
|
||||
type: number
|
||||
example: 6004.6675
|
||||
title:
|
||||
description: The title of the chapter.
|
||||
type: string
|
||||
example: Wizards First Rule 01 Chapter 1
|
39
docs/objects/metadata/FileMetadata.yaml
Normal file
39
docs/objects/metadata/FileMetadata.yaml
Normal file
@ -0,0 +1,39 @@
|
||||
components:
|
||||
schemas:
|
||||
fileMetadata:
|
||||
type: object
|
||||
description: The metadata for a file, including the path, size, and unix timestamps of the file.
|
||||
nullable: true
|
||||
properties:
|
||||
filename:
|
||||
description: The filename of the file.
|
||||
type: string
|
||||
example: Wizards First Rule 01.mp3
|
||||
ext:
|
||||
description: The file extension of the file.
|
||||
type: string
|
||||
example: .mp3
|
||||
path:
|
||||
description: The absolute path on the server of the file.
|
||||
type: string
|
||||
example: >-
|
||||
/audiobooks/Terry Goodkind/Sword of Truth/Wizards First Rule/Terry
|
||||
Goodkind - SOT Bk01 - Wizards First Rule 01.mp3
|
||||
relPath:
|
||||
description: The path of the file, relative to the book's or podcast's folder.
|
||||
type: string
|
||||
example: Wizards First Rule 01.mp3
|
||||
size:
|
||||
$ref: '../../schemas.yaml#/components/schemas/size'
|
||||
mtimeMs:
|
||||
description: The time (in ms since POSIX epoch) when the file was last modified on disk.
|
||||
type: integer
|
||||
example: 1632223180278
|
||||
ctimeMs:
|
||||
description: The time (in ms since POSIX epoch) when the file status was changed on disk.
|
||||
type: integer
|
||||
example: 1645978261001
|
||||
birthtimeMs:
|
||||
description: The time (in ms since POSIX epoch) when the file was created on disk. Will be 0 if unknown.
|
||||
type: integer
|
||||
example: 0
|
BIN
docs/openapi.json
Normal file
BIN
docs/openapi.json
Normal file
Binary file not shown.
157
docs/root.yaml
Normal file
157
docs/root.yaml
Normal file
@ -0,0 +1,157 @@
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: Audiobookshelf API
|
||||
version: 0.1.0
|
||||
description: Audiobookshelf API with autogenerated OpenAPI doc
|
||||
servers:
|
||||
- url: http://localhost:3000
|
||||
description: Development server
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
responses:
|
||||
ok200:
|
||||
description: OK
|
||||
security:
|
||||
- BearerAuth: []
|
||||
paths:
|
||||
/api/authors/{id}:
|
||||
get:
|
||||
operationId: getAuthorById
|
||||
summary: Get a single author by ID on server
|
||||
tags:
|
||||
- Authors
|
||||
parameters:
|
||||
- $ref: './controllers/AuthorController.yaml#/components/parameters/authorId'
|
||||
- $ref: './controllers/AuthorController.yaml#/components/parameters/authorInclude'
|
||||
- $ref: './controllers/AuthorController.yaml#/components/parameters/authorLibraryId'
|
||||
responses:
|
||||
200:
|
||||
description: getAuthorById OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- $ref: './objects/entities/Author.yaml#/components/schemas/author'
|
||||
- $ref: './objects/entities/Author.yaml#/components/schemas/authorWithItems'
|
||||
- $ref: './objects/entities/Author.yaml#/components/schemas/authorWithSeries'
|
||||
404:
|
||||
$ref: './controllers/AuthorController.yaml#/components/responses/author404'
|
||||
patch:
|
||||
operationId: updateAuthorById
|
||||
summary: Update a single author by ID on server. This endpoint will merge two authors if the new author name matches another author in the database.
|
||||
tags:
|
||||
- Authors
|
||||
parameters:
|
||||
- $ref: './controllers/AuthorController.yaml#/components/parameters/authorId'
|
||||
- $ref: './controllers/AuthorController.yaml#/components/parameters/asin'
|
||||
- $ref: './controllers/AuthorController.yaml#/components/parameters/authorName'
|
||||
- $ref: './controllers/AuthorController.yaml#/components/parameters/authorDescription'
|
||||
- $ref: './controllers/AuthorController.yaml#/components/parameters/authorImagePath'
|
||||
responses:
|
||||
200:
|
||||
description: updateAuthorById OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: './objects/entities/Author.yaml#/components/schemas/author'
|
||||
- $ref: './controllers/AuthorController.yaml#/components/schemas/authorUpdated'
|
||||
- type: object
|
||||
properties:
|
||||
merged:
|
||||
description: Will only exist and be `true` if the author was merged with another author
|
||||
type: boolean
|
||||
nullable: true
|
||||
404:
|
||||
$ref: './controllers/AuthorController.yaml#/components/responses/author404'
|
||||
delete:
|
||||
operationId: deleteAuthorById
|
||||
summary: Delete a single author by ID on server and remove author from all books.
|
||||
tags:
|
||||
- Authors
|
||||
parameters:
|
||||
- $ref: './controllers/AuthorController.yaml#/components/parameters/authorId'
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/ok200'
|
||||
404:
|
||||
$ref: './controllers/AuthorController.yaml#/components/responses/author404'
|
||||
/api/authors/{id}/image:
|
||||
post:
|
||||
operationId: setAuthorImageById
|
||||
summary: Set an author image using a provided URL.
|
||||
tags:
|
||||
- Authors
|
||||
parameters:
|
||||
- $ref: './controllers/AuthorController.yaml#/components/parameters/authorId'
|
||||
- $ref: './controllers/AuthorController.yaml#/components/parameters/imageUrl'
|
||||
responses:
|
||||
200:
|
||||
description: setAuthorImageById OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- $ref: './objects/entities/Author.yaml#/components/schemas/author'
|
||||
404:
|
||||
$ref: './controllers/AuthorController.yaml#/components/responses/author404'
|
||||
delete:
|
||||
operationId: deleteAuthorImageById
|
||||
summary: Delete an author image from the server and remove the image from the database.
|
||||
tags:
|
||||
- Authors
|
||||
parameters:
|
||||
- $ref: './controllers/AuthorController.yaml#/components/parameters/authorId'
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/ok200'
|
||||
404:
|
||||
$ref: './controllers/AuthorController.yaml#/components/responses/author404'
|
||||
patch:
|
||||
operationId: getAuthorImageById
|
||||
summary: Return the author image by author ID.
|
||||
tags:
|
||||
- Authors
|
||||
parameters:
|
||||
- $ref: './controllers/AuthorController.yaml#/components/parameters/authorId'
|
||||
- $ref: './controllers/AuthorController.yaml#/components/parameters/imageWidth'
|
||||
- $ref: './controllers/AuthorController.yaml#/components/parameters/imageHeight'
|
||||
- $ref: './controllers/AuthorController.yaml#/components/parameters/imageFormat'
|
||||
- $ref: './controllers/AuthorController.yaml#/components/parameters/imageRaw'
|
||||
responses:
|
||||
200:
|
||||
description: getAuthorImageById OK
|
||||
content:
|
||||
image/*:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
404:
|
||||
$ref: './controllers/AuthorController.yaml#/components/responses/author404'
|
||||
/api/authors/{id}/match:
|
||||
post:
|
||||
operationId: matchAuthorById
|
||||
summary: Match the author against Audible using quick match. Quick match updates the author's description and image (if no image already existed) with information from audible. Either `asin` or `q` must be provided, with `asin` taking priority if both are provided.
|
||||
tags:
|
||||
- Authors
|
||||
parameters:
|
||||
- $ref: './controllers/AuthorController.yaml#/components/parameters/authorId'
|
||||
- $ref: './controllers/AuthorController.yaml#/components/parameters/asin'
|
||||
- $ref: './controllers/AuthorController.yaml#/components/parameters/authorSearchName'
|
||||
responses:
|
||||
200:
|
||||
description: matchAuthorById OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- $ref: './objects/entities/Author.yaml#/components/schemas/author'
|
||||
- $ref: './controllers/AuthorController.yaml#/components/schemas/authorUpdated'
|
||||
404:
|
||||
$ref: './controllers/AuthorController.yaml#/components/responses/author404'
|
||||
tags:
|
||||
- name: Authors
|
||||
description: Author endpoints
|
33
docs/schemas.yaml
Normal file
33
docs/schemas.yaml
Normal file
@ -0,0 +1,33 @@
|
||||
components:
|
||||
schemas:
|
||||
addedAt:
|
||||
type: integer
|
||||
description: The time (in ms since POSIX epoch) when added to the server.
|
||||
example: 1633522963509
|
||||
createdAt:
|
||||
type: integer
|
||||
description: The time (in ms since POSIX epoch) when was created.
|
||||
example: 1633522963509
|
||||
updatedAt:
|
||||
type: integer
|
||||
description: The time (in ms since POSIX epoch) when last updated.
|
||||
example: 1633522963509
|
||||
size:
|
||||
description: The total size (in bytes) of the item or file.
|
||||
type: integer
|
||||
example: 268824228
|
||||
durationSec:
|
||||
description: The total length (in seconds) of the item or file.
|
||||
type: number
|
||||
example: 33854.905
|
||||
tags:
|
||||
description: Tags applied to items.
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: ["To Be Read", "Genre: Nonfiction"]
|
||||
inode:
|
||||
description: The inode of the item in the file system.
|
||||
type: string
|
||||
format: "[0-9]*"
|
||||
example: '649644248522215260'
|
@ -117,16 +117,20 @@ class LibraryItemController {
|
||||
zipHelpers.zipDirectoryPipe(libraryItemPath, filename, res)
|
||||
}
|
||||
|
||||
//
|
||||
// PATCH: will create new authors & series if in payload
|
||||
//
|
||||
/**
|
||||
* PATCH: /items/:id/media
|
||||
* Update media for a library item. Will create new authors & series when necessary
|
||||
*
|
||||
* @param {import('express').Request} req
|
||||
* @param {import('express').Response} res
|
||||
*/
|
||||
async updateMedia(req, res) {
|
||||
const libraryItem = req.libraryItem
|
||||
const mediaPayload = req.body
|
||||
|
||||
if (mediaPayload.url) {
|
||||
await LibraryItemController.prototype.uploadCover.bind(this)(req, res, false)
|
||||
if (res.writableEnded) return
|
||||
if (res.writableEnded || res.headersSent) return
|
||||
}
|
||||
|
||||
// Book specific
|
||||
|
@ -284,7 +284,7 @@ class MiscController {
|
||||
}
|
||||
|
||||
res.json({
|
||||
tags: tags
|
||||
tags: tags.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
|
||||
})
|
||||
}
|
||||
|
||||
@ -329,6 +329,7 @@ class MiscController {
|
||||
await libraryItem.media.update({
|
||||
tags: libraryItem.media.tags
|
||||
})
|
||||
await libraryItem.saveMetadataFile()
|
||||
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
|
||||
numItemsUpdated++
|
||||
@ -370,6 +371,7 @@ class MiscController {
|
||||
await libraryItem.media.update({
|
||||
tags: libraryItem.media.tags
|
||||
})
|
||||
await libraryItem.saveMetadataFile()
|
||||
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
|
||||
numItemsUpdated++
|
||||
@ -462,6 +464,7 @@ class MiscController {
|
||||
await libraryItem.media.update({
|
||||
genres: libraryItem.media.genres
|
||||
})
|
||||
await libraryItem.saveMetadataFile()
|
||||
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
|
||||
numItemsUpdated++
|
||||
@ -503,6 +506,7 @@ class MiscController {
|
||||
await libraryItem.media.update({
|
||||
genres: libraryItem.media.genres
|
||||
})
|
||||
await libraryItem.saveMetadataFile()
|
||||
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
|
||||
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
|
||||
numItemsUpdated++
|
||||
|
@ -1,8 +1,12 @@
|
||||
const Path = require('path')
|
||||
const { DataTypes, Model } = require('sequelize')
|
||||
const fsExtra = require('../libs/fsExtra')
|
||||
const Logger = require('../Logger')
|
||||
const oldLibraryItem = require('../objects/LibraryItem')
|
||||
const libraryFilters = require('../utils/queries/libraryFilters')
|
||||
const { areEquivalent } = require('../utils/index')
|
||||
const { filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils')
|
||||
const LibraryFile = require('../objects/files/LibraryFile')
|
||||
const Book = require('./Book')
|
||||
const Podcast = require('./Podcast')
|
||||
|
||||
@ -828,6 +832,147 @@ class LibraryItem extends Model {
|
||||
return this[mixinMethodName](options)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<Book|Podcast>}
|
||||
*/
|
||||
getMediaExpanded() {
|
||||
if (this.mediaType === 'podcast') {
|
||||
return this.getMedia({
|
||||
include: [
|
||||
{
|
||||
model: this.sequelize.models.podcastEpisode
|
||||
}
|
||||
]
|
||||
})
|
||||
} else {
|
||||
return this.getMedia({
|
||||
include: [
|
||||
{
|
||||
model: this.sequelize.models.author,
|
||||
through: {
|
||||
attributes: []
|
||||
}
|
||||
},
|
||||
{
|
||||
model: this.sequelize.models.series,
|
||||
through: {
|
||||
attributes: ['sequence']
|
||||
}
|
||||
}
|
||||
],
|
||||
order: [
|
||||
[this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'],
|
||||
[this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC']
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async saveMetadataFile() {
|
||||
let metadataPath = Path.join(global.MetadataPath, 'items', this.id)
|
||||
let storeMetadataWithItem = global.ServerSettings.storeMetadataWithItem
|
||||
if (storeMetadataWithItem && !this.isFile) {
|
||||
metadataPath = this.path
|
||||
} else {
|
||||
// Make sure metadata book dir exists
|
||||
storeMetadataWithItem = false
|
||||
await fsExtra.ensureDir(metadataPath)
|
||||
}
|
||||
|
||||
const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`)
|
||||
|
||||
// Expanded with series, authors, podcastEpisodes
|
||||
const mediaExpanded = this.media || await this.getMediaExpanded()
|
||||
|
||||
let jsonObject = {}
|
||||
if (this.mediaType === 'book') {
|
||||
jsonObject = {
|
||||
tags: mediaExpanded.tags || [],
|
||||
chapters: mediaExpanded.chapters?.map(c => ({ ...c })) || [],
|
||||
title: mediaExpanded.title,
|
||||
subtitle: mediaExpanded.subtitle,
|
||||
authors: mediaExpanded.authors.map(a => a.name),
|
||||
narrators: mediaExpanded.narrators,
|
||||
series: mediaExpanded.series.map(se => {
|
||||
const sequence = se.bookSeries?.sequence || ''
|
||||
if (!sequence) return se.name
|
||||
return `${se.name} #${sequence}`
|
||||
}),
|
||||
genres: mediaExpanded.genres || [],
|
||||
publishedYear: mediaExpanded.publishedYear,
|
||||
publishedDate: mediaExpanded.publishedDate,
|
||||
publisher: mediaExpanded.publisher,
|
||||
description: mediaExpanded.description,
|
||||
isbn: mediaExpanded.isbn,
|
||||
asin: mediaExpanded.asin,
|
||||
language: mediaExpanded.language,
|
||||
explicit: !!mediaExpanded.explicit,
|
||||
abridged: !!mediaExpanded.abridged
|
||||
}
|
||||
} else {
|
||||
jsonObject = {
|
||||
tags: mediaExpanded.tags || [],
|
||||
title: mediaExpanded.title,
|
||||
author: mediaExpanded.author,
|
||||
description: mediaExpanded.description,
|
||||
releaseDate: mediaExpanded.releaseDate,
|
||||
genres: mediaExpanded.genres || [],
|
||||
feedURL: mediaExpanded.feedURL,
|
||||
imageURL: mediaExpanded.imageURL,
|
||||
itunesPageURL: mediaExpanded.itunesPageURL,
|
||||
itunesId: mediaExpanded.itunesId,
|
||||
itunesArtistId: mediaExpanded.itunesArtistId,
|
||||
asin: mediaExpanded.asin,
|
||||
language: mediaExpanded.language,
|
||||
explicit: !!mediaExpanded.explicit,
|
||||
podcastType: mediaExpanded.podcastType
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return fsExtra.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2)).then(async () => {
|
||||
// Add metadata.json to libraryFiles array if it is new
|
||||
let metadataLibraryFile = this.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath))
|
||||
if (storeMetadataWithItem) {
|
||||
if (!metadataLibraryFile) {
|
||||
const newLibraryFile = new LibraryFile()
|
||||
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
|
||||
metadataLibraryFile = newLibraryFile.toJSON()
|
||||
this.libraryFiles.push(metadataLibraryFile)
|
||||
} else {
|
||||
const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
|
||||
if (fileTimestamps) {
|
||||
metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
|
||||
metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs
|
||||
metadataLibraryFile.metadata.size = fileTimestamps.size
|
||||
metadataLibraryFile.ino = fileTimestamps.ino
|
||||
}
|
||||
}
|
||||
const libraryItemDirTimestamps = await getFileTimestampsWithIno(this.path)
|
||||
if (libraryItemDirTimestamps) {
|
||||
this.mtime = libraryItemDirTimestamps.mtimeMs
|
||||
this.ctime = libraryItemDirTimestamps.ctimeMs
|
||||
let size = 0
|
||||
this.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0))
|
||||
this.size = size
|
||||
await this.save()
|
||||
}
|
||||
}
|
||||
|
||||
Logger.debug(`Success saving abmetadata to "${metadataFilePath}"`)
|
||||
|
||||
return metadataLibraryFile
|
||||
}).catch((error) => {
|
||||
Logger.error(`Failed to save json file at "${metadataFilePath}"`, error)
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize model
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
|
@ -195,7 +195,7 @@ class Stream extends EventEmitter {
|
||||
var current_chunk = []
|
||||
var last_seg_in_chunk = -1
|
||||
|
||||
var segments = Array.from(this.segmentsCreated).sort((a, b) => a - b);
|
||||
var segments = Array.from(this.segmentsCreated).sort((a, b) => a - b)
|
||||
var lastSegment = segments[segments.length - 1]
|
||||
if (lastSegment > this.furthestSegmentCreated) {
|
||||
this.furthestSegmentCreated = lastSegment
|
||||
@ -342,7 +342,7 @@ class Stream extends EventEmitter {
|
||||
Logger.error('Ffmpeg Err', '"' + err.message + '"')
|
||||
|
||||
// Temporary workaround for https://github.com/advplyr/audiobookshelf/issues/172 and https://github.com/advplyr/audiobookshelf/issues/2157
|
||||
const aacErrorMsg = 'ffmpeg exited with code 1:'
|
||||
const aacErrorMsg = 'ffmpeg exited with code 1'
|
||||
if (audioCodec === 'copy' && this.isAACEncodable && err.message?.startsWith(aacErrorMsg)) {
|
||||
Logger.info(`[Stream] Re-attempting stream with AAC encode`)
|
||||
this.transcodeOptions.forceAAC = true
|
||||
|
@ -79,12 +79,19 @@ class Audible {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a search title matches an ASIN. Supports lowercase letters
|
||||
*
|
||||
* @param {string} title
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isProbablyAsin(title) {
|
||||
return /^[0-9A-Z]{10}$/.test(title)
|
||||
return /^[0-9A-Za-z]{10}$/.test(title)
|
||||
}
|
||||
|
||||
asinSearch(asin, region) {
|
||||
asin = encodeURIComponent(asin)
|
||||
if (!asin) return []
|
||||
asin = encodeURIComponent(asin.toUpperCase())
|
||||
var regionQuery = region ? `?region=${region}` : ''
|
||||
var url = `https://api.audnex.us/books/${asin}${regionQuery}`
|
||||
Logger.debug(`[Audible] ASIN url: ${url}`)
|
||||
@ -124,7 +131,7 @@ class Audible {
|
||||
const url = `https://api.audible${tld}/1.0/catalog/products?${queryString}`
|
||||
Logger.debug(`[Audible] Search url: ${url}`)
|
||||
items = await axios.get(url).then((res) => {
|
||||
if (!res || !res.data || !res.data.products) return null
|
||||
if (!res?.data?.products) return null
|
||||
return Promise.all(res.data.products.map(result => this.asinSearch(result.asin, region)))
|
||||
}).catch(error => {
|
||||
Logger.error('[Audible] query search error', error)
|
||||
|
@ -43,7 +43,7 @@ class CustomProviderAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
const matches = await axios.get(`${provider.url}/search?${queryString}}`, axiosOptions).then((res) => {
|
||||
const matches = await axios.get(`${provider.url}/search?${queryString}`, axiosOptions).then((res) => {
|
||||
if (!res?.data || !Array.isArray(res.data.matches)) return null
|
||||
return res.data.matches
|
||||
}).catch(error => {
|
||||
|
@ -378,7 +378,7 @@ class AudioFileScanner {
|
||||
const MetadataMapArray = [
|
||||
{
|
||||
tag: 'tagComment',
|
||||
altTag: 'tagSubtitle',
|
||||
altTag: 'tagDescription',
|
||||
key: 'description'
|
||||
},
|
||||
{
|
||||
|
@ -359,7 +359,7 @@ class Scanner {
|
||||
}
|
||||
|
||||
offset += limit
|
||||
hasMoreChunks = libraryItems.length < limit
|
||||
hasMoreChunks = libraryItems.length === limit
|
||||
let oldLibraryItems = libraryItems.map(li => Database.libraryItemModel.getOldLibraryItem(li))
|
||||
|
||||
const shouldContinue = await this.matchLibraryItemsChunk(library, oldLibraryItems, libraryScan)
|
||||
|
@ -104,7 +104,8 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
|
||||
const ffmpeg = Ffmpeg(response.data)
|
||||
ffmpeg.addOption('-loglevel debug') // Debug logs printed on error
|
||||
ffmpeg.outputOptions(
|
||||
'-c', 'copy',
|
||||
'-c:a', 'copy',
|
||||
'-map', '0:a',
|
||||
'-metadata', 'podcast=1'
|
||||
)
|
||||
|
||||
|
@ -59,7 +59,7 @@ async function getFileTimestampsWithIno(path) {
|
||||
ino: String(stat.ino)
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error('[fileUtils] Failed to getFileTimestampsWithIno', err)
|
||||
Logger.error(`[fileUtils] Failed to getFileTimestampsWithIno for path "${path}"`, err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,10 @@ async function extractFileFromEpub(epubPath, filepath) {
|
||||
Logger.error(`[parseEpubMetadata] Failed to extract ${filepath} from epub at "${epubPath}"`, error)
|
||||
})
|
||||
const filedata = data?.toString('utf8')
|
||||
await zip.close()
|
||||
await zip.close().catch((error) => {
|
||||
Logger.error(`[parseEpubMetadata] Failed to close zip`, error)
|
||||
})
|
||||
|
||||
return filedata
|
||||
}
|
||||
|
||||
@ -68,6 +71,9 @@ async function parse(ebookFile) {
|
||||
Logger.debug(`Parsing metadata from epub at "${epubPath}"`)
|
||||
// Entrypoint of the epub that contains the filepath to the package document (opf file)
|
||||
const containerJson = await extractXmlToJson(epubPath, 'META-INF/container.xml')
|
||||
if (!containerJson) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Get package document opf filepath from container.xml
|
||||
const packageDocPath = containerJson.container?.rootfiles?.[0]?.rootfile?.[0]?.$?.['full-path']
|
||||
|
@ -451,7 +451,7 @@ module.exports = {
|
||||
libraryId: libraryId
|
||||
}
|
||||
},
|
||||
attributes: ['tags', 'genres']
|
||||
attributes: ['tags', 'genres', 'language']
|
||||
})
|
||||
for (const podcast of podcasts) {
|
||||
if (podcast.tags?.length) {
|
||||
@ -460,6 +460,9 @@ module.exports = {
|
||||
if (podcast.genres?.length) {
|
||||
podcast.genres.forEach((genre) => data.genres.add(genre))
|
||||
}
|
||||
if (podcast.language) {
|
||||
data.languages.add(podcast.language)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const books = await Database.bookModel.findAll({
|
||||
|
@ -34,6 +34,10 @@ module.exports = {
|
||||
attributes: ['sequence']
|
||||
}
|
||||
}
|
||||
],
|
||||
order: [
|
||||
[Database.authorModel, Database.bookAuthorModel, 'createdAt', 'ASC'],
|
||||
[Database.seriesModel, 'bookSeries', 'createdAt', 'ASC']
|
||||
]
|
||||
})
|
||||
for (const book of booksWithTag) {
|
||||
@ -68,7 +72,7 @@ module.exports = {
|
||||
/**
|
||||
* Get all library items that have genres
|
||||
* @param {string[]} genres
|
||||
* @returns {Promise<LibraryItem[]>}
|
||||
* @returns {Promise<import('../../models/LibraryItem')[]>}
|
||||
*/
|
||||
async getAllLibraryItemsWithGenres(genres) {
|
||||
const libraryItems = []
|
||||
|
@ -51,6 +51,8 @@ module.exports = {
|
||||
[Sequelize.Op.gte]: 1
|
||||
})
|
||||
replacements.filterValue = value
|
||||
} else if (group === 'languages') {
|
||||
mediaWhere['language'] = value
|
||||
}
|
||||
|
||||
return {
|
||||
|
Loading…
Reference in New Issue
Block a user