diff --git a/.gitignore b/.gitignore index d375bae0..12ebec1c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ sw.* .DS_STORE .idea/* tailwind.compiled.css +tailwind.config.js diff --git a/Dockerfile b/Dockerfile index 4e110a61..816bdd3c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,32 @@ +ARG NUSQLITE3_DIR="/usr/local/lib/nusqlite3" +ARG NUSQLITE3_PATH="${NUSQLITE3_DIR}/libnusqlite3.so" + ### STAGE 0: Build client ### -FROM node:20-alpine AS build +FROM node:20-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 +FROM node:20-alpine AS build-server + +ARG NUSQLITE3_DIR +ARG TARGETPLATFORM ENV NODE_ENV=production -RUN apk update && \ - apk add --no-cache --update \ +RUN apk add --no-cache --update \ curl \ - tzdata \ - ffmpeg \ make \ python3 \ g++ \ - tini \ unzip -COPY --from=build /client/dist /client/dist -COPY index.js package* / -COPY server server - -ARG TARGETPLATFORM - -ENV NUSQLITE3_DIR="/usr/local/lib/nusqlite3" -ENV NUSQLITE3_PATH="${NUSQLITE3_DIR}/libnusqlite3.so" +WORKDIR /server +COPY index.js package* /server +COPY /server /server/server RUN case "$TARGETPLATFORM" in \ "linux/amd64") \ @@ -42,14 +40,34 @@ RUN case "$TARGETPLATFORM" in \ RUN npm ci --only=production -RUN apk del make python3 g++ +### 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 \ + ffmpeg \ + tini + +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/client/components/app/BookShelfCategorized.vue b/client/components/app/BookShelfCategorized.vue index 8c680462..4bf8cfbb 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/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index 01ab4fa7..95e7c378 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -274,15 +274,10 @@ export default { isAuthorsPage() { return this.page === 'authors' }, - isAlbumsPage() { - return this.page === 'albums' - }, numShowing() { return this.totalEntities }, entityName() { - if (this.isAlbumsPage) return 'Albums' - if (this.isPodcastLibrary) return this.$strings.LabelPodcasts if (!this.page) return this.$strings.LabelBooks if (this.isSeriesPage) return this.$strings.LabelSeries diff --git a/client/components/app/ConfigSideNav.vue b/client/components/app/ConfigSideNav.vue index 50fa7a06..32e7e694 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 61331fb9..854b61b2 100644 --- a/client/components/app/LazyBookshelf.vue +++ b/client/components/app/LazyBookshelf.vue @@ -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 2b05ef36..5f364201 100644 --- a/client/components/app/SideRail.vue +++ b/client/components/app/SideRail.vue @@ -116,7 +116,7 @@
diff --git a/client/components/cards/AuthorCard.vue b/client/components/cards/AuthorCard.vue index 82645c57..05347393 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 87aa0a71..09b963c5 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 }}
+
diff --git a/client/components/cards/EpisodeSearchCard.vue b/client/components/cards/EpisodeSearchCard.vue
index e69de29b..8be6a3a3 100644
--- a/client/components/cards/EpisodeSearchCard.vue
+++ b/client/components/cards/EpisodeSearchCard.vue
@@ -0,0 +1,60 @@
+
+ {{ episodeTitle }} {{ podcastTitle }} {{ title }} {{ title }} {{ artist || ' ' }}
{{ $strings.LabelEpisodes }}
+ +{{ $strings.LabelAuthors }}
{{ title }}
+{{ title }}
+{{ $strings.LabelUser }}
-{{ _session.userId }}
+{{ username }}
{{ $strings.LabelMediaPlayer }}
{{ playMethodName }}
@@ -132,6 +132,9 @@ export default { _session() { return this.session || {} }, + username() { + return this._session.user?.username || this._session.userId || '' + }, deviceInfo() { return this._session.deviceInfo || {} }, @@ -159,10 +162,10 @@ export default { return 'Unknown' }, dateFormat() { - return this.$store.state.serverSettings.dateFormat + return this.$store.getters['getServerSetting']('dateFormat') }, timeFormat() { - return this.$store.state.serverSettings.timeFormat + return this.$store.getters['getServerSetting']('timeFormat') }, isOpenSession() { return !!this._session.open diff --git a/client/components/modals/Modal.vue b/client/components/modals/Modal.vue index a7d9c0ae..31ea1e61 100644 --- a/client/components/modals/Modal.vue +++ b/client/components/modals/Modal.vue @@ -23,7 +23,7 @@ export default { processing: Boolean, persistent: { type: Boolean, - default: true + default: false }, width: { type: [String, Number], @@ -99,7 +99,7 @@ export default { this.preventClickoutside = false return } - if (this.processing && this.persistent) return + if (this.processing || this.persistent) return if (ev.srcElement && ev.srcElement.classList.contains('modal-bg')) { this.show = false } diff --git a/client/components/modals/ShareModal.vue b/client/components/modals/ShareModal.vue index 24994b22..bd0c9acf 100644 --- a/client/components/modals/ShareModal.vue +++ b/client/components/modals/ShareModal.vue @@ -144,7 +144,7 @@ export default { expirationDateString() { if (!this.expireDurationSeconds) return this.$strings.LabelPermanent const dateMs = Date.now() + this.expireDurationSeconds * 1000 - return this.$formatDatetime(dateMs, this.$store.state.serverSettings.dateFormat, this.$store.state.serverSettings.timeFormat) + return this.$formatDatetime(dateMs, this.$store.getters['getServerSetting']('dateFormat'), this.$store.getters['getServerSetting']('timeFormat')) } }, methods: { diff --git a/client/components/modals/changelog/ViewModal.vue b/client/components/modals/changelog/ViewModal.vue index 1b332a1d..939ee71d 100644 --- a/client/components/modals/changelog/ViewModal.vue +++ b/client/components/modals/changelog/ViewModal.vue @@ -40,7 +40,7 @@ export default { } }, dateFormat() { - return this.$store.state.serverSettings.dateFormat + return this.$store.getters['getServerSetting']('dateFormat') }, releasesToShow() { return this.versionData?.releasesToShow || [] diff --git a/client/components/modals/item/tabs/Files.vue b/client/components/modals/item/tabs/Files.vue index 7be286fe..15c44261 100644 --- a/client/components/modals/item/tabs/Files.vue +++ b/client/components/modals/item/tabs/Files.vue @@ -29,9 +29,6 @@ export default { media() { return this.libraryItem.media || {} }, - userToken() { - return this.$store.getters['user/getToken'] - }, userCanUpdate() { return this.$store.getters['user/getUserCanUpdate'] }, diff --git a/client/components/modals/item/tabs/Tools.vue b/client/components/modals/item/tabs/Tools.vue index d7e19cc8..d9655088 100644 --- a/client/components/modals/item/tabs/Tools.vue +++ b/client/components/modals/item/tabs/Tools.vue @@ -74,19 +74,12 @@ export default { mediaTracks() { return this.media.tracks || [] }, - isSingleM4b() { - return this.mediaTracks.length === 1 && this.mediaTracks[0].metadata.ext.toLowerCase() === '.m4b' - }, chapters() { return this.media.chapters || [] }, showM4bDownload() { if (!this.mediaTracks.length) return false - return !this.isSingleM4b - }, - showMp3Split() { - if (!this.mediaTracks.length) return false - return this.isSingleM4b && this.chapters.length + return true }, queuedEmbedLIds() { return this.$store.state.tasks.queuedEmbedLIds || [] diff --git a/client/components/modals/podcast/EpisodeFeed.vue b/client/components/modals/podcast/EpisodeFeed.vue index 08f2f38c..6b99cee7 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(episode.durationSeconds) }}
+ +{{ $strings.LabelSize }}: {{ $bytesPretty(Number(episode.enclosure.length)) }}
+{{ $getString('MessageConfirmRemoveEpisodes', [episodes.length]) }}
-Note: This does not delete the audio file unless toggling "Hard delete file"
+{{ $strings.MessageConfirmRemoveEpisodeNote }}
{{ title }}
- +{{ $strings.MessageNoDescription }}
@@ -34,6 +34,12 @@ {{ audioFileSize }} +{{ $strings.LabelDuration }}
++ {{ audioFileDuration }} +
+