diff --git a/.github/workflows/component-tests.yml b/.github/workflows/component-tests.yml index fcc2c2138..b59001812 100644 --- a/.github/workflows/component-tests.yml +++ b/.github/workflows/component-tests.yml @@ -34,7 +34,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 cache: 'npm' - name: Install dependencies diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index fdb57fbc5..5ade98b91 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -19,6 +19,8 @@ on: - server/** - index.js - package.json + - tsconfig.server.json + - Dockerfile jobs: build: diff --git a/.github/workflows/i18n-integration.yml b/.github/workflows/i18n-integration.yml index 8b3a4678f..e472487c5 100644 --- a/.github/workflows/i18n-integration.yml +++ b/.github/workflows/i18n-integration.yml @@ -20,7 +20,7 @@ jobs: - name: Set up node uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 cache: 'npm' # The only argument is the `directory`, which is where the i18n files are diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 18c1d2dae..31eada873 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -12,6 +12,7 @@ on: - test/** - index.js - package.json + - tsconfig.server.json jobs: build: @@ -23,10 +24,10 @@ jobs: - name: setup node uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 cache: 'npm' - - name: install pkg (using yao-pkg fork for targeting node20) + - name: install pkg (using yao-pkg fork for targeting node24) run: npm install -g @yao-pkg/pkg - name: get client dependencies @@ -37,11 +38,11 @@ jobs: working-directory: client run: npm run generate - - name: get server dependencies - run: npm ci --only=production + - name: get server dependencies and compile server + run: npm ci && npm run build:server && npm prune --omit=dev - name: build binary - run: pkg -t node20-linux-x64 -o audiobookshelf . + run: pkg --config package.json -t node24-linux-x64 -o audiobookshelf dist-server/index.js - name: run audiobookshelf run: | diff --git a/.github/workflows/lint-openapi.yml b/.github/workflows/lint-openapi.yml index ec08ecb36..0a75f041c 100644 --- a/.github/workflows/lint-openapi.yml +++ b/.github/workflows/lint-openapi.yml @@ -23,7 +23,7 @@ jobs: - name: Set up node uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 cache: 'npm' # Install Redocly CLI diff --git a/.github/workflows/translate-credits.yml b/.github/workflows/translate-credits.yml new file mode 100644 index 000000000..74fbb97a5 --- /dev/null +++ b/.github/workflows/translate-credits.yml @@ -0,0 +1,25 @@ +name: Prepare translation credits + +on: + push: + branches: + - master + workflow_dispatch: + +jobs: + release-notes: + if: github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, 'weblate') + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - id: translator-credits + uses: audiobookshelf/get-translator-credits@v1.1.0 + + - name: Show translator credits + run: | + printf '%s\n' "${{ steps.translator-credits.outputs.credits }}" diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 91a22c716..ada128d1e 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -28,11 +28,14 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 cache: 'npm' - name: Install dependencies run: npm ci + - name: Run lint + run: npm run lint + - name: Run tests run: npm test diff --git a/.gitignore b/.gitignore index 12ebec1c2..9ca683854 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ /client/.nuxt/ /client/dist/ /dist/ +/dist-server/ /deploy/ /coverage/ /.nyc_output/ diff --git a/Dockerfile b/Dockerfile index 816bdd3c3..23a6a7bbb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,15 +2,23 @@ ARG NUSQLITE3_DIR="/usr/local/lib/nusqlite3" ARG NUSQLITE3_PATH="${NUSQLITE3_DIR}/libnusqlite3.so" ### STAGE 0: Build client ### -FROM node:20-alpine AS build-client +FROM node:24-alpine AS build-client WORKDIR /client COPY /client /client RUN npm ci && npm cache clean --force RUN npm run generate -### STAGE 1: Build server ### -FROM node:20-alpine AS build-server +### STAGE 1: Compile server on the builder CPU (avoid QEMU SIGILL from tsc on arm64) ### +FROM --platform=$BUILDPLATFORM node:24-alpine AS compile-server + +WORKDIR /server +COPY index.js package* tsconfig.server.json /server +COPY /server /server/server +RUN npm ci --include=dev --ignore-scripts && npm run build:server + +### STAGE 2: Install native server deps for the target arch ### +FROM node:24-alpine AS build-server ARG NUSQLITE3_DIR ARG TARGETPLATFORM @@ -27,6 +35,7 @@ RUN apk add --no-cache --update \ WORKDIR /server COPY index.js package* /server COPY /server /server/server +COPY --from=compile-server /server/dist-server /server/dist-server RUN case "$TARGETPLATFORM" in \ "linux/amd64") \ @@ -38,10 +47,10 @@ RUN case "$TARGETPLATFORM" in \ unzip /tmp/library.zip -d $NUSQLITE3_DIR && \ rm /tmp/library.zip -RUN npm ci --only=production +RUN npm ci --omit=dev -### STAGE 2: Create minimal runtime image ### -FROM node:20-alpine +### STAGE 3: Create minimal runtime image ### +FROM node:24-alpine ARG NUSQLITE3_DIR ARG NUSQLITE3_PATH @@ -70,4 +79,4 @@ ENV NUSQLITE3_DIR=${NUSQLITE3_DIR} ENV NUSQLITE3_PATH=${NUSQLITE3_PATH} ENTRYPOINT ["tini", "--"] -CMD ["node", "index.js"] +CMD ["node", "dist-server/index.js"] diff --git a/build/linuxpackager b/build/linuxpackager index 52a9beba9..501d83913 100755 --- a/build/linuxpackager +++ b/build/linuxpackager @@ -16,7 +16,7 @@ echo "--------------------" cd client rm -rf node_modules -npm ci --unsafe-perm=true --allow-root +npm ci npm run generate cd .. @@ -24,7 +24,8 @@ echo ">>> Building Server" echo "--------------------" rm -rf node_modules -npm ci --unsafe-perm=true --allow-root +npm ci +npm run build:server echo ">>> Packaging" echo "--------------------" @@ -48,7 +49,7 @@ Description: $DESCRIPTION" echo "$controlfile" > dist/debian/DEBIAN/control; # Package debian -pkg -t node20-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf . +pkg --config package.json -t node24-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf dist-server/index.js fakeroot dpkg-deb -Zxz --build dist/debian diff --git a/client/components/app/Appbar.vue b/client/components/app/Appbar.vue index f74134041..428ae6ebf 100644 --- a/client/components/app/Appbar.vue +++ b/client/components/app/Appbar.vue @@ -196,6 +196,7 @@ export default { requestBatchQuickEmbed() { const payload = { message: this.$strings.MessageConfirmQuickEmbed, + allowHtml: true, callback: (confirmed) => { if (confirmed) { this.$axios diff --git a/client/components/app/BookShelfCategorized.vue b/client/components/app/BookShelfCategorized.vue index 4bf8cfbbf..a8e8ae469 100644 --- a/client/components/app/BookShelfCategorized.vue +++ b/client/components/app/BookShelfCategorized.vue @@ -300,6 +300,8 @@ export default { }) }, userUpdated(user) { + if (user.id !== this.$store.state.user.user.id) return + if (user.seriesHideFromContinueListening && user.seriesHideFromContinueListening.length) { this.removeAllSeriesFromContinueSeries(user.seriesHideFromContinueListening) } diff --git a/client/components/app/LazyBookshelf.vue b/client/components/app/LazyBookshelf.vue index 4c72d0d78..9ec492a8f 100644 --- a/client/components/app/LazyBookshelf.vue +++ b/client/components/app/LazyBookshelf.vue @@ -22,7 +22,7 @@
{{ emptyMessageHelp }} - + help_outline diff --git a/client/components/controls/LibraryFilterSelect.vue b/client/components/controls/LibraryFilterSelect.vue index 62a9b8037..4834a1a25 100644 --- a/client/components/controls/LibraryFilterSelect.vue +++ b/client/components/controls/LibraryFilterSelect.vue @@ -338,6 +338,18 @@ export default { const series = this.series.find((se) => se.id == decoded) if (series) filterValue = series.name } + } else if (parts[0] === 'progress') { + const item = this.progress.find((p) => p.id == decoded) + if (item) filterValue = item.name + } else if (parts[0] === 'tracks') { + const item = this.tracks.find((t) => t.id == decoded) + if (item) filterValue = item.name + } else if (parts[0] === 'ebooks') { + const item = this.ebooks.find((e) => e.id == decoded) + if (item) filterValue = item.name + } else if (parts[0] === 'missing') { + const item = this.missing.find((m) => m.id == decoded) + if (item) filterValue = item.name } else { filterValue = decoded } diff --git a/client/components/modals/ShareModal.vue b/client/components/modals/ShareModal.vue index bd0c9acfb..3dc1d8057 100644 --- a/client/components/modals/ShareModal.vue +++ b/client/components/modals/ShareModal.vue @@ -8,7 +8,7 @@
- + help_outline diff --git a/client/components/modals/collections/AddCreateModal.vue b/client/components/modals/collections/AddCreateModal.vue index 24e8695d6..5cec2255a 100644 --- a/client/components/modals/collections/AddCreateModal.vue +++ b/client/components/modals/collections/AddCreateModal.vue @@ -25,7 +25,7 @@

{{ $strings.MessageBookshelfNoCollectionsHelp }}

- + help_outline @@ -227,7 +227,7 @@ export default { .catch((error) => { console.error('Failed to create collection', error) var errMsg = error.response ? error.response.data || '' : '' - this.$toast.error(this.$strings.ToastCollectionCreateFailed + ': ' + errMsg) + this.$toast.error(errMsg) this.processing = false }) } diff --git a/client/components/modals/item/tabs/Match.vue b/client/components/modals/item/tabs/Match.vue index 4b92f6cd8..a1fc41091 100644 --- a/client/components/modals/item/tabs/Match.vue +++ b/client/components/modals/item/tabs/Match.vue @@ -78,7 +78,7 @@
@@ -87,7 +87,7 @@

- {{ $strings.LabelCurrently }} {{ mediaMetadata.narratorName }} + {{ $strings.LabelCurrently }} {{ mediaMetadata.narratorName }}

@@ -96,7 +96,7 @@
@@ -105,7 +105,7 @@

- {{ $strings.LabelCurrently }} {{ mediaMetadata.publisher }} + {{ $strings.LabelCurrently }} {{ mediaMetadata.publisher }}

@@ -114,7 +114,7 @@

- {{ $strings.LabelCurrently }} {{ mediaMetadata.publishedYear }} + {{ $strings.LabelCurrently }} {{ mediaMetadata.publishedYear }}

diff --git a/client/components/modals/item/tabs/Schedule.vue b/client/components/modals/item/tabs/Schedule.vue index 2cd3af514..0faa29630 100644 --- a/client/components/modals/item/tabs/Schedule.vue +++ b/client/components/modals/item/tabs/Schedule.vue @@ -158,6 +158,8 @@ export default { this.isProcessing = true var updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, updatePayload).catch((error) => { console.error('Failed to update', error) + const errorMessage = typeof error?.response?.data === 'string' ? error?.response?.data : null + this.$toast.error(errorMessage || this.$strings.ToastFailedToUpdate) return false }) this.isProcessing = false diff --git a/client/components/modals/item/tabs/Tools.vue b/client/components/modals/item/tabs/Tools.vue index d96550887..8e40ca3c0 100644 --- a/client/components/modals/item/tabs/Tools.vue +++ b/client/components/modals/item/tabs/Tools.vue @@ -107,6 +107,7 @@ export default { quickEmbed() { const payload = { message: this.$strings.MessageConfirmQuickEmbed, + allowHtml: true, callback: (confirmed) => { if (confirmed) { this.$axios diff --git a/client/components/modals/libraries/LibraryScannerSettings.vue b/client/components/modals/libraries/LibraryScannerSettings.vue index b27925ce4..6a8856a0e 100644 --- a/client/components/modals/libraries/LibraryScannerSettings.vue +++ b/client/components/modals/libraries/LibraryScannerSettings.vue @@ -8,7 +8,7 @@

{{ $strings.LabelMetadataOrderOfPrecedenceDescription }}

- + help_outline diff --git a/client/components/modals/playlists/AddCreateModal.vue b/client/components/modals/playlists/AddCreateModal.vue index f8543f1de..9aa68bd80 100644 --- a/client/components/modals/playlists/AddCreateModal.vue +++ b/client/components/modals/playlists/AddCreateModal.vue @@ -25,7 +25,7 @@

{{ $strings.MessageNoUserPlaylistsHelp }}

- + help_outline diff --git a/client/components/player/PlayerPlaybackControls.vue b/client/components/player/PlayerPlaybackControls.vue index 368e3f35b..c7cbba63c 100644 --- a/client/components/player/PlayerPlaybackControls.vue +++ b/client/components/player/PlayerPlaybackControls.vue @@ -8,7 +8,7 @@ - diff --git a/client/components/prompt/Confirm.vue b/client/components/prompt/Confirm.vue index 361765e2d..d0828c7d1 100644 --- a/client/components/prompt/Confirm.vue +++ b/client/components/prompt/Confirm.vue @@ -3,7 +3,8 @@
-

+

+

{{ message }}

@@ -52,6 +53,17 @@ export default { message() { return this.confirmPromptOptions.message || '' }, + allowHtmlMessage() { + return !!this.confirmPromptOptions.allowHtml + }, + sanitizedMessage() { + if (!this.allowHtmlMessage) return this.message + + return this.escapeHtml(this.message) + .replace(/<br\s*\/?>/gi, '
') + .replace(/<code>/gi, '') + .replace(/<\/code>/gi, '') + }, callback() { return this.confirmPromptOptions.callback }, @@ -103,6 +115,14 @@ export default { if (this.callback) this.callback(true, this.checkboxValue) this.show = false }, + escapeHtml(value) { + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + }, setShow() { this.checkboxValue = this.checkboxDefaultValue this.$eventBus.$emit('showing-prompt', true) diff --git a/client/components/ui/TextInput.vue b/client/components/ui/TextInput.vue index dd0eaa738..cd3bc6d2d 100644 --- a/client/components/ui/TextInput.vue +++ b/client/components/ui/TextInput.vue @@ -1,6 +1,6 @@ @@ -26,7 +26,8 @@ export default { disabled: Boolean, inputClass: String, showCopy: Boolean, - trimWhitespace: Boolean + trimWhitespace: Boolean, + autocomplete: String }, data() { return {} diff --git a/client/nuxt.config.js b/client/nuxt.config.js index 7219c7847..65b0b3e47 100644 --- a/client/nuxt.config.js +++ b/client/nuxt.config.js @@ -60,6 +60,7 @@ module.exports = { proxy, io: { + server: false, sockets: [ { name: 'dev', diff --git a/client/package-lock.json b/client/package-lock.json index 1e2d52c11..0653b7864 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.32.1", + "version": "2.36.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.32.1", + "version": "2.36.1", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index 0eaffb106..3178e3181 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.32.1", + "version": "2.36.1", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/client/pages/account.vue b/client/pages/account.vue index e9b5da3cb..c371a969d 100644 --- a/client/pages/account.vue +++ b/client/pages/account.vue @@ -1,6 +1,6 @@