105 lines
4.1 KiB
TypeScript
105 lines
4.1 KiB
TypeScript
import type { Book, RatedTitle } from '#lib/content';
|
|
import type { AniListProfile } from '#lib/anilist/types';
|
|
import type { ListeningProfile } from '#lib/lastfm/types';
|
|
import type { AniListStats, BookStats, CountStat, ListeningStats, ScreenStats } from './types';
|
|
|
|
const recentDays = 30;
|
|
const topLimit = 5;
|
|
const statusColors: Record<string, string> = {
|
|
CURRENT: 'blue',
|
|
REPEATING: 'cyan',
|
|
COMPLETED: 'green',
|
|
PAUSED: 'yellow',
|
|
DROPPED: 'red',
|
|
PLANNING: 'magenta'
|
|
};
|
|
|
|
function average(values: number[]): string {
|
|
if (!values.length) return '-';
|
|
return `${(values.reduce((total, value) => total + value, 0) / values.length).toFixed(1)}/10`;
|
|
}
|
|
|
|
function ratingCounts(ratings: number[]): number[] {
|
|
const counts = Array<number>(11).fill(0);
|
|
ratings.forEach((rating) => counts[Math.round(rating)]++);
|
|
return counts;
|
|
}
|
|
|
|
function topCounts(values: string[], color: string): CountStat[] {
|
|
const counts = new Map<string, number>();
|
|
values.forEach((value) => counts.set(value, (counts.get(value) ?? 0) + 1));
|
|
return [...counts.entries()]
|
|
.sort((first, second) => second[1] - first[1])
|
|
.slice(0, topLimit)
|
|
.map(([label, count]) => ({ label, count, color }));
|
|
}
|
|
|
|
export function bookStats(books: Book[]): BookStats | null {
|
|
if (!books.length) return null;
|
|
const read = books.filter((book) => book.shelf === 'read');
|
|
const ratings = read.flatMap((book) => (book.rating === undefined ? [] : [book.rating]));
|
|
const seriesNames = new Set(books.flatMap((book) => (book.series ? [book.series.name] : [])));
|
|
const pagesRead = read.reduce((total, book) => total + (book.pages ?? 0), 0);
|
|
|
|
return {
|
|
shelves: [
|
|
{ label: 'read', count: read.length, color: 'green' },
|
|
{ label: 'reading', count: books.filter((book) => book.shelf === 'reading').length, color: 'blue' },
|
|
{ label: 'want to read', count: books.filter((book) => book.shelf === 'want-to-read').length, color: 'magenta' }
|
|
],
|
|
rows: [
|
|
{ label: 'Pages read', value: pagesRead.toLocaleString('en-US') },
|
|
{ label: 'Authors', value: `${new Set(read.flatMap((book) => book.authors)).size}` },
|
|
{ label: 'Series', value: `${seriesNames.size}` },
|
|
{ label: 'Avg rating', value: average(ratings) }
|
|
],
|
|
topGenres: topCounts(read.flatMap((book) => book.genres), 'cyan'),
|
|
ratingCounts: ratingCounts(ratings)
|
|
};
|
|
}
|
|
|
|
export function screenStats(titles: RatedTitle[]): ScreenStats {
|
|
const ratings = titles.flatMap((title) => (title.rating === undefined ? [] : [title.rating]));
|
|
const watching = titles.filter((title) => title.status === 'watching').map((title) => title.name);
|
|
const planningCount = titles.filter((title) => title.status === 'planning').length;
|
|
const groups = new Set(titles.flatMap((title) => (title.collection ? [title.collection] : [])));
|
|
|
|
return {
|
|
rows: [
|
|
{ label: 'Titles', value: `${titles.length}` },
|
|
{ label: 'Watching', value: `${watching.length}` },
|
|
{ label: 'Planning', value: `${planningCount}` },
|
|
{ label: 'Groups', value: `${groups.size}` },
|
|
{ label: 'Avg rating', value: average(ratings) }
|
|
],
|
|
ratingCounts: ratingCounts(ratings),
|
|
watching
|
|
};
|
|
}
|
|
|
|
export function listeningStats(profile: ListeningProfile): ListeningStats {
|
|
const artists = profile.top.month.artists.length ? profile.top.month.artists : profile.top.all.artists;
|
|
return {
|
|
profileUrl: profile.profileUrl,
|
|
rows: [
|
|
{ label: 'Scrobbles', value: profile.totals.scrobbles.toLocaleString('en-US') },
|
|
{ label: 'Artists', value: profile.totals.artists.toLocaleString('en-US') },
|
|
{ label: 'Per day', value: `${profile.averagePerDay}` },
|
|
{ label: 'Streak', value: `${profile.currentStreak}d (best ${profile.longestStreak}d)` }
|
|
],
|
|
daily: profile.daily.slice(-recentDays).map((bucket) => bucket.count),
|
|
hourly: profile.hourly.map((bucket) => bucket.count),
|
|
topArtists: artists.slice(0, topLimit).map((artist) => ({ label: artist.name, count: artist.count, color: 'red' }))
|
|
};
|
|
}
|
|
|
|
export function aniListStats(profile: AniListProfile): AniListStats {
|
|
return {
|
|
profileUrl: profile.listUrl,
|
|
rows: profile.stats,
|
|
statuses: profile.statusCounts
|
|
.filter((status) => status.count > 0)
|
|
.map((status) => ({ label: status.label, count: status.count, color: statusColors[status.status] ?? 'foreground' }))
|
|
};
|
|
}
|