67 lines
2.5 KiB
TypeScript
67 lines
2.5 KiB
TypeScript
|
|
import { FastifyInstance } from 'fastify'
|
||
|
|
import { requireAuth } from './authRoutes.js'
|
||
|
|
import { resolveRipStatusBatch } from '../ripstatus.js'
|
||
|
|
|
||
|
|
function countBy(values: string[]): { name: string; count: number }[] {
|
||
|
|
const map = new Map<string, number>()
|
||
|
|
for (const v of values) {
|
||
|
|
if (!v) continue
|
||
|
|
map.set(v, (map.get(v) ?? 0) + 1)
|
||
|
|
}
|
||
|
|
return [...map.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count)
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function registerStatsRoutes(app: FastifyInstance): Promise<void> {
|
||
|
|
app.get('/api/stats', { preHandler: [requireAuth] }, async (request) => {
|
||
|
|
const db = request.server.db
|
||
|
|
const userId = request.user!.id
|
||
|
|
const items = db
|
||
|
|
.prepare('SELECT id, title, artist, year, formats, genres, date_added, rip_override FROM collection_items WHERE user_id = ?')
|
||
|
|
.all(userId) as {
|
||
|
|
id: number
|
||
|
|
title: string
|
||
|
|
artist: string
|
||
|
|
year: number | null
|
||
|
|
formats: string
|
||
|
|
genres: string
|
||
|
|
date_added: string
|
||
|
|
rip_override: number | null
|
||
|
|
}[]
|
||
|
|
|
||
|
|
const statuses = resolveRipStatusBatch(db, userId, items)
|
||
|
|
let ripped = 0
|
||
|
|
const formats: string[] = []
|
||
|
|
const genres: string[] = []
|
||
|
|
const months = new Map<string, number>()
|
||
|
|
items.forEach((item, i) => {
|
||
|
|
if (statuses[i] === 'ripped') ripped++
|
||
|
|
for (const f of JSON.parse(item.formats) as string[]) formats.push(f)
|
||
|
|
for (const g of JSON.parse(item.genres) as string[]) genres.push(g)
|
||
|
|
const month = (item.date_added ?? '').slice(0, 7)
|
||
|
|
if (/^\d{4}-\d{2}$/.test(month)) months.set(month, (months.get(month) ?? 0) + 1)
|
||
|
|
})
|
||
|
|
|
||
|
|
const onLoan = (
|
||
|
|
db.prepare('SELECT COUNT(*) AS n FROM loans WHERE user_id = ? AND returned_at IS NULL').get(userId) as { n: number }
|
||
|
|
).n
|
||
|
|
|
||
|
|
const addedByMonth: { month: string; count: number }[] = []
|
||
|
|
const now = new Date()
|
||
|
|
for (let i = 11; i >= 0; i--) {
|
||
|
|
const d = new Date(now.getFullYear(), now.getMonth() - i, 1)
|
||
|
|
const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
|
||
|
|
addedByMonth.push({ month: key, count: months.get(key) ?? 0 })
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
totals: { items: items.length, ripped, notRipped: items.length - ripped, onLoan },
|
||
|
|
ripRatio: items.length === 0 ? 0 : ripped / items.length,
|
||
|
|
formats: countBy(formats),
|
||
|
|
decades: countBy(items.map((i) => (i.year ? `${Math.floor(i.year / 10) * 10}s` : ''))),
|
||
|
|
topGenres: countBy(genres).slice(0, 10),
|
||
|
|
topArtists: countBy(items.map((i) => i.artist)).slice(0, 10),
|
||
|
|
addedByMonth,
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|