New data model update stats page and routes, update users page

This commit is contained in:
advplyr 2022-03-13 17:33:50 -05:00
parent 4bdef893af
commit be1e1e7ba0
7 changed files with 62 additions and 62 deletions

View File

@ -5,7 +5,7 @@
<path fill="currentColor" d="M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z" /> <path fill="currentColor" d="M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z" />
</svg> </svg>
<div class="px-2"> <div class="px-2">
<p class="text-4xl md:text-5xl font-bold">{{ totalBooks }}</p> <p class="text-4xl md:text-5xl font-bold">{{ totalItems }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Books in Library</p> <p class="font-book text-xs md:text-sm text-white text-opacity-80">Books in Library</p>
</div> </div>
</div> </div>
@ -13,7 +13,7 @@
<div class="flex px-4"> <div class="flex px-4">
<span class="material-icons text-7xl">show_chart</span> <span class="material-icons text-7xl">show_chart</span>
<div class="px-1"> <div class="px-1">
<p class="text-4xl md:text-5xl font-bold">{{ totalAudiobookHours }}</p> <p class="text-4xl md:text-5xl font-bold">{{ totalHours }}</p>
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Overall Hours</p> <p class="font-book text-xs md:text-sm text-white text-opacity-80">Overall Hours</p>
</div> </div>
</div> </div>
@ -61,8 +61,8 @@ export default {
user() { user() {
return this.$store.state.user.user return this.$store.state.user.user
}, },
totalBooks() { totalItems() {
return this.libraryStats ? this.libraryStats.totalBooks : 0 return this.libraryStats ? this.libraryStats.totalItems : 0
}, },
totalAuthors() { totalAuthors() {
return this.libraryStats ? this.libraryStats.totalAuthors : 0 return this.libraryStats ? this.libraryStats.totalAuthors : 0
@ -70,11 +70,11 @@ export default {
numAudioTracks() { numAudioTracks() {
return this.libraryStats ? this.libraryStats.numAudioTracks : 0 return this.libraryStats ? this.libraryStats.numAudioTracks : 0
}, },
totalAudiobookDuration() { totalDuration() {
return this.libraryStats ? this.libraryStats.totalDuration : 0 return this.libraryStats ? this.libraryStats.totalDuration : 0
}, },
totalAudiobookHours() { totalHours() {
var totalHours = Math.round(this.totalAudiobookDuration / (60 * 60)) var totalHours = Math.round(this.totalDuration / (60 * 60))
return totalHours return totalHours
}, },
totalSizePretty() { totalSizePretty() {

View File

@ -11,12 +11,12 @@
<template v-for="genre in top5Genres"> <template v-for="genre in top5Genres">
<div :key="genre.genre" class="w-full py-2"> <div :key="genre.genre" class="w-full py-2">
<div class="flex items-end mb-1"> <div class="flex items-end mb-1">
<p class="text-2xl font-bold">{{ Math.round((100 * genre.count) / totalBooks) }}&nbsp;%</p> <p class="text-2xl font-bold">{{ Math.round((100 * genre.count) / totalItems) }}&nbsp;%</p>
<div class="flex-grow" /> <div class="flex-grow" />
<p class="text-base font-book text-white text-opacity-70">{{ genre.genre }}</p> <p class="text-base font-book text-white text-opacity-70">{{ genre.genre }}</p>
</div> </div>
<div class="w-full rounded-full h-3 bg-primary bg-opacity-50 overflow-hidden"> <div class="w-full rounded-full h-3 bg-primary bg-opacity-50 overflow-hidden">
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * genre.count) / totalBooks) + '%' }" /> <div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * genre.count) / totalItems) + '%' }" />
</div> </div>
</div> </div>
</template> </template>
@ -39,14 +39,14 @@
</template> </template>
</div> </div>
<div class="w-80 my-6 mx-auto"> <div class="w-80 my-6 mx-auto">
<h1 class="text-2xl mb-4 font-book">Longest Audiobooks (hrs)</h1> <h1 class="text-2xl mb-4 font-book">Longest Items (hrs)</h1>
<p v-if="!top10LongestAudiobooks.length">No Audiobooks</p> <p v-if="!top10LongestItems.length">No Items</p>
<template v-for="(ab, index) in top10LongestAudiobooks"> <template v-for="(ab, index) in top10LongestItems">
<div :key="index" class="w-full py-2"> <div :key="index" class="w-full py-2">
<div class="flex items-center mb-1"> <div class="flex items-center mb-1">
<p class="text-sm font-book text-white text-opacity-70 w-44 pr-2 truncate">{{ index + 1 }}.&nbsp;&nbsp;&nbsp;&nbsp;{{ ab.title }}</p> <p class="text-sm font-book text-white text-opacity-70 w-44 pr-2 truncate">{{ index + 1 }}.&nbsp;&nbsp;&nbsp;&nbsp;{{ ab.title }}</p>
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden"> <div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * ab.duration) / longestAudiobookDuration) + '%' }" /> <div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * ab.duration) / longestItemDuration) + '%' }" />
</div> </div>
<div class="w-4 ml-3"> <div class="w-4 ml-3">
<p class="text-sm font-bold">{{ (ab.duration / 3600).toFixed(1) }}</p> <p class="text-sm font-bold">{{ (ab.duration / 3600).toFixed(1) }}</p>
@ -77,8 +77,8 @@ export default {
user() { user() {
return this.$store.state.user.user return this.$store.state.user.user
}, },
totalBooks() { totalItems() {
return this.libraryStats ? this.libraryStats.totalBooks : 0 return this.libraryStats ? this.libraryStats.totalItems : 0
}, },
genresWithCount() { genresWithCount() {
return this.libraryStats ? this.libraryStats.genresWithCount : [] return this.libraryStats ? this.libraryStats.genresWithCount : []
@ -86,12 +86,12 @@ export default {
top5Genres() { top5Genres() {
return this.genresWithCount.slice(0, 5) return this.genresWithCount.slice(0, 5)
}, },
top10LongestAudiobooks() { top10LongestItems() {
return this.libraryStats ? this.libraryStats.longestAudiobooks || [] : [] return this.libraryStats ? this.libraryStats.longestItems || [] : []
}, },
longestAudiobookDuration() { longestItemDuration() {
if (!this.top10LongestAudiobooks.length) return 0 if (!this.top10LongestItems.length) return 0
return this.top10LongestAudiobooks[0].duration return this.top10LongestItems[0].duration
}, },
authorsWithCount() { authorsWithCount() {
return this.libraryStats ? this.libraryStats.authorsWithCount : [] return this.libraryStats ? this.libraryStats.authorsWithCount : []

View File

@ -59,8 +59,8 @@
export default { export default {
data() { data() {
return { return {
listeningStats: null, listeningStats: null
libraryStats: null // libraryStats: null
} }
}, },
watch: { watch: {
@ -103,11 +103,11 @@ export default {
}, },
methods: { methods: {
async init() { async init() {
this.libraryStats = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/stats`).catch((err) => { // this.libraryStats = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/stats`).catch((err) => {
console.error('Failed to get library stats', err) // console.error('Failed to get library stats', err)
var errorMsg = err.response ? err.response.data || 'Unknown Error' : 'Unknown Error' // var errorMsg = err.response ? err.response.data || 'Unknown Error' : 'Unknown Error'
this.$toast.error(`Failed to get library stats: ${errorMsg}`) // this.$toast.error(`Failed to get library stats: ${errorMsg}`)
}) // })
this.listeningStats = await this.$axios.$get(`/api/me/listening-stats`).catch((err) => { this.listeningStats = await this.$axios.$get(`/api/me/listening-stats`).catch((err) => {
console.error('Failed to load listening sesions', err) console.error('Failed to load listening sesions', err)
return [] return []

View File

@ -43,11 +43,11 @@
</tr> </tr>
<tr v-for="ab in userAudiobooks" :key="ab.audiobookId" :class="!ab.isRead ? '' : 'isRead'"> <tr v-for="ab in userAudiobooks" :key="ab.audiobookId" :class="!ab.isRead ? '' : 'isRead'">
<td> <td>
<covers-book-cover :width="50" :audiobook="ab" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-book-cover :width="50" :library-item="ab" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</td> </td>
<td class="font-book"> <td class="font-book">
<p>{{ ab.book ? ab.book.title : ab.audiobookTitle || 'Unknown' }}</p> <p>{{ ab.media && ab.media.metadata ? ab.media.metadata.title : ab.audiobookTitle || 'Unknown' }}</p>
<p v-if="ab.book && ab.book.author" class="text-white text-opacity-50 text-sm font-sans">by {{ ab.book.author }}</p> <p v-if="ab.media && ab.media.metadata && ab.media.metadata.authorName" class="text-white text-opacity-50 text-sm font-sans">by {{ ab.media.metadata.authorName }}</p>
</td> </td>
<td class="text-center">{{ Math.floor(ab.progress * 100) }}%</td> <td class="text-center">{{ Math.floor(ab.progress * 100) }}%</td>
<td class="text-center hidden sm:table-cell"> <td class="text-center hidden sm:table-cell">

View File

@ -370,11 +370,11 @@ class ApiController {
// User audiobook progress attach book details // User audiobook progress attach book details
if (json.audiobooks && Object.keys(json.audiobooks).length) { if (json.audiobooks && Object.keys(json.audiobooks).length) {
for (const audiobookId in json.audiobooks) { for (const audiobookId in json.audiobooks) {
var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId) var libraryItem = this.db.libraryItems.find(li => li.id === audiobookId)
if (!audiobook) { if (!libraryItem) {
Logger.error('[ApiController] Audiobook not found for users progress ' + audiobookId) Logger.error('[ApiController] Library item not found for users progress ' + audiobookId)
} else { } else {
json.audiobooks[audiobookId].book = audiobook.book.toJSON() json.audiobooks[audiobookId].media = libraryItem.media.toJSONExpanded()
} }
} }
} }

View File

@ -411,19 +411,19 @@ class LibraryController {
} }
async stats(req, res) { async stats(req, res) {
var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id) var libraryItems = req.libraryItems
var authorsWithCount = libraryHelpers.getAuthorsWithCount(audiobooksInLibrary) var authorsWithCount = libraryHelpers.getAuthorsWithCount(libraryItems)
var genresWithCount = libraryHelpers.getGenresWithCount(audiobooksInLibrary) var genresWithCount = libraryHelpers.getGenresWithCount(libraryItems)
var abDurationStats = libraryHelpers.getAudiobookDurationStats(audiobooksInLibrary) var durationStats = libraryHelpers.getItemDurationStats(libraryItems)
var stats = { var stats = {
totalBooks: audiobooksInLibrary.length, totalItems: libraryItems.length,
totalAuthors: Object.keys(authorsWithCount).length, totalAuthors: Object.keys(authorsWithCount).length,
totalGenres: Object.keys(genresWithCount).length, totalGenres: Object.keys(genresWithCount).length,
totalDuration: abDurationStats.totalDuration, totalDuration: durationStats.totalDuration,
longestAudiobooks: abDurationStats.longstAudiobooks, longestItems: durationStats.longestItems,
numAudioTracks: abDurationStats.numAudioTracks, numAudioTracks: durationStats.numAudioTracks,
totalSize: libraryHelpers.getAudiobooksTotalSize(audiobooksInLibrary), totalSize: libraryHelpers.getLibraryItemsTotalSize(libraryItems),
authorsWithCount, authorsWithCount,
genresWithCount genresWithCount
} }

View File

@ -244,10 +244,10 @@ module.exports = {
return seriesSortedByAddedAt.slice(0, limit) return seriesSortedByAddedAt.slice(0, limit)
}, },
getGenresWithCount(audiobooks) { getGenresWithCount(libraryItems) {
var genresMap = {} var genresMap = {}
audiobooks.forEach((ab) => { libraryItems.forEach((li) => {
var genres = ab.book.genres || [] var genres = li.media.metadata.genres || []
genres.forEach((genre) => { genres.forEach((genre) => {
if (genresMap[genre]) genresMap[genre].count++ if (genresMap[genre]) genresMap[genre].count++
else else
@ -260,15 +260,15 @@ module.exports = {
return Object.values(genresMap).sort((a, b) => b.count - a.count) return Object.values(genresMap).sort((a, b) => b.count - a.count)
}, },
getAuthorsWithCount(audiobooks) { getAuthorsWithCount(libraryItems) {
var authorsMap = {} var authorsMap = {}
audiobooks.forEach((ab) => { libraryItems.forEach((li) => {
var authors = ab.book.authorFL ? ab.book.authorFL.split(', ') : [] var authors = li.media.metadata.authors || []
authors.forEach((author) => { authors.forEach((author) => {
if (authorsMap[author]) authorsMap[author].count++ if (authorsMap[author.id]) authorsMap[author.id].count++
else else
authorsMap[author] = { authorsMap[author.id] = {
author, author: author.name,
count: 1 count: 1
} }
}) })
@ -276,26 +276,26 @@ module.exports = {
return Object.values(authorsMap).sort((a, b) => b.count - a.count) return Object.values(authorsMap).sort((a, b) => b.count - a.count)
}, },
getAudiobookDurationStats(audiobooks) { getItemDurationStats(libraryItems) {
var sorted = sort(audiobooks).desc(a => a.duration) var sorted = sort(libraryItems).desc(li => li.media.duration)
var top10 = sorted.slice(0, 10).map(ab => ({ title: ab.book.title, duration: ab.duration })).filter(ab => ab.duration > 0) var top10 = sorted.slice(0, 10).map(li => ({ title: li.media.metadata.title, duration: li.media.duration })).filter(i => i.duration > 0)
var totalDuration = 0 var totalDuration = 0
var numAudioTracks = 0 var numAudioTracks = 0
audiobooks.forEach((ab) => { libraryItems.forEach((li) => {
totalDuration += ab.duration totalDuration += li.media.duration
numAudioTracks += ab.tracks.length numAudioTracks += (li.media.tracks || []).length
}) })
return { return {
totalDuration, totalDuration,
numAudioTracks, numAudioTracks,
longstAudiobooks: top10 longestItems: top10
} }
}, },
getAudiobooksTotalSize(audiobooks) { getLibraryItemsTotalSize(libraryItems) {
var totalSize = 0 var totalSize = 0
audiobooks.forEach((ab) => { libraryItems.forEach((li) => {
totalSize += ab.size totalSize += li.media.size
}) })
return totalSize return totalSize
}, },