diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..c27aafa --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,87 @@ +# GoDeez + +GoDeez is a Go CLI that downloads music (tracks, albums, playlists, artist top +tracks) from Deezer in MP3 or lossless FLAC/WAV, embeds metadata tags, and +tracks what has already been downloaded so repeat runs can skip it. + +## Build, test, lint + +Mirror what CI (`.github/workflows/ci.yml`) runs: + +```bash +gofmt -l . # must print nothing; CI fails on unformatted files +go vet ./... +go test ./... +go build ./... +``` + +Run a single test: + +```bash +go test ./internal/download/ -run TestHashFile +go test ./internal/download/ -run TestHashFile -v +``` + +Release packaging is validated with `goreleaser check` (config in +`.goreleaser.yaml`); releases are cut by pushing a `v*` tag, which requires a +matching section in `CHANGELOG.md` (`.github/scripts/release-notes.sh` +extracts it and fails the release otherwise). + +## Architecture + +The flow is `cmd` → `download.Downloader` → `deezer` client → `store`/`tag`/`audio`: + +- **`cmd`** is a thin cobra layer (`download`, `login`, `logout`, `update`, + `version`). It parses flags and delegates; it does not contain download + logic. `cmd.Execute` also kicks off an async update check + (`internal/update.StartCheck`) before running the command and prints the + result after, so the network round trip overlaps with the command instead + of adding to startup time. +- **`internal/deezer`** is the API client. `Kind` (`album`/`playlist`/ + `artist`/`track`) is the single source of truth mapping a resource type to + its gw-light page method, request id field, and `Resource` implementation + (`Album`/`Playlist`/`Artist`/`Single`). Adding a new resource kind means + extending every switch in `kind.go`. `Resource`'s `decode` method is + unexported to seal the interface to this package. +- **`internal/download`** (package `download`) drives the per-track pipeline + via `Downloader.Run`: fetch resource → for each track, resolve a media + source → decide skip/hash-dedupe → stream and decrypt → optionally convert + FLAC to WAV → write tags → record in the store. Tracks are processed + sequentially, one at a time. Most per-track failures (bad cover, failed BPM + lookup, etc.) are collected as warnings rather than aborting the whole run; + only `context.Canceled` stops the loop early. +- **`internal/store`** is a bbolt-backed ledger (`.tracks.db`) written as a + hidden file inside the output directory, so it travels with the music + library it describes. Losing it is harmless — the only cost is + re-downloading. +- **`internal/tag`** writes metadata per container format (ID3v2 for MP3, + Vorbis comments for FLAC, ID3 chunk + RIFF LIST/INFO for WAV), dispatching + on file extension. Taggers write through a temp file so a mid-write failure + can't corrupt existing audio. +- **`internal/config`** has no config file — the output directory + (`~/Music/GoDeez`) is fixed, and the only setting is the `DEEZER_ARL` + env var, which is optional (falls back to keyring-stored credentials from + `godeez login`). +- **`internal/buildinfo`** holds the version/commit/date injected by + goreleaser via `-ldflags`; unreleased builds report `dev`, which disables + update-related behavior (`IsDev()`). +- **`internal/update`** implements self-update: checks GitHub releases, + verifies the downloaded binary's SHA256 against the published + `checksums.txt`, and replaces the running binary in place. + +## Conventions + +- Package- and function-level doc comments consistently explain *why*, not + just what — e.g. why a check happens where it does, or why an error is + handled a particular way. Preserve this style when touching existing code + or adding new exported functions. +- Most per-track errors during a download are non-fatal and surfaced as + warnings in the run summary; only cancellation (`context.Canceled`) should + abort the loop. Keep new failure modes inside this pattern unless they are + truly unrecoverable for the whole run. +- `Kind` in `internal/deezer/kind.go` is deliberately exhaustive with no + default case that silently returns a zero value in the id/method switches — + follow the same pattern (explicit cases, empty/error fallback) if extending + it. +- Credentials: `DEEZER_ARL` always takes precedence over keyring-stored + login credentials when both are present.