mp3-com-meta-browser/AGENTS.md

289 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This repository contains a static web app for searching a large music metadata database (no server/API calls). It uses:
- Bulma (`bulma.min.css`) for UI (https://bulma.io/documentation/)
- A single HTML file (`index.html`)
- A single JS file (`script.js`)
- A small site stylesheet (`site.css`) for projectspecific tweaks (no frameworks)
- A client-side SQLite database (WASM) fetched as a static asset (paged with HTTP Range if available)
Keep implementation details minimal; this file orients agents to project structure, constraints, and the data model.
---
## Goals (for agents)
- Provide a responsive search UI over a large SQLite metadata DB entirely in-browser.
- No server-side code; all queries run client-side.
- Fast startup, predictable UX, minimal dependencies.
## Non-Goals
- Hosting or streaming audio files.
- Write access to the DB.
- Complex build systems or frameworks.
---
## Repository Layout (expected)
/
- index.html # Single-page app shell
- bulma.min.css # Bulma CSS (pinned)
- site.css # Site-specific styles (tiny utilities like transitions)
- script.js # All app logic
- /assets/
- mp3com-meta.sqlite # Metadata DB (read-only)
- AGENTS.md
- If subfolders are added later (icons, fonts), prefer `/assets/...`.
- Do not introduce bundlers unless requested.
---
## UI/UX Conventions
- Use Bulma components (containers, navbar, form controls, tables, pagination).
- Default to semantic HTML + Bulma classes; avoid inline styles.
- Place any site-specific CSS (e.g., small transitions like fade utilities) in `site.css`.
- Accessibility: ensure focus states, labelinput associations, and ARIA for dynamic content.
---
## Data Access (Client-Side)
- Use SQLite compiled to WebAssembly in the browser.
- Prefer an HTTP VFS (range requests) to avoid fetching the entire DB up front.
- Queries should be read-only; no schema migrations/run-time writes.
Minimal runtime expectations
- One network fetch for the WASM runtime.
- The DB file (`/assets/mp3com-meta.sqlite`) fetched lazily by page(s) as needed.
---
## Database Schema (authoritative)
PRAGMA foreign_keys=ON;
CREATE TABLE IF NOT EXISTS artists(
id INTEGER PRIMARY KEY,
name TEXT NOT NULL COLLATE NOCASE UNIQUE
);
CREATE TABLE IF NOT EXISTS albums(
id INTEGER PRIMARY KEY,
artist_id INTEGER NOT NULL REFERENCES artists(id),
title TEXT COLLATE NOCASE,
year INTEGER,
UNIQUE(artist_id, title, year)
);
CREATE TABLE IF NOT EXISTS tracks(
id INTEGER PRIMARY KEY,
artist_id INTEGER NOT NULL REFERENCES artists(id),
album_id INTEGER REFERENCES albums(id),
title TEXT NOT NULL COLLATE NOCASE,
track_no INTEGER,
year INTEGER,
genre TEXT,
duration_sec INTEGER,
bitrate_kbps INTEGER,
samplerate_hz INTEGER,
channels INTEGER,
filesize_bytes INTEGER,
sha1 TEXT, -- optional; may be NULL
relpath TEXT NOT NULL
);
-- External-content FTS5 (search over key text fields)
CREATE VIRTUAL TABLE IF NOT EXISTS fts_tracks USING fts5(
title, artist, album, genre,
content='tracks', content_rowid='id',
tokenize = 'unicode61 remove_diacritics 2',
prefix='2 3 4'
);
-- Keep FTS index in sync
CREATE TRIGGER IF NOT EXISTS tracks_ai AFTER INSERT ON tracks BEGIN
INSERT INTO fts_tracks(rowid,title,artist,album,genre)
VALUES (new.id,
new.title,
(SELECT name FROM artists WHERE id=new.artist_id),
(SELECT title FROM albums WHERE id=new.album_id),
new.genre);
END;
CREATE TRIGGER IF NOT EXISTS tracks_ad AFTER DELETE ON tracks BEGIN
INSERT INTO fts_tracks(fts_tracks, rowid) VALUES('delete', old.id);
END;
CREATE TRIGGER IF NOT EXISTS tracks_au AFTER UPDATE ON tracks BEGIN
INSERT INTO fts_tracks(fts_tracks, rowid) VALUES('delete', old.id);
INSERT INTO fts_tracks(rowid,title,artist,album,genre)
VALUES (new.id,
new.title,
(SELECT name FROM artists WHERE id=new.artist_id),
(SELECT title FROM albums WHERE id=new.album_id),
new.genre);
END;
CREATE INDEX IF NOT EXISTS idx_tracks_artist_id ON tracks(artist_id);
CREATE INDEX IF NOT EXISTS idx_tracks_album_id ON tracks(album_id);
CREATE INDEX IF NOT EXISTS idx_tracks_year ON tracks(year);
Notes
- Text search should use `fts_tracks MATCH ?` and join back to `tracks` for details.
- Sorting for display can use `ORDER BY rank` (FTS) or `artist,title,year`.
---
## Example Queries (for agents)
Free text:
SELECT t.id, a.name AS artist, t.title, IFNULL(al.title,'') AS album, t.year, t.genre
FROM fts_tracks f
JOIN tracks t ON t.id=f.rowid
JOIN artists a ON a.id=t.artist_id
LEFT JOIN albums al ON al.id=t.album_id
WHERE f MATCH ? -- e.g., 'queen "bohemian rhapsody"'
ORDER BY rank LIMIT 50;
By artist prefix (fast via FTS `prefix`):
WHERE f MATCH 'artist:beatl*'
Count by year:
SELECT year, COUNT(*) FROM tracks GROUP BY year ORDER BY year;
---
## Local Development
- Serve statically (to enable range requests and avoid file:// issues):
- Python: `python3 -m http.server 8000`
- Node: `npx http-server -p 8000`
- Open `http://localhost:8000/`
- Ensure the server sends `Accept-Ranges: bytes` for `/assets/mp3com-meta.sqlite`.
---
## Performance Guidance
- Keep initial DOM minimal; render results incrementally (virtualized list if needed).
- Debounce search input (e.g., 200300 ms).
- Use `LIMIT`/pagination; avoid `SELECT *` on large result sets.
- Cache prepared statements in JS if the WASM wrapper allows.
---
## Quality Bar
- No console errors.
- Basic keyboard navigation works.
- Layout adapts from mobile → desktop via Bulma columns.
- Reasonable query latency for common searches (<300ms after warm-up).
---
## Security & Privacy
- No third-party trackers.
- Only static file loads; no credentials.
- If adding analytics, prefer privacy-preserving, self-hosted options and document them.
---
## Agent Etiquette
- Do not introduce new build steps or frameworks unless explicitly asked.
- Keep diffs small and focused.
- When editing `index.html`/`script.js`, include inline comments explaining assumptions.
- Verify changes against the schema above.
---
## TODOs / UX Elements Roadmap
Overview
- Views are "base" (replace main content) or "overlay" (stacked dialog) via `UX.replace(...)` and `UX.openOverlay(...)` in `script.js`.
- Each view pairs an HTML template in `index.html` (e.g., `tpl-...`) with a creator in `script.js` (e.g., `create...View(db)`), returning `{ kind, el, onShow?, destroy? }`.
- Use Bulma form/table/pagination patterns. Keep DOM small; paginate and debounce queries.
Shared Tasks
- [x] Add small table/list renderer util in `script.js` to build rows safely (uses `escapeHtml`). Implemented via `createTableRenderer`.
- [x] Add shared pagination component (Prev/Next, page size select). Propagate `LIMIT/OFFSET`. Implemented via `createPagination`.
- [x] Add common keyboard handlers: Enter to open selection; Esc to close overlays (already wired globally). Implemented via shared `Keyboard` helper in `script.js`.
- [x] Add loading/empty-state helpers for lists. Implemented via `createAsyncListState` utility.
Primary Navigation (Hub)
- [x] `tpl-nav` (base): Landing hub to choose "Search", "Browse Artists", "Browse Albums", "Browse Years", "Browse Genres", "Stats".
- [x] `createNavView(db)`: Buttons/cards trigger `UX.replace(...)` to corresponding base views.
- [x] Accessibility: initial focus on first action; arrow-key navigation across items; visible focus states.
Search (Existing)
- [x] `tpl-search` (base): Input is focusable; shows results area.
- [x] Implement query execution with FTS join; debounce 250 ms; paginate results. Wired into the new table + pagination helpers.
- SQL: `SELECT t.id, a.name AS artist, t.title, IFNULL(al.title,'') AS album, t.year, t.genre FROM fts_tracks f JOIN tracks t ON t.id=f.rowid JOIN artists a ON a.id=t.artist_id LEFT JOIN albums al ON al.id=t.album_id WHERE f MATCH ? ORDER BY rank LIMIT ? OFFSET ?`.
- [x] Column sorts (toggle rank vs artist,title,year).
- [x] Row activation opens Track overlay.
Browse Artists
- [x] `tpl-browse-artists` (base): Alphabetical list, AZ quick jump, mini filter box; paginated.
- [x] `createBrowseArtistsView(db)`: Loads pages; clicking row opens Artist overlay.
- SQL: `SELECT id, name FROM artists ORDER BY name LIMIT ? OFFSET ?`.
- [x] Optional prefix search using FTS prefix (`WHERE f MATCH 'artist:abc*'`) to accelerate filter. Browse Artists view now favors FTS when filters include 2 characters.
Browse Albums
- [x] `tpl-browse-albums` (base): List or grid with title, artist badge, year; paginated and sortable by artist/year/title.
- [x] `createBrowseAlbumsView(db)`: Clicking item opens Album overlay.
- SQL: `SELECT al.id, al.title, al.year, a.name AS artist FROM albums al JOIN artists a ON a.id=al.artist_id ORDER BY a.name, al.year, al.title LIMIT ? OFFSET ?`.
Browse Years
- [x] `tpl-browse-years` (base): Year histogram (counts) with list of tracks/albums when a year is selected; paginated.
- [x] `createBrowseYearsView(db)`: Selecting a year shows tracks; rows open Album/Track overlays.
- SQL (counts): `SELECT year, COUNT(*) AS cnt FROM tracks WHERE year IS NOT NULL GROUP BY year ORDER BY year`.
- SQL (tracks by year): `SELECT t.id, t.title, a.name AS artist, IFNULL(al.title,'') AS album, t.genre FROM tracks t JOIN artists a ON a.id=t.artist_id LEFT JOIN albums al ON al.id=t.album_id WHERE t.year=? ORDER BY a.name, t.title LIMIT ? OFFSET ?`.
Browse Genres
- [x] `tpl-browse-genres` (base): Genre chips with counts selecting shows paginated tracks.
- [x] `createBrowseGenresView(db)`: Genre list with counts; selecting lists tracks; rows open Track overlay.
- SQL (counts): `SELECT genre, COUNT(*) AS cnt FROM tracks WHERE genre IS NOT NULL AND genre!='' GROUP BY genre ORDER BY cnt DESC, genre`.
- SQL (tracks by genre): `SELECT t.id, t.title, a.name AS artist, IFNULL(al.title,'') AS album, t.year FROM tracks t JOIN artists a ON a.id=t.artist_id LEFT JOIN albums al ON al.id=t.album_id WHERE t.genre=? ORDER BY a.name, t.title LIMIT ? OFFSET ?`.
Stats
- [x] `tpl-stats` (base): Lightweight metrics (totals, top artists, year distribution) linking into browse views.
- [x] `createStatsView(db)`: Render summary cards; links navigate via `UX.replace(...)` with preselected filters.
- SQL (examples): totals from `COUNT(*)` on artists/albums/tracks; top artists via `SELECT a.name, COUNT(*) cnt FROM tracks t JOIN artists a ON a.id=t.artist_id GROUP BY a.id ORDER BY cnt DESC LIMIT 20`.
Artist Overlay
- [x] `tpl-artist` (overlay): Header: name + counts; tabs: Albums | Top Tracks.
- [x] `createArtistOverlay(db, artistId)`: Load artist name, counts, then tab content.
- SQL (albums): `SELECT id, title, year FROM albums WHERE artist_id=? ORDER BY year, title`.
- SQL (top tracks): `SELECT id, title, year, genre FROM tracks WHERE artist_id=? ORDER BY year, title LIMIT 100`.
- [x] Actions: clicking album opens Album overlay; clicking track opens Track overlay.
Album Overlay
- [x] `tpl-album` (overlay): Header with album title, artist, year; tracklist table with `track_no`, `title`, `duration_sec`, `bitrate_kbps`.
- [x] `createAlbumOverlay(db, albumId)`: Load album+artist header; then tracklist.
- SQL (header): `SELECT al.title, al.year, a.name AS artist FROM albums al JOIN artists a ON a.id=al.artist_id WHERE al.id=?`.
- SQL (tracks): `SELECT id, track_no, title, duration_sec, bitrate_kbps FROM tracks WHERE album_id=? ORDER BY track_no, title`.
- [x] Row activation opens Track overlay.
Track Overlay
- [x] `tpl-track` (overlay): Show title, artist, album, year, genre, duration, bitrate, samplerate, channels, filesize, sha1 (if present), and `relpath` with a Copy button.
- [x] `createTrackOverlay(db, trackId)`: Load detail from join; add Copy action for `relpath`.
- SQL: `SELECT t.*, a.name AS artist, al.title AS album FROM tracks t JOIN artists a ON a.id=t.artist_id LEFT JOIN albums al ON al.id=t.album_id WHERE t.id=?`.
Filters (Optional)
- [ ] `tpl-filters` (overlay): Advanced filters (year range, min bitrate, genre multi-select) applied to current base view.
- [ ] `createFiltersOverlay(db, onApply)`: Applies constraints and refreshes the invoking view.
Help/Meta Overlays
- [ ] `tpl-keyboard-shortcuts` (overlay): " / focus search", "Esc close overlay", "j/k navigate" if list navigation is added.
- [ ] `tpl-about` (overlay): About/help, privacy note.
- [ ] `tpl-error` (overlay): Friendly error with retry; used by views on failure.
Implementation Notes
- Use template IDs in `index.html` and instantiate via existing `instantiateTemplate` helper.
- Overlays must add `ux-view--overlay` class (done by UX manager) and include a close button that calls `UX.closeTop()`.
- Keep queries read-only; always `LIMIT ? OFFSET ?` for lists; avoid `SELECT *` except for single-row detail.
- Respect accessibility: labelinput associations, `aria-live` only for async status, focus returned to opener on overlay close.
- Performance: debounce search 200300 ms; cache prepared statements if beneficial; do not pre-render large lists.
- UX manager now guards against race conditions during fades: `UX.replace` updates `currentBase` before the fade-in completes and checks that the previous element is still connected before trying to animate it out.