Skip to content
Contributing

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 show dump (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 check and 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 everything

Single package or single test:

go test ./internal/media
go test ./internal/media -run TestParse

CGO is always off — the SQLite driver (modernc.org/sqlite) is pure Go, which keeps the binary static. Do not introduce cgo dependencies.

Layout

PackageResponsibility
internal/cmdcobra commands; root.go:setup() is the shared boilerplate
internal/configload, env overrides, fallbacks, validation
internal/medialibrary walk, path parsing, sidecar detection, globs
internal/storeSQLite persistence: items and downloads
internal/providerthe Provider interface, sentinel errors, ranking
internal/subtitlevalidation and atomic writes
internal/plex, internal/notifyoptional post-run hooks
internal/enginewires 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:

  1. a case in engine.New’s switch
  2. config.EnabledProviders
  3. config.DailyCap
  4. the known map in config.Validate

Plus the [providers.<name>] block in internal/cmd/subtrace.example.toml.

Return the sentinel errors. 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, then subtitle.Write. Never write provider bytes directly.
  • Quota is enforced locally, from the downloads table, so a run stops on its own terms rather than collecting API rejections.
  • fetchOne runs concurrently. New shared state needs a mutex, like quotaTracker and the run stats.
  • Env-var overrides belong in config.applyEnv; host-dependent defaults belong in config.applyFallbacks.

These docs

The site is Hugo + Hextra, sources under docs/content:

mise run docs-dev     # http://localhost:1313/
mise run docs-build   # -> docs/public

Pushes to main that touch docs/** deploy to GitHub Pages automatically. Every page has an “Edit this page” link at the bottom.