Compare commits

...

50 Commits

Author SHA1 Message Date
advplyr
36ef675556 Fix edit episode next/prev buttons showing when editing from home page 2025-02-08 13:05:50 -06:00
advplyr
0dd57a8912 Fix using next/prev in edit modals while rich text input is focused #3951 2025-02-08 13:02:27 -06:00
advplyr
25ae6dd59a Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2025-02-07 17:10:12 -06:00
advplyr
a37fe3c3d2 Fix: Users with update permission unable to remove books from collection #3947 2025-02-07 17:09:48 -06:00
advplyr
59bcbe0dfa Merge pull request #3946 from advplyr/details_trim_whitespace
Trim whitespace from podcast/book/episode & batch edit text inputs
2025-02-06 17:51:49 -06:00
advplyr
b5e69630de Update batch edit text inputs to trim whitespace 2025-02-06 17:29:27 -06:00
advplyr
0bba709124 Trim whitespace from book/podcast/episode details text inputs #3943 2025-02-06 17:27:33 -06:00
advplyr
e93bb5cb07 Merge pull request #3941 from Vynce/accept-encoding
Add `Accept-Encoding` header to `getPodcastFeed()`
2025-02-06 17:01:31 -06:00
Michael Vincent
3f7af8acfb Add Accept-Encoding header to getPodcastFeed()
This commit adds the Accept-Encoding header to getPodcastFeed() with
gzip, compress, and deflate support. This allows servers to send a
compressed response that'll be decompressed by axios transparently.

Audiobookshelf is currently using axios v0.27.2, which enables the
decompress option by default. The decompress feature supports gzip,
compress, and deflate algorithms (see axios/lib/adapters/http.js).
axios v0.27.2 does not add the Accept-Encoding header to requests
automatically, so that's the responsibility of the caller.
2025-02-05 23:12:58 -06:00
advplyr
5e5a604d03 Fix name parser to not use "last, first" format when not using comma separators. Adds unit tests #3940 2025-02-05 17:25:31 -06:00
advplyr
201e12ecc3 Update downloadFile to debug log percentage complete 2025-02-05 16:15:00 -06:00
advplyr
24d6e390f0 Fix Book/Podcast updateFromRequest to support null values in string fields #3938 2025-02-05 15:31:57 -06:00
advplyr
0cf7a6abec Merge pull request #3929 from mikiher/fix-trix-resize
Add resize to trix editor
2025-02-04 17:22:30 -06:00
mikiher
76ac0d001b Add resize to trix editor 2025-02-04 09:54:28 +02:00
advplyr
00343a953b Update Collection/Playlist and batch quick match modal bg colors to be consistent with other modals 2025-02-03 17:47:10 -06:00
advplyr
82ab95ab02 Version bump v2.19.0 2025-02-02 15:39:46 -06:00
advplyr
a1d8ebc01b Merge pull request #3893 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-02-02 15:36:43 -06:00
advplyr
eeaae5f934 Added translation using Weblate (Turkish) 2025-02-02 22:06:22 +01:00
thehijacker
4464276a6e Translated using Weblate (Slovenian)
Currently translated at 100.0% (1089 of 1089 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2025-02-02 00:07:53 +01:00
biuklija
3465790fe9 Translated using Weblate (Croatian)
Currently translated at 100.0% (1089 of 1089 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-02-02 00:07:53 +01:00
Jonathan
5fa4c5a2c3 Translated using Weblate (German)
Currently translated at 99.3% (1082 of 1089 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-02-02 00:07:52 +01:00
SunSpring
13f353596b Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1089 of 1089 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-02-02 00:07:51 +01:00
Simple16
3d9100e5b8 Translated using Weblate (Russian)
Currently translated at 100.0% (1089 of 1089 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-02-02 00:07:50 +01:00
Максим Горпиніч
b62309ead2 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1089 of 1089 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-02-02 00:07:50 +01:00
Andreas Morell-Reng
1fce94ad4a Translated using Weblate (Danish)
Currently translated at 100.0% (1089 of 1089 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2025-02-02 00:07:49 +01:00
thehijacker
9abd6698ae Translated using Weblate (Slovenian)
Currently translated at 100.0% (1087 of 1087 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2025-02-02 00:07:48 +01:00
Jan-Eric Myhrgren
88c10ad619 Translated using Weblate (Swedish)
Currently translated at 85.4% (929 of 1087 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-02-02 00:07:48 +01:00
biuklija
c62a6fbffd Translated using Weblate (Croatian)
Currently translated at 100.0% (1087 of 1087 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-02-02 00:07:47 +01:00
Michel Neuba
989388d3ed Translated using Weblate (French)
Currently translated at 99.7% (1084 of 1087 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2025-02-02 00:07:46 +01:00
Will Forde
4cc97a22f6 Translated using Weblate (Japanese)
Currently translated at 0.1% (1 of 1087 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ja/
2025-02-02 00:07:45 +01:00
Максим Горпиніч
8bd336a4ba Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1087 of 1087 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-02-02 00:07:45 +01:00
thehijacker
437c8dd09c Translated using Weblate (Slovenian)
Currently translated at 100.0% (1086 of 1086 strings)

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

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-02-02 00:07:43 +01:00
Andreas Morell-Reng
74c87a0bbd Translated using Weblate (Danish)
Currently translated at 100.0% (1086 of 1086 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2025-02-02 00:07:43 +01:00
biuklija
35eb5bcfc0 Translated using Weblate (Croatian)
Currently translated at 100.0% (1086 of 1086 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-02-02 00:07:42 +01:00
Simple16
0a29b549df Translated using Weblate (Russian)
Currently translated at 100.0% (1086 of 1086 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-02-02 00:07:41 +01:00
SunSpring
a38a92b948 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1086 of 1086 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-02-02 00:07:40 +01:00
Jan-Eric Myhrgren
d245c93da4 Translated using Weblate (Swedish)
Currently translated at 85.1% (925 of 1086 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-02-02 00:07:40 +01:00
Илья Червонный
bcf8f6b732 Translated using Weblate (Russian)
Currently translated at 100.0% (1086 of 1086 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-02-02 00:07:39 +01:00
advplyr
40e11db5e5 Merge pull request #3921 from advplyr/fix_content_url_basepath
Fix API including basepath in tracks contentUrl
2025-02-01 17:07:29 -06:00
advplyr
aebb3ff413 Fix API including basepath in tracks contentUrl 2025-02-01 16:47:36 -06:00
advplyr
a58d486c44 Fix:Collapsed subseries showing parent series name on hover #3713 2025-01-31 17:18:23 -06:00
advplyr
4a76ba0226 Remove copy of series numbers on book cards 2025-01-31 17:11:57 -06:00
advplyr
7afff57b5e Merge pull request #3916 from nichwall/add_collection_help_text
Add collection and playlist help text
2025-01-30 17:50:21 -06:00
advplyr
2e13c5bd50 Fix no collections message, ui updates 2025-01-30 17:47:41 -06:00
advplyr
344de941ff Merge pull request #3919 from advplyr/fix_logger_fatal
Fix Logger.fatal to ensure crash_logs.txt is written to
2025-01-30 17:36:37 -06:00
advplyr
c3aad9486c Fix Logger.fatal to be a promise to ensure crash_logs.txt write 2025-01-30 17:27:32 -06:00
Nicholas Wallace
5c0cd98cb3 Add: collection and playlist help text to modal 2025-01-29 22:55:34 -07:00
Nicholas Wallace
8974c582fc Add: collection and playlist link to guide 2025-01-29 22:46:53 -07:00
advplyr
5ee6005112 Merge pull request #3914 from advplyr/progress_bar_visibility
Adds box shadow to progress bar on covers for visibility #3825
2025-01-29 18:05:07 -06:00
48 changed files with 359 additions and 111 deletions

View File

@@ -99,6 +99,7 @@ export default {
this.$store.commit('showEditModal', libraryItem)
},
editEpisode({ libraryItem, episode }) {
this.$store.commit('setEpisodeTableEpisodeIds', [episode.id])
this.$store.commit('setSelectedLibraryItem', libraryItem)
this.$store.commit('globals/setSelectedEpisode', episode)
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)

View File

@@ -19,6 +19,14 @@
</div>
<div v-else-if="!totalShelves && initialized" class="w-full py-16">
<p class="text-xl text-center">{{ emptyMessage }}</p>
<div v-if="entityName === 'collections' || entityName === 'playlists'" class="flex justify-center mt-4">
{{ emptyMessageHelp }}
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/collections" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
</div>
<!-- Clear filter only available on Library bookshelf -->
<div v-if="entityName === 'items'" class="flex justify-center mt-2">
<ui-btn v-if="hasFilter" color="primary" @click="clearFilter">{{ $strings.ButtonClearFilter }}</ui-btn>
@@ -109,6 +117,11 @@ export default {
}
return this.$strings.MessageNoResults
},
emptyMessageHelp() {
if (this.page === 'collections') return this.$strings.MessageBookshelfNoCollectionsHelp
if (this.page === 'playlists') return this.$strings.MessageNoUserPlaylistsHelp
return ''
},
entityName() {
if (!this.page) return 'items'
return this.page

View File

@@ -31,13 +31,6 @@
<p cy-id="placeholderAuthorText" aria-hidden="true" class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'em' }">{{ authorCleaned }}</p>
</div>
<div v-if="seriesSequenceList" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20 text-right" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }" style="background-color: #78350f">
<p :style="{ fontSize: 0.8 + 'em' }">#{{ seriesSequenceList }}</p>
</div>
<div v-else-if="booksInSeries" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-20" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }" style="background-color: #cd9d49dd">
<p :style="{ fontSize: 0.8 + 'em' }">{{ booksInSeries }}</p>
</div>
<!-- No progress shown for podcasts (unless showing podcast episode) -->
<div cy-id="progressBar" v-if="!isPodcast || episodeProgress" class="absolute bottom-0 left-0 h-1e max-w-full z-20 rounded-b box-shadow-progressbar" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * userProgressPercent + 'px' }"></div>
@@ -244,6 +237,7 @@ export default {
return this.mediaMetadata.series
},
seriesName() {
if (this.collapsedSeries?.name) return this.collapsedSeries.name
return this.series?.name || null
},
seriesSequence() {

View File

@@ -6,7 +6,7 @@
</div>
</template>
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
<div v-if="show" class="w-full h-full py-4">
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
<div class="flex px-8 items-center py-2">

View File

@@ -6,7 +6,7 @@
</div>
</template>
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
<div v-if="show" class="w-full h-full">
<div class="py-4 px-4">
<h1 v-if="!showBatchCollectionModal" class="text-2xl">{{ $strings.LabelAddToCollection }}</h1>
@@ -19,9 +19,20 @@
</template>
</transition-group>
</div>
<div v-if="!collections.length" class="flex h-32 items-center justify-center">
<p class="text-xl">{{ $strings.MessageNoCollections }}</p>
<div v-if="!collections.length" class="flex h-32 items-center justify-center text-center px-2">
<div>
<p class="text-xl mb-2">{{ $strings.MessageNoCollections }}</p>
<div class="text-sm flex items-center justify-center text-gray-200">
<p>{{ $strings.MessageBookshelfNoCollectionsHelp }}</p>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/collections" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
</div>
</div>
</div>
<div class="w-full h-px bg-white bg-opacity-10" />
<form @submit.prevent="submitCreateCollection">
<div class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">

View File

@@ -1,5 +1,5 @@
<template>
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-bg" @mouseover="mouseover" @mouseleave="mouseleave">
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-black-400" @mouseover="mouseover" @mouseleave="mouseleave">
<div v-if="isBookIncluded" class="absolute top-0 left-0 h-full w-1 bg-success z-10" />
<div class="w-20 max-w-20 text-center">
<covers-collection-cover :book-items="books" :width="80" :height="40 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />

View File

@@ -196,6 +196,9 @@ export default {
methods: {
async goPrevBook() {
if (this.currentBookshelfIndex - 1 < 0) return
// Remove focus from active input
document.activeElement?.blur?.()
var prevBookId = this.bookshelfBookIds[this.currentBookshelfIndex - 1]
this.processing = true
var prevBook = await this.$axios.$get(`/api/items/${prevBookId}?expanded=1`).catch((error) => {
@@ -215,6 +218,9 @@ export default {
},
async goNextBook() {
if (this.currentBookshelfIndex >= this.bookshelfBookIds.length - 1) return
// Remove focus from active input
document.activeElement?.blur?.()
this.processing = true
var nextBookId = this.bookshelfBookIds[this.currentBookshelfIndex + 1]
var nextBook = await this.$axios.$get(`/api/items/${nextBookId}?expanded=1`).catch((error) => {
@@ -300,4 +306,4 @@ export default {
.tab.tab-selected {
height: 41px;
}
</style>
</style>

View File

@@ -6,7 +6,7 @@
</div>
</template>
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
<div v-if="show" class="w-full h-full">
<div class="py-4 px-4">
<h1 v-if="!isBatch" class="text-2xl">{{ $strings.LabelAddToPlaylist }}</h1>
@@ -19,8 +19,18 @@
</template>
</transition-group>
</div>
<div v-if="!playlists.length" class="flex h-32 items-center justify-center">
<p class="text-xl">{{ $strings.MessageNoUserPlaylists }}</p>
<div v-if="!playlists.length" class="flex h-32 items-center justify-center text-center px-2">
<div>
<p class="text-xl mb-2">{{ $strings.MessageNoUserPlaylists }}</p>
<div class="text-sm flex items-center justify-center text-gray-200">
<p>{{ $strings.MessageNoUserPlaylistsHelp }}</p>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/collections" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
</div>
</div>
</div>
<div class="w-full h-px bg-white bg-opacity-10" />
<form @submit.prevent="submitCreatePlaylist">

View File

@@ -1,5 +1,5 @@
<template>
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-bg" @mouseover="mouseover" @mouseleave="mouseleave">
<div class="flex items-center px-4 py-2 justify-start relative hover:bg-black-400" @mouseover="mouseover" @mouseleave="mouseleave">
<div v-if="isItemIncluded" class="absolute top-0 left-0 h-full w-1 bg-success z-10" />
<div class="w-16 max-w-16 text-center">
<covers-playlist-cover :items="items" :width="64" :height="64" />

View File

@@ -117,8 +117,12 @@ export default {
methods: {
async goPrevEpisode() {
if (this.currentEpisodeIndex - 1 < 0) return
// Remove focus from active input
document.activeElement?.blur?.()
const prevEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex - 1]
this.processing = true
const prevEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${prevEpisodeId}`).catch((error) => {
const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch episode'
this.$toast.error(errorMsg)
@@ -134,8 +138,12 @@ export default {
},
async goNextEpisode() {
if (this.currentEpisodeIndex >= this.episodeTableEpisodeIds.length - 1) return
// Remove focus from active input
document.activeElement?.blur?.()
this.processing = true
const nextEpisodeId = this.episodeTableEpisodeIds[this.currentEpisodeIndex + 1]
const nextEpisode = await this.$axios.$get(`/api/podcasts/${this.libraryItem.id}/episode/${nextEpisodeId}`).catch((error) => {
const errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book'
this.$toast.error(errorMsg)

View File

@@ -2,10 +2,10 @@
<div>
<div class="flex flex-wrap">
<div class="w-1/5 p-1">
<ui-text-input-with-label v-model="newEpisode.season" :label="$strings.LabelSeason" />
<ui-text-input-with-label v-model="newEpisode.season" trim-whitespace :label="$strings.LabelSeason" />
</div>
<div class="w-1/5 p-1">
<ui-text-input-with-label v-model="newEpisode.episode" :label="$strings.LabelEpisode" />
<ui-text-input-with-label v-model="newEpisode.episode" trim-whitespace :label="$strings.LabelEpisode" />
</div>
<div class="w-1/5 p-1">
<ui-dropdown v-model="newEpisode.episodeType" :label="$strings.LabelEpisodeType" :items="episodeTypes" small />
@@ -14,10 +14,10 @@
<ui-text-input-with-label v-model="pubDateInput" ref="pubdate" type="datetime-local" :label="$strings.LabelPubDate" @input="updatePubDate" />
</div>
<div class="w-full p-1">
<ui-text-input-with-label v-model="newEpisode.title" :label="$strings.LabelTitle" />
<ui-text-input-with-label v-model="newEpisode.title" :label="$strings.LabelTitle" trim-whitespace />
</div>
<div class="w-full p-1">
<ui-textarea-with-label v-model="newEpisode.subtitle" :label="$strings.LabelSubtitle" :rows="3" />
<ui-textarea-with-label v-model="newEpisode.subtitle" :label="$strings.LabelSubtitle" :rows="3" trim-whitespace />
</div>
<div class="w-full p-1">
<ui-rich-text-editor :label="$strings.LabelDescription" v-model="newEpisode.description" />

View File

@@ -215,6 +215,10 @@ export default {
inputBlur() {
if (!this.isFocused) return
if (typeof this.textInput === 'string') {
this.textInput = this.textInput.trim()
}
setTimeout(() => {
if (document.activeElement === this.$refs.input) {
return
@@ -231,6 +235,11 @@ export default {
},
forceBlur() {
this.isFocused = false
if (typeof this.textInput === 'string') {
this.textInput = this.textInput.trim()
}
if (this.textInput) this.submitForm()
if (this.$refs.input) this.$refs.input.blur()
},
@@ -289,11 +298,12 @@ export default {
this.selectedMenuItemIndex = null
},
submitForm() {
if (!this.textInput) return
if (!this.textInput || !this.textInput.trim?.()) return
this.textInput = this.textInput.trim()
const cleaned = this.textInput.trim()
const matchesItem = this.items.find((i) => {
return i.name === cleaned
return i.name === this.textInput
})
if (matchesItem) {

View File

@@ -40,7 +40,8 @@ export default {
showCopy: Boolean,
step: [String, Number],
min: [String, Number],
customInputClass: String
customInputClass: String,
trimWhitespace: Boolean
},
data() {
return {
@@ -101,9 +102,13 @@ export default {
this.$emit('focus')
},
blurred() {
if (this.trimWhitespace && typeof this.inputValue === 'string') {
this.inputValue = this.inputValue.trim()
}
this.isFocused = false
this.$emit('blur')
},
change(e) {
this.$emit('change', e.target.value)
},

View File

@@ -6,7 +6,7 @@
<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
</label>
</slot>
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" :show-copy="showCopy" class="w-full" :class="inputClass" @blur="inputBlurred" />
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" :show-copy="showCopy" class="w-full" :class="inputClass" :trim-whitespace="trimWhitespace" @blur="inputBlurred" />
</div>
</template>
@@ -24,7 +24,8 @@ export default {
readonly: Boolean,
disabled: Boolean,
inputClass: String,
showCopy: Boolean
showCopy: Boolean,
trimWhitespace: Boolean
},
data() {
return {}

View File

@@ -351,8 +351,10 @@ export default {
background-color: white;
}
trix-editor {
max-height: calc(4 * 1lh);
height: calc(4 * 1lh);
min-height: calc(4 * 1lh);
overflow-y: auto;
resize: vertical;
}
trix-editor * {

View File

@@ -3,10 +3,10 @@
<form class="w-full h-full px-2 md:px-4 py-6" @submit.prevent="submitForm">
<div class="flex flex-wrap -mx-1">
<div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" @input="handleInputChange" />
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" trim-whitespace @input="handleInputChange" />
</div>
<div class="flex-grow px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" :label="$strings.LabelSubtitle" @input="handleInputChange" />
<ui-text-input-with-label ref="subtitleInput" v-model="details.subtitle" :label="$strings.LabelSubtitle" trim-whitespace @input="handleInputChange" />
</div>
</div>
@@ -42,19 +42,19 @@
<ui-multi-select ref="narratorsSelect" v-model="details.narrators" :label="$strings.LabelNarrators" :items="narrators" @input="handleInputChange" />
</div>
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" @input="handleInputChange" />
<ui-text-input-with-label ref="isbnInput" v-model="details.isbn" label="ISBN" trim-whitespace @input="handleInputChange" />
</div>
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" @input="handleInputChange" />
<ui-text-input-with-label ref="asinInput" v-model="details.asin" label="ASIN" trim-whitespace @input="handleInputChange" />
</div>
</div>
<div class="flex flex-wrap mt-2 -mx-1">
<div class="w-full md:w-1/4 px-1">
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" @input="handleInputChange" />
<ui-text-input-with-label ref="publisherInput" v-model="details.publisher" :label="$strings.LabelPublisher" trim-whitespace @input="handleInputChange" />
</div>
<div class="w-1/2 md:w-1/4 px-1 mt-2 md:mt-0">
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" @input="handleInputChange" />
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" trim-whitespace @input="handleInputChange" />
</div>
<div class="flex-grow px-1 pt-6 mt-2 md:mt-0">
<div class="flex justify-center">

View File

@@ -124,6 +124,7 @@ export default {
this.updateSelectionMode(false)
},
editEpisode({ libraryItem, episode }) {
this.$store.commit('setEpisodeTableEpisodeIds', [episode.id])
this.$store.commit('setSelectedLibraryItem', libraryItem)
this.$store.commit('globals/setSelectedEpisode', episode)
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)

View File

@@ -3,14 +3,14 @@
<form class="w-full h-full px-4 py-6" @submit.prevent="submitForm">
<div class="flex -mx-1">
<div class="w-1/2 px-1">
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" @input="handleInputChange" />
<ui-text-input-with-label ref="titleInput" v-model="details.title" :label="$strings.LabelTitle" trim-whitespace @input="handleInputChange" />
</div>
<div class="flex-grow px-1">
<ui-text-input-with-label ref="authorInput" v-model="details.author" :label="$strings.LabelAuthor" @input="handleInputChange" />
<ui-text-input-with-label ref="authorInput" v-model="details.author" :label="$strings.LabelAuthor" trim-whitespace @input="handleInputChange" />
</div>
</div>
<ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" :label="$strings.LabelRSSFeedURL" class="mt-2" @input="handleInputChange" />
<ui-text-input-with-label ref="feedUrlInput" v-model="details.feedUrl" :label="$strings.LabelRSSFeedURL" trim-whitespace class="mt-2" @input="handleInputChange" />
<ui-textarea-with-label ref="descriptionInput" v-model="details.description" :rows="3" :label="$strings.LabelDescription" class="mt-2" @input="handleInputChange" />
@@ -25,13 +25,13 @@
<div class="flex mt-2 -mx-1">
<div class="w-1/4 px-1">
<ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" :label="$strings.LabelReleaseDate" @input="handleInputChange" />
<ui-text-input-with-label ref="releaseDateInput" v-model="details.releaseDate" :label="$strings.LabelReleaseDate" trim-whitespace @input="handleInputChange" />
</div>
<div class="w-1/4 px-1">
<ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" @input="handleInputChange" />
<ui-text-input-with-label ref="itunesIdInput" v-model="details.itunesId" label="iTunes ID" trim-whitespace @input="handleInputChange" />
</div>
<div class="w-1/4 px-1">
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" @input="handleInputChange" />
<ui-text-input-with-label ref="languageInput" v-model="details.language" :label="$strings.LabelLanguage" trim-whitespace @input="handleInputChange" />
</div>
<div class="flex-grow px-1 pt-6">
<div class="flex justify-center">

View File

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

View File

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

View File

@@ -414,11 +414,8 @@ export default {
const audioEl = this.audioEl || document.createElement('audio')
var src = audioTrack.contentUrl + `?token=${this.userToken}`
if (this.$isDev) {
src = `${process.env.serverUrl}${src}`
}
audioEl.src = src
audioEl.src = `${process.env.serverUrl}${src}`
audioEl.id = 'chapter-audio'
document.body.appendChild(audioEl)

View File

@@ -22,7 +22,7 @@
<div v-if="openMapOptions" class="flex flex-wrap">
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.subtitle" />
<ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" class="mb-5 ml-4" />
<ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" trim-whitespace class="mb-5 ml-4" />
</div>
<div v-if="!isPodcastLibrary" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.authors" />
@@ -31,7 +31,7 @@
</div>
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.publishedYear" />
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" class="mb-5 ml-4" />
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" trim-whitespace class="mb-5 ml-4" />
</div>
<div v-if="!isPodcastLibrary" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.series" />
@@ -51,11 +51,11 @@
</div>
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.publisher" />
<ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" class="mb-5 ml-4" />
<ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" trim-whitespace class="mb-5 ml-4" />
</div>
<div v-if="!isMapAppend" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.language" />
<ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" class="mb-5 ml-4" />
<ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" trim-whitespace class="mb-5 ml-4" />
</div>
<div v-if="!isMapAppend" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.explicit" />

View File

@@ -1,5 +1,5 @@
export default class AudioTrack {
constructor(track, userToken) {
constructor(track, userToken, routerBasePath) {
this.index = track.index || 0
this.startOffset = track.startOffset || 0 // Total time of all previous tracks
this.duration = track.duration || 0
@@ -9,20 +9,27 @@ export default class AudioTrack {
this.metadata = track.metadata || {}
this.userToken = userToken
this.routerBasePath = routerBasePath || ''
}
/**
* Used for CastPlayer
*/
get fullContentUrl() {
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
if (process.env.NODE_ENV === 'development') {
return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
}
return `${window.location.origin}${this.contentUrl}?token=${this.userToken}`
return `${window.location.origin}${this.routerBasePath}${this.contentUrl}?token=${this.userToken}`
}
/**
* Used for LocalPlayer
*/
get relativeContentUrl() {
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
return this.contentUrl + `?token=${this.userToken}`
return `${this.routerBasePath}${this.contentUrl}?token=${this.userToken}`
}
}

View File

@@ -226,7 +226,7 @@ export default class PlayerHandler {
console.log('[PlayerHandler] Preparing Session', session)
var audioTracks = session.audioTracks.map((at) => new AudioTrack(at, this.userToken))
var audioTracks = session.audioTracks.map((at) => new AudioTrack(at, this.userToken, this.ctx.$config.routerBasePath))
this.ctx.playerLoading = true
this.isHlsTranscode = true

View File

@@ -5,11 +5,13 @@
"ButtonAddLibrary": "Tilføj Bibliotek",
"ButtonAddPodcasts": "Tilføj podcasts",
"ButtonAddUser": "Tilføj bruger",
"ButtonAddYourFirstLibrary": "Tilføj din første bibliotek",
"ButtonAddYourFirstLibrary": "Tilføj dit første bibliotek",
"ButtonApply": "Anvend",
"ButtonApplyChapters": "Anvend kapitler",
"ButtonAuthors": "Forfattere",
"ButtonBack": "Tilbage",
"ButtonBatchEditPopulateFromExisting": "Opret fra eksisterende",
"ButtonBatchEditPopulateMapDetails": "Opret fra kortlægnings detaljer",
"ButtonBrowseForFolder": "Gennemse mappe",
"ButtonCancel": "Annuller",
"ButtonCancelEncode": "Annuller kodning",
@@ -91,7 +93,7 @@
"ButtonScrollLeft": "Rul til Venstre",
"ButtonScrollRight": "Rul til Højre",
"ButtonSearch": "Søg",
"ButtonSelectFolderPath": "Vælg Mappen Sti",
"ButtonSelectFolderPath": "Vælg Mappe Sti",
"ButtonSeries": "Serier",
"ButtonSetChaptersFromTracks": "Sæt kapitler fra spor",
"ButtonShare": "Del",
@@ -213,7 +215,7 @@
"LabelAbridgedChecked": "Forkortet (kontrolleret)",
"LabelAbridgedUnchecked": "Uforkortet (ikke kontrolleret)",
"LabelAccessibleBy": "Tilgængelig af",
"LabelAccountType": "Kontotype",
"LabelAccountType": "Brugertype",
"LabelAccountTypeAdmin": "Administrator",
"LabelAccountTypeGuest": "Gæst",
"LabelAccountTypeUser": "Bruger",
@@ -224,7 +226,7 @@
"LabelAddToPlaylistBatch": "Tilføj {0} Elementer til Afspilningsliste",
"LabelAddedAt": "Tilføjet",
"LabelAddedDate": "Tilføjet {0}",
"LabelAdminUsersOnly": "Kun Administratorbrugere",
"LabelAdminUsersOnly": "Kun Administratorer",
"LabelAll": "Alle",
"LabelAllUsers": "Alle Brugere",
"LabelAllUsersExcludingGuests": "Alle bruger eksklusiv gæster",
@@ -443,7 +445,7 @@
"LabelNarrator": "Fortæller",
"LabelNarrators": "Fortællere",
"LabelNew": "Ny",
"LabelNewPassword": "Nyt kodeord",
"LabelNewPassword": "Ny adgangskode",
"LabelNewestAuthors": "Nyeste forfattere",
"LabelNewestEpisodes": "Nyeste episoder",
"LabelNextBackupDate": "Næste sikkerhedskopi dato",
@@ -465,12 +467,12 @@
"LabelNumberOfBooks": "Antal bøger",
"LabelNumberOfEpisodes": "# afsnit",
"LabelOpenIDAdvancedPermsClaimDescription": "Navnet af OpenID claimet som indeholder avancerede brugerhandlinger inden i applikationen som vil gælde for ikke administrative roller (<b>hvis konfigureret</b>). Hvis et claim mangler fra svaret vil adgang til ABS blive nægtet. Hvis en enkelt indstilling/option mangler, vil det bliver behandlet som <code>false</code>. Sørg for at identity provider's claim matcher den forventede struktur:",
"LabelOpenIDClaims": "Efterlad de følgende indstillinger tomme for at deaktivere avancerede grupper og adgangsstyring for automatisk at tilføje dem til 'User' gruppen.",
"LabelOpenIDClaims": "Efterlad de følgende indstillinger tomme for at deaktivere avanceret gruppe og adgangsindstilling, ved automatisk at assigne 'Bruger' grupper.",
"LabelOpenIDGroupClaimDescription": "Navnet af det OpenID claim som skal indeholde brugerens grupper. Mest kendt som <code>groups</code>. <b>hvis konfigureret</b>, vil applikationen automatiske tildele roller baseret p[ brugerens gruppemedlemsskaber, givet disse grupper er navngivet (uden forbehold for store og små bogstaver) 'admin', 'user' eller 'guest' i claimet. Claimet burde indeholde en liste (og hvis brugeren tilhøre flere grupper) som applikationen vil tildele roller med højeste adgangsnvieau. Hvis ingen grupper matcher vil adgang blive nægtet.",
"LabelOpenRSSFeed": "Åbn RSS-feed",
"LabelOverwrite": "Overskriv",
"LabelPaginationPageXOfY": "Side {0} af {1}",
"LabelPassword": "Kodeord",
"LabelPassword": "Adgangskode",
"LabelPath": "Sti",
"LabelPermanent": "Permanent",
"LabelPermissionsAccessAllLibraries": "Kan få adgang til alle biblioteker",
@@ -484,6 +486,7 @@
"LabelPersonalYearReview": "Dit år i review ({0})",
"LabelPhotoPathURL": "Foto sti/URL",
"LabelPlayMethod": "Afspilningsmetode",
"LabelPlaybackRateIncrementDecrement": "Afspilningshastighed øges/sænkes med",
"LabelPlayerChapterNumberMarker": "{0} af {1}",
"LabelPlaylists": "Afspilningslister",
"LabelPodcast": "Podcast",
@@ -573,12 +576,12 @@
"LabelSettingsLibraryMarkAsFinishedWhen": "Marker medie indhold som færdigt når",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Spring til tidligere bøger i Fortsæt serie",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Fortsæt Serien siden hylde viser de første bøger som ikke er startet i serier med mindst en bog som ikke er startet og ingen bøger i gang. Aktivering af denne indstilling vil fortsætte serien fra den sidst gennemførte bog modsat den først ikke startede bog.",
"LabelSettingsParseSubtitles": "Fortolk undertekster",
"LabelSettingsParseSubtitles": "Fortolk undertitler",
"LabelSettingsParseSubtitlesHelp": "Udtræk undertekster fra lydbogsmappenavne.<br>Undertitler skal adskilles af \" - \"<br>f.eks. \"Bogtitel - En undertitel her\" har undertitlen \"En undertitel her\"",
"LabelSettingsPreferMatchedMetadata": "Foretræk matchede metadata",
"LabelSettingsPreferMatchedMetadataHelp": "Matchede data vil tilsidesætte elementdetaljer ved brug af Hurtig Match. Som standard udfylder Hurtig Match kun manglende detaljer.",
"LabelSettingsSkipMatchingBooksWithASIN": "Spring over matchende bøger, der allerede har en ASIN",
"LabelSettingsSkipMatchingBooksWithISBN": "Spring over matchende bøger, der allerede har en ISBN",
"LabelSettingsSkipMatchingBooksWithISBN": "Spring matchende bøger over, som allerede har et ISBN-nummer",
"LabelSettingsSortingIgnorePrefixes": "Ignorer præfikser ved sortering",
"LabelSettingsSortingIgnorePrefixesHelp": "f.eks. for præfikset \"the\" vil bogtitlen \"The Book Title\" blive sorteret som \"Book Title, The\"",
"LabelSettingsSquareBookCovers": "Brug kvadratiske bogomslag",
@@ -662,7 +665,7 @@
"LabelTrailer": "Trailer",
"LabelType": "Type",
"LabelUnabridged": "Uforkortet",
"LabelUndo": "Undo",
"LabelUndo": "Fortryd",
"LabelUnknown": "Ukendt",
"LabelUnknownPublishDate": "Ukendt publiceringsdato",
"LabelUpdateCover": "Opdater omslag",
@@ -704,8 +707,11 @@
"MessageBackupsLocationEditNote": "Note: Opdatering af backup sti vil ikke fjerne eller modificere eksisterende backups",
"MessageBackupsLocationNoEditNote": "Note: Backup sti er sat igennem miljøvariabel og kan ikke ændres her.",
"MessageBackupsLocationPathEmpty": "Backup sti kan ikke være tom",
"MessageBatchEditPopulateMapDetailsAllHelp": "Opret felter slået til med data fra alle genstande. Felter med flere værdier vil blive sammenflettet",
"MessageBatchEditPopulateMapDetailsItemHelp": "Opret kort med værdier der er slået til fra felter med data fra denne genstand",
"MessageBatchQuickMatchDescription": "Quick Match vil forsøge at tilføje manglende omslag og metadata til de valgte elementer. Aktivér indstillingerne nedenfor for at tillade Quick Match at overskrive eksisterende omslag og/eller metadata.",
"MessageBookshelfNoCollections": "Du har ikke oprettet nogen samlinger endnu",
"MessageBookshelfNoCollectionsHelp": "Samlinger er offentlige. Alle brugere med adgang til biblioteket kan se dem.",
"MessageBookshelfNoRSSFeeds": "Ingen RSS-feeds er åbne",
"MessageBookshelfNoResultsForFilter": "Ingen resultater for filter \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Intet resultat for query",
@@ -816,6 +822,7 @@
"MessageNoTasksRunning": "Ingen opgaver kører",
"MessageNoUpdatesWereNecessary": "Ingen opdateringer var nødvendige",
"MessageNoUserPlaylists": "Du har ingen afspilningslister",
"MessageNoUserPlaylistsHelp": "Playlister er private. Kun brugere som opretter dem kan se dem.",
"MessageNotYetImplemented": "Endnu ikke implementeret",
"MessageOpmlPreviewNote": "Note: Dette er en forhåndsvisning af den indlæste OPML fil. Podcast titel vil blive taget fra RSS feedet.",
"MessageOr": "eller",

View File

@@ -645,7 +645,7 @@
"LabelTimeToShift": "Zeit bis zum Wechsel in Sekunden",
"LabelTitle": "Titel",
"LabelToolsEmbedMetadata": "Metadaten einbetten",
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.",
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodateien ein.",
"LabelToolsM4bEncoder": "M4B Kodierer",
"LabelToolsMakeM4b": "M4B-Datei erstellen",
"LabelToolsMakeM4bDescription": "Erstellt eine M4B-Datei (Endung \".m4b\") welche mehrere mp3-Dateien in einer einzigen Datei inkl. derer Metadaten (Beschreibung, Titelbild, Kapitel, ...) zusammenfasst. M4B-Datei können darüber hinaus Lesezeichen speichern und mit einem Abspielschutz (Passwort) versehen werden.",

View File

@@ -711,6 +711,7 @@
"MessageBatchEditPopulateMapDetailsItemHelp": "Populate enabled map details fields with data from this item",
"MessageBatchQuickMatchDescription": "Quick Match will attempt to add missing covers and metadata for the selected items. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.",
"MessageBookshelfNoCollections": "You haven't made any collections yet",
"MessageBookshelfNoCollectionsHelp": "Collections are public. All users with access to the library can see them.",
"MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
"MessageBookshelfNoResultsForFilter": "No results for filter \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "No results for query",
@@ -821,6 +822,7 @@
"MessageNoTasksRunning": "No Tasks Running",
"MessageNoUpdatesWereNecessary": "No updates were necessary",
"MessageNoUserPlaylists": "You have no playlists",
"MessageNoUserPlaylistsHelp": "Playlists are private. Only the user who creates them can see them.",
"MessageNotYetImplemented": "Not yet implemented",
"MessageOpmlPreviewNote": "Note: This is a preview of the parsed OPML file. The actual podcast title will be taken from the RSS feed.",
"MessageOr": "or",

View File

@@ -10,6 +10,8 @@
"ButtonApplyChapters": "Appliquer aux chapitres",
"ButtonAuthors": "Auteurs",
"ButtonBack": "Retour",
"ButtonBatchEditPopulateFromExisting": "Remplir à partir de l'existant",
"ButtonBatchEditPopulateMapDetails": "Remplir les détails de la carte",
"ButtonBrowseForFolder": "Naviguer vers le répertoire",
"ButtonCancel": "Annuler",
"ButtonCancelEncode": "Annuler lencodage",
@@ -484,6 +486,7 @@
"LabelPersonalYearReview": "Bilan de lannée ({0})",
"LabelPhotoPathURL": "Chemin / URL des photos",
"LabelPlayMethod": "Méthode découte",
"LabelPlaybackRateIncrementDecrement": "Augmentation/Diminition de la vitesse de lecture",
"LabelPlayerChapterNumberMarker": "{0} sur {1}",
"LabelPlaylists": "Listes de lecture",
"LabelPodcast": "Podcast",
@@ -704,6 +707,7 @@
"MessageBackupsLocationEditNote": "Remarque: Mettre à jour l'emplacement de sauvegarde ne déplacera pas ou ne modifiera pas les sauvegardes existantes",
"MessageBackupsLocationNoEditNote": "Remarque: lemplacement de sauvegarde est défini via une variable denvironnement et ne peut pas être modifié ici.",
"MessageBackupsLocationPathEmpty": "L'emplacement de secours ne peut pas être vide",
"MessageBatchEditPopulateMapDetailsAllHelp": "Remplir les champs disponibles avec les données de tous les éléments. les champs avec des valeurs multiples seront fusionnés",
"MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera dajouter les couvertures et métadonnées manquantes pour les éléments sélectionnés. Activez les options ci-dessous pour permettre la Recherche par correspondance décraser les couvertures et/ou métadonnées existantes.",
"MessageBookshelfNoCollections": "Vous navez pas encore de collections",
"MessageBookshelfNoRSSFeeds": "Aucun flux RSS nest ouvert",

View File

@@ -10,6 +10,8 @@
"ButtonApplyChapters": "Primijeni poglavlja",
"ButtonAuthors": "Autori",
"ButtonBack": "Natrag",
"ButtonBatchEditPopulateFromExisting": "Popuni iz postojećeg",
"ButtonBatchEditPopulateMapDetails": "Popuni mapirane pojedinosti",
"ButtonBrowseForFolder": "Pronađi mapu",
"ButtonCancel": "Odustani",
"ButtonCancelEncode": "Otkaži kodiranje",
@@ -288,7 +290,7 @@
"LabelCustomCronExpression": "Prilagođeni CRON izraz:",
"LabelDatetime": "Datum i vrijeme",
"LabelDays": "Dani",
"LabelDeleteFromFileSystemCheckbox": "Izbriši datoteke (uklonite oznaku ako stavku želite izbrisati samo iz baze podataka)",
"LabelDeleteFromFileSystemCheckbox": "Izbriši datoteke (uklonite kvačicu ako stavku želite izbrisati samo iz baze podataka)",
"LabelDescription": "Opis",
"LabelDeselectAll": "Odznači sve",
"LabelDevice": "Uređaj",
@@ -399,7 +401,7 @@
"LabelLastBookAdded": "Zadnja dodana knjiga",
"LabelLastBookUpdated": "Zadnja ažurirana knjiga",
"LabelLastSeen": "Zadnji puta viđen",
"LabelLastTime": "Zadnji puta",
"LabelLastTime": "Zadnje vrijeme",
"LabelLastUpdate": "Zadnje ažuriranje",
"LabelLayout": "Prikaz",
"LabelLayoutSinglePage": "Jedna stranica",
@@ -484,6 +486,7 @@
"LabelPersonalYearReview": "Vaš godišnji pregled ({0})",
"LabelPhotoPathURL": "Putanja ili URL fotografije",
"LabelPlayMethod": "Način reprodukcije",
"LabelPlaybackRateIncrementDecrement": "Korak povećanja/smanjenja brzine reprodukcije",
"LabelPlayerChapterNumberMarker": "{0} od {1}",
"LabelPlaylists": "Popisi za izvođenje",
"LabelPodcast": "Podcast",
@@ -704,8 +707,11 @@
"MessageBackupsLocationEditNote": "Napomena: Uređivanje lokacije za sigurnosne kopije ne premješta ili mijenja postojeće sigurnosne kopije",
"MessageBackupsLocationNoEditNote": "Napomena: Lokacija za sigurnosne kopije zadana je kroz varijablu okoline i ovdje se ne može izmijeniti.",
"MessageBackupsLocationPathEmpty": "Putanja do lokacije za sigurnosne kopije ne može ostati prazna",
"MessageBatchEditPopulateMapDetailsAllHelp": "Nadopunjuje omogućena polja podatcima iz svih stavki. Polja s višestrukim podatcima će se spojiti",
"MessageBatchEditPopulateMapDetailsItemHelp": "Popuni omogućena polja mapiranih pojedinosti s podatcima iz ove stavke",
"MessageBatchQuickMatchDescription": "Brzo prepoznavanje za odabrane će stavke pokušati dodati naslovnice i meta-podatke koji nedostaju. Uključite donje opcije ako želite da Brzo prepoznavanje prepiše postojeće naslovnice i/ili meta-podatke.",
"MessageBookshelfNoCollections": "Niste izradili niti jednu zbirku",
"MessageBookshelfNoCollectionsHelp": "Zbirke su javne. Svi korisnici s pristupom knjižnici mogu ih vidjeti.",
"MessageBookshelfNoRSSFeeds": "Nema otvorenih RSS izvora",
"MessageBookshelfNoResultsForFilter": "Nema rezultata za filter \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Vaš upit nema rezultata",
@@ -721,7 +727,7 @@
"MessageConfirmDeleteDevice": "Sigurno želite izbrisati e-čitač \"{0}\"?",
"MessageConfirmDeleteFile": "Ovo će izbrisati datoteke s datotečnog sustava. Jeste li sigurni?",
"MessageConfirmDeleteLibrary": "Sigurno želite trajno izbrisati knjižnicu \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "Ovo će izbrisati knjižničku stavku iz datoteke i vašeg datotečnog sustava. Jeste li sigurni?",
"MessageConfirmDeleteLibraryItem": "Ovo će izbrisati knjižničku stavku iz baze podataka i s datotečnog sustava. Jeste li sigurni?",
"MessageConfirmDeleteLibraryItems": "Ovo će izbrisati {0} knjižničkih stavki iz baze podataka i datotečnog sustava. Jeste li sigurni?",
"MessageConfirmDeleteMetadataProvider": "Sigurno želite izbrisati prilagođenog pružatelja meta-podataka \"{0}\"?",
"MessageConfirmDeleteNotification": "Sigurno želite izbrisati ovu obavijest?",
@@ -816,6 +822,7 @@
"MessageNoTasksRunning": "Nema zadataka koji se izvode",
"MessageNoUpdatesWereNecessary": "Ažuriranje nije bilo potrebno",
"MessageNoUserPlaylists": "Nemate popisa za izvođenje",
"MessageNoUserPlaylistsHelp": "Popisi za izvođenje su privatni. Može ih vidjeti samo korisnik koji ih je izradio.",
"MessageNotYetImplemented": "Još nije implementirano",
"MessageOpmlPreviewNote": "Napomena: Ovo je pretpregled raščlanjene OPML datoteke. Stvarni naslov podcasta preuzet će se iz RSS izvora.",
"MessageOr": "ili",

View File

@@ -1 +1,3 @@
{}
{
"ButtonAdd": "追加"
}

View File

@@ -10,6 +10,8 @@
"ButtonApplyChapters": "Применить главы",
"ButtonAuthors": "Авторы",
"ButtonBack": "Назад",
"ButtonBatchEditPopulateFromExisting": "Заполнить из существующих",
"ButtonBatchEditPopulateMapDetails": "Заполнить данные карты",
"ButtonBrowseForFolder": "Выбрать папку",
"ButtonCancel": "Отмена",
"ButtonCancelEncode": "Отменить кодирование",
@@ -301,7 +303,7 @@
"LabelDownload": "Скачать",
"LabelDownloadNEpisodes": "Скачать {0} эпизодов",
"LabelDownloadable": "Загружаемый",
"LabelDuration": "Длина",
"LabelDuration": "Продолжительность",
"LabelDurationComparisonExactMatch": "(точное совпадение)",
"LabelDurationComparisonLonger": "({0} дольше)",
"LabelDurationComparisonShorter": "({0} короче)",
@@ -432,7 +434,7 @@
"LabelMetadataProvider": "Провайдер",
"LabelMinute": "Минуты",
"LabelMinutes": "Минуты",
"LabelMissing": "Потеряно",
"LabelMissing": "Отсутствует",
"LabelMissingEbook": "Нет e-книги",
"LabelMissingSupplementaryEbook": "Нет дополнительной e-книги",
"LabelMobileRedirectURIs": "Разрешенные URI перенаправления с мобильных устройств",
@@ -463,7 +465,7 @@
"LabelNotificationsMaxQueueSize": "Макс. размер очереди для событий уведомлений",
"LabelNotificationsMaxQueueSizeHelp": "События ограничены 1 в секунду. События будут игнорированы если в очереди максимальное количество. Это предотвращает спам сообщениями.",
"LabelNumberOfBooks": "Количество книг",
"LabelNumberOfEpisodes": "# Эпизодов",
"LabelNumberOfEpisodes": "# из эпизодов",
"LabelOpenIDAdvancedPermsClaimDescription": "Имя утверждения OpenID, содержащего расширенные разрешения на действия пользователя в приложении, которые будут применяться к ролям, не являющимся администраторами (<b>если они настроены</b>). Если утверждение отсутствует в ответе, в доступе к ABS будет отказано. Если одна опция отсутствует, она будет рассматриваться как <code>false</code>. Убедитесь, что утверждение поставщика удостоверений соответствует ожидаемой структуре:",
"LabelOpenIDClaims": "Оставьте следующие параметры пустыми, чтобы отключить расширенное назначение групп и разрешений, будет автоматически присвоена группа «Пользователь».",
"LabelOpenIDGroupClaimDescription": "Имя утверждения OpenID, содержащего список групп пользователя. Обычно их называют <code>groups</code>. <b>Если эта настройка</b> настроена, приложение будет автоматически назначать роли на основе членства пользователя в группах при условии, что эти группы названы в утверждении без учета регистра \"admin\", \"user\" или \"guest\". Утверждение должно содержать список, и если пользователь принадлежит к нескольким группам, то приложение назначит роль, соответствующую самому высокому уровню доступа. Если ни одна из групп не совпадает, доступ будет запрещен.",
@@ -484,6 +486,7 @@
"LabelPersonalYearReview": "Итоги прошедшего года ({0})",
"LabelPhotoPathURL": "Путь к фото/URL",
"LabelPlayMethod": "Метод воспроизведения",
"LabelPlaybackRateIncrementDecrement": "Величина увеличения/уменьшения скорости воспроизведения",
"LabelPlayerChapterNumberMarker": "{0} из {1}",
"LabelPlaylists": "Плейлисты",
"LabelPodcast": "Подкаст",
@@ -651,7 +654,7 @@
"LabelToolsMakeM4bDescription": "Создает .M4B файл аудиокниги с встроенными метаданными, обложкой и главами.",
"LabelToolsSplitM4b": "Разделить M4B на MP3 файлы",
"LabelToolsSplitM4bDescription": "Создает MP3 файла из M4B, разделяет на главы с встроенными метаданными, обложкой и главами.",
"LabelTotalDuration": "Общая длина",
"LabelTotalDuration": "Общая продолжительность",
"LabelTotalTimeListened": "Всего прослушано",
"LabelTrackFromFilename": "Трек из Имени файла",
"LabelTrackFromMetadata": "Трек из Метаданных",
@@ -704,8 +707,11 @@
"MessageBackupsLocationEditNote": "Примечание: Обновление местоположения резервной копии не приведет к перемещению или изменению существующих резервных копий",
"MessageBackupsLocationNoEditNote": "Примечание: Местоположение резервного копирования задается с помощью переменной среды и не может быть изменено здесь.",
"MessageBackupsLocationPathEmpty": "Путь к расположению резервной копии не может быть пустым",
"MessageBatchEditPopulateMapDetailsAllHelp": "Заполнить включенные поля данными из всех элементов. Поля с несколькими значениями будут объединены",
"MessageBatchEditPopulateMapDetailsItemHelp": "Заполнить активированные поля сведений о карте данными из этого элемента",
"MessageBatchQuickMatchDescription": "Быстрый Поиск попытается добавить отсутствующие обложки и метаданные для выбранных элементов. Включите параметры ниже, чтобы разрешить Быстрому Поиску перезаписывать существующие обложки и/или метаданные.",
"MessageBookshelfNoCollections": "Вы еще не создали ни одной коллекции",
"MessageBookshelfNoCollectionsHelp": "Коллекции являются общедоступными. Все пользователи, имеющие доступ к библиотеке, могут их просматривать.",
"MessageBookshelfNoRSSFeeds": "Нет открытых RSS-каналов",
"MessageBookshelfNoResultsForFilter": "Нет Результатов для фильтра \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Нет результатов для запроса",
@@ -816,6 +822,7 @@
"MessageNoTasksRunning": "Нет выполняемых задач",
"MessageNoUpdatesWereNecessary": "Обновления не требовались",
"MessageNoUserPlaylists": "У вас нет плейлистов",
"MessageNoUserPlaylistsHelp": "Списки воспроизведения являются конфиденциальными. Только пользователь, который их создает, может их видеть.",
"MessageNotYetImplemented": "Пока не реализовано",
"MessageOpmlPreviewNote": "Примечание: Это предварительный просмотр разобранного файла OPML. Фактическое название подкаста будет взято из RSS-канала.",
"MessageOr": "или",

View File

@@ -434,7 +434,7 @@
"LabelMetadataProvider": "Ponudnik metapodatkov",
"LabelMinute": "Minuta",
"LabelMinutes": "Minute",
"LabelMissing": "Manjkajoče",
"LabelMissing": "Manjka",
"LabelMissingEbook": "Nima nobene e-knjige",
"LabelMissingSupplementaryEbook": "Nima nobene dodatne e-knjige",
"LabelMobileRedirectURIs": "Dovoljeni mobilni preusmeritveni URI-ji",
@@ -486,6 +486,7 @@
"LabelPersonalYearReview": "Pregled tvojega leta ({0})",
"LabelPhotoPathURL": "Slika pot/URL",
"LabelPlayMethod": "Metoda predvajanja",
"LabelPlaybackRateIncrementDecrement": "Korak povečanja/zmanjšanja hitrosti predvajanja",
"LabelPlayerChapterNumberMarker": "{0} od {1}",
"LabelPlaylists": "Seznami predvajanja",
"LabelPodcast": "Podcast",
@@ -710,6 +711,7 @@
"MessageBatchEditPopulateMapDetailsItemHelp": "Napolni omogočena polja s podrobnostmi zemljevida s podatki iz tega elementa",
"MessageBatchQuickMatchDescription": "Hitro ujemanje bo poskušal dodati manjkajoče naslovnice in metapodatke za izbrane elemente. Omogočite spodnje možnosti, da omogočite hitremu ujemanju, da prepiše obstoječe naslovnice in/ali metapodatke.",
"MessageBookshelfNoCollections": "Ustvaril nisi še nobene zbirke",
"MessageBookshelfNoCollectionsHelp": "Zbirke so javne. Vsi uporabniki z dostopom do knjižnice jih lahko vidijo.",
"MessageBookshelfNoRSSFeeds": "Noben vir RSS ni odprt",
"MessageBookshelfNoResultsForFilter": "Ni rezultatov za filter \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Ni rezultatov za poizvedbo",
@@ -820,6 +822,7 @@
"MessageNoTasksRunning": "Nobeno opravili ne teče",
"MessageNoUpdatesWereNecessary": "Posodobitve niso bile potrebne",
"MessageNoUserPlaylists": "Nimate seznamov predvajanja",
"MessageNoUserPlaylistsHelp": "Seznami predvajanj so zasebni. Samo uporabniki, ki jih ustvarijo, jih lahko vidijo.",
"MessageNotYetImplemented": "Še ni implementirano",
"MessageOpmlPreviewNote": "Opomba: To je predogled razčlenjene datoteke OPML. Dejanski naslov podcasta bo vzet iz vira RSS.",
"MessageOr": "ali",

View File

@@ -110,7 +110,7 @@
"HeaderAccount": "Konto",
"HeaderAddCustomMetadataProvider": "Addera egen källa för metadata",
"HeaderAdvanced": "Avancerad",
"HeaderAppriseNotificationSettings": "Apprise Meddelandeinställningar",
"HeaderAppriseNotificationSettings": "Inställningar av meddelanden med Apprise",
"HeaderAudioTracks": "Ljudspår",
"HeaderAudiobookTools": "Hantering av ljudboksfil",
"HeaderAuthentication": "Autentisering",
@@ -153,6 +153,7 @@
"HeaderMetadataToEmbed": "Metadata som kommer att adderas",
"HeaderNewAccount": "Nytt konto",
"HeaderNewLibrary": "Nytt bibliotek",
"HeaderNotificationCreate": "Addera ett meddelande",
"HeaderNotifications": "Meddelanden",
"HeaderOpenRSSFeed": "Öppna RSS-flöde",
"HeaderOtherFiles": "Andra filer",
@@ -205,7 +206,7 @@
"LabelAccountTypeUser": "Användare",
"LabelActivity": "Aktivitet",
"LabelAddToCollection": "Lägg till i en samling",
"LabelAddToCollectionBatch": "Lägg till {0} böcker i en Samling",
"LabelAddToCollectionBatch": "Lägg till {0} böcker i samlingen",
"LabelAddToPlaylist": "Lägg till i en spellista",
"LabelAddToPlaylistBatch": "Lägg till {0} objekt i Spellistan",
"LabelAddedAt": "Datum adderad",
@@ -215,7 +216,7 @@
"LabelAllUsers": "Alla användare",
"LabelAllUsersExcludingGuests": "Alla användare utom gäster",
"LabelAllUsersIncludingGuests": "Alla användare inklusive gäster",
"LabelAlreadyInYourLibrary": "Redan i din samling",
"LabelAlreadyInYourLibrary": "Finns redan i samlingen",
"LabelApiToken": "API-token",
"LabelAppend": "Lägg till",
"LabelAudioBitrate": "Bitrate för ljud (t.ex. 128k)",
@@ -483,7 +484,7 @@
"LabelSelectEpisodesShowing": "Välj {0} avsnitt som visas",
"LabelSelectUsers": "Välj användare",
"LabelSendEbookToDevice": "Skicka e-bok till...",
"LabelSequence": "Sekvens",
"LabelSequence": "Sekvensnummer",
"LabelSeries": "Serier",
"LabelSeriesName": "Serienamn",
"LabelSeriesProgress": "Status för serier",
@@ -637,7 +638,7 @@
"MessageAddToPlayerQueue": "Lägg till i spellistan",
"MessageAppriseDescription": "För att använda den här funktionen behöver du ha en instans av <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> igång eller en API som hanterar dessa begäranden. <br />Apprise API-urlen bör vara hela URL-sökvägen för att skicka meddelandet, t.ex., om din API-instans är tillgänglig på <code>http://192.168.1.1:8337</code>, bör du ange <code>http://192.168.1.1:8337/notify</code>.",
"MessageBackupsDescription": "Säkerhetskopior inkluderar användare, användarnas framsteg, biblioteksobjekt, serverinställningar<br>och bilder lagrade i <code>/metadata/items</code> & <code>/metadata/authors</code>.<br>De inkluderar <strong>INTE</strong> några filer lagrade i dina biblioteksmappar.",
"MessageBackupsLocationEditNote": "OBS: När du ändrar plats för säkerhetskopiorna så flyttas INTE gamla säkerhetskopior dit",
"MessageBackupsLocationEditNote": "OBS: När du ändrar plats för säkerhetskopiorna så flyttas INTE gamla säkerhetskopior dit.",
"MessageBackupsLocationNoEditNote": "OBS: Platsen där säkerhetskopiorna lagras bestäms av en central inställning och kan inte ändras här.",
"MessageBackupsLocationPathEmpty": "Uppgiften om platsen för lagring av säkerhetskopior kan inte lämnas tom",
"MessageBatchQuickMatchDescription": "Quick Match kommer försöka lägga till saknade omslag och metadata för de valda föremålen. Aktivera alternativen nedan för att tillåta Quick Match att överskriva befintliga omslag och/eller metadata.",
@@ -660,8 +661,10 @@
"MessageConfirmDeleteLibraryItem": "Detta kommer att radera objektet från databasen och ditt filsystem. Är du säker?",
"MessageConfirmDeleteLibraryItems": "Detta kommer att radera {0} biblioteksobjekt från databasen och ditt filsystem. Är du säker?",
"MessageConfirmDeleteMetadataProvider": "Är du säker på att du vill radera din egen källa för metadata \"{0}\"?",
"MessageConfirmDeleteNotification": "Är du säker på att du vill radera detta meddelande?",
"MessageConfirmDeleteSession": "Är du säker på att du vill radera detta lyssningstillfälle?",
"MessageConfirmForceReScan": "Är du säker på att du vill tvinga omgenomsökning?",
"MessageConfirmEmbedMetadataInAudioFiles": "Är du säker på att du vill infoga metadata i {0} ljudfiler?",
"MessageConfirmForceReScan": "Är du säker på att du vill starta en ny skanning?",
"MessageConfirmMarkAllEpisodesFinished": "Är du säker på att du vill markera alla avsnitt som avslutade?",
"MessageConfirmMarkAllEpisodesNotFinished": "Är du säker på att du vill markera alla avsnitt som ej avslutade?",
"MessageConfirmMarkItemFinished": "Är du säker på att du vill markera \"{0}\" som avslutad?",
@@ -678,7 +681,7 @@
"MessageConfirmRemoveEpisode": "Är du säker på att du vill ta bort avsnittet \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Är du säker på att du vill ta bort {0} avsnitt?",
"MessageConfirmRemoveListeningSessions": "Är du säker på att du vill radera {0} lyssningstillfällen?",
"MessageConfirmRemoveMetadataFiles": "Är du säker på att du vill radera 'metadata.{0}' filerna i alla mappar i ditt bibliotek?",
"MessageConfirmRemoveMetadataFiles": "Är du säker på att du vill radera filerna 'metadata.{0}' i alla mappar i ditt bibliotek?",
"MessageConfirmRemoveNarrator": "Är du säker på att du vill ta bort uppläsaren \"{0}\"?",
"MessageConfirmRemovePlaylist": "Är du säker på att du vill ta bort din spellista \"{0}\"?",
"MessageConfirmRenameGenre": "Är du säker på att du vill byta namn på kategorin \"{0}\" till \"{1}\" för alla objekt?",
@@ -748,7 +751,7 @@
"MessageOr": "eller",
"MessagePauseChapter": "Pausa kapiteluppspelning",
"MessagePlayChapter": "Lyssna på kapitlets början",
"MessagePlaylistCreateFromCollection": "Skapa spellista från samling",
"MessagePlaylistCreateFromCollection": "Skapa en spellista från samlingen",
"MessagePleaseWait": "Vänta ett ögonblick...",
"MessagePodcastHasNoRSSFeedForMatching": "Podcasten har ingen RSS-flödes-URL att använda för matchning",
"MessageQuickMatchDescription": "Adderar uppgifter som saknas samt en omslagsbild från<br>första träffen i resultatet vid sökningen från '{0}'.<br>Skriver inte över befintliga uppgifter om inte<br>inställningen 'Prioritera matchad metadata' är aktiverad.",
@@ -760,6 +763,7 @@
"MessageResetChaptersConfirm": "Är du säker på att du vill återställa alla kapitel och ångra de ändringarna du gjort?",
"MessageRestoreBackupConfirm": "Är du säker på att du vill läsa in säkerhetskopian som skapades den",
"MessageRestoreBackupWarning": "Att återställa en säkerhetskopia kommer att skriva över hela databasen som finns i /config och omslagsbilder i /metadata/items & /metadata/authors.<br /><br />Säkerhetskopior ändrar inte några filer i dina biblioteksmappar. Om du har aktiverat serverinställningar för att lagra omslagskonst och metadata i dina biblioteksmappar säkerhetskopieras eller skrivs de inte över.<br /><br />Alla klienter som använder din server kommer att uppdateras automatiskt.",
"MessageScheduleLibraryScanNote": "För de flesta användare rekommenderas att denna funktion ej aktiveras. Istället bör funktionen 'Watcher' vara aktiverad. Watcher kommer då automatiskt identifiera förändringar i biblioteket. För vissa filsystem (som t.ex. NFS) fungerar inte Watcher. Då kan schemalagda skanningar av biblioteken användas istället.",
"MessageSearchResultsFor": "Sökresultat för",
"MessageSelected": "{0} valda",
"MessageServerCouldNotBeReached": "Servern kunde inte nås",
@@ -783,15 +787,15 @@
"MessageXLibraryIsEmpty": "Biblioteket {0} är tomt!",
"MessageYourAudiobookDurationIsLonger": "Varaktigheten på din ljudbok är längre än den hittade varaktigheten",
"MessageYourAudiobookDurationIsShorter": "Varaktigheten på din ljudbok är kortare än den hittade varaktigheten",
"NoteChangeRootPassword": "Rotanvändaren är den enda användaren som kan ha ett tomt lösenord",
"NoteChangeRootPassword": "Användaren 'root' är den enda användaren som kan vara utan lösenord",
"NoteChapterEditorTimes": "OBS: Starttiden för första kapitlet måste vara 0:00 och starttiden för det sista kapitlet får inte överstiga ljudbokens totala varaktighet.",
"NoteFolderPicker": "Obs: Mappar som redan är kartlagda kommer inte att visas",
"NoteRSSFeedPodcastAppsHttps": "Varning: De flesta podcastappar kräver att RSS-flödets URL används med HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Varning: 1 eller flera av dina avsnitt har inte ett publiceringsdatum. Vissa podcastappar kräver detta.",
"NoteUploaderFoldersWithMediaFiles": "Mappar med flera mediefiler hanteras som separata objekt i biblioteket.",
"NoteFolderPicker": "OBS: Mappar som redan är kopplade kommer inte att visas",
"NoteRSSFeedPodcastAppsHttps": "VARNING: De flesta applikationer för podcasts kräver att URL:en för RSS-flödet använder HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "VARNING: Ett eller flera av dina avsnitt har inte ett publiceringsdatum. Vissa applikationer för podcasts kräver detta.",
"NoteUploaderFoldersWithMediaFiles": "Mappar som innehåller mediefiler hanteras som separata objekt i biblioteket.",
"NoteUploaderOnlyAudioFiles": "Om du bara laddar upp ljudfiler kommer varje ljudfil att hanteras som en separat ljudbok.",
"NoteUploaderUnsupportedFiles": "Oaccepterade filer ignoreras. När du väljer eller släpper en mapp ignoreras andra filer som inte finns i ett objektmapp.",
"PlaceholderNewCollection": "Nytt samlingsnamn",
"PlaceholderNewCollection": "Nytt namn på samlingen",
"PlaceholderNewFolderPath": "Nytt sökväg till mappen",
"PlaceholderNewPlaylist": "Nytt namn på spellistan",
"PlaceholderSearch": "Sök...",
@@ -851,7 +855,9 @@
"ToastDeviceTestEmailFailed": "Misslyckades med att skicka ett testmail",
"ToastDeviceTestEmailSuccess": "Ett testmail har skickats",
"ToastEmailSettingsUpdateSuccess": "Inställningarna av e-post har uppdaterats",
"ToastEncodeCancelSucces": "Omkodningen avbruten",
"ToastFailedToLoadData": "Misslyckades med att ladda data",
"ToastFailedToUpdate": "Misslyckades med att uppdatera",
"ToastInvalidImageUrl": "Felaktig URL-adress till omslagsbilden",
"ToastInvalidMaxEpisodesToDownload": "Ogiltigt maximalt antal avsnitt att ladda ner",
"ToastInvalidUrl": "Felaktig URL-adress",
@@ -878,7 +884,12 @@
"ToastNameRequired": "Ett namn måste anges",
"ToastNewUserCreatedFailed": "Misslyckades med att skapa kontot \"{0}\"",
"ToastNewUserCreatedSuccess": "Ett nytt konto har skapats",
"ToastNewUserLibraryError": "Minst ett bibliotek måste anges",
"ToastNewUserPasswordError": "Ett lösenord måste anges. Endast användaren 'root' kan vara utan lösenord.",
"ToastNewUserUsernameError": "Ange ett användarnamn",
"ToastNoUpdatesNecessary": "Inga uppdateringar var nödvändiga",
"ToastNotificationCreateFailed": "Misslyckades med att skapa meddelandet",
"ToastNotificationDeleteFailed": "Misslyckades med att radera meddelandet",
"ToastPlaylistCreateFailed": "Det gick inte att skapa spellistan",
"ToastPlaylistCreateSuccess": "Spellistan skapad",
"ToastPlaylistRemoveSuccess": "Spellistan har tagits bort",
@@ -887,11 +898,14 @@
"ToastPodcastCreateSuccess": "Podcasten skapad framgångsrikt",
"ToastProviderCreatedFailed": "Misslyckades med att addera en källa",
"ToastProviderCreatedSuccess": "En ny källa har adderats",
"ToastProviderNameAndUrlRequired": "Ett namn och en URL-adress krävs",
"ToastProviderRemoveSuccess": "Källan har tagits bort",
"ToastRSSFeedCloseFailed": "Misslyckades med att stänga RSS-flödet",
"ToastRSSFeedCloseSuccess": "RSS-flödet stängt",
"ToastRemoveFailed": "Misslyckades med att radera",
"ToastRemoveItemFromCollectionFailed": "Misslyckades med att ta bort objektet från samlingen",
"ToastRemoveItemFromCollectionSuccess": "Objektet borttaget från samlingen",
"ToastRenameFailed": "Misslyckades med att ändra namn",
"ToastSelectAtLeastOneUser": "Åtminstone en användare måste väljas",
"ToastSendEbookToDeviceFailed": "Misslyckades med att skicka e-boken till enheten",
"ToastSendEbookToDeviceSuccess": "E-boken skickad till enheten \"{0}\"",
@@ -912,5 +926,6 @@
"ToastUserDeleteSuccess": "Användaren borttagen",
"ToastUserPasswordChangeSuccess": "Lösenordet har ändrats",
"ToastUserPasswordMismatch": "Lösenorden är inte identiska",
"ToastUserPasswordMustChange": "Det nya lösenordet kan inte vara samma som det gamla"
"ToastUserPasswordMustChange": "Det nya lösenordet kan inte vara samma som det gamla",
"ToastUserRootRequireName": "Ett användarnamn för 'root' måste anges"
}

1
client/strings/tr.json Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -434,7 +434,7 @@
"LabelMetadataProvider": "Джерело метаданих",
"LabelMinute": "Хвилина",
"LabelMinutes": "Хвилини",
"LabelMissing": "Бракує",
"LabelMissing": "Відсутня",
"LabelMissingEbook": "Без електронної книги",
"LabelMissingSupplementaryEbook": "Без додаткової електронної книги",
"LabelMobileRedirectURIs": "Дозволені адреси перенаправлення",
@@ -486,6 +486,7 @@
"LabelPersonalYearReview": "Ваші підсумки року ({0})",
"LabelPhotoPathURL": "Шлях/URL фото",
"LabelPlayMethod": "Метод відтворення",
"LabelPlaybackRateIncrementDecrement": "Величина збільшення/зменшення швидкості відтворення",
"LabelPlayerChapterNumberMarker": "{0} з {1}",
"LabelPlaylists": "Списки відтворення",
"LabelPodcast": "Подкаст",
@@ -710,6 +711,7 @@
"MessageBatchEditPopulateMapDetailsItemHelp": "Заповніть увімкнені поля деталей карти даними з цього елемента",
"MessageBatchQuickMatchDescription": "Швидкий пошук спробує знайти відсутні обкладинки та метадані обраних елементів. Увімкніть налаштування нижче, аби дозволити заміну наявних обкладинок та/або метаданих під час швидкого пошуку.",
"MessageBookshelfNoCollections": "Ви не створили жодної добірки",
"MessageBookshelfNoCollectionsHelp": "Колекції публічні. Їх можуть бачити всі користувачі, які мають доступ до бібліотеки.",
"MessageBookshelfNoRSSFeeds": "Немає відкритих RSS-каналів",
"MessageBookshelfNoResultsForFilter": "Немає результатів з фільтром \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Немає результатів за запитом",
@@ -820,6 +822,7 @@
"MessageNoTasksRunning": "Немає активних завдань",
"MessageNoUpdatesWereNecessary": "Оновлень не потрібно",
"MessageNoUserPlaylists": "У вас немає списків відтворення",
"MessageNoUserPlaylistsHelp": "Списки відтворення приватні. Лише користувач, який їх створює, може бачити їх.",
"MessageNotYetImplemented": "Ще не реалізовано",
"MessageOpmlPreviewNote": "Примітка: це попередній перегляд OPML-файлу. Актуальна назва подкасту буде завантажена з RSS-каналу.",
"MessageOr": "або",

View File

@@ -10,6 +10,8 @@
"ButtonApplyChapters": "应用到章节",
"ButtonAuthors": "作者",
"ButtonBack": "返回",
"ButtonBatchEditPopulateFromExisting": "用现有内容填充",
"ButtonBatchEditPopulateMapDetails": "填充地图详细信息",
"ButtonBrowseForFolder": "浏览文件夹",
"ButtonCancel": "取消",
"ButtonCancelEncode": "取消编码",
@@ -432,7 +434,7 @@
"LabelMetadataProvider": "元数据提供商",
"LabelMinute": "分钟",
"LabelMinutes": "分钟",
"LabelMissing": "丢失",
"LabelMissing": "丢失",
"LabelMissingEbook": "没有电子书",
"LabelMissingSupplementaryEbook": "没有补充电子书",
"LabelMobileRedirectURIs": "允许移动应用重定向 URI",
@@ -484,6 +486,7 @@
"LabelPersonalYearReview": "你的年度回顾 ({0})",
"LabelPhotoPathURL": "图片路径或 URL",
"LabelPlayMethod": "播放方法",
"LabelPlaybackRateIncrementDecrement": "播放速率增加/减少量",
"LabelPlayerChapterNumberMarker": "{0} 于 {1}",
"LabelPlaylists": "播放列表",
"LabelPodcast": "播客",
@@ -704,8 +707,11 @@
"MessageBackupsLocationEditNote": "注意: 更新备份位置不会移动或修改现有备份",
"MessageBackupsLocationNoEditNote": "注意: 备份位置是通过环境变量设置的, 不能在此处更改.",
"MessageBackupsLocationPathEmpty": "备份位置路径不能为空",
"MessageBatchEditPopulateMapDetailsAllHelp": "使用所有项目的数据填充已启用的字段. 具有多个值的字段将被合并",
"MessageBatchEditPopulateMapDetailsItemHelp": "使用此项目的数据填充已启用的地图详细信息字段",
"MessageBatchQuickMatchDescription": "快速匹配将尝试为所选项目添加缺少的封面和元数据. 启用以下选项以允许快速匹配覆盖现有封面和或元数据.",
"MessageBookshelfNoCollections": "你尚未进行任何收藏",
"MessageBookshelfNoCollectionsHelp": "收藏是公开的. 所有有权访问图书馆的用户都可以看到它们.",
"MessageBookshelfNoRSSFeeds": "没有打开的 RSS 源",
"MessageBookshelfNoResultsForFilter": "过滤器无结果 \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "没有可查询的结果",
@@ -816,6 +822,7 @@
"MessageNoTasksRunning": "没有正在运行的任务",
"MessageNoUpdatesWereNecessary": "无需更新",
"MessageNoUserPlaylists": "你没有播放列表",
"MessageNoUserPlaylistsHelp": "播放列表是私密的. 只有创建播放列表的用户才能看到.",
"MessageNotYetImplemented": "尚未实施",
"MessageOpmlPreviewNote": "注意: 这是解析的OPML文件的预览. 实际的播客标题将从 RSS 提要中获取.",
"MessageOr": "或",

4
package-lock.json generated
View File

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

View File

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

View File

@@ -117,7 +117,7 @@ class Logger {
if (level < LogLevel.FATAL && level < this.logLevel) return
const consoleMethod = Logger.ConsoleMethods[levelName]
console[consoleMethod](`[${this.timestamp}] ${levelName}:`, ...args)
this.#logToFileAndListeners(level, levelName, args, source)
return this.#logToFileAndListeners(level, levelName, args, source)
}
trace(...args) {
@@ -141,7 +141,7 @@ class Logger {
}
fatal(...args) {
this.#log('FATAL', this.source, ...args)
return this.#log('FATAL', this.source, ...args)
}
note(...args) {

View File

@@ -251,6 +251,7 @@ class CollectionController {
/**
* DELETE: /api/collections/:id/book/:bookId
* Remove a single book from a collection. Re-order books
* Users with update permission can remove books from collections
* TODO: bookId is actually libraryItemId. Clients need updating to use bookId
*
* @param {CollectionControllerRequest} req
@@ -427,7 +428,8 @@ class CollectionController {
req.collection = collection
}
if (req.method == 'DELETE' && !req.user.canDelete) {
// Users with update permission can remove books from collections
if (req.method == 'DELETE' && !req.params.bookId && !req.user.canDelete) {
Logger.warn(`[CollectionController] User "${req.user.username}" attempted to delete without permission`)
return res.sendStatus(403)
} else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) {

View File

@@ -286,7 +286,7 @@ class Book extends Model {
const track = structuredClone(af)
track.title = af.metadata.filename
track.startOffset = startOffset
track.contentUrl = `${global.RouterBasePath}/api/items/${libraryItemId}/file/${track.ino}`
track.contentUrl = `/api/items/${libraryItemId}/file/${track.ino}`
startOffset += track.duration
return track
})
@@ -365,7 +365,7 @@ class Book extends Model {
if (payload.metadata) {
const metadataStringKeys = ['title', 'subtitle', 'publishedYear', 'publishedDate', 'publisher', 'description', 'isbn', 'asin', 'language']
metadataStringKeys.forEach((key) => {
if (typeof payload.metadata[key] === 'string' && this[key] !== payload.metadata[key]) {
if ((typeof payload.metadata[key] === 'string' || payload.metadata[key] === null) && this[key] !== payload.metadata[key]) {
this[key] = payload.metadata[key] || null
if (key === 'title') {

View File

@@ -202,8 +202,9 @@ class Podcast extends Model {
} else if (key === 'itunesPageUrl') {
newKey = 'itunesPageURL'
}
if (typeof payload.metadata[key] === 'string' && payload.metadata[key] !== this[newKey]) {
this[newKey] = payload.metadata[key]
if ((typeof payload.metadata[key] === 'string' || payload.metadata[key] === null) && payload.metadata[key] !== this[newKey]) {
this[newKey] = payload.metadata[key] || null
if (key === 'title') {
this.titleIgnorePrefix = getTitleIgnorePrefix(this.title)
}

View File

@@ -169,7 +169,7 @@ class PodcastEpisode extends Model {
const track = structuredClone(this.audioFile)
track.startOffset = 0
track.title = this.audioFile.metadata.filename
track.contentUrl = `${global.RouterBasePath}/api/items/${libraryItemId}/file/${track.ino}`
track.contentUrl = `/api/items/${libraryItemId}/file/${track.ino}`
return track
}

View File

@@ -29,7 +29,7 @@ class AudioTrack {
this.duration = audioFile.duration
this.title = audioFile.metadata.filename || ''
this.contentUrl = `${global.RouterBasePath}/api/items/${itemId}/file/${audioFile.ino}`
this.contentUrl = `/api/items/${itemId}/file/${audioFile.ino}`
this.mimeType = audioFile.mimeType
this.codec = audioFile.codec || null
this.metadata = audioFile.metadata.clone()
@@ -44,4 +44,4 @@ class AudioTrack {
this.mimeType = 'application/vnd.apple.mpegurl'
}
}
module.exports = AudioTrack
module.exports = AudioTrack

View File

@@ -286,10 +286,23 @@ module.exports.downloadFile = (url, filepath, contentTypeFilter = null) => {
return reject(new Error(`Invalid content type "${response.headers?.['content-type'] || ''}"`))
}
const totalSize = parseInt(response.headers['content-length'], 10)
let downloadedSize = 0
// Write to filepath
const writer = fs.createWriteStream(filepath)
response.data.pipe(writer)
let lastProgress = 0
response.data.on('data', (chunk) => {
downloadedSize += chunk.length
const progress = totalSize ? Math.round((downloadedSize / totalSize) * 100) : 0
if (progress >= lastProgress + 5) {
Logger.debug(`[fileUtils] File "${Path.basename(filepath)}" download progress: ${progress}% (${downloadedSize}/${totalSize} bytes)`)
lastProgress = progress
}
})
writer.on('finish', resolve)
writer.on('error', reject)
})

View File

@@ -35,11 +35,18 @@ module.exports.nameToLastFirst = (firstLast) => {
return `${nameObj.last_name}, ${nameObj.first_name}`
}
// Handle any name string
/**
* Parses a name string into an array of names
*
* @param {string} nameString - The name string to parse
* @returns {{ names: string[] }} Array of names
*/
module.exports.parse = (nameString) => {
if (!nameString) return null
var splitNames = []
let splitNames = []
const isCommaSeparated = nameString.includes(',')
// Example &LF: Friedman, Milton & Friedman, Rose
if (nameString.includes('&')) {
nameString.split('&').forEach((asa) => (splitNames = splitNames.concat(asa.split(','))))
@@ -59,17 +66,18 @@ module.exports.parse = (nameString) => {
}
}
var names = []
let names = []
// 1 name FIRST LAST
if (splitNames.length === 1) {
names.push(parseName(nameString))
} else {
var firstChunkIsALastName = checkIsALastName(splitNames[0])
var isEvenNum = splitNames.length % 2 === 0
// Determines whether this is formatted as last, first or first last (only if using comma separator)
// Example: "Smith; James Jones" -> ["Smith", "James Jones"]
let firstChunkIsALastName = !isCommaSeparated ? false : checkIsALastName(splitNames[0])
let isEvenNum = splitNames.length % 2 === 0
if (!isEvenNum && firstChunkIsALastName) {
// console.error('Multi-name LAST,FIRST entry has a straggler (could be roman numerals or a suffix), ignore it')
splitNames = splitNames.slice(0, splitNames.length - 1)
}

View File

@@ -311,6 +311,7 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => {
responseType: 'arraybuffer',
headers: {
Accept: 'application/rss+xml, application/xhtml+xml, application/xml, */*;q=0.8',
'Accept-Encoding': 'gzip, compress, deflate',
'User-Agent': userAgent
},
httpAgent: global.DisableSsrfRequestFilter?.(feedUrl) ? null : ssrfFilter(feedUrl),

View File

@@ -0,0 +1,99 @@
const chai = require('chai')
const expect = chai.expect
const { parse, nameToLastFirst } = require('../../../../server/utils/parsers/parseNameString')
describe('parseNameString', () => {
describe('parse', () => {
it('returns null if nameString is empty', () => {
const result = parse('')
expect(result).to.be.null
})
it('parses single name in First Last format', () => {
const result = parse('John Smith')
expect(result.names).to.deep.equal(['John Smith'])
})
it('parses single name in Last, First format', () => {
const result = parse('Smith, John')
expect(result.names).to.deep.equal(['John Smith'])
})
it('parses multiple names separated by &', () => {
const result = parse('John Smith & Jane Doe')
expect(result.names).to.deep.equal(['John Smith', 'Jane Doe'])
})
it('parses multiple names separated by "and"', () => {
const result = parse('John Smith and Jane Doe')
expect(result.names).to.deep.equal(['John Smith', 'Jane Doe'])
})
it('parses multiple names separated by comma and "and"', () => {
const result = parse('John Smith, Jane Doe and John Doe')
expect(result.names).to.deep.equal(['John Smith', 'Jane Doe', 'John Doe'])
})
it('parses multiple names separated by semicolon', () => {
const result = parse('John Smith; Jane Doe')
expect(result.names).to.deep.equal(['John Smith', 'Jane Doe'])
})
it('parses multiple names in Last, First format', () => {
const result = parse('Smith, John, Doe, Jane')
expect(result.names).to.deep.equal(['John Smith', 'Jane Doe'])
})
it('parses multiple names with single word name', () => {
const result = parse('John Smith, Jones, James Doe, Ludwig von Mises')
expect(result.names).to.deep.equal(['John Smith', 'Jones', 'James Doe', 'Ludwig von Mises'])
})
it('parses multiple names with single word name listed first (semicolon separator)', () => {
const result = parse('Jones; John Smith; James Doe; Ludwig von Mises')
expect(result.names).to.deep.equal(['Jones', 'John Smith', 'James Doe', 'Ludwig von Mises'])
})
it('handles names with suffixes', () => {
const result = parse('Smith, John Jr.')
expect(result.names).to.deep.equal(['John Jr. Smith'])
})
it('handles compound last names', () => {
const result = parse('von Mises, Ludwig')
expect(result.names).to.deep.equal(['Ludwig von Mises'])
})
it('handles Chinese/Japanese/Korean names', () => {
const result = parse('张三, 李四')
expect(result.names).to.deep.equal(['张三', '李四'])
})
it('removes duplicate names', () => {
const result = parse('John Smith & John Smith')
expect(result.names).to.deep.equal(['John Smith'])
})
it('filters out empty names', () => {
const result = parse('John Smith,')
expect(result.names).to.deep.equal(['John Smith'])
})
})
describe('nameToLastFirst', () => {
it('converts First Last to Last, First format', () => {
const result = nameToLastFirst('John Smith')
expect(result).to.equal('Smith, John')
})
it('returns last name only when no first name', () => {
const result = nameToLastFirst('Smith')
expect(result).to.equal('Smith')
})
it('handles names with middle names', () => {
const result = nameToLastFirst('John Middle Smith')
expect(result).to.equal('Smith, John Middle')
})
})
})