Providers
Two providers ship today. They are tried in providers.order, left to right,
for every item.
| Provider | Credentials | Notes |
|---|---|---|
| opensubtitles.com | API key + account username/password | Largest catalogue. The API key alone allows searching; downloads need the login. Free accounts get a small daily download quota. |
| subdl.com | API key | No login step, more generous quota, smaller catalogue. Good fallback. |
Getting credentials
opensubtitles.com — register a consumer at https://www.opensubtitles.com/en/consumers for the API key, then supply the username and password of the account itself. Both are needed: search works with the key, but every download is charged against the logged-in account.
subdl.com — grab a key from https://subdl.com/panel/api. That is the whole setup.
[providers]
order = ["opensubtitles", "subdl"]
[providers.opensubtitles]
enabled = true
api_key = "" # or SUBTRACE_OPENSUBTITLES_API_KEY
username = "" # or SUBTRACE_OPENSUBTITLES_USERNAME
password = "" # or SUBTRACE_OPENSUBTITLES_PASSWORD
daily_cap = 100
[providers.subdl]
enabled = true
api_key = "" # or SUBTRACE_SUBDL_API_KEY
daily_cap = 100Every credential can come from the environment instead — see Environment variables.
How a provider is searched
Both providers are queried the same way: by external id when the path carried
one (tmdb_id, or imdb_id), and by title plus year only as a fallback. For TV,
the season and episode numbers are sent as filters — and results that come back
with the wrong season/episode anyway are dropped rather than written to disk.
Fallback and quota
flowchart TD
A[item needs a subtitle] --> B{provider 1<br>quota left?}
B -- no --> D
B -- yes --> C[search]
C -- ErrNotFound --> D{provider 2<br>quota left?}
C -- ErrQuotaExceeded --> P[park provider<br>for the rest of the run] --> D
C -- results --> R[rank, try top 3,<br>validate, write] --> Z[downloaded]
D -- yes --> E[search] --> Z
D -- no --> N[leave item pending]
The chain is driven by sentinel errors, not by strings:
| Error | Consequence |
|---|---|
ErrNotFound | This provider has nothing. Move to the next one. |
ErrQuotaExceeded | Account is out of downloads. Park the provider for the rest of the run and move on. |
ErrRateLimited | Going too fast; back off. |
ErrNotConfigured | Credentials missing. Skipped quietly, so scan and --dry-run work on a fresh install with no accounts. |
Providers missing credentials are still constructed, which is why a first run with no API keys reports items as still pending rather than failing.
Local quota enforcement
daily_cap is enforced by subtrace itself, from a per-day counter in the
downloads table — not by asking the provider. A run therefore stops on its own
terms instead of collecting rejections from the API.
Keep the cap at or below the account’s real quota. Today’s usage is visible in
subtrace status:
downloads today
opensubtitles 100 / 100
subdl 37 / 100The counter is keyed by UTC date, so it resets at midnight UTC regardless of the
container’s TZ.
When both providers come up empty
The item is marked not_found, and becomes eligible again after
run.retry_after_hours (default a week), up to run.max_attempts times. New
subtitles do get uploaded over time, so the retry is not pointless — but for a
file that has failed several times, the cause is usually the folder name rather
than the catalogue. See Naming and matching.
Adding a provider
Implement provider.Provider — Name, Search, Download, Quota — return
the sentinel errors above, then register it. Details on
Contributing.