mp3-com-meta-browser/AGENTS.md

13 KiB
Raw Blame History

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

  • Add small table/list renderer util in script.js to build rows safely (uses escapeHtml). Implemented via createTableRenderer.
  • Add shared pagination component (Prev/Next, page size select). Propagate LIMIT/OFFSET. Implemented via createPagination.
  • Add common keyboard handlers: Enter to open selection; Esc to close overlays (already wired globally). Implemented via shared Keyboard helper in script.js.
  • Add loading/empty-state helpers for lists. Implemented via createAsyncListState utility.

Primary Navigation (Hub)

  • tpl-nav (base): Landing hub to choose "Search", "Browse Artists", "Browse Albums", "Browse Years", "Browse Genres", "Stats".
  • createNavView(db): Buttons/cards trigger UX.replace(...) to corresponding base views.
  • Accessibility: initial focus on first action; arrow-key navigation across items; visible focus states.

Search (Existing)

  • tpl-search (base): Input is focusable; shows results area.
  • 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 ?.
  • Column sorts (toggle rank vs artist,title,year).
  • Row activation opens Track overlay.

Browse Artists

  • tpl-browse-artists (base): Alphabetical list, AZ quick jump, mini filter box; paginated.
  • createBrowseArtistsView(db): Loads pages; clicking row opens Artist overlay.
  • SQL: SELECT id, name FROM artists ORDER BY name LIMIT ? OFFSET ?.
  • 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

  • tpl-browse-albums (base): List or grid with title, artist badge, year; paginated and sortable by artist/year/title.
  • 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

  • tpl-browse-years (base): Year histogram (counts) with list of tracks/albums when a year is selected; paginated.
  • 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

  • tpl-browse-genres (base): Genre chips with counts → selecting shows paginated tracks.
  • 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

  • tpl-stats (base): Lightweight metrics (totals, top artists, year distribution) linking into browse views.
  • 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

  • tpl-artist (overlay): Header: name + counts; tabs: Albums | Top Tracks.
  • 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.
  • Actions: clicking album opens Album overlay; clicking track opens Track overlay.

Album Overlay

  • tpl-album (overlay): Header with album title, artist, year; tracklist table with track_no, title, duration_sec, bitrate_kbps.
  • 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.
  • Row activation opens Track overlay.

Track Overlay

  • tpl-track (overlay): Show title, artist, album, year, genre, duration, bitrate, samplerate, channels, filesize, sha1 (if present), and relpath with a Copy button.
  • 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.