From 33e0987d7325191b82bc99b51ef3426b5d300e80 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Sat, 7 Dec 2024 10:09:14 +0100 Subject: [PATCH 01/95] Added mediaMetadata to playbackSessions --- server/managers/PlaybackSessionManager.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index ce43fc8c4..d0daa3ba4 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -175,6 +175,12 @@ class PlaybackSessionManager { // New session from local session = new PlaybackSession(sessionJson) session.deviceInfo = deviceInfo + // This makes sure that the client's metadata is preferred over the library's metadata, if available, to make a non-breaking change + if(session.mediaMetadata == null) { + // Only sync important metadata + const { title, subtitle, narrators, authors, series, genres } = libraryItem.media.metadata || {}; + session.mediaMetadata = { title, subtitle, narrators, authors, series, genres }; + } session.setDuration(libraryItem, sessionJson.episodeId) Logger.debug(`[PlaybackSessionManager] Inserting new session for "${session.displayTitle}" (${session.id})`) await Database.createPlaybackSession(session) From 89167543fa18e43eab06bf194bbc1996bb08b648 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Sat, 7 Dec 2024 10:25:52 +0100 Subject: [PATCH 02/95] added author for podcasts --- server/managers/PlaybackSessionManager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index d0daa3ba4..8dd7bc366 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -178,8 +178,8 @@ class PlaybackSessionManager { // This makes sure that the client's metadata is preferred over the library's metadata, if available, to make a non-breaking change if(session.mediaMetadata == null) { // Only sync important metadata - const { title, subtitle, narrators, authors, series, genres } = libraryItem.media.metadata || {}; - session.mediaMetadata = { title, subtitle, narrators, authors, series, genres }; + const { title, subtitle, narrators, authors, author, series, genres } = libraryItem.media.metadata || {}; + session.mediaMetadata = { title, subtitle, narrators, authors, series, genres, author}; } session.setDuration(libraryItem, sessionJson.episodeId) Logger.debug(`[PlaybackSessionManager] Inserting new session for "${session.displayTitle}" (${session.id})`) From 2fbb31e0ea84fabe0e08564141d427e3ab385cbf Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Sat, 7 Dec 2024 10:37:00 +0100 Subject: [PATCH 03/95] added null saftey and added displayTitle and displayAuthor --- server/managers/PlaybackSessionManager.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index 8dd7bc366..e87044706 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -178,8 +178,14 @@ class PlaybackSessionManager { // This makes sure that the client's metadata is preferred over the library's metadata, if available, to make a non-breaking change if(session.mediaMetadata == null) { // Only sync important metadata - const { title, subtitle, narrators, authors, author, series, genres } = libraryItem.media.metadata || {}; - session.mediaMetadata = { title, subtitle, narrators, authors, series, genres, author}; + const { title, subtitle, narrators, authors, author, series, genres } = libraryItem?.media?.metadata || {} + session.mediaMetadata = { title, subtitle, narrators, authors, series, genres, author} + } + if(session.displayTitle == null || session.displayTitle === '') { + session.displayTitle = libraryItem?.media?.metadata?.title ?? '' + } + if(session.displayAuthor == null || session.displayAuthor === '') { + session.displayAuthor = libraryItem?.media?.metadata?.authors?.map(a => a.name).join(', ') ?? libraryItem?.media?.metadata?.author ?? '' } session.setDuration(libraryItem, sessionJson.episodeId) Logger.debug(`[PlaybackSessionManager] Inserting new session for "${session.displayTitle}" (${session.id})`) From f9bbd7117405e393c8bef56653cb279575896c32 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:27:37 +0100 Subject: [PATCH 04/95] added type to be saved. Should support podcasts It should support everything important from the podcast metadata: https://api.audiobookshelf.org/#podcast-metadata And the book metadata: https://api.audiobookshelf.org/#book-metadata --- server/managers/PlaybackSessionManager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index e87044706..1f40f38ed 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -178,8 +178,8 @@ class PlaybackSessionManager { // This makes sure that the client's metadata is preferred over the library's metadata, if available, to make a non-breaking change if(session.mediaMetadata == null) { // Only sync important metadata - const { title, subtitle, narrators, authors, author, series, genres } = libraryItem?.media?.metadata || {} - session.mediaMetadata = { title, subtitle, narrators, authors, series, genres, author} + const { title, subtitle, narrators, authors, author, series, genres, type } = libraryItem?.media?.metadata || {} + session.mediaMetadata = { title, subtitle, narrators, authors, author, series, genres, type } } if(session.displayTitle == null || session.displayTitle === '') { session.displayTitle = libraryItem?.media?.metadata?.title ?? '' From b18da959dbf4a8727e55f744196ec678d6d70476 Mon Sep 17 00:00:00 2001 From: mikiher Date: Sun, 23 Mar 2025 20:40:58 +0200 Subject: [PATCH 05/95] Fix broken component tests --- client/cypress/tests/components/cards/AuthorCard.cy.js | 8 +++++--- client/cypress/tests/components/cards/LazyBookCard.cy.js | 3 ++- .../cypress/tests/components/cards/LazySeriesCard.cy.js | 8 ++++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/client/cypress/tests/components/cards/AuthorCard.cy.js b/client/cypress/tests/components/cards/AuthorCard.cy.js index 21c638e18..4c4a1cb88 100644 --- a/client/cypress/tests/components/cards/AuthorCard.cy.js +++ b/client/cypress/tests/components/cards/AuthorCard.cy.js @@ -19,7 +19,9 @@ describe('AuthorCard', () => { const mocks = { $strings: { LabelBooks: 'Books', - ButtonQuickMatch: 'Quick Match' + ButtonQuickMatch: 'Quick Match', + ToastAuthorUpdateSuccess: 'Author updated', + ToastAuthorUpdateSuccessNoImageFound: 'Author updated (no image found)' }, $store: { getters: { @@ -167,7 +169,7 @@ describe('AuthorCard', () => { cy.get('&match').click() cy.get('&spinner').should('be.hidden') - cy.get('@success').should('have.been.calledOnceWithExactly', 'Author John Doe was updated (no image found)') + cy.get('@success').should('have.been.calledOnceWithExactly', 'Author updated (no image found)') cy.get('@error').should('not.have.been.called') cy.get('@info').should('not.have.been.called') }) @@ -189,7 +191,7 @@ describe('AuthorCard', () => { cy.get('&match').click() cy.get('&spinner').should('be.hidden') - cy.get('@success').should('have.been.calledOnceWithExactly', 'Author John Doe was updated') + cy.get('@success').should('have.been.calledOnceWithExactly', 'Author updated') cy.get('@error').should('not.have.been.called') cy.get('@info').should('not.have.been.called') }) diff --git a/client/cypress/tests/components/cards/LazyBookCard.cy.js b/client/cypress/tests/components/cards/LazyBookCard.cy.js index c39c03023..dbe33f81d 100644 --- a/client/cypress/tests/components/cards/LazyBookCard.cy.js +++ b/client/cypress/tests/components/cards/LazyBookCard.cy.js @@ -172,6 +172,7 @@ describe('LazyBookCard', () => { }) it('shows titleImageNotReady and sets opacity 0 on coverImage when image not ready', () => { + mountOptions.mocks.$store.getters['globals/getLibraryItemCoverSrc'] = () => 'https://my.server.com/notfound.jpg' cy.mount(LazyBookCard, mountOptions) cy.get('&titleImageNotReady').should('be.visible') @@ -257,7 +258,7 @@ describe('LazyBookCard', () => { cy.get('#book-card-0').trigger('mouseover') cy.get('&titleImageNotReady').should('be.hidden') - cy.get('&seriesNameOverlay').should('be.visible').and('have.text', 'Middle Earth Chronicles') + cy.get('&seriesNameOverlay').should('be.visible').and('have.text', 'The Lord of the Rings') }) it('shows the seriesSequenceList when collapsed series has a sequence list', () => { diff --git a/client/cypress/tests/components/cards/LazySeriesCard.cy.js b/client/cypress/tests/components/cards/LazySeriesCard.cy.js index c637c604e..346259d27 100644 --- a/client/cypress/tests/components/cards/LazySeriesCard.cy.js +++ b/client/cypress/tests/components/cards/LazySeriesCard.cy.js @@ -30,6 +30,14 @@ describe('LazySeriesCard', () => { } const mocks = { + $getString: (id, args) => { + switch (id) { + case 'LabelAddedDate': + return `Added ${args[0]}` + default: + return null + } + }, $store: { getters: { 'user/getUserCanUpdate': true, From 59e099f9500b838731d4ea41d3c65320f22a332f Mon Sep 17 00:00:00 2001 From: mikiher Date: Mon, 24 Mar 2025 07:42:26 +0200 Subject: [PATCH 06/95] Add GitHub Actions workflow for running component tests --- .github/workflows/component-tests.yml | 48 +++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/component-tests.yml diff --git a/.github/workflows/component-tests.yml b/.github/workflows/component-tests.yml new file mode 100644 index 000000000..fcc2c2138 --- /dev/null +++ b/.github/workflows/component-tests.yml @@ -0,0 +1,48 @@ +name: Run Component Tests + +on: + workflow_dispatch: + inputs: + ref: + description: 'Branch/Tag/SHA to test' + required: true + pull_request: + paths: + - 'client/**' + - '.github/workflows/component-tests.yml' + push: + paths: + - 'client/**' + - '.github/workflows/component-tests.yml' + +jobs: + run-component-tests: + name: Run Component Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout (push/pull request) + uses: actions/checkout@v4 + if: github.event_name != 'workflow_dispatch' + + - name: Checkout (workflow_dispatch) + uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} + if: github.event_name == 'workflow_dispatch' + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + + - name: Install dependencies + run: | + cd client + npm ci + + - name: Run tests + run: | + cd client + npm test From 718433183bea533b94e1744ffa45c3c8246beab7 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 27 Mar 2025 17:37:25 -0500 Subject: [PATCH 07/95] Fix Cover modal showing error image for items with no cover, update placeholder cover url to all come from global store --- client/components/cards/LazyBookCard.vue | 3 +-- client/components/covers/BookCover.vue | 4 ++-- client/components/covers/PreviewCover.vue | 7 ++++--- client/components/modals/item/tabs/Cover.vue | 8 +++++++- client/components/modals/player/QueueItemRow.vue | 2 +- client/pages/share/_slug.vue | 2 +- client/store/globals.js | 11 ++++++----- 7 files changed, 22 insertions(+), 15 deletions(-) diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index e9ad8c8de..35c959fad 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -223,8 +223,7 @@ export default { return this.mediaMetadata.explicit || false }, placeholderUrl() { - const config = this.$config || this.$nuxt.$config - return `${config.routerBasePath}/book_placeholder.jpg` + return this.store.getters['globals/getPlaceholderCoverSrc'] }, bookCoverSrc() { return this.store.getters['globals/getLibraryItemCoverSrc'](this._libraryItem, this.placeholderUrl) diff --git a/client/components/covers/BookCover.vue b/client/components/covers/BookCover.vue index 0a9248d22..e55d38c17 100644 --- a/client/components/covers/BookCover.vue +++ b/client/components/covers/BookCover.vue @@ -96,8 +96,8 @@ export default { return this.author }, placeholderUrl() { - const config = this.$config || this.$nuxt.$config - return `${config.routerBasePath}/book_placeholder.jpg` + const store = this.$store || this.$nuxt.$store + return store.getters['globals/getPlaceholderCoverSrc'] }, fullCoverUrl() { if (!this.libraryItem) return null diff --git a/client/components/covers/PreviewCover.vue b/client/components/covers/PreviewCover.vue index 0b73b009c..0ce7e55ed 100644 --- a/client/components/covers/PreviewCover.vue +++ b/client/components/covers/PreviewCover.vue @@ -18,7 +18,7 @@ -

{{ resolution }}

+

{{ resolution }}

@@ -65,11 +65,12 @@ export default { return 0.8 * this.sizeMultiplier }, resolution() { + if (!this.naturalWidth || !this.naturalHeight) return null return `${this.naturalWidth}×${this.naturalHeight}px` }, placeholderUrl() { - const config = this.$config || this.$nuxt.$config - return `${config.routerBasePath}/book_placeholder.jpg` + const store = this.$store || this.$nuxt.$store + return store.getters['globals/getPlaceholderCoverSrc'] } }, methods: { diff --git a/client/components/modals/item/tabs/Cover.vue b/client/components/modals/item/tabs/Cover.vue index fd258760a..17979f708 100644 --- a/client/components/modals/item/tabs/Cover.vue +++ b/client/components/modals/item/tabs/Cover.vue @@ -2,7 +2,7 @@
- +
@@ -157,6 +157,12 @@ export default { coverPath() { return this.media.coverPath }, + coverUrl() { + if (!this.coverPath) { + return this.$store.getters['globals/getPlaceholderCoverSrc'] + } + return this.$store.getters['globals/getLibraryItemCoverSrcById'](this.libraryItemId, this.libraryItemUpdatedAt, true) + }, mediaMetadata() { return this.media.metadata || {} }, diff --git a/client/components/modals/player/QueueItemRow.vue b/client/components/modals/player/QueueItemRow.vue index 2eb1bc3b6..9ac01a167 100644 --- a/client/components/modals/player/QueueItemRow.vue +++ b/client/components/modals/player/QueueItemRow.vue @@ -55,7 +55,7 @@ export default { return this.item.coverPath }, coverUrl() { - if (!this.coverPath) return `${this.$config.routerBasePath}/book_placeholder.jpg` + if (!this.coverPath) return this.$store.getters['globals/getPlaceholderCoverSrc'] return this.$store.getters['globals/getLibraryItemCoverSrcById'](this.libraryItemId) }, bookCoverAspectRatio() { diff --git a/client/pages/share/_slug.vue b/client/pages/share/_slug.vue index e7d00f005..bcc779d96 100644 --- a/client/pages/share/_slug.vue +++ b/client/pages/share/_slug.vue @@ -64,7 +64,7 @@ export default { return this.mediaItemShare.playbackSession }, coverUrl() { - if (!this.playbackSession.coverPath) return `${this.$config.routerBasePath}/book_placeholder.jpg` + if (!this.playbackSession.coverPath) return this.$store.getters['globals/getPlaceholderCoverSrc'] return `${this.$config.routerBasePath}/public/share/${this.mediaItemShare.slug}/cover` }, downloadUrl() { diff --git a/client/store/globals.js b/client/store/globals.js index 65878fb44..7b416196a 100644 --- a/client/store/globals.js +++ b/client/store/globals.js @@ -87,7 +87,7 @@ export const getters = { getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = null, raw = false) => { - if (!placeholder) placeholder = `${rootState.routerBasePath}/book_placeholder.jpg` + if (!placeholder) placeholder = getters.getPlaceholderCoverSrc if (!libraryItem) return placeholder const media = libraryItem.media if (!media?.coverPath || media.coverPath === placeholder) return placeholder @@ -95,7 +95,6 @@ export const getters = { // Absolute URL covers (should no longer be used) if (media.coverPath.startsWith('http:') || media.coverPath.startsWith('https:')) return media.coverPath - const userToken = rootGetters['user/getToken'] const lastUpdate = libraryItem.updatedAt || Date.now() const libraryItemId = libraryItem.libraryItemId || libraryItem.id // Workaround for /users/:id page showing media progress covers return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?ts=${lastUpdate}${raw ? '&raw=1' : ''}` @@ -103,11 +102,13 @@ export const getters = { getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, timestamp = null, raw = false) => { - const placeholder = `${rootState.routerBasePath}/book_placeholder.jpg` - if (!libraryItemId) return placeholder - const userToken = rootGetters['user/getToken'] + if (!libraryItemId) return getters.getPlaceholderCoverSrc + return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?${raw ? '&raw=1' : ''}${timestamp ? `&ts=${timestamp}` : ''}` }, + getPlaceholderCoverSrc: (state, getters, rootState, rootGetters) => { + return `${rootState.routerBasePath}/book_placeholder.jpg` + }, getIsBatchSelectingMediaItems: (state) => { return state.selectedMediaItems.length } From f853cff920f291ef59179226244c01c21b2ac221 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 28 Mar 2025 17:42:41 -0500 Subject: [PATCH 08/95] Fix LazyBookCard test with new globals getter for placeholder url --- client/cypress/tests/components/cards/LazyBookCard.cy.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/cypress/tests/components/cards/LazyBookCard.cy.js b/client/cypress/tests/components/cards/LazyBookCard.cy.js index dbe33f81d..ab685b0d1 100644 --- a/client/cypress/tests/components/cards/LazyBookCard.cy.js +++ b/client/cypress/tests/components/cards/LazyBookCard.cy.js @@ -49,6 +49,7 @@ function createMountOptions() { 'libraries/getLibraryProvider': () => 'audible.us', 'libraries/getBookCoverAspectRatio': 1, 'globals/getLibraryItemCoverSrc': () => 'https://my.server.com/book_placeholder.jpg', + 'globals/getPlaceholderCoverSrc': 'https://my.server.com/book_placeholder.jpg', getLibraryItemsStreaming: () => null, getIsMediaQueued: () => false, getIsStreamingFromDifferentLibrary: () => false From 4fb53303087659065ae0bacf2cac5714dd62ad72 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 29 Mar 2025 17:34:17 -0500 Subject: [PATCH 09/95] Create new StatsController and move year in review stats endpoint --- server/controllers/StatsController.js | 75 +++++++++++++++++++++++++++ server/routers/ApiRouter.js | 10 ++-- server/utils/queries/adminStats.js | 46 ++++++++++++++++ 3 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 server/controllers/StatsController.js diff --git a/server/controllers/StatsController.js b/server/controllers/StatsController.js new file mode 100644 index 000000000..36df43300 --- /dev/null +++ b/server/controllers/StatsController.js @@ -0,0 +1,75 @@ +const { Request, Response, NextFunction } = require('express') +const Logger = require('../Logger') + +const adminStats = require('../utils/queries/adminStats') + +/** + * @typedef RequestUserObject + * @property {import('../models/User')} user + * + * @typedef {Request & RequestUserObject} RequestWithUser + */ + +class StatsController { + constructor() {} + + /** + * GET: /api/stats/server + * Currently not in use + * + * @param {RequestWithUser} req + * @param {Response} res + */ + async getServerStats(req, res) { + Logger.debug('[StatsController] getServerStats') + const totalSize = await adminStats.getTotalSize() + const numAudioFiles = await adminStats.getNumAudioFiles() + + res.json({ + books: { + ...totalSize.books, + numAudioFiles: numAudioFiles.numBookAudioFiles + }, + podcasts: { + ...totalSize.podcasts, + numAudioFiles: numAudioFiles.numPodcastAudioFiles + }, + total: { + ...totalSize.total, + numAudioFiles: numAudioFiles.numAudioFiles + } + }) + } + + /** + * GET: /api/stats/year/:year + * + * @param {RequestWithUser} req + * @param {Response} res + */ + async getAdminStatsForYear(req, res) { + const year = Number(req.params.year) + if (isNaN(year) || year < 2000 || year > 9999) { + Logger.error(`[StatsController] Invalid year "${year}"`) + return res.status(400).send('Invalid year') + } + const stats = await adminStats.getStatsForYear(year) + res.json(stats) + } + + /** + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ + async middleware(req, res, next) { + if (!req.user.isAdminOrUp) { + Logger.error(`[StatsController] Non-root user "${req.user.username}" attempted to access stats route`) + return res.sendStatus(403) + } + + next() + } +} +module.exports = new StatsController() diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 78a5291d3..67a2ffbc8 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -33,8 +33,7 @@ const RSSFeedController = require('../controllers/RSSFeedController') const CustomMetadataProviderController = require('../controllers/CustomMetadataProviderController') const MiscController = require('../controllers/MiscController') const ShareController = require('../controllers/ShareController') - -const { getTitleIgnorePrefix } = require('../utils/index') +const StatsController = require('../controllers/StatsController') class ApiRouter { constructor(Server) { @@ -320,6 +319,12 @@ class ApiRouter { this.router.post('/share/mediaitem', ShareController.createMediaItemShare.bind(this)) this.router.delete('/share/mediaitem/:id', ShareController.deleteMediaItemShare.bind(this)) + // + // Stats Routes + // + this.router.get('/stats/year/:year', StatsController.getAdminStatsForYear.bind(this)) + this.router.get('/stats/server', StatsController.getServerStats.bind(this)) + // // Misc Routes // @@ -338,7 +343,6 @@ class ApiRouter { this.router.get('/auth-settings', MiscController.getAuthSettings.bind(this)) this.router.patch('/auth-settings', MiscController.updateAuthSettings.bind(this)) this.router.post('/watcher/update', MiscController.updateWatchedPath.bind(this)) - this.router.get('/stats/year/:year', MiscController.getAdminStatsForYear.bind(this)) this.router.get('/logger-data', MiscController.getLoggerData.bind(this)) } diff --git a/server/utils/queries/adminStats.js b/server/utils/queries/adminStats.js index 9d7f572a3..3184fbe38 100644 --- a/server/utils/queries/adminStats.js +++ b/server/utils/queries/adminStats.js @@ -167,5 +167,51 @@ module.exports = { topNarrators, topGenres } + }, + + /** + * Get total file size and number of items for books and podcasts + * + * @typedef {Object} SizeObject + * @property {number} totalSize + * @property {number} numItems + * + * @returns {Promise<{books: SizeObject, podcasts: SizeObject, total: SizeObject}}>} + */ + async getTotalSize() { + const [mediaTypeStats] = await Database.sequelize.query(`SELECT li.mediaType, SUM(li.size) AS totalSize, COUNT(*) AS numItems FROM libraryItems li group by li.mediaType;`) + const bookStats = mediaTypeStats.find((m) => m.mediaType === 'book') + const podcastStats = mediaTypeStats.find((m) => m.mediaType === 'podcast') + + return { + books: { + totalSize: bookStats?.totalSize || 0, + numItems: bookStats?.numItems || 0 + }, + podcasts: { + totalSize: podcastStats?.totalSize || 0, + numItems: podcastStats?.numItems || 0 + }, + total: { + totalSize: (bookStats?.totalSize || 0) + (podcastStats?.totalSize || 0), + numItems: (bookStats?.numItems || 0) + (podcastStats?.numItems || 0) + } + } + }, + + /** + * Get total number of audio files for books and podcasts + * + * @returns {Promise<{numBookAudioFiles: number, numPodcastAudioFiles: number, numAudioFiles: number}>} + */ + async getNumAudioFiles() { + const [numBookAudioFilesRow] = await Database.sequelize.query(`SELECT SUM(json_array_length(b.audioFiles)) AS numAudioFiles FROM books b;`) + const numBookAudioFiles = numBookAudioFilesRow[0]?.numAudioFiles || 0 + const numPodcastAudioFiles = await Database.podcastEpisodeModel.count() + return { + numBookAudioFiles, + numPodcastAudioFiles, + numAudioFiles: numBookAudioFiles + numPodcastAudioFiles + } } } From 73c1ea92f345f24341ea88bcdf634e9ef97e86ca Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 29 Mar 2025 17:37:13 -0500 Subject: [PATCH 10/95] Add admin middleware for StatsController --- server/controllers/StatsController.js | 2 +- server/routers/ApiRouter.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/controllers/StatsController.js b/server/controllers/StatsController.js index 36df43300..32ed19734 100644 --- a/server/controllers/StatsController.js +++ b/server/controllers/StatsController.js @@ -65,7 +65,7 @@ class StatsController { */ async middleware(req, res, next) { if (!req.user.isAdminOrUp) { - Logger.error(`[StatsController] Non-root user "${req.user.username}" attempted to access stats route`) + Logger.error(`[StatsController] Non-admin user "${req.user.username}" attempted to access stats route`) return res.sendStatus(403) } diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 67a2ffbc8..ecb1555f1 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -322,8 +322,8 @@ class ApiRouter { // // Stats Routes // - this.router.get('/stats/year/:year', StatsController.getAdminStatsForYear.bind(this)) - this.router.get('/stats/server', StatsController.getServerStats.bind(this)) + this.router.get('/stats/year/:year', StatsController.middleware.bind(this), StatsController.getAdminStatsForYear.bind(this)) + this.router.get('/stats/server', StatsController.middleware.bind(this), StatsController.getServerStats.bind(this)) // // Misc Routes From ddcda197b4a709680089e030ab5ca0b489762786 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 30 Mar 2025 17:27:36 -0500 Subject: [PATCH 11/95] Fix manage, chapters edit tracks and library stats page not setting the current library properly #4170 --- client/pages/audiobook/_id/chapters.vue | 5 +++++ client/pages/audiobook/_id/edit.vue | 6 ++++++ client/pages/audiobook/_id/manage.vue | 7 ++++++- client/pages/library/_library/stats.vue | 8 +++++--- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/client/pages/audiobook/_id/chapters.vue b/client/pages/audiobook/_id/chapters.vue index 71abef33b..7b56f5c95 100644 --- a/client/pages/audiobook/_id/chapters.vue +++ b/client/pages/audiobook/_id/chapters.vue @@ -221,6 +221,11 @@ export default { return redirect('/') } + // Fetch and set library if this items library does not match the current + if (store.state.libraries.currentLibraryId !== libraryItem.libraryId || !store.state.libraries.filterData) { + await store.dispatch('libraries/fetch', libraryItem.libraryId) + } + var previousRoute = from ? from.fullPath : null if (from && from.path === '/login') previousRoute = null return { diff --git a/client/pages/audiobook/_id/edit.vue b/client/pages/audiobook/_id/edit.vue index e29ab9e32..3aad3f800 100644 --- a/client/pages/audiobook/_id/edit.vue +++ b/client/pages/audiobook/_id/edit.vue @@ -103,6 +103,12 @@ export default { console.error('No need to edit library item that is 1 file...') return redirect('/') } + + // Fetch and set library if this items library does not match the current + if (store.state.libraries.currentLibraryId !== libraryItem.libraryId || !store.state.libraries.filterData) { + await store.dispatch('libraries/fetch', libraryItem.libraryId) + } + return { libraryItem, files: libraryItem.media.audioFiles ? libraryItem.media.audioFiles.map((af) => ({ ...af, include: !af.exclude })) : [] diff --git a/client/pages/audiobook/_id/manage.vue b/client/pages/audiobook/_id/manage.vue index 30e615bbf..3d9ac0512 100644 --- a/client/pages/audiobook/_id/manage.vue +++ b/client/pages/audiobook/_id/manage.vue @@ -195,10 +195,15 @@ export default { return redirect('/?error=invalid media type') } if (!libraryItem.media.audioFiles.length) { - cnosole.error('No audio files') + console.error('No audio files') return redirect('/?error=no audio files') } + // Fetch and set library if this items library does not match the current + if (store.state.libraries.currentLibraryId !== libraryItem.libraryId || !store.state.libraries.filterData) { + await store.dispatch('libraries/fetch', libraryItem.libraryId) + } + return { libraryItem } diff --git a/client/pages/library/_library/stats.vue b/client/pages/library/_library/stats.vue index 575c4676d..0fcc2a414 100644 --- a/client/pages/library/_library/stats.vue +++ b/client/pages/library/_library/stats.vue @@ -89,14 +89,16 @@