Compare commits

...

20 Commits

Author SHA1 Message Date
advplyr 2054accdc9 Update library sort dropdown to use max height available 2025-07-28 15:07:57 -04:00
advplyr 7d8b857c77 Add book library sort by progress updated #1215 2025-07-28 14:58:28 -04:00
advplyr 0107cb4782 UI/UX fix x overflow for sessions tables on mobile 2025-07-26 09:45:44 -05:00
advplyr f273eee807 Merge pull request #4534 from michaeldvinci/sepia-theme
Add 'sepia' theme to EpubReader
2025-07-25 17:34:34 -05:00
advplyr 4af21b079a Fix epub toc search input colors for themes 2025-07-25 17:31:59 -05:00
Michael Vinci c9eaf2db2d Add 'sepia' theme to EpubReader 2025-07-25 17:01:16 -05:00
advplyr a5fb0d9cdb Merge pull request #4530 from advplyr/fix_ereader_socket_event
Fix ereader update socket event sending all devices #4529
2025-07-24 17:33:42 -05:00
advplyr 53c80d9798 Merge pull request #4528 from FelixSche/master
Update SideRail.vue
2025-07-24 17:32:44 -05:00
advplyr 832165716b Fix ereader update socket event sending all devices #4529 2025-07-24 17:29:08 -05:00
Felix d9f2d8bf1d Update SideRail.vue
Changed cursor at version to pointer
2025-07-24 13:57:26 +02:00
advplyr a7a3a56509 Version bump v2.26.3 2025-07-23 17:18:51 -05:00
advplyr 4082fadf90 Merge pull request #4525 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-07-23 17:17:51 -05:00
FiendFEARing 93160b83bf Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1130 of 1130 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-07-23 13:06:32 +02:00
Максим Горпиніч 472240f994 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1130 of 1130 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-07-23 13:06:31 +02:00
Dmitry c3f0fb8e5e Translated using Weblate (Russian)
Currently translated at 100.0% (1130 of 1130 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-07-23 13:06:30 +02:00
Daniel Schosser b156ebeb9f Translated using Weblate (German)
Currently translated at 99.9% (1129 of 1130 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-07-23 13:06:29 +02:00
advplyr e4c775c847 Merge pull request #4523 from advplyr/fix_change_empty_root_password
Update change password to support null or empty string passwords #4522
2025-07-22 17:01:07 -05:00
advplyr 45e8e72759 Update change password to support null or empty string passwords #4522 2025-07-22 15:17:00 -05:00
advplyr 0ae7340889 Merge pull request #4520 from advplyr/fix_podcast_session_track_index
Fix podcast episode track index null in playback session
2025-07-22 15:04:22 -05:00
advplyr 8c38987d92 Fix podcast episode track index null in playback session 2025-07-22 14:44:36 -05:00
21 changed files with 204 additions and 135 deletions
+1 -1
View File
@@ -116,7 +116,7 @@
</div> </div>
<div class="w-full h-12 px-1 py-2 border-t border-black/20 bg-bg absolute left-0" :style="{ bottom: streamLibraryItem ? '224px' : '65px' }"> <div class="w-full h-12 px-1 py-2 border-t border-black/20 bg-bg absolute left-0" :style="{ bottom: streamLibraryItem ? '224px' : '65px' }">
<p class="underline font-mono text-xs text-center text-gray-300 leading-3 mb-1" @click="clickChangelog">v{{ $config.version }}</p> <p class="underline font-mono text-xs text-center text-gray-300 leading-3 mb-1 cursor-pointer" @click="clickChangelog">v{{ $config.version }}</p>
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xxs text-center block leading-3">Update</a> <a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xxs text-center block leading-3">Update</a>
<p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p> <p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p>
</div> </div>
+13 -1
View File
@@ -101,7 +101,8 @@
<!-- Podcast Episode # --> <!-- Podcast Episode # -->
<div cy-id="podcastEpisodeNumber" v-if="recentEpisodeNumber !== null && !isHovering && !isSelectionMode && !processing" class="absolute rounded-lg bg-black/90 box-shadow-md z-10" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }"> <div cy-id="podcastEpisodeNumber" v-if="recentEpisodeNumber !== null && !isHovering && !isSelectionMode && !processing" class="absolute rounded-lg bg-black/90 box-shadow-md z-10" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }">
<p :style="{ fontSize: 0.8 + 'em' }"> <p :style="{ fontSize: 0.8 + 'em' }">
Episode<span v-if="recentEpisodeNumber"> #{{ recentEpisodeNumber }}</span> Episode
<span v-if="recentEpisodeNumber">#{{ recentEpisodeNumber }}</span>
</p> </p>
</div> </div>
@@ -200,6 +201,9 @@ export default {
dateFormat() { dateFormat() {
return this.store.getters['getServerSetting']('dateFormat') return this.store.getters['getServerSetting']('dateFormat')
}, },
timeFormat() {
return this.store.getters['getServerSetting']('timeFormat')
},
_libraryItem() { _libraryItem() {
return this.libraryItem || {} return this.libraryItem || {}
}, },
@@ -345,6 +349,10 @@ export default {
if (this.mediaMetadata.publishedYear) return this.$getString('LabelPublishedDate', [this.mediaMetadata.publishedYear]) if (this.mediaMetadata.publishedYear) return this.$getString('LabelPublishedDate', [this.mediaMetadata.publishedYear])
return '\u00A0' return '\u00A0'
} }
if (this.orderBy === 'progress') {
if (!this.userProgressLastUpdated) return '\u00A0'
return this.$getString('LabelLastProgressDate', [this.$formatDatetime(this.userProgressLastUpdated, this.dateFormat, this.timeFormat)])
}
return null return null
}, },
episodeProgress() { episodeProgress() {
@@ -377,6 +385,10 @@ export default {
let progressPercent = this.itemIsFinished ? 1 : this.booksInSeries ? this.seriesProgressPercent : this.useEBookProgress ? this.userProgress?.ebookProgress || 0 : this.userProgress?.progress || 0 let progressPercent = this.itemIsFinished ? 1 : this.booksInSeries ? this.seriesProgressPercent : this.useEBookProgress ? this.userProgress?.ebookProgress || 0 : this.userProgress?.progress || 0
return Math.max(Math.min(1, progressPercent), 0) return Math.max(Math.min(1, progressPercent), 0)
}, },
userProgressLastUpdated() {
if (!this.userProgress) return null
return this.userProgress.lastUpdate
},
itemIsFinished() { itemIsFinished() {
if (this.booksInSeries) return this.seriesIsFinished if (this.booksInSeries) return this.seriesIsFinished
return this.userProgress ? !!this.userProgress.isFinished : false return this.userProgress ? !!this.userProgress.isFinished : false
@@ -7,7 +7,7 @@
</span> </span>
</button> </button>
<ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 ring-1 ring-black/5 overflow-auto focus:outline-hidden text-sm" role="menu"> <ul v-show="showMenu" class="librarySortMenu absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 ring-1 ring-black/5 overflow-auto focus:outline-hidden text-sm" role="menu">
<template v-for="item in selectItems"> <template v-for="item in selectItems">
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="menuitem" @click="clickedOption(item.value)"> <li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="menuitem" @click="clickedOption(item.value)">
<div class="flex items-center"> <div class="flex items-center">
@@ -130,6 +130,10 @@ export default {
text: this.$strings.LabelFileModified, text: this.$strings.LabelFileModified,
value: 'mtimeMs' value: 'mtimeMs'
}, },
{
text: this.$strings.LabelLibrarySortByProgress,
value: 'progress'
},
{ {
text: this.$strings.LabelRandomly, text: this.$strings.LabelRandomly,
value: 'random' value: 'random'
@@ -191,3 +195,9 @@ export default {
} }
} }
</script> </script>
<style scoped>
.librarySortMenu {
max-height: calc(100vh - 125px);
}
</style>
+17 -7
View File
@@ -99,22 +99,32 @@ export default {
return `/api/items/${this.libraryItemId}/ebook` return `/api/items/${this.libraryItemId}/ebook`
}, },
themeRules() { themeRules() {
const isDark = this.ereaderSettings.theme === 'dark' const theme = this.ereaderSettings.theme
const fontColor = isDark ? '#fff' : '#000' const isDark = theme === 'dark'
const backgroundColor = isDark ? 'rgb(35 35 35)' : 'rgb(255, 255, 255)' const isSepia = theme === 'sepia'
const fontColor = isDark
? '#fff'
: isSepia
? '#5b4636'
: '#000'
const backgroundColor = isDark
? 'rgb(35 35 35)'
: isSepia
? 'rgb(244, 236, 216)'
: 'rgb(255, 255, 255)'
const lineSpacing = this.ereaderSettings.lineSpacing / 100 const lineSpacing = this.ereaderSettings.lineSpacing / 100
const fontScale = this.ereaderSettings.fontScale / 100 const fontScale = this.ereaderSettings.fontScale / 100
const textStroke = this.ereaderSettings.textStroke / 100 const textStroke = this.ereaderSettings.textStroke / 100
return { return {
'*': { '*': {
color: `${fontColor}!important`, color: `${fontColor}!important`,
'background-color': `${backgroundColor}!important`, 'background-color': `${backgroundColor}!important`,
'line-height': lineSpacing * fontScale + 'rem!important', 'line-height': `${lineSpacing * fontScale}rem!important`,
'-webkit-text-stroke': textStroke + 'px ' + fontColor + '!important' '-webkit-text-stroke': `${textStroke}px ${fontColor}!important`
}, },
a: { a: {
color: `${fontColor}!important` color: `${fontColor}!important`
+12 -3
View File
@@ -1,5 +1,5 @@
<template> <template>
<div v-if="show" id="reader" :data-theme="ereaderTheme" class="group absolute top-0 left-0 w-full z-60 data-[theme=dark]:bg-primary data-[theme=dark]:text-white data-[theme=light]:bg-white data-[theme=light]:text-black" :class="{ 'reader-player-open': !!streamLibraryItem }"> <div v-if="show" id="reader" :data-theme="ereaderTheme" class="group absolute top-0 left-0 w-full z-60 data-[theme=dark]:bg-primary data-[theme=dark]:text-white data-[theme=light]:bg-white data-[theme=light]:text-black data-[theme=sepia]:bg-[rgb(244,236,216)] data-[theme=sepia]:text-[#5b4636]" :class="{ 'reader-player-open': !!streamLibraryItem }">
<div class="absolute top-4 left-4 z-20 flex items-center"> <div class="absolute top-4 left-4 z-20 flex items-center">
<button v-if="isEpub" @click="toggleToC" type="button" aria-label="Table of contents menu" class="inline-flex opacity-80 hover:opacity-100"> <button v-if="isEpub" @click="toggleToC" type="button" aria-label="Table of contents menu" class="inline-flex opacity-80 hover:opacity-100">
<span class="material-symbols text-2xl">menu</span> <span class="material-symbols text-2xl">menu</span>
@@ -27,7 +27,12 @@
<!-- TOC side nav --> <!-- TOC side nav -->
<div v-if="tocOpen" class="w-full h-full overflow-y-scroll absolute inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div> <div v-if="tocOpen" class="w-full h-full overflow-y-scroll absolute inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div>
<div v-if="isEpub" class="w-96 h-full max-h-full absolute top-0 left-0 shadow-xl transition-transform z-30 group-data-[theme=dark]:bg-primary group-data-[theme=dark]:text-white group-data-[theme=light]:bg-white group-data-[theme=light]:text-black" :class="tocOpen ? 'translate-x-0' : '-translate-x-96'" @click.stop.prevent> <div
v-if="isEpub"
class="w-96 h-full max-h-full absolute top-0 left-0 shadow-xl transition-transform z-30 group-data-[theme=dark]:bg-primary group-data-[theme=dark]:text-white group-data-[theme=light]:bg-white group-data-[theme=light]:text-black group-data-[theme=sepia]:bg-[rgb(244,236,216)] group-data-[theme=sepia]:text-[#5b4636]"
:class="tocOpen ? 'translate-x-0' : '-translate-x-96'"
@click.stop.prevent
>
<div class="flex flex-col p-4 h-full"> <div class="flex flex-col p-4 h-full">
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<button @click.stop.prevent="toggleToC" type="button" aria-label="Close table of contents" class="inline-flex opacity-80 hover:opacity-100"> <button @click.stop.prevent="toggleToC" type="button" aria-label="Close table of contents" class="inline-flex opacity-80 hover:opacity-100">
@@ -37,7 +42,7 @@
<p class="text-lg font-semibold ml-2">{{ $strings.HeaderTableOfContents }}</p> <p class="text-lg font-semibold ml-2">{{ $strings.HeaderTableOfContents }}</p>
</div> </div>
<form @submit.prevent="searchBook" @click.stop.prevent> <form @submit.prevent="searchBook" @click.stop.prevent>
<ui-text-input clearable ref="input" @clear="searchBook" v-model="searchQuery" :placeholder="$strings.PlaceholderSearch" class="h-8 w-full text-sm flex mb-2" /> <ui-text-input clearable ref="input" @clear="searchBook" v-model="searchQuery" :placeholder="$strings.PlaceholderSearch" custom-input-class="text-inherit !bg-inherit" class="h-8 w-full text-sm flex mb-2" />
</form> </form>
<div class="overflow-y-auto"> <div class="overflow-y-auto">
@@ -181,6 +186,10 @@ export default {
text: this.$strings.LabelThemeDark, text: this.$strings.LabelThemeDark,
value: 'dark' value: 'dark'
}, },
{
text: this.$strings.LabelThemeSepia,
value: 'sepia'
},
{ {
text: this.$strings.LabelThemeLight, text: this.$strings.LabelThemeLight,
value: 'light' value: 'light'
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.26.2", "version": "2.26.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.26.2", "version": "2.26.3",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@nuxtjs/axios": "^5.13.6", "@nuxtjs/axios": "^5.13.6",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.26.2", "version": "2.26.3",
"buildNumber": 1, "buildNumber": 1,
"description": "Self-hosted audiobook and podcast client", "description": "Self-hosted audiobook and podcast client",
"main": "index.js", "main": "index.js",
+2
View File
@@ -6,6 +6,7 @@
</div> </div>
<div v-if="listeningSessions.length" class="block max-w-full relative"> <div v-if="listeningSessions.length" class="block max-w-full relative">
<div class="overflow-x-auto">
<table class="userSessionsTable"> <table class="userSessionsTable">
<tr class="bg-primary/40"> <tr class="bg-primary/40">
<th class="w-6 min-w-6 text-left hidden md:table-cell h-11"> <th class="w-6 min-w-6 text-left hidden md:table-cell h-11">
@@ -80,6 +81,7 @@
</td> </td>
</tr> </tr>
</table> </table>
</div>
<!-- table bottom options --> <!-- table bottom options -->
<div class="flex items-center my-2"> <div class="flex items-center my-2">
<div class="grow" /> <div class="grow" />
@@ -19,6 +19,7 @@
<div class="py-2"> <div class="py-2">
<h1 class="text-lg mb-2 text-white/90 px-2 sm:px-0">{{ $strings.HeaderListeningSessions }}</h1> <h1 class="text-lg mb-2 text-white/90 px-2 sm:px-0">{{ $strings.HeaderListeningSessions }}</h1>
<div v-if="listeningSessions.length"> <div v-if="listeningSessions.length">
<div class="overflow-x-auto">
<table class="userSessionsTable"> <table class="userSessionsTable">
<tr class="bg-primary/40"> <tr class="bg-primary/40">
<th class="w-48 min-w-48 text-left">{{ $strings.LabelItem }}</th> <th class="w-48 min-w-48 text-left">{{ $strings.LabelItem }}</th>
@@ -52,6 +53,7 @@
</td> </td>
</tr> </tr>
</table> </table>
</div>
<div class="flex items-center justify-end py-1"> <div class="flex items-center justify-end py-1">
<ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" /> <ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
<p class="text-sm mx-1">{{ $getString('LabelPaginationPageXOfY', [currentPage + 1, numPages]) }}</p> <p class="text-sm mx-1">{{ $getString('LabelPaginationPageXOfY', [currentPage + 1, numPages]) }}</p>
+1 -1
View File
@@ -92,7 +92,7 @@ export const actions = {
if (state.settings.orderBy == 'media.duration') { if (state.settings.orderBy == 'media.duration') {
settingsUpdate.orderBy = 'media.numTracks' settingsUpdate.orderBy = 'media.numTracks'
} }
if (state.settings.orderBy == 'media.metadata.publishedYear') { if (state.settings.orderBy == 'media.metadata.publishedYear' || state.settings.orderBy == 'progress') {
settingsUpdate.orderBy = 'media.metadata.title' settingsUpdate.orderBy = 'media.metadata.title'
} }
const invalidFilters = ['series', 'authors', 'narrators', 'publishers', 'publishedDecades', 'languages', 'progress', 'issues', 'ebooks', 'abridged'] const invalidFilters = ['series', 'authors', 'narrators', 'publishers', 'publishedDecades', 'languages', 'progress', 'issues', 'ebooks', 'abridged']
+2
View File
@@ -438,6 +438,7 @@
"LabelLogLevelWarn": "Warnungen", "LabelLogLevelWarn": "Warnungen",
"LabelLookForNewEpisodesAfterDate": "Suche nach neuen Episoden nach diesem Datum", "LabelLookForNewEpisodesAfterDate": "Suche nach neuen Episoden nach diesem Datum",
"LabelLowestPriority": "Niedrigste Priorität", "LabelLowestPriority": "Niedrigste Priorität",
"LabelMatchConfidence": "Zuversicht",
"LabelMatchExistingUsersBy": "Zuordnen existierender Benutzer mit", "LabelMatchExistingUsersBy": "Zuordnen existierender Benutzer mit",
"LabelMatchExistingUsersByDescription": "Wird zum Verbinden vorhandener Benutzer verwendet. Sobald die Verbindung hergestellt ist, wird den Benutzern eine eindeutige ID vom SSO-Anbieter zugeordnet", "LabelMatchExistingUsersByDescription": "Wird zum Verbinden vorhandener Benutzer verwendet. Sobald die Verbindung hergestellt ist, wird den Benutzern eine eindeutige ID vom SSO-Anbieter zugeordnet",
"LabelMaxEpisodesToDownload": "Max. Anzahl an Episoden zum Herunterladen, 0 für unbegrenzte Episoden.", "LabelMaxEpisodesToDownload": "Max. Anzahl an Episoden zum Herunterladen, 0 für unbegrenzte Episoden.",
@@ -723,6 +724,7 @@
"MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen", "MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen",
"MessageAppriseDescription": "Um diese Funktion nutzen zu können, musst du eine Instanz von <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann. <br />Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter <code>http://192.168.1.1:8337</code> läuft, würdest du <code>http://192.168.1.1:8337/notify</code> eingeben.", "MessageAppriseDescription": "Um diese Funktion nutzen zu können, musst du eine Instanz von <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann. <br />Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter <code>http://192.168.1.1:8337</code> läuft, würdest du <code>http://192.168.1.1:8337/notify</code> eingeben.",
"MessageAsinCheck": "Stellen Sie sicher, dass Sie die ASIN aus der richtigen Audible Region verwenden, nicht Amazon.", "MessageAsinCheck": "Stellen Sie sicher, dass Sie die ASIN aus der richtigen Audible Region verwenden, nicht Amazon.",
"MessageAuthenticationLegacyTokenWarning": "Alte API tokens werden in Zukunft entfernt. Benutze stattdessen <a href=\"/config/api-keys\">API Keys</a>.",
"MessageAuthenticationOIDCChangesRestart": "Nach dem Speichern muss der Server neugestartet werden um die OIDC Änderungen zu übernehmen.", "MessageAuthenticationOIDCChangesRestart": "Nach dem Speichern muss der Server neugestartet werden um die OIDC Änderungen zu übernehmen.",
"MessageAuthenticationSecurityMessage": "Die Anmeldung wurde abgesichert. Benutzersitzungen werden getrennt, alle Benutzer müssen sich erneut anmelden.", "MessageAuthenticationSecurityMessage": "Die Anmeldung wurde abgesichert. Benutzersitzungen werden getrennt, alle Benutzer müssen sich erneut anmelden.",
"MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in <code>/metadata/items</code> & <code>/metadata/authors</code> gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Medien-Ordnern) gespeichert sind.", "MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in <code>/metadata/items</code> & <code>/metadata/authors</code> gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Medien-Ordnern) gespeichert sind.",
+3
View File
@@ -418,6 +418,7 @@
"LabelLanguages": "Languages", "LabelLanguages": "Languages",
"LabelLastBookAdded": "Last Book Added", "LabelLastBookAdded": "Last Book Added",
"LabelLastBookUpdated": "Last Book Updated", "LabelLastBookUpdated": "Last Book Updated",
"LabelLastProgressDate": "Last progress: {0}",
"LabelLastSeen": "Last Seen", "LabelLastSeen": "Last Seen",
"LabelLastTime": "Last Time", "LabelLastTime": "Last Time",
"LabelLastUpdate": "Last Update", "LabelLastUpdate": "Last Update",
@@ -430,6 +431,7 @@
"LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryFilterSublistEmpty": "No {0}",
"LabelLibraryItem": "Library Item", "LabelLibraryItem": "Library Item",
"LabelLibraryName": "Library Name", "LabelLibraryName": "Library Name",
"LabelLibrarySortByProgress": "Progress Updated",
"LabelLimit": "Limit", "LabelLimit": "Limit",
"LabelLineSpacing": "Line spacing", "LabelLineSpacing": "Line spacing",
"LabelListenAgain": "Listen Again", "LabelListenAgain": "Listen Again",
@@ -656,6 +658,7 @@
"LabelTheme": "Theme", "LabelTheme": "Theme",
"LabelThemeDark": "Dark", "LabelThemeDark": "Dark",
"LabelThemeLight": "Light", "LabelThemeLight": "Light",
"LabelThemeSepia": "Sepia",
"LabelTimeBase": "Time Base", "LabelTimeBase": "Time Base",
"LabelTimeDurationXHours": "{0} hours", "LabelTimeDurationXHours": "{0} hours",
"LabelTimeDurationXMinutes": "{0} minutes", "LabelTimeDurationXMinutes": "{0} minutes",
+2
View File
@@ -438,6 +438,7 @@
"LabelLogLevelWarn": "Предупреждение", "LabelLogLevelWarn": "Предупреждение",
"LabelLookForNewEpisodesAfterDate": "Искать новые эпизоды после этой даты", "LabelLookForNewEpisodesAfterDate": "Искать новые эпизоды после этой даты",
"LabelLowestPriority": "Самый низкий приоритет", "LabelLowestPriority": "Самый низкий приоритет",
"LabelMatchConfidence": "Уверенность",
"LabelMatchExistingUsersBy": "Сопоставление существующих пользователей по", "LabelMatchExistingUsersBy": "Сопоставление существующих пользователей по",
"LabelMatchExistingUsersByDescription": "Используется для подключения существующих пользователей. После подключения пользователям будет присвоен уникальный идентификатор от поставщика единого входа", "LabelMatchExistingUsersByDescription": "Используется для подключения существующих пользователей. После подключения пользователям будет присвоен уникальный идентификатор от поставщика единого входа",
"LabelMaxEpisodesToDownload": "Максимальное количество эпизодов для загрузки. Используйте 0 для неограниченного количества.", "LabelMaxEpisodesToDownload": "Максимальное количество эпизодов для загрузки. Используйте 0 для неограниченного количества.",
@@ -723,6 +724,7 @@
"MessageAddToPlayerQueue": "Добавить в очередь проигрывателя", "MessageAddToPlayerQueue": "Добавить в очередь проигрывателя",
"MessageAppriseDescription": "Для использования этой функции необходимо иметь запущенный экземпляр <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> или api которое обрабатывает те же самые запросы. <br />URL-адрес API Apprise должен быть полным URL-адресом для отправки уведомления, т.е., если API запущено по адресу <code>http://192.168.1.1:8337</code> тогда нужно указать <code>http://192.168.1.1:8337/notify</code>.", "MessageAppriseDescription": "Для использования этой функции необходимо иметь запущенный экземпляр <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> или api которое обрабатывает те же самые запросы. <br />URL-адрес API Apprise должен быть полным URL-адресом для отправки уведомления, т.е., если API запущено по адресу <code>http://192.168.1.1:8337</code> тогда нужно указать <code>http://192.168.1.1:8337/notify</code>.",
"MessageAsinCheck": "Убедитесь, что вы используете ASIN из правильной региональной зоны Audible, а не из Amazon.", "MessageAsinCheck": "Убедитесь, что вы используете ASIN из правильной региональной зоны Audible, а не из Amazon.",
"MessageAuthenticationLegacyTokenWarning": "Устаревшие токены API в будущем будут удалены. Вместо них используйте <a href=\"/config/api-keys\">API-ключи</a>.",
"MessageAuthenticationOIDCChangesRestart": "Перезапустите ваш сервер после сохранения для применения изменений в OIDC.", "MessageAuthenticationOIDCChangesRestart": "Перезапустите ваш сервер после сохранения для применения изменений в OIDC.",
"MessageAuthenticationSecurityMessage": "В целях безопасности была улучшена аутентификация. Всем пользователям необходимо повторно войти в систему.", "MessageAuthenticationSecurityMessage": "В целях безопасности была улучшена аутентификация. Всем пользователям необходимо повторно войти в систему.",
"MessageBackupsDescription": "Бэкап включает пользователей, прогресс пользователей, данные элементов библиотеки, настройки сервера и изображения хранящиеся в <code>/metadata/items</code> и <code>/metadata/authors</code>. Бэкапы <strong>НЕ</strong> сохраняют файлы из папок библиотек.", "MessageBackupsDescription": "Бэкап включает пользователей, прогресс пользователей, данные элементов библиотеки, настройки сервера и изображения хранящиеся в <code>/metadata/items</code> и <code>/metadata/authors</code>. Бэкапы <strong>НЕ</strong> сохраняют файлы из папок библиотек.",
+2 -1
View File
@@ -438,6 +438,7 @@
"LabelLogLevelWarn": "Увага", "LabelLogLevelWarn": "Увага",
"LabelLookForNewEpisodesAfterDate": "Шукати нові епізоди після вказаної дати", "LabelLookForNewEpisodesAfterDate": "Шукати нові епізоди після вказаної дати",
"LabelLowestPriority": "Найнижчий пріоритет", "LabelLowestPriority": "Найнижчий пріоритет",
"LabelMatchConfidence": "Впевненість",
"LabelMatchExistingUsersBy": "Шукати наявних користувачів за", "LabelMatchExistingUsersBy": "Шукати наявних користувачів за",
"LabelMatchExistingUsersByDescription": "Використовується для підключення наявних користувачів. Після підключення користувач отримає унікальний id від вашого сервісу SSO", "LabelMatchExistingUsersByDescription": "Використовується для підключення наявних користувачів. Після підключення користувач отримає унікальний id від вашого сервісу SSO",
"LabelMaxEpisodesToDownload": "Максимальна кількість епізодів для скачування. Використовуйте 0 для необмеженої кількості.", "LabelMaxEpisodesToDownload": "Максимальна кількість епізодів для скачування. Використовуйте 0 для необмеженої кількості.",
@@ -837,7 +838,7 @@
"MessageNoItems": "Елементи відсутні", "MessageNoItems": "Елементи відсутні",
"MessageNoItemsFound": "Елементів не знайдено", "MessageNoItemsFound": "Елементів не знайдено",
"MessageNoListeningSessions": "Сеанси прослуховування відсутні", "MessageNoListeningSessions": "Сеанси прослуховування відсутні",
"MessageNoLogs": "Немає журнали", "MessageNoLogs": "Немає журналів'",
"MessageNoMediaProgress": "Прогрес відсутній", "MessageNoMediaProgress": "Прогрес відсутній",
"MessageNoNotifications": "Сповіщення відсутні", "MessageNoNotifications": "Сповіщення відсутні",
"MessageNoPodcastFeed": "Некоректний подкаст: немає каналу", "MessageNoPodcastFeed": "Некоректний подкаст: немає каналу",
+1
View File
@@ -438,6 +438,7 @@
"LabelLogLevelWarn": "警告", "LabelLogLevelWarn": "警告",
"LabelLookForNewEpisodesAfterDate": "在此日期后查找新剧集", "LabelLookForNewEpisodesAfterDate": "在此日期后查找新剧集",
"LabelLowestPriority": "最低优先级", "LabelLowestPriority": "最低优先级",
"LabelMatchConfidence": "置信度",
"LabelMatchExistingUsersBy": "匹配现有用户", "LabelMatchExistingUsersBy": "匹配现有用户",
"LabelMatchExistingUsersByDescription": "用于连接现有用户. 连接后, 用户将通过 SSO 提供商提供的唯一 id 进行匹配", "LabelMatchExistingUsersByDescription": "用于连接现有用户. 连接后, 用户将通过 SSO 提供商提供的唯一 id 进行匹配",
"LabelMaxEpisodesToDownload": "可下载的最大集数. 输入 0 表示无限制.", "LabelMaxEpisodesToDownload": "可下载的最大集数. 输入 0 表示无限制.",
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.26.2", "version": "2.26.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.26.2", "version": "2.26.3",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"axios": "^0.27.2", "axios": "^0.27.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.26.2", "version": "2.26.3",
"buildNumber": 1, "buildNumber": 1,
"description": "Self-hosted audiobook and podcast server", "description": "Self-hosted audiobook and podcast server",
"main": "index.js", "main": "index.js",
+2 -2
View File
@@ -280,7 +280,7 @@ class MeController {
} }
const { password, newPassword } = req.body const { password, newPassword } = req.body
if (!password || !newPassword || typeof password !== 'string' || typeof newPassword !== 'string') { if ((typeof password !== 'string' && password !== null) || (typeof newPassword !== 'string' && newPassword !== null)) {
return res.status(400).send('Missing or invalid password or new password') return res.status(400).send('Missing or invalid password or new password')
} }
@@ -450,7 +450,7 @@ class MeController {
if (updated) { if (updated) {
await Database.updateSetting(Database.emailSettings) await Database.updateSetting(Database.emailSettings)
SocketAuthority.clientEmitter(req.user.id, 'ereader-devices-updated', { SocketAuthority.clientEmitter(req.user.id, 'ereader-devices-updated', {
ereaderDevices: Database.emailSettings.ereaderDevices ereaderDevices: Database.emailSettings.getEReaderDevices(req.user)
}) })
} }
res.json({ res.json({
+6 -1
View File
@@ -288,7 +288,12 @@ class SessionController {
return res.sendStatus(404) return res.sendStatus(404)
} }
const audioTrack = playbackSession.audioTracks.find((t) => t.index === audioTrackIndex) let audioTrack = playbackSession.audioTracks.find((t) => toNumber(t.index, 1) === audioTrackIndex)
// Support clients passing 0 or 1 for podcast episode audio track index (handles old episodes pre-v2.21.0 having null index)
if (!audioTrack && playbackSession.mediaType === 'podcast' && audioTrackIndex === 0) {
audioTrack = playbackSession.audioTracks[0]
}
if (!audioTrack) { if (!audioTrack) {
Logger.error(`[SessionController] Unable to find audio track with index=${audioTrackIndex}`) Logger.error(`[SessionController] Unable to find audio track with index=${audioTrackIndex}`)
return res.sendStatus(404) return res.sendStatus(404)
+1
View File
@@ -185,6 +185,7 @@ class PodcastEpisode extends Model {
const track = structuredClone(this.audioFile) const track = structuredClone(this.audioFile)
track.startOffset = 0 track.startOffset = 0
track.title = this.audioFile.metadata.filename track.title = this.audioFile.metadata.filename
track.index = 1 // Podcast episodes only have one track
track.contentUrl = `/api/items/${libraryItemId}/file/${track.ino}` track.contentUrl = `/api/items/${libraryItemId}/file/${track.ino}`
return track return track
} }
@@ -399,9 +399,6 @@ module.exports = {
if (filterGroup !== 'series' && sortBy === 'sequence') { if (filterGroup !== 'series' && sortBy === 'sequence') {
sortBy = 'media.metadata.title' sortBy = 'media.metadata.title'
} }
if (filterGroup !== 'progress' && sortBy === 'progress') {
sortBy = 'media.metadata.title'
}
const includeRSSFeed = include.includes('rssfeed') const includeRSSFeed = include.includes('rssfeed')
const includeMediaItemShare = !!user?.isAdminOrUp && include.includes('share') const includeMediaItemShare = !!user?.isAdminOrUp && include.includes('share')
@@ -532,6 +529,18 @@ module.exports = {
} }
} }
// When sorting by progress but not filtering by progress, include media progresses
if (filterGroup !== 'progress' && sortBy === 'progress') {
bookIncludes.push({
model: Database.mediaProgressModel,
attributes: ['id', 'isFinished', 'currentTime', 'ebookProgress', 'updatedAt'],
where: {
userId: user.id
},
required: false
})
}
let { mediaWhere, replacements } = this.getMediaGroupQuery(filterGroup, filterValue) let { mediaWhere, replacements } = this.getMediaGroupQuery(filterGroup, filterValue)
let bookWhere = Array.isArray(mediaWhere) ? mediaWhere : [mediaWhere] let bookWhere = Array.isArray(mediaWhere) ? mediaWhere : [mediaWhere]