Skip to content

ourdorm/charts

Repository files navigation

Dorm Charts

A Billboard-style music chart app that pools listening data from Last.FM and ListenBrainz accounts across a group of users. Think of it as a living "what is our dorm/friend group listening to?" leaderboard, rendered in a terminal aesthetic.

Tech Stack

Layer Technology
Frontend Angular 21+ (standalone components, signals)
Backend Node.js + TypeScript + Express
Database PostgreSQL via Prisma ORM
UI Library @webtui/css + Catppuccin theme
Auth JWT + bcrypt
External APIs Last.FM REST API, ListenBrainz API v1
Package Manager pnpm (workspace monorepo)

Prerequisites

Quick Start

1. Start the database

docker compose up -d

2. Configure environment

cp server/.env.example server/.env

Open server/.env and fill in your values:

LASTFM_API_KEY=your_key_here
LASTFM_API_SECRET=your_secret_here
JWT_SECRET=a-long-random-string-at-least-32-chars

3. Install dependencies

pnpm install

4. Run database migrations

pnpm --filter server db:migrate

5. Start development servers

Instead of running the client and server in separate terminals, you can start both simultaneously from the root:

pnpm run dev

This will start:

Linking Music Accounts

Last.FM

  1. Register/log in at /register
  2. Go to /dashboard and click Link Last.FM
  3. Authorize on Last.FM's site — you'll be redirected back automatically
  4. The app immediately starts an initial scrobble sync in the background

ListenBrainz

  1. Copy your user token from https://listenbrainz.org/profile/
  2. Go to /dashboard, paste the token, and click Link ListenBrainz

How Charts Work

Charts are generated from the aggregated scrobble history of all registered users:

  • Weekly — plays in the last 7 days
  • Monthly — plays in the last 30 days
  • All Time — all plays ever synced

Each entry tracks rank, movement (↑ / ↓ / NEW / —), peak rank, and weeks on chart — just like a real Billboard chart.

Charts are cached for 1 hour and regenerated daily by a background cron job. You can also force a regeneration from the chart view.

Development

# Install all workspace deps
pnpm install

# Backend watch mode
pnpm --filter server dev

# Frontend dev server
pnpm --filter client start

# Open Prisma Studio (visual DB browser)
pnpm --filter server dlx prisma studio

# Re-run after a schema change
pnpm --filter server db:migrate

# Reset the database (⚠️ destroys all data)
pnpm --filter server db:reset

Project Structure

dorm-charts/
├── client/               # Angular 21 frontend
│   └── src/app/
│       ├── core/         # Auth service, API client, guards, interceptors
│       ├── features/     # Page-level components (charts, dashboard, auth, profile)
│       └── shared/       # Nav and other reusable components
│
├── server/               # Express + TypeScript backend
│   └── src/
│       ├── config/       # Env validation (zod)
│       ├── db/           # Prisma client singleton
│       ├── jobs/         # Cron scheduler
│       ├── lib/          # Shared axios instance with retry logic
│       ├── middleware/   # JWT auth, error handler
│       └── modules/
│           ├── auth/     # Register, login, /me
│           ├── charts/   # Chart generation engine + REST endpoints
│           ├── sync/     # Last.FM & ListenBrainz sync services
│           └── users/    # Public profiles, linked account management
│
├── shared/               # TypeScript types/DTOs shared between client & server
└── docker-compose.yml    # PostgreSQL container

Project Architecture

This project is a monorepo managed with pnpm workspaces. It consists of three primary packages:

  1. client: An Angular 21+ web application. It uses Signals for state management and Standalone Components by default.
  2. server: A Node.js / Express backend using Prisma ORM for database access.
  3. shared: A lightweight package containing common TypeScript interfaces and DTOs used by both the frontend and backend. This ensures type safety across the entire stack.

Shared Types Structure

The shared package is organized by domain to improve maintainability:

  • auth.types.ts: Authentication and period definitions.
  • user.types.ts: User profiles and linked account data.
  • track.types.ts: Music track metadata.
  • chart.types.ts: Chart result and history structures.
  • sync.types.ts: Background synchronization status.
  • invite.types.ts: Invitation management types.

API Reference

All endpoints are prefixed with /api. Authenticated routes require Authorization: Bearer <token>.

Auth

Method Path Auth Description
POST /auth/register Create account; returns JWT + user
POST /auth/login Sign in; returns JWT + user
GET /auth/me Get current user with linked accounts

Accounts

Method Path Description
GET /accounts/lastfm/auth Get Last.FM OAuth redirect URL
POST /accounts/lastfm Exchange OAuth token for session key and link account
DELETE /accounts/lastfm Unlink Last.FM
POST /accounts/listenbrainz Validate and link a ListenBrainz token
DELETE /accounts/listenbrainz Unlink ListenBrainz
POST /accounts/:id/sync Manually trigger a sync for a linked account

Charts

Method Path Description
GET /charts?period=WEEKLY Fetch current chart (WEEKLY | MONTHLY | ALLTIME)
POST /charts/generate?period=WEEKLY Force-regenerate chart
GET /charts/history/:trackId Chart history for a specific track

Users

Method Path Description
GET /users/:username Public profile with top 10 all-time tracks
PATCH /users/me Update username

Environment Variables

# Database
DATABASE_URL=postgresql://dormcharts:dormcharts@localhost:5432/dormcharts

# Server
PORT=3000

# JWT
JWT_SECRET=change-me-to-a-long-random-string
JWT_EXPIRES_IN=7d

# Last.FM (required — https://www.last.fm/api/account/create)
LASTFM_API_KEY=
LASTFM_API_SECRET=
LASTFM_CALLBACK_URL=http://localhost:4200/auth/lastfm/callback

# ListenBrainz
LISTENBRAINZ_BASE_URL=https://api.listenbrainz.org

# Background jobs
SYNC_INTERVAL_MINUTES=60        # How often to auto-sync accounts
CHART_REGEN_CRON=0 3 * * *     # When to regenerate charts (default: 3 AM daily)

CLOUDFLARE_R2_ACCOUNT_ID=
CLOUDFLARE_R2_ACCESS_KEY_ID=
CLOUDFLARE_R2_SECRET_ACCESS_KEY=
CLOUDFLARE_R2_BUCKET_NAME=
CLOUDFLARE_R2_PUBLIC_URL=

Troubleshooting

"Invalid environment variables" on server start:

  • Make sure server/.env exists and all required keys are filled in. Copy from server/.env.example.

Last.FM OAuth callback fails:

  • Confirm LASTFM_CALLBACK_URL in .env exactly matches the callback URL registered in your Last.FM API account settings.

Charts show no data:

  • Scrobble data must be synced first. Link an account on /dashboard, wait for the initial sync, then use Generate Chart Now or wait for the cron to run.

ListenBrainz token invalid:

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors