CLI tool
yutto-dev/yutto avatar
yutto-dev/yutto

Inside yutto: one Bilibili URL, fifteen video streams

A cute and willful Bilibili video downloader.

2,109 stars178 forksPythonGPL-3.0

At a glance

What is it?
yutto is a Python command line downloader that takes an av, BV or ep identifier, prints every video and audio stream bilibili offers, and writes the ones you choose. The stream menu is its real feature, and the docs gap around config keys and flags is where it gets awkward.
Who is it for?
Judge yutto by its stream menu rather than its feature list: anyone archiving bilibili uploads, bangumi episodes or course videos they already have access to will find the identifier handling and the 15-stream inventory more useful than a generic scraper, while a library spanning other hosts needs a scheduler it does not ship. Before scripting it, read the hosted option docs for the version you installed, put FFmpeg on PATH, and check the pinned `biliass==2.5.0`.
Can I use it commercially?
Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 5 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One URL resolves into fifteen video streams and three audio streams

A bare URL is the whole first command, and what follows is an inventory printed before a single byte moves. The invocation is `yutto https://www.bilibili.com/video/BV1CTMHziEaB/`; the log names the upload, then counts 15 video streams and 3 audio streams.

The video list is a matrix, not a ladder: 1920 by 1080, 1280 by 720, 852 by 480 and 640 by 360 each appear three times over, once as AVC, once as HEVC and once as AV1, which is where 4 resolutions times 3 codecs adds up to 15. Audio is simpler, three MP4A bitrates at 320 kbps, 64 kbps and 128 kbps. Entries carry the platform's own labels such as 1080P 高码率, so the same height can arrive at more than one bitrate, and each line ends with a `#3` marker rather than a plain format name.

Nothing about that choice is buried in a default; the menu is printed in full so you can see what the account is offered. Danmaku ride along in the same pass, announced by `弹幕 ASS 弹幕已生成` before the transfer starts, so the ASS subtitle lands beside the media without a second tool. The progress line then shows bytes against a total rather than a percentage, which is the one piece of state a wrapper script can parse without guessing.

Every argument resolves to an av, BV, ep, md or ss identifier

The parser does not care whether you hand it a full page address or a bare id. Single uploads work as an av number, a BV number, a submission page carrying a p=n parameter, or an episode page identified by ep (episode_id). One bangumi episode is demonstrated twice, once as a full link and once shortened to `yutto ep395211`, and the two forms are called equivalent.

Batch work is a separate flag, `-b/--batch`, which accepts a single-video page and expands it into the series around it, and which also takes md pages (media_id) and ss pages (season_id) that name a series outright. Pointed at the same episode address with the flag, it pulls every episode of the show that is already out.

Beyond that the option list is not in the repository. `yutto -h` is the only discovery command offered, and the README states that further features are left out for length and points you to yutto.nyakku.moe instead. Meanwhile `[project.urls]` fills Homepage, Documentation and Repository with the same repository URL in all three slots, so the packaging metadata never records the docs site that actually holds the flag reference. A script written against that site is a script written against a page the repository does not pin.

A config file named yutto.toml loads without being asked for

The run does not start with a config flag. Its first line after the command is a discovery message, `INFO 发现配置文件 yutto.toml,加载中……`, which means a file named yutto.toml in the working directory is picked up on its own. Custom config paths were handled in the 2.0.0 block of the roadmap, where both the config support entry and its custom-path follow-up are ticked.

The keys themselves are not written out anywhere in the checkout. What exists is a `schemas/` directory and a `scripts/generate-schema.py` wired to a `generate-schema` recipe in the justfile, so a schema can be produced from the code, yet no generated schema file sits at the top level next to `pyproject.toml`.

One tension deserves a plain statement: `pydantic>=2.13.4` is already a runtime `dependencies` entry, while the roadmap still carries an empty checkbox for reusing pydantic to validate config parameters. Validation is therefore partly wired up before the config refactor lands, and anyone writing `yutto.toml` by hand still has to find the key names in that generated schema or on the hosted docs.

Four install routes, and the FFmpeg prerequisite none of them carry

Every route asks for the same two things first: Python 3.11 or newer, and an FFmpeg you have put on PATH yourself. Neither is optional. `requires-python = ">=3.11"` in `pyproject.toml` sets the floor, the classifiers stop at CPython 3.14, and the install notes link a quick-start page for downloading FFmpeg before anything else happens.

On macOS the tap comes before the install:

bash
brew tap siguremo/tap
brew install yutto

Both lines are needed once per machine, since the tap is separate from the formula. Arch users follow the same shape through AUR, with yay and paru spellings or the archlinuxcn repository instead. Python-side installs are a single line, and pipx or uv keep the tool in an isolated environment rather than your site-packages:

bash
pipx install yutto      # 使用 pipx
uv tool install yutto   # 或者使用 uv

If you want main-branch behaviour ahead of a published build, the same three installers accept a git descriptor:

bash
pip install git+https://github.com/yutto-dev/yutto@main
pipx install git+https://github.com/yutto-dev/yutto@main
uv tool install git+https://github.com/yutto-dev/yutto.git@main

A fourth route installs an agent skill instead of the program itself:

bash
npx skills add https://github.com/yutto-dev/yutto --skill bilibili-video-download

which pulls the `skills/` directory into your agent setup and leaves your Python environment alone.

The Alpine image fixes the download directory and the clock

The container image is small and unusually opinionated. It starts from `alpine:3.24`, installs `ffmpeg python3 tzdata` with apk, creates a virtual environment at `/opt/venv` and installs the package into it with `--no-cache-dir --compile`. Three of those choices have consequences you inherit every time you run it:

dockerfile
FROM alpine:3.24
WORKDIR /app
ENTRYPOINT ["/opt/venv/bin/yutto", "-d", "/app"]

The first is the download path. Because the entrypoint passes the directory flag with `/app` already filled in, an image without a bind mount has nowhere to write, which is why the documented run command mounts a host path:

bash
docker run --rm -it -v /path/to/download:/app siguremo/yutto <url> [options]

Mount your current directory instead and the behaviour matches a native run, which writes into the working directory by default.

The second is `cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime`, baked into the build. Every container reads the clock in China Standard Time regardless of where the host sits, and the README does not say which timezone yutto's own file naming uses, so check that against real output before scripting around it. The third is the label: `version="2.3.1"` in the Dockerfile sits beside `version = "2.3.1"` in `pyproject.toml`, so a changed package version needs two edits and a rebuild before the label agrees. The package declares `GPL-3.0-only`, and `license-files` puts the `LICENSE` text inside the wheel, so the terms travel with anything you redistribute.

Two dependencies are pinned where the others float

The runtime dependency list in `pyproject.toml` holds 8 entries, and most of them are open ended: `pydantic>=2.13.4`, `returns>=0.29.0`, `dict2xml>=1.7.8`, `segno>=1.6.6`, `typing-extensions>=4.16.0`, plus `colorama>=0.4.6` which carries a `sys_platform == 'win32'` marker and installs on Windows only. Two entries are not open ended. `biliass==2.5.0` is pinned to one exact build, and `websockets>=17,<18` refuses the next major.

That shape has a practical consequence: an installer cannot quietly pull a biliass build you did not ask for, but it cannot take a fix that lands later either, so picking up new behaviour is a deliberate act rather than a side effect of reinstalling yutto.

The pinned biliass is not a PyPI wheel when you work from a checkout. `[tool.uv.sources]` redirects it with `{ workspace = true }`, which points at `packages/`, and the tree also carries `rust/` and a `.gitmodules`, because biliass was rewritten in Rust. The cache keys under `[tool.uv]` name `rust/Cargo.toml`, `rust/Cargo.lock`, `MATURIN_PEP517_ARGS` and `RUSTFLAGS`, which is the giveaway that a source install compiles Rust rather than copying a wheel. Contributors also get a justfile whose recipes wrap `uv run pytest` and `uv run ruff check .`, so the day-to-day loop is one command instead of a remembered set of flags.

Every documented target is a bilibili.com address

Nothing in the repository generalises past one site. Every identifier, every example and every keyword points at bilibili: submission videos, bangumi episodes, series pages, and danmaku that exist on that platform alone. If your archive spans other hosts or an RSS feed that carries media, yutto has no documented route to any of it, and the top level of the tree shows no adapter package for sites you could write one into.

The second wrong-tool case is automation without reading hosted docs. The option list in the README stays short, more features are pushed to yutto.nyakku.moe, and the flag names, config keys and exit codes you would need for an unattended job are all outside the checkout. Reading a web page to script a local binary is a real dependency, and it should be pinned against the version you installed rather than against whatever the site shows that day.

One narrower gap: nothing describes what happens when a requested stream is missing, or what exit code a partial failure produces. The justfile shows the suite is split by pytest markers, with a default run excluding ci_only and ignore, a continuous run excluding e2e, and a separate e2e-only run, so end-to-end paths are exercised deliberately and are the best place to look for the answer.

KubeSpider puts yutto inside a multi-site scheduler

The repository names exactly one alternative, and it changes the level rather than the feature set. KubeSpider is presented as a multi-function global resource orchestration download system that downloads and subscribes to many kinds of resource sites, with yutto as one downloader it can drive. The choice is between a single-purpose command line tool aimed at one URL and an orchestration layer that owns schedules and subscriptions across sources.

The same repository also points at the agent route, and a `skills/` directory at the top level backs a skill named bilibili-video-download that the npx skills add command installs, so a scripted assistant can reach the downloader without a human pasting addresses by hand. Between the two, yutto stays the piece that knows bilibili specifics and KubeSpider stays the piece that knows what to fetch and when. If you want both, the division of labour is clean. If you want either one alone, the orchestration layer is more machinery than a single command needs.

Editorial conclusion

Judge yutto by its stream menu rather than its feature list: anyone archiving bilibili uploads, bangumi episodes or course videos they already have access to will find the identifier handling and the 15-stream inventory more useful than a generic scraper, while a library spanning other hosts needs a scheduler it does not ship. Before scripting it, read the hosted option docs for the version you installed, put FFmpeg on PATH, and check the pinned `biliass==2.5.0`. The last push landed on 2026-10-05.

Frequently asked questions

Does yutto need FFmpeg before it will run?

Yes. The install notes ask you to have Python 3.11 or newer and FFmpeg configured before installing, and the container image installs ffmpeg with apk before installing yutto itself. pyproject.toml sets requires-python to >=3.11.

What is the difference between yutto <url> and yutto -b/--batch <url>?

A plain URL downloads only what that page points at. The batch flag expands a single-video page into the series around it, and also accepts md pages (media_id) and ss pages (season_id) that name a series outright.

Can yutto download from YouTube or sites other than bilibili?

No documented route exists. Every identifier and example in the repository is a bilibili.com address, and the only alternative named is KubeSpider, which drives downloads across many resource sites from a layer above yutto.

Where does yutto put downloaded files, and what does Docker change?

A native run writes into the current directory. The Docker image differs, because its entrypoint passes the directory flag with /app already set, so you bind mount a host directory onto /app or the container has nowhere to write.

How do I write a yutto.toml file if the keys are not in the repository?

A file named yutto.toml in the working directory loads on its own, without a config flag. The key names are not written out, so you need either a schema produced from scripts/generate-schema.py through the justfile or the hosted documentation to learn them.

Official sources

  1. License: GPL-3.0
  2. Project website
  3. README
  4. Releases
  5. yutto-dev/yutto on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/yutto-dev-yutto.svg)](https://hysenlabs.com/projects/yutto-dev-yutto)

Was this useful?