Contributing
Contributions are welcome — issues, bug reports, and pull requests all help. This started as a personal tool for one specific setup, so rough edges outside that setup are expected, and pointing them out is genuinely useful.
- Found a bug or want a feature? Open an issue. Include your folder layout
and a
subtrace config showdump (secrets are masked) when it is about matching. - New subtitle provider or media layout? These are the most useful additions.
- Before opening a PR: run
mise run checkand keep changes focused. New behavior should come with a test.
Development
mise pins the toolchain and provides the tasks; the
first run in the repo needs mise trust.
mise run check # gofmt lint + go vet + go test — run before committing
mise run build # static CGO-free binary -> ./subtrace
mise run test # go test ./...
mise run docker # build the container image
mise tasks # list everythingSingle package or single test:
go test ./internal/media
go test ./internal/media -run TestParseCGO is always off — the SQLite driver (modernc.org/sqlite) is pure Go, which
keeps the binary static. Do not introduce cgo dependencies.
Layout
| Package | Responsibility |
|---|---|
internal/cmd | cobra commands; root.go:setup() is the shared boilerplate |
internal/config | load, env overrides, fallbacks, validation |
internal/media | library walk, path parsing, sidecar detection, globs |
internal/store | SQLite persistence: items and downloads |
internal/provider | the Provider interface, sentinel errors, ranking |
internal/subtitle | validation and atomic writes |
internal/plex, internal/notify | optional post-run hooks |
internal/engine | wires it together: Scan and Fetch |
Adding a provider
Implement provider.Provider:
type Provider interface {
Name() string
Search(ctx context.Context, q Query) ([]Result, error)
Download(ctx context.Context, r Result, w io.Writer) error
Quota(ctx context.Context) (int, error)
}Then touch exactly four places:
- a case in
engine.New’s switch config.EnabledProvidersconfig.DailyCap- the
knownmap inconfig.Validate
Plus the [providers.<name>] block in internal/cmd/subtrace.example.toml.
ErrNotFound, ErrQuotaExceeded,
ErrRateLimited, and ErrNotConfigured are what drive the fallback chain in
engine.fetchOne. A provider that returns opaque errors instead breaks
fall-through for every item it touches.Other invariants worth knowing before you change things:
- Everything downloaded goes through
subtitle.Normalize, thensubtitle.Write. Never write provider bytes directly. - Quota is enforced locally, from the
downloadstable, so a run stops on its own terms rather than collecting API rejections. fetchOneruns concurrently. New shared state needs a mutex, likequotaTrackerand the run stats.- Env-var overrides belong in
config.applyEnv; host-dependent defaults belong inconfig.applyFallbacks.
These docs
The site is Hugo + Hextra, sources under
docs/content:
mise run docs-dev # http://localhost:1313/
mise run docs-build # -> docs/publicPushes to main that touch docs/** deploy to GitHub Pages automatically.
Every page has an “Edit this page” link at the bottom.