Merge branch 'master' into openid_signing_algorithm

This commit is contained in:
advplyr 2024-04-21 15:38:33 -05:00
commit af856ce1ec
56 changed files with 2150 additions and 118 deletions

View File

@ -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

View File

@ -30,8 +30,7 @@
}
.bookshelf-row {
/* Sidebar width + scrollbar width */
width: calc(100vw - 88px);
width: calc(100vw - (100vw - 100%));
}
@media (max-width: 768px) {

View File

@ -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">

View File

@ -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

View File

@ -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)

View File

@ -235,6 +235,11 @@ export default {
value: 'tags',
sublist: true
},
{
text: this.$strings.LabelLanguage,
value: 'languages',
sublist: true
},
{
text: this.$strings.ButtonIssues,
value: 'issues',

View File

@ -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'

View File

@ -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)">

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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)

View File

@ -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)

View File

@ -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>

View File

@ -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
View 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": "ব্যবহারকারী মুছে ফেলা হয়েছে"
}

View File

@ -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"
}
}

View File

@ -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": "Пароль",

View File

@ -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}\"?",

View File

@ -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
View 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
```

View 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
View 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
View 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

View 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'

View 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

View 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

View 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

View 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

View 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'

View 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

View 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

View 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

Binary file not shown.

157
docs/root.yaml Normal file
View 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
View 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'

View File

@ -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

View File

@ -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++

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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 => {

View File

@ -378,7 +378,7 @@ class AudioFileScanner {
const MetadataMapArray = [
{
tag: 'tagComment',
altTag: 'tagSubtitle',
altTag: 'tagDescription',
key: 'description'
},
{

View File

@ -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)

View File

@ -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'
)

View File

@ -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
}
}

View File

@ -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']

View File

@ -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({

View File

@ -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 = []

View File

@ -51,6 +51,8 @@ module.exports = {
[Sequelize.Op.gte]: 1
})
replacements.filterValue = value
} else if (group === 'languages') {
mediaWhere['language'] = value
}
return {