diff --git a/CLAUDE.md b/CLAUDE.md index 115ac5f..d3b5942 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,22 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co YellowJacket is a cross-platform desktop music player built with Go (backend) and TypeScript/Lit (frontend), using the Wails framework to bridge them. It supports MP3, FLAC, OGG Vorbis, and WAV playback. +**The three prose documents are split by reader, not by topic** (#50). +`README.md` is the landing page and answers *a user's* questions only — +what it does, which channel installs it on which platform, where its +data lives — with three screenshots in `docs/images/`, captured from the +fixture library (`make sandbox-seed NAME=default` → `make dev-headless +SEED=default`) so they can be retaken by anyone. `CONTRIBUTING.md` holds +what used to be the second half of that README — prerequisites, the +system libraries, the build and codegen commands, which verification +tier a change demands, the tracker workflow and the commit grammar. This +file stays the deep reference both of them point at, and is the only one +of the three that explains *why* a shape is what it is. A fact that +belongs to a user goes in one place; the packaging channels keep their +own documents (`packaging/*/README.md`, `docs/android-release.md`) and +are linked rather than summarised, because a version-restart note copied +into the README is a second copy to keep true. + ## Issues **The tracker is the source of truth for what is wanted and what is diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5311019 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,173 @@ +# Contributing to YellowJacket + +This is the contributor's half of the [README](README.md): how to build it, how +to check a change, and how a change gets in. [`CLAUDE.md`](CLAUDE.md) is the +deep reference — the architecture, and the reasons behind the shape of it — +and is worth reading before a change of any size, because most of this +codebase's traps are written down there and nowhere else. + +## Building from source + +YellowJacket is [Go](https://go.dev/) with a [Lit](https://lit.dev/)/TypeScript +frontend, bridged by [Wails v3](https://wails.io/). + +| Tool | Version | +|------|---------| +| Go | 1.25+ | +| Node.js | 22+ | +| pnpm | 10+ | +| Wails CLI | v3 — vendored, no install needed (`go tool wails3`) | + +The Wails v3 CLI resolves from the `tool` block in `go.mod`, so there is nothing +to install globally; `make setup` fetches it with the rest of the tooling. + +On Linux, install the system libraries Wails needs. v3 builds against GTK4 + +WebKitGTK 6.0 by default: + +```bash +sudo apt-get install libasound2-dev libgtk-4-dev libwebkitgtk-6.0-dev # Debian/Ubuntu +sudo pacman -S alsa-lib gtk4 webkitgtk-6.0 # Arch +``` + +A machine without `webkitgtk-6.0` can still build with `-tags gtk3` against the +older WebKit2GTK 4.1 stack, but that is an escape hatch, not what CI or a +release builds. macOS and Windows need no extra system packages. Run +`go tool wails3 doctor` to check your environment. + +```bash +make setup # install tooling, frontend packages and the git hooks +make dev # run with hot-reload +make build-dev # debug build with symbols +make build-prod # production build (stripped and trimmed) +make android # the arm64 APK, into bin/ +``` + +The `Makefile` is the front door and carries a one-line description against +each target; `Taskfile.yml` and `build//Taskfile.yml` are the build +implementation behind it and are not called directly. + +## Generated code + +Two generators run from `go generate ./...`, which `make generate` wraps: +**sqlc** turns `backend/database/sql/queries/` into Go in +`backend/database/sql/sqlcgen/`, and **templ** turns `.templ` files into +`*_templ.go` beside them. Never edit either output by hand — run +`make generate` after touching a `.sql` or a `.templ` file. + +The TypeScript bindings in `frontend/bindings/` are generated by `wails3` +rather than by `go generate`, so they are a separate step: `make bindings` +regenerates them and `make bindings-check` fails if they are stale. +`frontend/src/events.ts` is generated too, from `backend/events/events.go`. + +A pre-commit hook checks that all of this is fresh, so the usual way to meet it +is a failing commit rather than a bug. + +## Checking a change + +Run the tier the change actually demands, not the cheapest one. + +| Change | Command | +|---|---| +| Go | `make lint` and `make test` — both cover all three build configurations | +| A frontend component or store | `make ui-test` (Vitest in a real Chromium, no backend) | +| A user-visible flow | `make e2e`, against a running `make dev-headless` | +| CSS | `make css-check` — see the Chrome 113 note below | +| Anything cosmetic | look at a screenshot; several bugs here were invisible to every assertion and obvious in an image | + +`make test` needs the fixture library, which is generated rather than +committed — it runs `make testdata` itself (about a second). + +The end-to-end tier drives the real app with no display at all: `make +dev-headless` starts it in the background on `:34115` (add `SEED=` for a +seeded library, built by `make sandbox-seed NAME=`), `make dev-logs` tails +it and `make dev-stop` stops it. **Check the port before starting one** — if +`:34115` is already answering, someone else's app is there, and a green result +about their build is worse than no result. + +Two smaller checks exist because the failure they catch is silent: +`make bindings-check` (stale generated bindings) and `make css-check`, which is +two passes — one fails on a `css` literal ended early by a backtick inside a +comment, the other on a nested CSS rule that begins with a bare element +selector. Chrome 113 is what the reference Android device renders with, and it +drops such a rule without a word. + +`make vulncheck` runs govulncheck over the module. + +## The issue tracker is the source of truth + +Work is described by issues before it is described by branches, and the tracker +is shared with people who cannot see your terminal. + +- **Search before starting**, closed issues included: `./scripts/issue.sh search + `. "That was fixed three weeks ago" is the cheapest possible answer. +- **Claim before the first edit**, not before the commit: + `./scripts/issue.sh claim ` sets the assignee, applies `Status/In Progress` + and comments with the branch, so the work is visibly taken *while it is being + done*. It refuses if somebody else holds it — talk to them rather than working + around it. +- **If no issue covers the work, open one first** (`./scripts/issue.sh new`). +- **Findings get filed.** A bug tripped over on the way to something else is an + issue with a reproduction, not a wider diff and not a sentence in a chat log. +- **#73 is the roadmap** and states the order the backlog should be worked in. + +`scripts/issue.sh` is the whole interface (`list`, `mine`, `search`, `show`, +`new`, `claim`, `unclaim`, `comment`, `close`, `label`, `depends`, `labels`) and +wants a `GITEA_TOKEN` with `write:issue`. The labels are a taxonomy rather than +tags: `Kind/*`, `Area/*`, `Priority/*`, `Platform/*`, plus `Reviewed/*` and +`Status/*`, of which the last two are exclusive scopes. + +## Commits and pull requests + +`main` is protected, so a branch and a PR are the only way in. Branch from +`origin/main`, and name the branch after the issue (`fix/140-…`, `feat/25-…`). + +Commit subjects are [Conventional Commits](https://www.conventionalcommits.org/) +— `type(scope): subject`, imperative, ≤72 characters — and are enforced by a +`commit-msg` hook and by CI (`make commit-check`). This is load-bearing rather +than decorative: semantic-release reads the **type** to decide the next version, +so a CI-only change is `ci:` and never `fix(ci):`, which would ship a patch +release. `make release-dry` prints what a release would cut right now. + +**The closing keyword goes in the commit body**, one issue per line, because +Gitea parses commit messages that reach `main` and does not parse the PR body: + +``` +docs: rewrite the README as a landing page + + + +Closes #50 +``` + +A PR body carries a commit-to-issue table, the verification you actually ran +(with results), and a `Closes` list for whoever reads it. + +## Style + +- **Go** — golangci-lint v2, strict: `err113` (static errors), `nlreturn`, + `wsl_v5`, `godot`, `sloglint`, `perfsprint`, and imports grouped stdlib → + third-party → `yellowjacket/…` by gci. +- **TypeScript** — strict mode, no implicit `any`, no unused locals or + parameters. +- Match the surrounding code. Where `CLAUDE.md` explains why something is shaped + the way it is, that shape is load-bearing and there is usually a test pinning + it. + +Hooks do most of the enforcing (`lefthook.yml`, installed by `make setup`): +pre-commit runs vet, lint, the codegen checks, the frontend typecheck and the +two CSS checks in parallel; pre-push runs the Go suite and the UI tier, +deliberately one after the other rather than together. + +## Where the rest of the documentation is + +- [`CLAUDE.md`](CLAUDE.md) — architecture and constraints, in depth. +- [`docs/PROFILING.md`](docs/PROFILING.md) — Go pprof and frontend profiling. +- [`docs/android-release.md`](docs/android-release.md) — the APK, its signing + key, and what the release workflow checks. +- [`docs/index-cache.md`](docs/index-cache.md) — the search-index build cache + and why it has a snapshot. +- [`packaging/arch/README.md`](packaging/arch/README.md), + [`packaging/homebrew/README.md`](packaging/homebrew/README.md) — the two + package channels. +- `.planning/` — design documents and measured history, not a queue. The queue + is the tracker. diff --git a/README.md b/README.md index 778617e..1b73805 100644 --- a/README.md +++ b/README.md @@ -2,112 +2,139 @@ *Music how it was meant to bee.* -YellowJacket is a fast, cross-platform desktop music player for your local -collection. It plays your files, keeps your library tidy, and helps you discover -and organize your music — all in a clean, responsive interface. No accounts, no -streaming, no telemetry: just your music on your machine. +YellowJacket plays the music you already own. Point it at your folders and it +scans them, reads the tags and the cover art, and gives you a library you can +browse, search, queue and tidy up — on your own machine, with no account, no +streaming service and no telemetry. -Runs on **Linux**, **macOS**, and **Windows**. +It plays **MP3**, **FLAC**, **OGG Vorbis** and **WAV**, on **Linux** and +**Android**, and builds from source on **macOS**. -## Features +![The track list, with something playing](docs/images/library.png) -### Play your music -- Plays **MP3, FLAC, OGG Vorbis, and WAV** -- Play, pause, seek, and volume control with a mute toggle -- Gapless, glitch-free seeking backed by a read-ahead buffer -- A queue you can add to, reorder, and shuffle, with play-next support -- Shuffle and repeat (off / all / one) -- Picks up right where you left off — remembers your track, position, and volume between sessions -- Media-key and MPRIS support on Linux, so your desktop's playback controls just work +## What it does -### Keep your library organized -- Point it at your music folders and it scans them automatically -- Reads tags and embedded cover art, and de-duplicates artwork so it isn't stored twice -- Incremental sync — only new or changed files get reprocessed, and deleted files are cleaned up -- Browse by **album**, **artist**, or **genre**, or search across everything -- Mark favorites and see what you've been listening to with play history -- Edit track tags directly when something's off +**Plays your files.** Play, pause, seek and volume with a mute toggle; a +read-ahead buffer so seeking is instant rather than gappy; a queue you can add +to, reorder and shuffle, with play-next; shuffle and repeat (off / all / one). +It remembers the track, the position and the queue between sessions, and it +answers your desktop's media keys — MPRIS on Linux, a media notification and +lock-screen controls on Android. -### Playlists -- Create playlists, drag tracks in, and reorder them -- **Smart playlists** that build themselves from rules (by genre, rating, play count, and more) -- Pin a default playlist and spot duplicate tracks at a glance +**Keeps the library tidy.** It scans the folders you give it and rescans only +what changed, so a big library costs its full scan once. It de-duplicates +embedded cover art rather than storing the same image a hundred times, notices +files that have gone away, and spots duplicate tracks. Browse by album, artist +or genre, search across everything, mark favourites, and see what you have been +playing. -### Discover and clean up (powered by MusicBrainz) -- **Explore** — browse artists, releases, and genres from the MusicBrainz catalog, not just what's already in your library -- **Auto-tag** — match your files against MusicBrainz to fill in correct artist, album, and track metadata, with a review step before anything is written -- **Lyrics search** — find a track by a line you remember +**Playlists, and playlists that write themselves.** Drag tracks in and reorder +them, or describe what you want — genre, play count, how long since you played +it — and let a smart playlist keep itself up to date. + +**Explore and auto-tag, from the MusicBrainz catalog.** Explore browses artists, +releases and genres from the catalog rather than only from what you own, so an +album page can tell you that you have nine of its twelve tracks. Auto-tag +matches your files against MusicBrainz and fills in the metadata that is +missing, with a review step before anything is written to disk. Lyrics search +finds a track from a line you remember. + +Explore needs its catalog, which is a one-off ~0.6 GB download from +**Settings → Search Index**. It asks first on a metered connection, and +everything else in the app works without it. ## Install -Download the latest build for your platform from the +Every download comes from the [releases page](https://git.ljones.me/yonlu/yellowjacket/releases). -| Platform | Download | -|----------|----------| -| Linux | `yellowjacket-linux-amd64` | -| macOS | `yellowjacket-darwin-universal.app.zip` (Apple Silicon + Intel) | -| Windows | `yellowjacket-windows-amd64.exe` | +### Linux -Prefer to build it yourself? See [Building from source](#building-from-source). +Download `yellowjacket--linux-amd64.tar.gz` from the latest release and +unpack it. It holds the binary, a `.desktop` entry and an icon. -## Getting started +On **Arch**, install it from the package registry instead and get updates with +the rest of your system — the one-time key import and `pacman.conf` block are in +[`packaging/arch/README.md`](packaging/arch/README.md): + +```bash +sudo pacman -Sy yellowjacket +``` + +### Android + +Install the APK from the release page, or from the URL below, which always +points at the newest build: + +``` +https://git.ljones.me/api/packages/yonlu/generic/yellowjacket-android/latest/yellowjacket.apk +``` + +That URL needs no credentials, so [Obtainium](https://obtainium.imranr.dev/) can +poll it directly and keep the app up to date. The build is `arm64-v8a` only, and +[`docs/android-release.md`](docs/android-release.md) says why. + +### macOS + +Homebrew builds it from source on your own Mac — there is no prebuilt `.app`, +because a signed macOS bundle needs a macOS machine to produce it and the +release runner is a Linux container. + +```bash +brew install shadow-puppet/yellowjacket/yellowjacket +``` + +See [`packaging/homebrew/README.md`](packaging/homebrew/README.md). + +### Windows + +Not published. It cross-compiles cleanly, but no Windows build of this app has +ever been *run*, and nothing here can exercise one — so shipping it would be a +promise that cannot be kept. You can still build it yourself: see +[`CONTRIBUTING.md`](CONTRIBUTING.md). + +### Coming from a 1.x install? + +Versions restarted at **0.0.1** when releases became automatic, which every +package manager reads as a downgrade. It costs one reinstall, once — the details +are with each channel: [Homebrew](packaging/homebrew/README.md#upgrading-from-1x-needs-a-reinstall-once), +[Android](docs/android-release.md#the-1x-installs-cannot-be-upgraded-to-00x). + +## First run 1. Launch YellowJacket. -2. Open **Settings** and add the folder(s) where your music lives. -3. Let the initial scan finish — you'll see progress as it works. -4. Browse by album, artist, or genre, queue something up, and press play. +2. Add the folder your music lives in — the first-run wizard asks, and + **Settings → Libraries** is where you add more later. +3. Watch the scan finish. It reports progress, and you can browse while it runs. +4. Queue something and press play. -Your library and settings are stored locally: +Your library and settings stay on your machine: | | Linux / macOS | Windows | |---|---|---| | Config | `~/.config/yellowjacket/` | `%LOCALAPPDATA%\yellowjacket\config` | | Library data | `~/.local/share/yellowjacket/` | `%LOCALAPPDATA%\yellowjacket\data` | -## Building from source +Setting `YJ_HOME` moves both, which is how you keep a second library separate. -YellowJacket is built with [Go](https://go.dev/) and a -[Lit](https://lit.dev/)/TypeScript frontend, bridged by the -[Wails](https://wails.io/) framework. +## More screenshots -**Prerequisites** +An album page knows what you own, and says so: -| Tool | Version | -|------|---------| -| Go | 1.25+ | -| Node.js | 22+ | -| pnpm | 10+ | -| Wails CLI | v3 — vendored, no install needed (`go tool wails3`) | +![An album page, with two discs and the transport playing](docs/images/album.png) -The Wails v3 CLI resolves from the `tool` block in `go.mod`, so there is nothing -to install globally; `make setup` fetches it with the rest of the tooling. +The home page suggests somewhere to start rather than opening on a wall of +everything: -On Linux, install the system libraries Wails needs. v3 builds against GTK4 + -WebKitGTK 6.0 by default: +![The home page's shelves](docs/images/home.png) -```bash -sudo apt-get install libasound2-dev libgtk-4-dev libwebkitgtk-6.0-dev # Debian/Ubuntu -sudo pacman -S alsa-lib gtk4 webkitgtk-6.0 # Arch -``` +## Contributing, and the rest of the documentation -A machine without `webkitgtk-6.0` can still build with `-tags gtk3` against the -older WebKit2GTK 4.1 stack, but that is an escape hatch, not what CI or a -release builds. - -macOS and Windows need no extra system packages. Run `go tool wails3 doctor` to -check your environment. - -**Build** - -```bash -make setup # install tooling and git hooks -make dev # run with hot-reload -make build-prod # produce a release binary -``` - -More detail for contributors lives in [`CLAUDE.md`](./CLAUDE.md) — the -architecture, the conventions and the reasons behind them. What is -being worked on is [the issue -tracker](https://git.ljones.me/yonlu/yellowjacket/issues); #73 is the -roadmap. +- [`CONTRIBUTING.md`](CONTRIBUTING.md) — build it from source, run the tests, + and how a change gets in. +- [`CLAUDE.md`](CLAUDE.md) — the deep reference: the architecture and the reasons + behind the shape of it. +- [The issue tracker](https://git.ljones.me/yonlu/yellowjacket/issues) is what + is wanted and what is being worked on; **#73** is the roadmap. +- [Releases](https://git.ljones.me/yonlu/yellowjacket/releases) double as the + changelog — every one is generated from the commits it contains. diff --git a/docs/images/album.png b/docs/images/album.png new file mode 100644 index 0000000..a49a926 Binary files /dev/null and b/docs/images/album.png differ diff --git a/docs/images/home.png b/docs/images/home.png new file mode 100644 index 0000000..c544b06 Binary files /dev/null and b/docs/images/home.png differ diff --git a/docs/images/library.png b/docs/images/library.png new file mode 100644 index 0000000..b860b33 Binary files /dev/null and b/docs/images/library.png differ