Files
website/src/lib/btop/hobbies.ts
T

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' }))
};
}