Update library stats API route to load from db

This commit is contained in:
advplyr 2023-08-19 16:53:33 -05:00
parent ff0d6326d3
commit 332078e6c1
8 changed files with 308 additions and 52 deletions

View File

@ -18,7 +18,7 @@
</div> </div>
</div> </div>
<div class="flex px-4"> <div v-if="isBookLibrary" class="flex px-4">
<svg class="h-14 w-14 md:h-18 md:w-18" viewBox="0 0 24 24"> <svg class="h-14 w-14 md:h-18 md:w-18" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" /> <path fill="currentColor" d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" />
</svg> </svg>
@ -58,26 +58,32 @@ export default {
return {} return {}
}, },
computed: { computed: {
currentLibraryMediaType() {
return this.$store.getters['libraries/getCurrentLibraryMediaType']
},
isBookLibrary() {
return this.currentLibraryMediaType === 'book'
},
user() { user() {
return this.$store.state.user.user return this.$store.state.user.user
}, },
totalItems() { totalItems() {
return this.libraryStats ? this.libraryStats.totalItems : 0 return this.libraryStats?.totalItems || 0
}, },
totalAuthors() { totalAuthors() {
return this.libraryStats ? this.libraryStats.totalAuthors : 0 return this.libraryStats?.totalAuthors || 0
}, },
numAudioTracks() { numAudioTracks() {
return this.libraryStats ? this.libraryStats.numAudioTracks : 0 return this.libraryStats?.numAudioTracks || 0
}, },
totalDuration() { totalDuration() {
return this.libraryStats ? this.libraryStats.totalDuration : 0 return this.libraryStats?.totalDuration || 0
}, },
totalHours() { totalHours() {
return Math.round(this.totalDuration / (60 * 60)) return Math.round(this.totalDuration / (60 * 60))
}, },
totalSizePretty() { totalSizePretty() {
var totalSize = this.libraryStats ? this.libraryStats.totalSize : 0 var totalSize = this.libraryStats?.totalSize || 0
return this.$bytesPretty(totalSize, 1) return this.$bytesPretty(totalSize, 1)
}, },
totalSizeNum() { totalSizeNum() {

View File

@ -22,7 +22,7 @@
</div> </div>
</template> </template>
</div> </div>
<div class="w-80 my-6 mx-auto"> <div v-if="isBookLibrary" class="w-80 my-6 mx-auto">
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsTop10Authors }}</h1> <h1 class="text-2xl mb-4">{{ $strings.HeaderStatsTop10Authors }}</h1>
<p v-if="!top10Authors.length">{{ $strings.MessageNoAuthors }}</p> <p v-if="!top10Authors.length">{{ $strings.MessageNoAuthors }}</p>
<template v-for="(author, index) in top10Authors"> <template v-for="(author, index) in top10Authors">
@ -114,43 +114,49 @@ export default {
return this.$store.state.user.user return this.$store.state.user.user
}, },
totalItems() { totalItems() {
return this.libraryStats ? this.libraryStats.totalItems : 0 return this.libraryStats?.totalItems || 0
}, },
genresWithCount() { genresWithCount() {
return this.libraryStats ? this.libraryStats.genresWithCount : [] return this.libraryStats?.genresWithCount || []
}, },
top5Genres() { top5Genres() {
return this.genresWithCount.slice(0, 5) return this.genresWithCount?.slice(0, 5) || []
}, },
top10LongestItems() { top10LongestItems() {
return this.libraryStats ? this.libraryStats.longestItems || [] : [] return this.libraryStats?.longestItems || []
}, },
longestItemDuration() { longestItemDuration() {
if (!this.top10LongestItems.length) return 0 if (!this.top10LongestItems.length) return 0
return this.top10LongestItems[0].duration return this.top10LongestItems[0].duration
}, },
top10LargestItems() { top10LargestItems() {
return this.libraryStats ? this.libraryStats.largestItems || [] : [] return this.libraryStats?.largestItems || []
}, },
largestItemSize() { largestItemSize() {
if (!this.top10LargestItems.length) return 0 if (!this.top10LargestItems.length) return 0
return this.top10LargestItems[0].size return this.top10LargestItems[0].size
}, },
authorsWithCount() { authorsWithCount() {
return this.libraryStats ? this.libraryStats.authorsWithCount : [] return this.libraryStats?.authorsWithCount || []
}, },
mostUsedAuthorCount() { mostUsedAuthorCount() {
if (!this.authorsWithCount.length) return 0 if (!this.authorsWithCount.length) return 0
return this.authorsWithCount[0].count return this.authorsWithCount[0].count
}, },
top10Authors() { top10Authors() {
return this.authorsWithCount.slice(0, 10) return this.authorsWithCount?.slice(0, 10) || []
}, },
currentLibraryId() { currentLibraryId() {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
}, },
currentLibraryName() { currentLibraryName() {
return this.$store.getters['libraries/getCurrentLibraryName'] return this.$store.getters['libraries/getCurrentLibraryName']
},
currentLibraryMediaType() {
return this.$store.getters['libraries/getCurrentLibraryMediaType']
},
isBookLibrary() {
return this.currentLibraryMediaType === 'book'
} }
}, },
methods: { methods: {

View File

@ -17,6 +17,7 @@ const naturalSort = createNewSortInstance({
const Database = require('../Database') const Database = require('../Database')
const libraryFilters = require('../utils/queries/libraryFilters') const libraryFilters = require('../utils/queries/libraryFilters')
const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters') const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters')
const authorFilters = require('../utils/queries/authorFilters')
class LibraryController { class LibraryController {
constructor() { } constructor() { }
@ -809,23 +810,44 @@ class LibraryController {
res.json(matches) res.json(matches)
} }
/**
* GET: /api/libraries/:id/stats
* Get stats for library
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
async stats(req, res) { async stats(req, res) {
var libraryItems = req.libraryItems const stats = {
var authorsWithCount = libraryHelpers.getAuthorsWithCount(libraryItems) largestItems: await libraryItemFilters.getLargestItems(req.library.id, 10)
var genresWithCount = libraryHelpers.getGenresWithCount(libraryItems) }
var durationStats = libraryHelpers.getItemDurationStats(libraryItems)
var sizeStats = libraryHelpers.getItemSizeStats(libraryItems) if (req.library.isBook) {
var stats = { const authors = await authorFilters.getAuthorsWithCount(req.library.id)
totalItems: libraryItems.length, const genres = await libraryItemsBookFilters.getGenresWithCount(req.library.id)
totalAuthors: Object.keys(authorsWithCount).length, const bookStats = await libraryItemsBookFilters.getBookLibraryStats(req.library.id)
totalGenres: Object.keys(genresWithCount).length, const longestBooks = await libraryItemsBookFilters.getLongestBooks(req.library.id, 10)
totalDuration: durationStats.totalDuration,
longestItems: durationStats.longestItems, stats.totalAuthors = authors.length
numAudioTracks: durationStats.numAudioTracks, stats.authorsWithCount = authors
totalSize: libraryHelpers.getLibraryItemsTotalSize(libraryItems), stats.totalGenres = genres.length
largestItems: sizeStats.largestItems, stats.genresWithCount = genres
authorsWithCount, stats.totalItems = bookStats.totalItems
genresWithCount 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) res.json(stats)
} }

View File

@ -87,7 +87,7 @@ class ApiRouter {
this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalizedOptimal.bind(this)) this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalizedOptimal.bind(this))
this.router.get('/libraries/:id/filterdata', LibraryController.middlewareNew.bind(this), LibraryController.getLibraryFilterData.bind(this)) this.router.get('/libraries/:id/filterdata', LibraryController.middlewareNew.bind(this), LibraryController.getLibraryFilterData.bind(this))
this.router.get('/libraries/:id/search', LibraryController.middlewareNew.bind(this), LibraryController.search.bind(this)) this.router.get('/libraries/:id/search', LibraryController.middlewareNew.bind(this), LibraryController.search.bind(this))
this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this)) this.router.get('/libraries/:id/stats', LibraryController.middlewareNew.bind(this), LibraryController.stats.bind(this))
this.router.get('/libraries/:id/authors', LibraryController.middlewareNew.bind(this), LibraryController.getAuthors.bind(this)) this.router.get('/libraries/:id/authors', LibraryController.middlewareNew.bind(this), LibraryController.getAuthors.bind(this))
this.router.get('/libraries/:id/narrators', LibraryController.middlewareNew.bind(this), LibraryController.getNarrators.bind(this)) this.router.get('/libraries/:id/narrators', LibraryController.middlewareNew.bind(this), LibraryController.getNarrators.bind(this))
this.router.patch('/libraries/:id/narrators/:narratorId', LibraryController.middlewareNew.bind(this), LibraryController.updateNarrator.bind(this)) this.router.patch('/libraries/:id/narrators/:narratorId', LibraryController.middlewareNew.bind(this), LibraryController.updateNarrator.bind(this))

View File

@ -0,0 +1,69 @@
const Sequelize = require('sequelize')
const Logger = require('../../Logger')
const Database = require('../../Database')
module.exports = {
/**
* Get authors with count of num books
* @param {string} libraryId
* @returns {{id:string, name:string, count:number}}
*/
async getAuthorsWithCount(libraryId) {
const authors = await Database.authorModel.findAll({
where: [
{
libraryId
},
Sequelize.where(Sequelize.literal('count'), {
[Sequelize.Op.gt]: 0
})
],
attributes: [
'id',
'name',
[Sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 'count']
],
order: [
['count', 'DESC']
]
})
return authors.map(au => {
return {
id: au.id,
name: au.name,
count: au.dataValues.count
}
})
},
/**
* Search authors
* @param {string} libraryId
* @param {string} query
* @returns {object[]} oldAuthor with numBooks
*/
async search(libraryId, query) {
const authors = await Database.authorModel.findAll({
where: {
name: {
[Sequelize.Op.substring]: query
},
libraryId
},
attributes: {
include: [
[Sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 'numBooks']
]
},
limit,
offset
})
const authorMatches = []
for (const author of authors) {
const oldAuthor = author.getOldAuthor().toJSON()
oldAuthor.numBooks = author.dataValues.numBooks
authorMatches.push(oldAuthor)
}
return authorMatches
}
}

View File

@ -180,5 +180,41 @@ module.exports = {
} else { } else {
return libraryItemsPodcastFilters.search(oldUser, oldLibrary, query, limit, 0) return libraryItemsPodcastFilters.search(oldUser, oldLibrary, query, limit, 0)
} }
},
/**
* Get largest items in library
* @param {string} libraryId
* @param {number} limit
* @returns {Promise<{ id:string, title:string, size:number }[]>}
*/
async getLargestItems(libraryId, limit) {
const libraryItems = await Database.libraryItemModel.findAll({
attributes: ['id', 'mediaId', 'mediaType', 'size'],
where: {
libraryId
},
include: [
{
model: Database.bookModel,
attributes: ['id', 'title']
},
{
model: Database.podcastModel,
attributes: ['id', 'title']
}
],
order: [
['size', 'DESC']
],
limit
})
return libraryItems.map(libraryItem => {
return {
id: libraryItem.id,
title: libraryItem.media.title,
size: libraryItem.size
}
})
} }
} }

View File

@ -1,6 +1,7 @@
const Sequelize = require('sequelize') const Sequelize = require('sequelize')
const Database = require('../../Database') const Database = require('../../Database')
const Logger = require('../../Logger') const Logger = require('../../Logger')
const authorFilters = require('./authorFilters')
module.exports = { module.exports = {
/** /**
@ -1098,27 +1099,7 @@ module.exports = {
} }
// Search authors // Search authors
const authors = await Database.authorModel.findAll({ const authorMatches = await authorFilters.search(oldLibrary.id, query)
where: {
name: {
[Sequelize.Op.substring]: query
},
libraryId: oldLibrary.id
},
attributes: {
include: [
[Sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 'numBooks']
]
},
limit,
offset
})
const authorMatches = []
for (const author of authors) {
const oldAuthor = author.getOldAuthor().toJSON()
oldAuthor.numBooks = author.dataValues.numBooks
authorMatches.push(oldAuthor)
}
return { return {
book: itemMatches, book: itemMatches,
@ -1127,5 +1108,71 @@ module.exports = {
series: seriesMatches, series: seriesMatches,
authors: authorMatches authors: authorMatches
} }
},
/**
* Genres with num books
* @param {string} libraryId
* @returns {{genre:string, count:number}[]}
*/
async getGenresWithCount(libraryId) {
const genres = []
const [genreResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM books b, libraryItems li, json_each(b.genres) WHERE json_valid(b.genres) AND b.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value ORDER BY numItems DESC;`, {
replacements: {
libraryId
},
raw: true
})
for (const row of genreResults) {
genres.push({
genre: row.value,
count: row.numItems
})
}
return genres
},
/**
* Get stats for book library
* @param {string} libraryId
* @returns {Promise<{ totalSize:number, totalDuration:number, numAudioFiles:number, totalItems:number}>}
*/
async getBookLibraryStats(libraryId) {
const [statResults] = await Database.sequelize.query(`SELECT SUM(li.size) AS totalSize, SUM(b.duration) AS totalDuration, SUM(json_array_length(b.audioFiles)) AS numAudioFiles, COUNT(*) AS totalItems FROM libraryItems li, books b WHERE b.id = li.mediaId AND li.libraryId = :libraryId;`, {
replacements: {
libraryId
}
})
return statResults[0]
},
/**
* Get longest books in library
* @param {string} libraryId
* @param {number} limit
* @returns {Promise<{ id:string, title:string, duration:number }[]>}
*/
async getLongestBooks(libraryId, limit) {
const books = await Database.bookModel.findAll({
attributes: ['id', 'title', 'duration'],
include: {
model: Database.libraryItemModel,
attributes: ['id', 'libraryId'],
where: {
libraryId
}
},
order: [
['duration', 'DESC']
],
limit
})
return books.map(book => {
return {
id: book.libraryItem.id,
title: book.title,
duration: book.duration
}
})
} }
} }

View File

@ -455,5 +455,75 @@ module.exports = {
}) })
return episodeResults return episodeResults
},
/**
* Get stats for podcast library
* @param {string} libraryId
* @returns {Promise<{ totalSize:number, totalDuration:number, numAudioFiles:number, totalItems:number}>}
*/
async getPodcastLibraryStats(libraryId) {
const [statResults] = await Database.sequelize.query(`SELECT SUM(json_extract(pe.audioFile, '$.duration')) AS totalDuration, SUM(li.size) AS totalSize, COUNT(DISTINCT(li.id)) AS totalItems, COUNT(pe.id) AS numAudioFiles FROM libraryItems li, podcasts p LEFT OUTER JOIN podcastEpisodes pe ON pe.podcastId = p.id WHERE p.id = li.mediaId AND li.libraryId = :libraryId;`, {
replacements: {
libraryId
}
})
return statResults[0]
},
/**
* Genres with num podcasts
* @param {string} libraryId
* @returns {{genre:string, count:number}[]}
*/
async getGenresWithCount(libraryId) {
const genres = []
const [genreResults] = await Database.sequelize.query(`SELECT value, count(*) AS numItems FROM podcasts p, libraryItems li, json_each(p.genres) WHERE json_valid(p.genres) AND p.id = li.mediaId AND li.libraryId = :libraryId GROUP BY value ORDER BY numItems DESC;`, {
replacements: {
libraryId
},
raw: true
})
for (const row of genreResults) {
genres.push({
genre: row.value,
count: row.numItems
})
}
return genres
},
/**
* Get longest podcasts in library
* @param {string} libraryId
* @param {number} limit
* @returns {Promise<{ id:string, title:string, duration:number }[]>}
*/
async getLongestPodcasts(libraryId, limit) {
const podcasts = await Database.podcastModel.findAll({
attributes: [
'id',
'title',
[Sequelize.literal(`(SELECT SUM(json_extract(pe.audioFile, '$.duration')) FROM podcastEpisodes pe WHERE pe.podcastId = podcast.id)`), 'duration']
],
include: {
model: Database.libraryItemModel,
attributes: ['id', 'libraryId'],
where: {
libraryId
}
},
order: [
['duration', 'DESC']
],
limit
})
return podcasts.map(podcast => {
return {
id: podcast.libraryItem.id,
title: podcast.title,
duration: podcast.dataValues.duration
}
})
} }
} }