Skip to content
bili

v0.3.0

A refusal must never look like a result: seven response states, eight exit codes, a provenance envelope on every record, and a new download command.

This release is about one idea, applied everywhere it was not being applied: a refusal must never look like a result. bilibili answers some requests with the success code, the success message, and nothing inside, and the previous versions handed that to you as an empty list. They are different answers and they now get different treatment, different messages, and different exit codes.

Behaviour changes

These are the ones that can break a script that already works, so they come first.

Exit codes. v0.2.0 exited 0 or 1. v0.3.0 exits 0 through 7, and the code says what happened: 2 for a risk control refusal, 3 for a genuinely empty result, 4 for a refusal wearing a success code, 5 for the network, 6 for rate limiting, 7 for not found, and 1 for everything it could not name. Anything written as if bili ...; then still works, because 0 still means it worked. Anything testing $? -eq 1 needs the table in the CLI reference.

The three worth branching on in a loop are 2, which says stop and get a cookie, 6, which says sleep and try again, and 3, which says this one is empty and the next one is still worth asking for.

A refusal is no longer an empty result. bili favorites <mid> used to print [] and exit 0 on a creator whose folder listing the API had declined to send. It now names the endpoint, says a logged-in cookie is what changes it, and exits 4. If you built on the old behaviour this is a break, and it is also the bug this release exists to fix. The same correction applies to a dynamics feed that returns no items and to a favorites folder that reports a count and then sends nothing.

Absent counts are absent rather than zero. video_count, total_view and total_like no longer appear as 0 when the endpoint carrying them refused to say. In JSON the key is gone, in a table or a csv the cell is empty, and a consumer reading .total_view gets null where it used to get 0. A creator whose totals really are zero still gets a zero, which is the entire point of the distinction. Anything summing those columns was quietly wrong before.

A 412 is no longer retried. An HTTP 412 and a -352 are one refusal in two forms, and the old code retried the first four times with backoff, which is the worst available response to risk control turning an address away. Retries now apply to 429 and 5xx, which are a server saying it is busy, and to nothing else.

Added

A provenance envelope on every record. Every record carries an envelope describing the reading rather than the thing read: which endpoint answered, whether the request was signed, what state the response was sorted into, when, and how large the body was. envelope.missed names the fields the record does not carry and says what stopped each one, which matters most on bili user, where four requests stand behind one row. It is in the JSON, out of the table and the csv, and reachable with --fields envelope. See output formats.

bili download. Audio, by wrapping BBDown, with ffmpeg doing the transcode for mp3, flac and wav. Whatever you paste is resolved to a bvid first, so a URL, a bare av number and a bvid all behave the way they do everywhere else, and a bangumi or audio id is refused by name rather than becoming a BBDown error about a page that does not exist. Neither binary is bundled, and a missing one is reported before the transfer starts rather than at the end of a forty minute download. See downloading audio.

bili verify --live. Re-measures the recorded endpoint requirement matrix against the real API and prints what each endpoint answered, signed and unsigned. --strict exits non-zero when a row no longer matches what is recorded. A row that refuses is asked again before it is reported, and a row that was rate limited on both passes is reported as not measured rather than as drift.

A weekly drift job. The same measurement on a schedule, opening at most one issue when the site moves rather than turning the build red.

Typed errors. bili.Kind(err) reports the state an error came from, so a program embedding the library can branch on the same distinction the exit codes expose.

Changed

Go 1.26.6 and current dependencies. x/space/upstat refusing anonymous callers is recorded as a measured fact with a note rather than as an empty result. The cache key for a signed request no longer includes wts and w_rid, which are how a request was asked and not what was asked, so the cache is useful on the gated half of the API for the first time.

Fixed

video_count was never fetched at all. It was declared on the record and written by nothing, which is why it printed 0 for a creator with 924 uploads. It comes from the listing endpoint's own pagination total now.

--dry-run was being classified as a silent refusal, because the invented response carries no payload by design and the payload rule is what catches a refusal wearing a success code.

bili audio au1 exited 1 with an untranslated 4511001, and exits 7 with an English message.

The cached WBI signing key was never used by anything that pins the clock. The six hour freshness window was measured against wall time while the moment it was stored came from the injected clock, so the two never agreed and every signed call refetched nav. This affects a library consumer calling SetNow, and it is the reason one test in this repository was reaching the network.

Install

go install github.com/tamnd/bilibili-cli/cmd/bili@latest

Prebuilt archives for Linux, macOS, and Windows, plus Linux packages (deb, rpm, apk) and a signed checksums.txt, are on the release page. The container image is on GHCR:

docker run --rm ghcr.io/tamnd/bili:0.3.0 search 'lofi' -n 10