diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 2e5f4bced..809563018 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -60,7 +60,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -73,6 +73,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: '/language:${{matrix.language}}' diff --git a/Dockerfile b/Dockerfile index 1ba107fd8..816bdd3c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,9 @@ +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 + WORKDIR /client COPY /client /client RUN npm ci && npm cache clean --force @@ -8,6 +12,9 @@ RUN npm run generate ### STAGE 1: Build server ### FROM node:20-alpine AS build-server +ARG NUSQLITE3_DIR +ARG TARGETPLATFORM + ENV NODE_ENV=production RUN apk add --no-cache --update \ @@ -21,11 +28,6 @@ WORKDIR /server COPY index.js package* /server COPY /server /server/server -ARG TARGETPLATFORM - -ENV NUSQLITE3_DIR="/usr/local/lib/nusqlite3" -ENV NUSQLITE3_PATH="${NUSQLITE3_DIR}/libnusqlite3.so" - RUN case "$TARGETPLATFORM" in \ "linux/amd64") \ curl -L -o /tmp/library.zip "https://github.com/mikiher/nunicode-sqlite/releases/download/v1.2/libnusqlite3-linux-musl-x64.zip" ;; \ @@ -41,6 +43,9 @@ RUN npm ci --only=production ### STAGE 2: Create minimal runtime image ### FROM node:20-alpine +ARG NUSQLITE3_DIR +ARG NUSQLITE3_PATH + # Install only runtime dependencies RUN apk add --no-cache --update \ tzdata \ @@ -52,13 +57,17 @@ 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 ${NUSQLITE3_PATH} ${NUSQLITE3_PATH} EXPOSE 80 ENV PORT=80 +ENV NODE_ENV=production ENV CONFIG_PATH="/config" ENV METADATA_PATH="/metadata" ENV SOURCE="docker" +ENV NUSQLITE3_DIR=${NUSQLITE3_DIR} +ENV NUSQLITE3_PATH=${NUSQLITE3_PATH} ENTRYPOINT ["tini", "--"] CMD ["node", "index.js"] diff --git a/build/debian/DEBIAN/preinst b/build/debian/DEBIAN/preinst index e30bc490c..241a47010 100644 --- a/build/debian/DEBIAN/preinst +++ b/build/debian/DEBIAN/preinst @@ -22,7 +22,7 @@ add_user() { declare -r descr="${4:-No description}" declare -r shell="${5:-/bin/false}" - if ! getent passwd | grep -q "^$user:"; then + if ! getent passwd "$user" 2>&1 >/dev/null; then echo "Creating system user: $user in $group with $descr and shell $shell" useradd $uid_flags --gid $group --no-create-home --system --shell $shell -c "$descr" $user fi @@ -39,7 +39,7 @@ add_group() { declare -r gid_flags="--gid $gid" fi - if ! getent group | grep -q "^$group:" ; then + if ! getent group "$group" 2>&1 >/dev/null; then echo "Creating system group: $group" groupadd $gid_flags --system $group fi diff --git a/client/components/app/BookShelfCategorized.vue b/client/components/app/BookShelfCategorized.vue index 8c6804625..4bf8cfbbf 100644 --- a/client/components/app/BookShelfCategorized.vue +++ b/client/components/app/BookShelfCategorized.vue @@ -217,6 +217,16 @@ export default { }) } + if (this.results.episodes?.length) { + shelves.push({ + id: 'episodes', + label: 'Episodes', + labelStringKey: 'LabelEpisodes', + type: 'episode', + entities: this.results.episodes.map((res) => res.libraryItem) + }) + } + if (this.results.series?.length) { shelves.push({ id: 'series', diff --git a/client/components/app/BookShelfRow.vue b/client/components/app/BookShelfRow.vue index 082f9fe37..fac89a70b 100644 --- a/client/components/app/BookShelfRow.vue +++ b/client/components/app/BookShelfRow.vue @@ -93,10 +93,10 @@ export default { editAuthor(author) { this.$store.commit('globals/showEditAuthorModal', author) }, - editItem(libraryItem) { + editItem(libraryItem, tab = 'details') { var itemIds = this.shelf.entities.map((e) => e.id) this.$store.commit('setBookshelfBookIds', itemIds) - this.$store.commit('showEditModal', libraryItem) + this.$store.commit('showEditModalOnTab', { libraryItem, tab: tab || 'details' }) }, editEpisode({ libraryItem, episode }) { this.$store.commit('setEpisodeTableEpisodeIds', [episode.id]) diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index 95e7c378c..b7ecff624 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -3,24 +3,18 @@

{{ $strings.ButtonHome }}

- - - + home

{{ $strings.ButtonLibrary }}

- - - + import_contacts

{{ $strings.ButtonLatest }}

{{ $strings.ButtonSeries }}

- - - + view_column

{{ $strings.ButtonPlaylists }}

@@ -32,12 +26,7 @@

{{ $strings.ButtonAuthors }}

- - - + groups

{{ $strings.ButtonAdd }}

diff --git a/client/components/app/ConfigSideNav.vue b/client/components/app/ConfigSideNav.vue index 50fa7a06f..32e7e694a 100644 --- a/client/components/app/ConfigSideNav.vue +++ b/client/components/app/ConfigSideNav.vue @@ -70,6 +70,11 @@ export default { title: this.$strings.HeaderUsers, path: '/config/users' }, + { + id: 'config-api-keys', + title: this.$strings.HeaderApiKeys, + path: '/config/api-keys' + }, { id: 'config-sessions', title: this.$strings.HeaderListeningSessions, diff --git a/client/components/app/LazyBookshelf.vue b/client/components/app/LazyBookshelf.vue index 61331fb9e..4c72d0d78 100644 --- a/client/components/app/LazyBookshelf.vue +++ b/client/components/app/LazyBookshelf.vue @@ -232,11 +232,11 @@ export default { clearFilter() { this.$store.dispatch('user/updateUserSettings', { filterBy: 'all' }) }, - editEntity(entity) { + editEntity(entity, tab = 'details') { if (this.entityName === 'items' || this.entityName === 'series-books') { const bookIds = this.entities.map((e) => e.id) this.$store.commit('setBookshelfBookIds', bookIds) - this.$store.commit('showEditModal', entity) + this.$store.commit('showEditModalOnTab', { libraryItem: entity, tab: tab || 'details' }) } else if (this.entityName === 'collections') { this.$store.commit('globals/setEditCollection', entity) } else if (this.entityName === 'playlists') { @@ -778,10 +778,6 @@ export default { windowResize() { this.executeRebuild() }, - socketInit() { - // Server settings are set on socket init - this.executeRebuild() - }, initListeners() { window.addEventListener('resize', this.windowResize) @@ -794,7 +790,6 @@ export default { }) this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities) - this.$eventBus.$on('socket_init', this.socketInit) this.$eventBus.$on('user-settings', this.settingsUpdated) if (this.$root.socket) { @@ -826,7 +821,6 @@ export default { } this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities) - this.$eventBus.$off('socket_init', this.socketInit) this.$eventBus.$off('user-settings', this.settingsUpdated) if (this.$root.socket) { diff --git a/client/components/app/SideRail.vue b/client/components/app/SideRail.vue index 2b05ef360..9fa7661a1 100644 --- a/client/components/app/SideRail.vue +++ b/client/components/app/SideRail.vue @@ -5,9 +5,7 @@
-

v{{ $config.version }}

+

v{{ $config.version }}

Update

{{ Source }}

diff --git a/client/components/cards/AuthorCard.vue b/client/components/cards/AuthorCard.vue index 82645c570..053473934 100644 --- a/client/components/cards/AuthorCard.vue +++ b/client/components/cards/AuthorCard.vue @@ -71,9 +71,6 @@ export default { coverHeight() { return this.cardHeight }, - userToken() { - return this.store.getters['user/getToken'] - }, _author() { return this.author || {} }, diff --git a/client/components/cards/BookMatchCard.vue b/client/components/cards/BookMatchCard.vue index 87aa0a711..09b963c50 100644 --- a/client/components/cards/BookMatchCard.vue +++ b/client/components/cards/BookMatchCard.vue @@ -13,9 +13,17 @@

{{ book.publishedYear }}

-

{{ $getString('LabelByAuthor', [book.author]) }}

-

{{ $strings.LabelNarrators }}: {{ book.narrator }}

-

{{ $strings.LabelDuration }}: {{ $elapsedPrettyExtended(bookDuration, false) }} {{ bookDurationComparison }}

+ +
+
+

{{ $getString('LabelByAuthor', [book.author]) }}

+

{{ $strings.LabelNarrators }}: {{ book.narrator }}

+

{{ $strings.LabelDuration }}: {{ $elapsedPrettyExtended(bookDuration, false) }} {{ bookDurationComparison }}

+
+
+
{{ $strings.LabelMatchConfidence }}: {{ (book.matchConfidence * 100).toFixed(0) }}%
+
+

diff --git a/client/components/cards/EpisodeSearchCard.vue b/client/components/cards/EpisodeSearchCard.vue index e69de29bb..8be6a3a3b 100644 --- a/client/components/cards/EpisodeSearchCard.vue +++ b/client/components/cards/EpisodeSearchCard.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/client/components/cards/ItemUploadCard.vue b/client/components/cards/ItemUploadCard.vue index 40836b8e4..adbf3adbe 100644 --- a/client/components/cards/ItemUploadCard.vue +++ b/client/components/cards/ItemUploadCard.vue @@ -62,7 +62,24 @@

- + +
+ + {{ nonInteractionLabel }} + +
+ +
+
+ + {{ uploadProgressText }} + +
+
+
+
+
+
@@ -91,7 +108,11 @@ export default { isUploading: false, uploadFailed: false, uploadSuccess: false, - isFetchingMetadata: false + isFetchingMetadata: false, + uploadProgress: { + loaded: 0, + total: 0 + } } }, computed: { @@ -116,6 +137,15 @@ export default { } else if (this.isFetchingMetadata) { return this.$strings.LabelFetchingMetadata } + }, + uploadProgressPercent() { + if (this.uploadProgress.total === 0) return 0 + return Math.min(100, Math.round((this.uploadProgress.loaded / this.uploadProgress.total) * 100)) + }, + uploadProgressText() { + const loaded = this.$bytesPretty(this.uploadProgress.loaded) + const total = this.$bytesPretty(this.uploadProgress.total) + return `${this.uploadProgressPercent}% (${loaded} / ${total})` } }, methods: { @@ -123,6 +153,21 @@ export default { this.isUploading = status === 'uploading' this.uploadFailed = status === 'failed' this.uploadSuccess = status === 'success' + + if (status !== 'uploading') { + this.uploadProgress = { + loaded: 0, + total: 0 + } + } + }, + setUploadProgress(progress) { + if (this.isUploading && progress) { + this.uploadProgress = { + loaded: progress.loaded || 0, + total: progress.total || 0 + } + } }, titleUpdated() { this.error = '' diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index 35c959fad..51f657dbc 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -78,7 +78,7 @@
- +
priority_high
@@ -101,7 +101,8 @@

- Episode #{{ recentEpisodeNumber }} + Episode + #{{ recentEpisodeNumber }}

@@ -120,12 +121,12 @@
- +

{{ displayTitle }}

- +

{{ displaySubtitle }}

{{ displayLineTwo || ' ' }}

@@ -198,7 +199,10 @@ export default { return this.store.getters['user/getSizeMultiplier'] }, dateFormat() { - return this.store.state.serverSettings.dateFormat + return this.store.getters['getServerSetting']('dateFormat') + }, + timeFormat() { + return this.store.getters['getServerSetting']('timeFormat') }, _libraryItem() { return this.libraryItem || {} @@ -345,6 +349,18 @@ export default { if (this.mediaMetadata.publishedYear) return this.$getString('LabelPublishedDate', [this.mediaMetadata.publishedYear]) return '\u00A0' } + if (this.orderBy === 'progress') { + if (!this.userProgressLastUpdated) return '\u00A0' + return this.$getString('LabelLastProgressDate', [this.$formatDatetime(this.userProgressLastUpdated, this.dateFormat, this.timeFormat)]) + } + if (this.orderBy === 'progress.createdAt') { + if (!this.userProgressStartedDate) return '\u00A0' + return this.$getString('LabelStartedDate', [this.$formatDatetime(this.userProgressStartedDate, this.dateFormat, this.timeFormat)]) + } + if (this.orderBy === 'progress.finishedAt') { + if (!this.userProgressFinishedDate) return '\u00A0' + return this.$getString('LabelFinishedDate', [this.$formatDatetime(this.userProgressFinishedDate, this.dateFormat, this.timeFormat)]) + } return null }, episodeProgress() { @@ -377,6 +393,18 @@ export default { 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) }, + userProgressLastUpdated() { + if (!this.userProgress) return null + return this.userProgress.lastUpdate + }, + userProgressStartedDate() { + if (!this.userProgress) return null + return this.userProgress.startedAt + }, + userProgressFinishedDate() { + if (!this.userProgress) return null + return this.userProgress.finishedAt + }, itemIsFinished() { if (this.booksInSeries) return this.seriesIsFinished return this.userProgress ? !!this.userProgress.isFinished : false @@ -760,11 +788,11 @@ export default { }, showEditModalFiles() { // More menu func - this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'files' }) + this.$emit('edit', this.libraryItem, 'files') }, showEditModalMatch() { // More menu func - this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'match' }) + this.$emit('edit', this.libraryItem, 'match') }, sendToDevice(deviceName) { // More menu func diff --git a/client/components/cards/LazySeriesCard.vue b/client/components/cards/LazySeriesCard.vue index 3532095b9..34cea7e22 100644 --- a/client/components/cards/LazySeriesCard.vue +++ b/client/components/cards/LazySeriesCard.vue @@ -71,7 +71,7 @@ export default { return this.height * this.sizeMultiplier }, dateFormat() { - return this.store.state.serverSettings.dateFormat + return this.store.getters['getServerSetting']('dateFormat') }, labelFontSize() { if (this.width < 160) return 0.75 diff --git a/client/components/controls/GlobalSearch.vue b/client/components/controls/GlobalSearch.vue index bc9a23681..6f3a819bf 100644 --- a/client/components/controls/GlobalSearch.vue +++ b/client/components/controls/GlobalSearch.vue @@ -39,6 +39,15 @@ +

{{ $strings.LabelEpisodes }}

+ +

{{ $strings.LabelAuthors }}