From ba4061e5a406c03bffe178232a0a5f7172649ceb Mon Sep 17 00:00:00 2001
From: Hadrien Patte
Date: Mon, 16 Jun 2025 23:03:02 +0200
Subject: [PATCH 01/11] Make `NUSQLITE3_PATH` build arg configurable
---
Dockerfile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dockerfile b/Dockerfile
index f9c46117..816bdd3c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -57,7 +57,7 @@ WORKDIR /app
# Copy compiled frontend and server from build stages
COPY --from=build-client /client/dist /app/client/dist
COPY --from=build-server /server /app
-COPY --from=build-server /usr/local/lib/nusqlite3 /usr/local/lib/nusqlite3
+COPY --from=build-server ${NUSQLITE3_PATH} ${NUSQLITE3_PATH}
EXPOSE 80
From 1f7be58124dce737ac252d08a7eefaa6990a6077 Mon Sep 17 00:00:00 2001
From: advplyr
Date: Mon, 16 Jun 2025 17:50:53 -0500
Subject: [PATCH 02/11] Fix database cleanup query pulling duplicate
mediaProgresses
---
server/Database.js | 27 +++++++++++++++++++--------
1 file changed, 19 insertions(+), 8 deletions(-)
diff --git a/server/Database.js b/server/Database.js
index eb384f25..a260e89f 100644
--- a/server/Database.js
+++ b/server/Database.js
@@ -766,14 +766,25 @@ class Database {
Logger.warn(`Removed ${badSessionsRemoved} sessions that were 3 seconds or less`)
}
- // Remove mediaProgresses with duplicate mediaItemId (remove the oldest updatedAt)
- // const [duplicateMediaProgresses] = await this.sequelize.query(`SELECT id, mediaItemId FROM mediaProgresses WHERE (mediaItemId, userId, updatedAt) IN (SELECT mediaItemId, userId, MIN(updatedAt) FROM mediaProgresses GROUP BY mediaItemId, userId HAVING COUNT(*) > 1)`)
- // for (const duplicateMediaProgress of duplicateMediaProgresses) {
- // Logger.warn(`Found duplicate mediaProgress for mediaItem "${duplicateMediaProgress.mediaItemId}" - removing it`)
- // await this.mediaProgressModel.destroy({
- // where: { id: duplicateMediaProgress.id }
- // })
- // }
+ // Remove mediaProgresses with duplicate mediaItemId (remove the oldest updatedAt or if updatedAt is the same, remove arbitrary one)
+ const [duplicateMediaProgresses] = await this.sequelize.query(`SELECT mp1.id, mp1.mediaItemId
+FROM mediaProgresses mp1
+WHERE EXISTS (
+ SELECT 1
+ FROM mediaProgresses mp2
+ WHERE mp2.mediaItemId = mp1.mediaItemId
+ AND mp2.userId = mp1.userId
+ AND (
+ mp2.updatedAt > mp1.updatedAt
+ OR (mp2.updatedAt = mp1.updatedAt AND mp2.id < mp1.id)
+ )
+)`)
+ for (const duplicateMediaProgress of duplicateMediaProgresses) {
+ Logger.warn(`Found duplicate mediaProgress for mediaItem "${duplicateMediaProgress.mediaItemId}" - removing it`)
+ await this.mediaProgressModel.destroy({
+ where: { id: duplicateMediaProgress.id }
+ })
+ }
}
async createTextSearchQuery(query) {
From 9d4303ef7b48f6b1854d09c8ee4bc01c2e0867b8 Mon Sep 17 00:00:00 2001
From: advplyr
Date: Tue, 17 Jun 2025 16:25:30 -0500
Subject: [PATCH 03/11] Update book library secondary title sort to use title
ignore prefixes #4414
---
.../utils/queries/libraryItemsBookFilters.js | 28 +++++++++----------
1 file changed, 14 insertions(+), 14 deletions(-)
diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js
index 73d36c58..7fa5eb41 100644
--- a/server/utils/queries/libraryItemsBookFilters.js
+++ b/server/utils/queries/libraryItemsBookFilters.js
@@ -251,6 +251,15 @@ module.exports = {
*/
getOrder(sortBy, sortDesc, collapseseries) {
const dir = sortDesc ? 'DESC' : 'ASC'
+
+ const getTitleOrder = () => {
+ if (global.ServerSettings.sortingIgnorePrefix) {
+ return [Sequelize.literal('`libraryItem`.`titleIgnorePrefix` COLLATE NOCASE'), dir]
+ } else {
+ return [Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir]
+ }
+ }
+
if (sortBy === 'addedAt') {
return [[Sequelize.literal('libraryItem.createdAt'), dir]]
} else if (sortBy === 'size') {
@@ -264,25 +273,16 @@ module.exports = {
} else if (sortBy === 'media.metadata.publishedYear') {
return [[Sequelize.literal(`CAST(\`book\`.\`publishedYear\` AS INTEGER)`), dir]]
} else if (sortBy === 'media.metadata.authorNameLF') {
- return [
- [Sequelize.literal('`libraryItem`.`authorNamesLastFirst` COLLATE NOCASE'), dir],
- [Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir]
- ]
+ // Sort by author name last first, secondary sort by title
+ return [[Sequelize.literal('`libraryItem`.`authorNamesLastFirst` COLLATE NOCASE'), dir], getTitleOrder()]
} else if (sortBy === 'media.metadata.authorName') {
- return [
- [Sequelize.literal('`libraryItem`.`authorNamesFirstLast` COLLATE NOCASE'), dir],
- [Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir]
- ]
+ // Sort by author name first last, secondary sort by title
+ return [[Sequelize.literal('`libraryItem`.`authorNamesFirstLast` COLLATE NOCASE'), dir], getTitleOrder()]
} else if (sortBy === 'media.metadata.title') {
if (collapseseries) {
return [[Sequelize.literal('display_title COLLATE NOCASE'), dir]]
}
-
- if (global.ServerSettings.sortingIgnorePrefix) {
- return [[Sequelize.literal('`libraryItem`.`titleIgnorePrefix` COLLATE NOCASE'), dir]]
- } else {
- return [[Sequelize.literal('`libraryItem`.`title` COLLATE NOCASE'), dir]]
- }
+ return [getTitleOrder()]
} else if (sortBy === 'sequence') {
const nullDir = sortDesc ? 'DESC NULLS FIRST' : 'ASC NULLS LAST'
return [[Sequelize.literal(`CAST(\`series.bookSeries.sequence\` AS FLOAT) ${nullDir}`)]]
From 9bf8d7de11b969bb7ece861d0321db33d1b016c9 Mon Sep 17 00:00:00 2001
From: advplyr
Date: Tue, 17 Jun 2025 17:21:21 -0500
Subject: [PATCH 04/11] Fix server crash when FantLab provider request times
out #4410
---
server/providers/FantLab.js | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/server/providers/FantLab.js b/server/providers/FantLab.js
index dd9f60cc..f33934ca 100644
--- a/server/providers/FantLab.js
+++ b/server/providers/FantLab.js
@@ -52,9 +52,7 @@ class FantLab {
return []
})
- return Promise.all(items.map(async (item) => await this.getWork(item, timeout))).then((resArray) => {
- return resArray.filter((res) => res)
- })
+ return Promise.all(items.map(async (item) => await this.getWork(item, timeout))).then((resArray) => resArray.filter(Boolean))
}
/**
@@ -83,6 +81,10 @@ class FantLab {
return null
})
+ if (!bookData) {
+ return null
+ }
+
return this.cleanBookData(bookData, timeout)
}
From b8849677de53859aa7f49d944984d7de06890e73 Mon Sep 17 00:00:00 2001
From: advplyr
Date: Wed, 18 Jun 2025 17:20:36 -0500
Subject: [PATCH 05/11] Episode view modal makes timestamps in description
clickable
---
.../components/modals/podcast/ViewEpisode.vue | 40 +++++++++++++++++--
1 file changed, 37 insertions(+), 3 deletions(-)
diff --git a/client/components/modals/podcast/ViewEpisode.vue b/client/components/modals/podcast/ViewEpisode.vue
index 5a520ef4..b4358c5d 100644
--- a/client/components/modals/podcast/ViewEpisode.vue
+++ b/client/components/modals/podcast/ViewEpisode.vue
@@ -16,7 +16,7 @@
{{ title }}
-
+
{{ $strings.MessageNoDescription }}
@@ -68,7 +68,7 @@ export default {
return this.episode.title || 'No Episode Title'
},
description() {
- return this.episode.description || ''
+ return this.parseDescription(this.episode.description || '')
},
media() {
return this.libraryItem?.media || {}
@@ -94,7 +94,41 @@ export default {
return this.$store.getters['libraries/getBookCoverAspectRatio']
}
},
- methods: {},
+ methods: {
+ handleDescriptionClick(e) {
+ if (e.target.matches('span.time-marker')) {
+ const time = parseInt(e.target.dataset.time)
+ if (!isNaN(time)) {
+ this.$eventBus.$emit('play-item', {
+ episodeId: this.episodeId,
+ libraryItemId: this.libraryItem.id,
+ startTime: time
+ })
+ }
+ e.preventDefault()
+ }
+ },
+ parseDescription(description) {
+ const timeMarkerLinkRegex = /(.*?)<\/a>/g
+ const timeMarkerRegex = /\b\d{1,2}:\d{1,2}(?::\d{1,2})?\b/g
+
+ function convertToSeconds(time) {
+ const timeParts = time.split(':').map(Number)
+ return timeParts.reduce((acc, part, index) => acc * 60 + part, 0)
+ }
+
+ return description
+ .replace(timeMarkerLinkRegex, (match, href, displayTime) => {
+ const time = displayTime.match(timeMarkerRegex)[0]
+ const seekTimeInSeconds = convertToSeconds(time)
+ return `${displayTime}`
+ })
+ .replace(timeMarkerRegex, (match) => {
+ const seekTimeInSeconds = convertToSeconds(match)
+ return `${match}`
+ })
+ }
+ },
mounted() {}
}
From c150ed4e989a668501edfffad6b0754c501d35df Mon Sep 17 00:00:00 2001
From: advplyr
Date: Thu, 19 Jun 2025 17:14:56 -0500
Subject: [PATCH 06/11] Update view episode modal to include duration & episode
feed modal to include duration & size
---
client/components/modals/podcast/EpisodeFeed.vue | 9 ++++++++-
client/components/modals/podcast/ViewEpisode.vue | 10 ++++++++++
2 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/client/components/modals/podcast/EpisodeFeed.vue b/client/components/modals/podcast/EpisodeFeed.vue
index 7ec14ccd..e62342af 100644
--- a/client/components/modals/podcast/EpisodeFeed.vue
+++ b/client/components/modals/podcast/EpisodeFeed.vue
@@ -35,7 +35,14 @@
{{ episode.subtitle }}
- Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}
+
+
+
Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}
+
+
{{ $strings.LabelDuration }}: {{ $elapsedPretty(Number(episode.duration)) }}
+
+
{{ $strings.LabelSize }}: {{ $bytesPretty(Number(episode.enclosure.length)) }}
+
diff --git a/client/components/modals/podcast/ViewEpisode.vue b/client/components/modals/podcast/ViewEpisode.vue
index b4358c5d..2502a5ea 100644
--- a/client/components/modals/podcast/ViewEpisode.vue
+++ b/client/components/modals/podcast/ViewEpisode.vue
@@ -34,6 +34,12 @@
{{ audioFileSize }}
+
+
{{ $strings.LabelDuration }}
+
+ {{ audioFileDuration }}
+
+
@@ -90,6 +96,10 @@ export default {
return this.$bytesPretty(size)
},
+ audioFileDuration() {
+ const duration = this.episode.duration || 0
+ return this.$elapsedPretty(duration)
+ },
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
}
From 7b92c15a465fa6eae62145250d5c6709a2dda1b7 Mon Sep 17 00:00:00 2001
From: advplyr
Date: Thu, 19 Jun 2025 17:28:21 -0500
Subject: [PATCH 07/11] Include durationSeconds on RSS podcast episode parsed
from duration
---
client/components/modals/podcast/EpisodeFeed.vue | 2 +-
server/utils/podcastUtils.js | 9 ++++++---
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/client/components/modals/podcast/EpisodeFeed.vue b/client/components/modals/podcast/EpisodeFeed.vue
index e62342af..6b99cee7 100644
--- a/client/components/modals/podcast/EpisodeFeed.vue
+++ b/client/components/modals/podcast/EpisodeFeed.vue
@@ -39,7 +39,7 @@
Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}
- {{ $strings.LabelDuration }}: {{ $elapsedPretty(Number(episode.duration)) }}
+ {{ $strings.LabelDuration }}: {{ $elapsedPretty(episode.durationSeconds) }}
{{ $strings.LabelSize }}: {{ $bytesPretty(Number(episode.enclosure.length)) }}
diff --git a/server/utils/podcastUtils.js b/server/utils/podcastUtils.js
index 12469160..b62e024f 100644
--- a/server/utils/podcastUtils.js
+++ b/server/utils/podcastUtils.js
@@ -25,6 +25,7 @@ const Fuse = require('../libs/fusejs')
* @property {string} episode
* @property {string} author
* @property {string} duration
+ * @property {number|null} durationSeconds - Parsed from duration string if duration is valid
* @property {string} explicit
* @property {number} publishedAt - Unix timestamp
* @property {{ url: string, type?: string, length?: string }} enclosure
@@ -217,8 +218,9 @@ function extractEpisodeData(item) {
})
// Extract psc:chapters if duration is set
- let episodeDuration = !isNaN(episode.duration) ? timestampToSeconds(episode.duration) : null
- if (item['psc:chapters']?.[0]?.['psc:chapter']?.length && episodeDuration) {
+ episode.durationSeconds = episode.duration ? timestampToSeconds(episode.duration) : null
+
+ if (item['psc:chapters']?.[0]?.['psc:chapter']?.length && episode.durationSeconds) {
// Example chapter:
// {"id":0,"start":0,"end":43.004286,"title":"chapter 1"}
@@ -244,7 +246,7 @@ function extractEpisodeData(item) {
} else {
episode.chapters = cleanedChapters.map((chapter, index) => {
const nextChapter = cleanedChapters[index + 1]
- const end = nextChapter ? nextChapter.start : episodeDuration
+ const end = nextChapter ? nextChapter.start : episode.durationSeconds
return {
id: chapter.id,
title: chapter.title,
@@ -273,6 +275,7 @@ function cleanEpisodeData(data) {
episode: data.episode || '',
author: data.author || '',
duration: data.duration || '',
+ durationSeconds: data.durationSeconds || null,
explicit: data.explicit || '',
publishedAt,
enclosure: data.enclosure,
From bb4eec935515f5fe945b970ec2cef2b3355a4e2d Mon Sep 17 00:00:00 2001
From: Vito0912 <86927734+Vito0912@users.noreply.github.com>
Date: Sat, 21 Jun 2025 12:02:44 +0200
Subject: [PATCH 08/11] add explicit
---
client/components/controls/LibraryFilterSelect.vue | 5 +++++
server/utils/queries/libraryItemsBookFilters.js | 2 ++
2 files changed, 7 insertions(+)
diff --git a/client/components/controls/LibraryFilterSelect.vue b/client/components/controls/LibraryFilterSelect.vue
index f5eec41a..7bfa1fe8 100644
--- a/client/components/controls/LibraryFilterSelect.vue
+++ b/client/components/controls/LibraryFilterSelect.vue
@@ -228,6 +228,11 @@ export default {
value: 'abridged',
sublist: false
},
+ {
+ text: this.$strings.LabelExplicit,
+ value: 'explicit',
+ sublist: false
+ },
{
text: this.$strings.ButtonIssues,
value: 'issues',
diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js
index 7fa5eb41..ded712cf 100644
--- a/server/utils/queries/libraryItemsBookFilters.js
+++ b/server/utils/queries/libraryItemsBookFilters.js
@@ -186,6 +186,8 @@ module.exports = {
mediaWhere['$series.id$'] = null
} else if (group === 'abridged') {
mediaWhere['abridged'] = true
+ } else if (group === 'explicit') {
+ mediaWhere['explicit'] = true
} else if (['genres', 'tags', 'narrators'].includes(group)) {
mediaWhere[group] = Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(${group}) WHERE json_valid(${group}) AND json_each.value = :filterValue)`), {
[Sequelize.Op.gte]: 1
From 5336d0525e7bd46150369f50e08d06e1145a55b3 Mon Sep 17 00:00:00 2001
From: Vito0912 <86927734+Vito0912@users.noreply.github.com>
Date: Sat, 21 Jun 2025 12:29:54 +0200
Subject: [PATCH 09/11] add explicit to podcasts
---
client/components/controls/LibraryFilterSelect.vue | 5 +++++
server/utils/queries/libraryItemsPodcastFilters.js | 2 ++
2 files changed, 7 insertions(+)
diff --git a/client/components/controls/LibraryFilterSelect.vue b/client/components/controls/LibraryFilterSelect.vue
index 7bfa1fe8..32bb9a86 100644
--- a/client/components/controls/LibraryFilterSelect.vue
+++ b/client/components/controls/LibraryFilterSelect.vue
@@ -277,6 +277,11 @@ export default {
value: 'languages',
sublist: true
},
+ {
+ text: this.$strings.LabelExplicit,
+ value: 'explicit',
+ sublist: false
+ },
{
text: this.$strings.ButtonIssues,
value: 'issues',
diff --git a/server/utils/queries/libraryItemsPodcastFilters.js b/server/utils/queries/libraryItemsPodcastFilters.js
index 33bac28f..8bb5dc11 100644
--- a/server/utils/queries/libraryItemsPodcastFilters.js
+++ b/server/utils/queries/libraryItemsPodcastFilters.js
@@ -59,6 +59,8 @@ module.exports = {
replacements.filterValue = value
} else if (group === 'languages') {
mediaWhere['language'] = value
+ } else if (group === 'explicit') {
+ mediaWhere['explicit'] = true
}
return {
From af684e6a69bf4c0d58f2f3c019e9788d6e20876c Mon Sep 17 00:00:00 2001
From: advplyr
Date: Sat, 21 Jun 2025 17:01:13 -0500
Subject: [PATCH 10/11] Explicit library filter not shown for users without
permission
---
.../controls/LibraryFilterSelect.vue | 34 +++++++++++++------
client/store/user.js | 3 ++
2 files changed, 26 insertions(+), 11 deletions(-)
diff --git a/client/components/controls/LibraryFilterSelect.vue b/client/components/controls/LibraryFilterSelect.vue
index 32bb9a86..62a9b803 100644
--- a/client/components/controls/LibraryFilterSelect.vue
+++ b/client/components/controls/LibraryFilterSelect.vue
@@ -94,6 +94,9 @@ export default {
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
+ userCanAccessExplicitContent() {
+ return this.$store.getters['user/getUserCanAccessExplicitContent']
+ },
libraryMediaType() {
return this.$store.getters['libraries/getCurrentLibraryMediaType']
},
@@ -228,11 +231,6 @@ export default {
value: 'abridged',
sublist: false
},
- {
- text: this.$strings.LabelExplicit,
- value: 'explicit',
- sublist: false
- },
{
text: this.$strings.ButtonIssues,
value: 'issues',
@@ -244,6 +242,15 @@ export default {
sublist: false
}
]
+
+ if (this.userCanAccessExplicitContent) {
+ items.push({
+ text: this.$strings.LabelExplicit,
+ value: 'explicit',
+ sublist: false
+ })
+ }
+
if (this.userIsAdminOrUp) {
items.push({
text: this.$strings.LabelShareOpen,
@@ -254,7 +261,7 @@ export default {
return items
},
podcastItems() {
- return [
+ const items = [
{
text: this.$strings.LabelAll,
value: 'all'
@@ -277,11 +284,6 @@ export default {
value: 'languages',
sublist: true
},
- {
- text: this.$strings.LabelExplicit,
- value: 'explicit',
- sublist: false
- },
{
text: this.$strings.ButtonIssues,
value: 'issues',
@@ -293,6 +295,16 @@ export default {
sublist: false
}
]
+
+ if (this.userCanAccessExplicitContent) {
+ items.push({
+ text: this.$strings.LabelExplicit,
+ value: 'explicit',
+ sublist: false
+ })
+ }
+
+ return items
},
selectItems() {
if (this.isSeries) return this.seriesItems
diff --git a/client/store/user.js b/client/store/user.js
index d5aad19b..41e12cad 100644
--- a/client/store/user.js
+++ b/client/store/user.js
@@ -58,6 +58,9 @@ export const getters = {
getUserCanAccessAllLibraries: (state) => {
return !!state.user?.permissions?.accessAllLibraries
},
+ getUserCanAccessExplicitContent: (state) => {
+ return !!state.user?.permissions?.accessExplicitContent
+ },
getLibrariesAccessible: (state, getters) => {
if (!state.user) return []
if (getters.getUserCanAccessAllLibraries) return []
From a992400d6abd9ca96e9b637e987ec46e4fbc5a08 Mon Sep 17 00:00:00 2001
From: advplyr
Date: Mon, 23 Jun 2025 16:56:08 -0500
Subject: [PATCH 11/11] Add ENV REACT_CLIENT_PATH to target a Nextjs frontend
instead of Nuxt
---
index.js | 1 +
server/Auth.js | 13 ++++++++-
server/Server.js | 74 ++++++++++++++++++++++++++++--------------------
3 files changed, 57 insertions(+), 31 deletions(-)
diff --git a/index.js b/index.js
index 2839c238..7fcd90fc 100644
--- a/index.js
+++ b/index.js
@@ -28,6 +28,7 @@ if (isDev) {
if (devEnv.SkipBinariesCheck) process.env.SKIP_BINARIES_CHECK = '1'
if (devEnv.AllowIframe) process.env.ALLOW_IFRAME = '1'
if (devEnv.BackupPath) process.env.BACKUP_PATH = devEnv.BackupPath
+ if (devEnv.ReactClientPath) process.env.REACT_CLIENT_PATH = devEnv.ReactClientPath
process.env.SOURCE = 'local'
process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath ?? '/audiobookshelf'
}
diff --git a/server/Auth.js b/server/Auth.js
index ba72947a..4e76ee33 100644
--- a/server/Auth.js
+++ b/server/Auth.js
@@ -442,7 +442,17 @@ class Auth {
// Local strategy login route (takes username and password)
router.post('/login', passport.authenticate('local'), async (req, res) => {
// return the user login response json if the login was successfull
- res.json(await this.getUserLoginResponsePayload(req.user))
+ const userResponse = await this.getUserLoginResponsePayload(req.user)
+
+ // Experimental Next.js client uses bearer token in cookies
+ res.cookie('auth_token', userResponse.user.token, {
+ httpOnly: true,
+ secure: req.secure || req.get('x-forwarded-proto') === 'https',
+ sameSite: 'strict',
+ maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
+ })
+
+ res.json(userResponse)
})
// openid strategy login route (this redirects to the configured openid login provider)
@@ -718,6 +728,7 @@ class Auth {
const authMethod = req.cookies.auth_method
res.clearCookie('auth_method')
+ res.clearCookie('auth_token')
let logoutUrl = null
diff --git a/server/Server.js b/server/Server.js
index 5c6f3c16..22a53a3a 100644
--- a/server/Server.js
+++ b/server/Server.js
@@ -220,6 +220,7 @@ class Server {
async start() {
Logger.info('=== Starting Server ===')
+
this.initProcessEventListeners()
await this.init()
@@ -281,6 +282,7 @@ class Server {
await this.auth.initPassportJs()
const router = express.Router()
+
// if RouterBasePath is set, modify all requests to include the base path
app.use((req, res, next) => {
const urlStartsWithRouterBasePath = req.url.startsWith(global.RouterBasePath)
@@ -313,10 +315,6 @@ class Server {
router.use('/hls', this.hlsRouter.router)
router.use('/public', this.publicRouter.router)
- // Static path to generated nuxt
- const distPath = Path.join(global.appRoot, '/client/dist')
- router.use(express.static(distPath))
-
// Static folder
router.use(express.static(Path.join(global.appRoot, 'static')))
@@ -336,32 +334,6 @@ class Server {
// Auth routes
await this.auth.initAuthRoutes(router)
- // Client dynamic routes
- const dynamicRoutes = [
- '/item/:id',
- '/author/:id',
- '/audiobook/:id/chapters',
- '/audiobook/:id/edit',
- '/audiobook/:id/manage',
- '/library/:library',
- '/library/:library/search',
- '/library/:library/bookshelf/:id?',
- '/library/:library/authors',
- '/library/:library/narrators',
- '/library/:library/stats',
- '/library/:library/series/:id?',
- '/library/:library/podcast/search',
- '/library/:library/podcast/latest',
- '/library/:library/podcast/download-queue',
- '/config/users/:id',
- '/config/users/:id/sessions',
- '/config/item-metadata-utils/:id',
- '/collection/:id',
- '/playlist/:id',
- '/share/:slug'
- ]
- dynamicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
-
router.post('/init', (req, res) => {
if (Database.hasRootUser) {
Logger.error(`[Server] attempt to init server when server already has a root user`)
@@ -392,6 +364,48 @@ class Server {
})
router.get('/healthcheck', (req, res) => res.sendStatus(200))
+ const ReactClientPath = process.env.REACT_CLIENT_PATH
+ if (!ReactClientPath) {
+ // Static path to generated nuxt
+ const distPath = Path.join(global.appRoot, '/client/dist')
+ router.use(express.static(distPath))
+
+ // Client dynamic routes
+ const dynamicRoutes = [
+ '/item/:id',
+ '/author/:id',
+ '/audiobook/:id/chapters',
+ '/audiobook/:id/edit',
+ '/audiobook/:id/manage',
+ '/library/:library',
+ '/library/:library/search',
+ '/library/:library/bookshelf/:id?',
+ '/library/:library/authors',
+ '/library/:library/narrators',
+ '/library/:library/stats',
+ '/library/:library/series/:id?',
+ '/library/:library/podcast/search',
+ '/library/:library/podcast/latest',
+ '/library/:library/podcast/download-queue',
+ '/config/users/:id',
+ '/config/users/:id/sessions',
+ '/config/item-metadata-utils/:id',
+ '/collection/:id',
+ '/playlist/:id',
+ '/share/:slug'
+ ]
+ dynamicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
+ } else {
+ // This is for using the experimental Next.js client
+ Logger.info(`Using React client at ${ReactClientPath}`)
+ const nextPath = Path.join(ReactClientPath, 'node_modules/next')
+ const next = require(nextPath)
+ const nextApp = next({ dev: Logger.isDev, dir: ReactClientPath })
+ const handle = nextApp.getRequestHandler()
+ await nextApp.prepare()
+ router.get('*', (req, res) => handle(req, res))
+ }
+
const unixSocketPrefix = 'unix/'
if (this.Host?.startsWith(unixSocketPrefix)) {
const sockPath = this.Host.slice(unixSocketPrefix.length)