docs: apply simplified technical english across docs and docstrings.

This commit is contained in:
imSp4rky
2026-08-19 16:31:06 -06:00
parent 4177cb2267
commit 66971230c3
135 changed files with 2651 additions and 2570 deletions
+82 -79
View File
@@ -1,23 +1,23 @@
# Download Job Lifecycle
When you submit a download over the REST API, the server does not block the request
until the file is written. Instead it creates a **job**, hands it back to you with an
until it writes the file. Instead it creates a **job**, hands it back to you with an
ID, and works through it in the background. Everything you do afterwards (watching
progress, cancelling, retrying, reading the result) is done by referring to that job
ID.
This page explains the states a job moves through, how live progress is reported, how
to cancel or retry, and, in clearly marked developer sections, how the worker and
manager behind the queue actually run the download.
This page explains the states a job moves through, how the server reports live
progress, and how to cancel or retry. Clearly marked developer sections tell you how
the job worker and the manager behind the queue operate the download.
!!! info "Who this page is for"
The first half is for anyone driving the API: the states, the fields you can read,
and the calls that move a job around. The **Under the hood** sections near the end
describe the internal worker/manager model and are aimed at contributors and people
debugging the server. You do not need them to use the API.
give the internal job worker and manager model, for contributors and for people
who debug the server. You do not need them to use the API.
For the full request and response shape of each endpoint, see
[Endpoints](endpoints.md). For how errors are reported, see
[Endpoints](endpoints.md). For how the server reports errors, see
[Error Responses](errors.md).
## The five states
@@ -26,15 +26,15 @@ A job is always in exactly one of five states, exposed as the `status` field:
| Status | Meaning |
|---|---|
| `queued` | Created and waiting for a free worker. No download has started yet. |
| `downloading` | A worker has picked the job up and the download is in progress. |
| `queued` | Created and waiting for a free job worker. No download has started yet. |
| `downloading` | A job worker has picked the job up and the download is in progress. |
| `completed` | The download finished successfully; `output_files` lists the results. |
| `failed` | The download stopped with an error; `error_message` and `error_code` explain why. |
| `cancelled` | The job was cancelled before it could finish, from either the queued or downloading state. |
The last three (`completed`, `failed`, and `cancelled`) are **terminal**: once a
job reaches one of them it never changes state again. Terminal jobs can be removed,
retried, or read from history, but they will not resume.
job reaches one of them it never changes state again. You can remove, retry, or read
terminal jobs from history, but they will not resume.
## How a job moves between states
@@ -59,7 +59,7 @@ The transitions in prose:
- **`queued` → `downloading`** happens automatically. As soon as one of the server's
workers is free, it takes the next job off the queue and starts it. You do not
trigger this; you only observe it.
trigger this. You only observe it.
- **`downloading` → `completed`** is the happy path: the download engine wrote all the
requested files and reported success.
- **`downloading` → `failed`** is any error during the download: a service failure, a
@@ -72,12 +72,12 @@ The transitions in prose:
!!! note "Priority does not change status"
Moving a job to the front of the queue with the priority endpoint reorders *when*
it will start, but it stays `queued` until a worker actually picks it up. It is not
it will start, but it stays `queued` until a job worker picks it up. It is not
a state change.
## Creating a job
A download is queued with `POST /api/download`. The request needs at least a `service`
You queue a download with `POST /api/download`. The request needs at least a `service`
and a `title_id`, plus any download options you want to set. On success the server
replies with **`202 Accepted`** and the new job's identity:
@@ -101,14 +101,15 @@ curl -X POST http://127.0.0.1:8786/api/download \
}
```
The job starts in `queued`. Workers auto-start on the first job submitted, so there is
nothing else to switch on. Keep the `job_id`; every later call is addressed to it.
The job starts in `queued`. The job workers auto-start on the first job you submit, so
you switch nothing else on. Keep the `job_id`. Every later call uses it.
!!! tip "The full parameter set"
`POST /api/download` accepts the same download options as the `dl` command
(quality, codecs, language selection, `wanted` episode ranges, and many more), with
server-side defaults filling in anything you omit. The complete list, their
defaults, and validation rules are documented on the [Endpoints](endpoints.md) page.
server-side defaults filling in anything you do not write. The
[Endpoints](endpoints.md) page gives the complete list, their defaults, and the
validation rules.
## Reading a job's status and progress
@@ -136,7 +137,7 @@ There are two ways to look at jobs.
-H "X-Secret-Key: your-api-secret"
```
The list can be filtered by `status` and `service`, and ordered with `sort_by`
You can filter the list by `status` and `service`, and order it with `sort_by`
(`created_time`, `started_time`, `completed_time`, `progress`, `status`, or
`service`) and `sort_order` (`asc` / `desc`). See [Endpoints](endpoints.md) for the
query-parameter reference.
@@ -179,7 +180,7 @@ The full detail adds `started_time`, `completed_time`, `output_files`, the redac
!!! tip "Or subscribe instead of polling"
`GET /api/download/jobs/{job_id}/events` pushes the same job object as Server-Sent
Events, and closes the stream when the job ends. See
Events, and closes the event stream when the job ends. See
[Endpoints](endpoints.md#get-apidownloadjobsjob_idevents).
!!! note "What the percentage means"
@@ -201,15 +202,15 @@ The full detail adds `started_time`, `completed_time`, `output_files`, the redac
### Completed
A `completed` job has its `output_files` populated with the paths of the muxed
results, and `progress` is `100`. This is the state you are waiting for.
results, and `progress` is `100`. This is the state you want.
### Failed
A `failed` job carries the error. `error_message` is the short human message,
`error_code` is one of the API's structured error codes (for example `NETWORK_ERROR`,
`DRM_ERROR`, `AUTH_FAILED`, or `DOWNLOAD_ERROR`), and `error_details` gives more
context. When the server was started with `--debug-api`, `error_traceback` and
`worker_stderr` are also filled in for diagnosis.
context. When you start the server with `--debug-api`, `error_traceback` and
`worker_stderr` are also filled in, to help you find the cause.
!!! tip "Secrets are scrubbed from error text"
A service can echo a credential or a proxy URL into its output. The server masks
@@ -221,8 +222,8 @@ The error-code catalogue and how each maps to an HTTP status is on the
### Cancelled
A `cancelled` job was stopped on request before finishing. It may have partial output,
but it is not considered complete. See below.
The server stopped a `cancelled` job on request, before it finished. It may have
partial output, but it is not considered complete. See below.
## Cancelling a job
@@ -241,7 +242,7 @@ both stops in-flight work and tidies up afterwards.
!!! warning "Do not parse the body of a 204"
Removing a terminal job returns `204 No Content` with an empty body. A client must
not try to decode JSON from it; check the status code first.
not try to decode JSON from it. Examine the status code first.
```shell title="Cancel a running job"
curl -X DELETE http://127.0.0.1:8786/api/download/jobs/b1e6c7de-6d0a-4f2f-9d1e-3a2b4c5d6e7f \
@@ -254,14 +255,15 @@ which returns `{"removed": <count>}`.
## Retrying a job
`POST /api/download/jobs/{job_id}/retry` re-runs a finished job by creating a **new**
job with the same service, title, and parameters. The original job is untouched and
stays in its terminal state; the response gives you the new job's ID with `202`.
job with the same service, title, and parameters. The retry does not change the
original job. It stays in its terminal state, and the response gives you the new job's
ID with `202`.
- Only terminal jobs can be retried. Retrying a `queued` or `downloading` job returns
- You can retry only terminal jobs. A retry of a `queued` or `downloading` job returns
`409` with error code `CONFLICT`.
- A missing job returns `404` (`JOB_NOT_FOUND`).
- The service allowlist and any download gates are re-checked for the new job, exactly
as they were for the original submission, so a retry can be rejected with
- The server examines the service allowlist and any download gates again for the new
job, exactly as for the original submission. Thus the server can reject a retry with
`INVALID_SERVICE` or `FORBIDDEN` if permissions changed.
```json title="202 Accepted, a fresh job"
@@ -275,38 +277,39 @@ stays in its terminal state; the response gives you the new job's ID with `202`.
## Prioritising a job
`POST /api/download/jobs/{job_id}/priority` moves a **queued** job to the front of the
line, so it will be the next one a free worker picks up.
line, so it will be the next one a free job worker picks up.
- The job must be `queued`. Prioritising a job in any other state returns `409`
(`CONFLICT`); a missing job returns `404`.
(`CONFLICT`). A missing job returns `404`.
- On success the response is `{"job_id": "<id>", "position": "front"}`.
This only reorders the queue; the job's status stays `queued` until a worker starts
it.
This only reorders the queue. The job's status stays `queued` until a job worker
starts it.
## History and retention
Every job that reaches a terminal state is also written once to a persistent history
log, so it survives even after the job is removed from the live list.
The server also writes every job that reaches a terminal state once to a persistent
history log. The job survives there even after the server removes it from the live
list.
- Read it with `GET /api/history` (newest first; supports `limit` and `service`
- Read it with `GET /api/history` (newest first, with `limit` and `service`
filters).
- Delete a single entry with `DELETE /api/history/{job_id}` (returns `204`).
The live in-memory jobs are separate from this on-disk history. A background cleanup
task periodically drops old terminal jobs from the live list based on a retention
period; the history file keeps them for later reference. See [Endpoints](endpoints.md)
period. The history file keeps them for later reference. See [Endpoints](endpoints.md)
for the history response shape.
!!! note "Why the history file lives under the cache directory"
`api_history.jsonl` is placed under the cache directory on purpose, so it stays out
of the config/data tree. It is disposable runtime state, not user configuration or
unshackle puts `api_history.jsonl` under the cache directory on purpose, so it
stays out of the config/data tree. It is disposable runtime state, not user configuration or
curated data, which is exactly why clearing the cache is a safe operation against
it.
---
## Under the hood: the worker/manager model
## Under the hood: the job worker and manager model
!!! info "Developer section"
Everything below describes server internals. It is useful for contributors and for
@@ -316,18 +319,18 @@ for the history response shape.
A single process-wide **download queue manager** owns all jobs. It holds them in
memory keyed by ID, backs the wait list with a FIFO queue, and keeps a separate
priority deque for jobs that have been bumped to the front. On the first job, it starts
a fixed pool of worker tasks plus one cleanup task; starting is idempotent, so later
jobs reuse the same pool.
priority deque for the jobs it bumped to the front. On the first job, it starts
a fixed pool of job worker tasks plus one cleanup task. The start is idempotent, so
later jobs reuse the same pool.
- **Concurrency** is capped by `max_concurrent_downloads` (default **2**). That many
jobs can be in `downloading` at once; the rest wait in `queued`.
- **Retention** is governed by `job_retention_hours` (default **24**). An hourly
- **Concurrency**: `max_concurrent_downloads` (default **2**) caps it. That many
jobs can be in `downloading` at once. The rest wait in `queued`.
- **Retention**: `job_retention_hours` (default **24**) controls it. An hourly
cleanup task removes terminal jobs whose completion time (or, if never started,
creation time) is older than the retention window.
!!! note "Two concurrency knobs that are easy to conflate"
`max_concurrent_downloads` caps how many separate download **jobs** run at once.
`max_concurrent_downloads` caps how many separate download **jobs** operate at once.
The separate `serve.downloads` setting controls how many **tracks** download in
parallel *within* a single job. They are independent, and raising one does not affect
the other.
@@ -336,64 +339,64 @@ jobs reuse the same pool.
The manager reads these as top-level config attributes
(`config.max_concurrent_downloads` and `config.download_job_retention_hours`).
Those keys are not part of the standard config schema, so in practice the built-in
defaults of **2** and **24** apply. The live values are surfaced by
[`GET /api/config`](endpoints.md). The related `serve.history_limit` (default
defaults of **2** and **24** apply. [`GET /api/config`](endpoints.md) gives the
live values. The related `serve.history_limit` (default
**100**) instead controls how many entries the on-disk history file keeps.
Each worker loop drains the priority deque first, then the FIFO queue. A job that was
promoted has its stale FIFO copy skipped so it does not run twice. When a worker takes
a job it flips it to `downloading`, stamps `started_time`, runs it, and on finish
stamps `completed_time` and records history.
Each job worker loop drains the priority deque first, then the FIFO queue. The loop
skips the stale FIFO copy of a promoted job, so it does not do the job twice. When
a job worker takes a job it flips it to `downloading`, stamps `started_time`, does the
download, and on finish stamps `completed_time` and records history.
### Each job runs in its own subprocess
The manager does **not** run the download in the server's event loop or even in the
The manager does **not** operate the download in the server's event loop or even in the
server process. For each job it spawns a separate Python subprocess:
```text
python -m unshackle.core.api.download_worker <payload.json> <result.json> <progress.json>
```
Three temporary files bridge the parent and the worker:
Three temporary files bridge the parent and the job worker:
- **`payload.json`**: written by the manager before launch; contains the job ID,
- **`payload.json`**: the manager writes it before launch. It contains the job ID,
service, title ID, and merged parameters.
- **`progress.json`**: written continuously by the worker as the download proceeds.
- **`result.json`**: written by the worker at the end.
- **`progress.json`**: the job worker writes it continuously as the download proceeds.
- **`result.json`**: the job worker writes it at the end.
Running each download out-of-process isolates the server from crashes, memory growth,
and blocking work inside the download engine, and makes hard cancellation (killing the
process) reliable.
The worker's exit codes are `0` for success, `1` for a download error, and `2` for bad
The job worker's exit codes are `0` for success, `1` for a download error, and `2` for bad
arguments. Its `result.json` is either `{"status": "success", "output_files": [...]}`
or `{"status": "error", "message", "error_details", "error_code", "traceback"}`. If the
process exits non-zero, or the result is missing or not a success, the parent marks the
job `failed` and adopts the worker's `error_code` (or derives one by categorising the
job `failed` and adopts the job worker's `error_code` (or derives one by categorising the
exception).
### How progress reaches the API
While the worker runs, the manager polls `progress.json` every **0.5 s** and copies the
While the job worker operates, the manager polls `progress.json` every **0.5 s** and copies the
fields it finds onto the live `DownloadJob`. That is what the `GET` endpoints return.
Inside the worker, progress is aggregated across all selected tracks. Each track's
completion fraction is weighted by its bitrate, so video and audio dominate the bar and
subtitles barely move it (subtitles get a small fixed weight). Track fractions are kept
**monotonic**: the percentage only ever climbs. The download portion is scaled to fill
`0` to `90`; the remaining `90` to `100` is driven by the download engine as it repackages
and muxes. A track counts as done when it reaches a terminal state such as
The job worker aggregates progress across all selected tracks. It weights each track's
completion fraction by its bitrate, so video and audio dominate the bar and subtitles
barely move it (subtitles get a small fixed weight). Track fractions are kept
**monotonic**: the percentage only ever climbs. The job worker scales the download
portion to fill `0` to `90`. The download engine drives the remaining `90` to `100` as
it repackages and muxes. A track counts as done when it reaches a terminal state such as
`Downloaded`, `Decrypted`, or a skipped subtitle.
### How cancellation actually works
### How cancellation works
Each job carries a cancellation event. When you `DELETE` a live job:
- If it is **`queued`**, it flips straight to `cancelled`, its history is recorded
immediately (it never reaches a worker), and the worker loop skips it if it later
pops it off the queue.
- If it is **`queued`**, it flips straight to `cancelled`, the manager records its
history immediately (it never reaches a job worker), and the job worker loop skips it
if it later pops it off the queue.
- If it is **`downloading`**, the manager sets the cancellation event and flips the job
to `cancelled`. The polling loop notices the event, `terminate()`s the worker
to `cancelled`. The polling loop notices the event, `terminate()`s the job worker
subprocess (escalating to `kill()` if it does not exit within 5 seconds), and raises
a cancellation so the task unwinds. The subprocess model is what makes this a clean,
immediate stop rather than a cooperative one.
@@ -403,11 +406,11 @@ Each job carries a cancellation event. When you `DELETE` a live job:
Job parameters may carry secrets: a raw `user:pass` credential, or a proxy URL with
embedded userinfo. Whenever the manager serializes a job (for an API response or the
history file), it masks the sensitive keys (`credential`, `credentials`, `password`,
`token`, `api_key`) and strips userinfo from proxy URLs. Free-text fields that a
service might have leaked a secret into (`error_message`, `error_details`,
`error_traceback`, `worker_stderr`, and worker stdout/stderr in the logs) are scrubbed
of the job's known secret values, longest-match first, before they are ever returned or
logged.
`token`, `api_key`) and strips userinfo from proxy URLs. The manager also scrubs
free-text fields that a service might have leaked a secret into (`error_message`,
`error_details`, `error_traceback`, `worker_stderr`, and job worker stdout/stderr in the
logs). It removes the job's known secret values, longest-match first, before it returns
or logs them.
## See also