Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature - New endpoint /libraries/stats #3330

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 30 additions & 33 deletions server/controllers/LibraryController.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,30 @@ const authorFilters = require('../utils/queries/authorFilters')
class LibraryController {
constructor() {}

/**
* GET: /api/libraries/stats
* Get stats for all libraries and respond with JSON
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
async allStats(req, res) {
try {
const allLibrariesIds = await Database.libraryModel.getAllLibraryIds();
const allStats = [];

for (let i = 0; i < allLibrariesIds.length; i++) {
const library = await Database.libraryModel.getOldById(allLibrariesIds[i]);
req.library = library
const libraryStats = await libraryHelpers.getLibraryStats(req);
allStats.push({ library: library, stats: libraryStats });
}

res.json(allStats);
} catch (error) {
res.status(500).json({ error: error.message });
}
}

/**
* POST: /api/libraries
* Create a new library
Expand Down Expand Up @@ -940,39 +964,12 @@ class LibraryController {
* @param {Response} res
*/
async stats(req, res) {
const stats = {
largestItems: await libraryItemFilters.getLargestItems(req.library.id, 10)
}

if (req.library.mediaType === 'book') {
const authors = await authorFilters.getAuthorsWithCount(req.library.id, 10)
const genres = await libraryItemsBookFilters.getGenresWithCount(req.library.id)
const bookStats = await libraryItemsBookFilters.getBookLibraryStats(req.library.id)
const longestBooks = await libraryItemsBookFilters.getLongestBooks(req.library.id, 10)

stats.totalAuthors = await authorFilters.getAuthorsTotalCount(req.library.id)
stats.authorsWithCount = authors
stats.totalGenres = genres.length
stats.genresWithCount = genres
stats.totalItems = bookStats.totalItems
stats.longestItems = longestBooks
stats.totalSize = bookStats.totalSize
stats.totalDuration = bookStats.totalDuration
stats.numAudioTracks = bookStats.numAudioFiles
} else {
const genres = await libraryItemsPodcastFilters.getGenresWithCount(req.library.id)
const podcastStats = await libraryItemsPodcastFilters.getPodcastLibraryStats(req.library.id)
const longestPodcasts = await libraryItemsPodcastFilters.getLongestPodcasts(req.library.id, 10)

stats.totalGenres = genres.length
stats.genresWithCount = genres
stats.totalItems = podcastStats.totalItems
stats.longestItems = longestPodcasts
stats.totalSize = podcastStats.totalSize
stats.totalDuration = podcastStats.totalDuration
stats.numAudioTracks = podcastStats.numAudioFiles
}
res.json(stats)
try {
const stats = await libraryHelpers.getLibraryStats(req);
res.json(stats);
} catch (error) {
res.status(500).json({ error: error.message });
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions server/routers/ApiRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@
//
// Library Routes
//
this.router.get(/^\/libraries/, this.apiCacheManager.middleware)

Check failure

Code scanning / CodeQL

Case-sensitive middleware path High

This route uses a case-sensitive path
pattern
, but is guarding a
case-insensitive path
. A path such as '/LIBRARIES' will bypass the middleware.
This route uses a case-sensitive path
pattern
, but is guarding a
case-insensitive path
. A path such as '/LIBRARIES/1' will bypass the middleware.
This route uses a case-sensitive path
pattern
, but is guarding a
case-insensitive path
. A path such as '/LIBRARIES/1/ITEMS' will bypass the middleware.
This route uses a case-sensitive path
pattern
, but is guarding a
case-insensitive path
. A path such as '/LIBRARIES/1/EPISODE-DOWNLOADS' will bypass the middleware.
This route uses a case-sensitive path
pattern
, but is guarding a
case-insensitive path
. A path such as '/LIBRARIES/1/SERIES' will bypass the middleware.
This route uses a case-sensitive path
pattern
, but is guarding a
case-insensitive path
. A path such as '/LIBRARIES/1/SERIES/1' will bypass the middleware.
This route uses a case-sensitive path
pattern
, but is guarding a
case-insensitive path
. A path such as '/LIBRARIES/1/COLLECTIONS' will bypass the middleware.
This route uses a case-sensitive path
pattern
, but is guarding a
case-insensitive path
. A path such as '/LIBRARIES/1/PLAYLISTS' will bypass the middleware.
This route uses a case-sensitive path
pattern
, but is guarding a
case-insensitive path
. A path such as '/LIBRARIES/1/PERSONALIZED' will bypass the middleware.
This route uses a case-sensitive path
pattern
, but is guarding a
case-insensitive path
. A path such as '/LIBRARIES/1/FILTERDATA' will bypass the middleware.
This route uses a case-sensitive path
pattern
, but is guarding a
case-insensitive path
. A path such as '/LIBRARIES/1/SEARCH' will bypass the middleware.
This route uses a case-sensitive path
pattern
, but is guarding a
case-insensitive path
. A path such as '/LIBRARIES/1/STATS' will bypass the middleware.
This route uses a case-sensitive path
pattern
, but is guarding a
case-insensitive path
. A path such as '/LIBRARIES/1/AUTHORS' will bypass the middleware.
This route uses a case-sensitive path
pattern
, but is guarding a
case-insensitive path
. A path such as '/LIBRARIES/1/NARRATORS' will bypass the middleware.
This route uses a case-sensitive path
pattern
, but is guarding a
case-insensitive path
. A path such as '/LIBRARIES/1/MATCHALL' will bypass the middleware.
This route uses a case-sensitive path
pattern
, but is guarding a
case-insensitive path
. A path such as '/LIBRARIES/1/RECENT-EPISODES' will bypass the middleware.
This route uses a case-sensitive path
pattern
, but is guarding a
case-insensitive path
. A path such as '/LIBRARIES/1/OPML' will bypass the middleware.
This route uses a case-sensitive path
pattern
, but is guarding a
case-insensitive path
. A path such as '/LIBRARIES/1/PODCAST-TITLES' will bypass the middleware.
this.router.post('/libraries', LibraryController.create.bind(this))
this.router.get('/libraries', LibraryController.findAll.bind(this))
this.router.get('/libraries/stats', LibraryController.allStats.bind(this))
this.router.get('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.findOne.bind(this))
this.router.patch('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.update.bind(this))
this.router.delete('/libraries/:id', LibraryController.middleware.bind(this), LibraryController.delete.bind(this))
Expand Down
46 changes: 46 additions & 0 deletions server/utils/libraryHelpers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
const { createNewSortInstance } = require('../libs/fastSort')
const Database = require('../Database')
const { getTitlePrefixAtEnd, isNullOrNaN, getTitleIgnorePrefix } = require('../utils/index')
const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters')
const authorFilters = require('../utils/queries/authorFilters')
const libraryItemFilters = require('../utils/queries/libraryItemFilters')
const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters')
const naturalSort = createNewSortInstance({
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
})
Expand Down Expand Up @@ -78,6 +82,48 @@ module.exports = {
return filteredLibraryItems
},

/**
* Helper method to get stats for a specific library
* @param {import('express').Request} req
* @returns {Promise<Object>} stats
*/
async getLibraryStats(req) {
const stats = {
largestItems: await libraryItemFilters.getLargestItems(req.library.id, 10)
};

if (req.library.mediaType === 'book') {
const authors = await authorFilters.getAuthorsWithCount(req.library.id, 10)
const genres = await libraryItemsBookFilters.getGenresWithCount(req.library.id)
const bookStats = await libraryItemsBookFilters.getBookLibraryStats(req.library.id)
const longestBooks = await libraryItemsBookFilters.getLongestBooks(req.library.id, 10)

stats.totalAuthors = await authorFilters.getAuthorsTotalCount(req.library.id)
stats.authorsWithCount = authors;
stats.totalGenres = genres.length;
stats.genresWithCount = genres;
stats.totalItems = bookStats.totalItems;
stats.longestItems = longestBooks;
stats.totalSize = bookStats.totalSize;
stats.totalDuration = bookStats.totalDuration;
stats.numAudioTracks = bookStats.numAudioFiles;
} else {
const genres = await libraryItemsPodcastFilters.getGenresWithCount(req.library.id)
const podcastStats = await libraryItemsPodcastFilters.getPodcastLibraryStats(req.library.id)
const longestPodcasts = await libraryItemsPodcastFilters.getLongestPodcasts(req.library.id, 10)

stats.totalGenres = genres.length;
stats.genresWithCount = genres;
stats.totalItems = podcastStats.totalItems;
stats.longestItems = longestPodcasts;
stats.totalSize = podcastStats.totalSize;
stats.totalDuration = podcastStats.totalDuration;
stats.numAudioTracks = podcastStats.numAudioFiles;
}

return stats;
},

/**
*
* @param {*} payload
Expand Down
Loading