A self-hosted Telegram download bot for aria2 and yt-dlp. Send it a link, a magnet/torrent, a video-page URL, or just forward a Telegram file — it downloads with live progress and organizes everything into a clean media library automatically.
English-only Telegram bot that drives aria2 and yt-dlp, organizes finished downloads into a media library, and supports multi-user access with admin approval.
Send the bot an HTTP/FTP link, a magnet/torrent, a video-page URL (YouTube, Twitter/X, Instagram, Bilibili, …) — or just forward a Telegram file — and it downloads with live progress, then files everything neatly by type.
A heavy remod / derivative of DownloadBot by gaowanliang (Gaowan Liang) — Apache-2.0, with full attribution. See NOTICE and LICENSE.
- Features
- How it works
- Requirements
- Quick start
- Full setup, step by step
- Configuration reference
- Media library layout
- yt-dlp layout
- Use as a Jellyfin/Plex downloader
- Message style
- Multi-user model
- Telegram file forwarding & the 20 MB limit
- Building from source
- Updating
- Troubleshooting
- Security notes
- Credits & license
- aria2 control — add http/https/ftp/magnet/torrent links; pause, resume and remove tasks; live progress that pops up automatically when a download starts.
- Accurate progress bars — the same 13-segment bar everywhere:
[●●●●●●●○○○○○○] 42.69 %- aria2: name, downloaded/total, speed, ETA, threads, GID.
- yt-dlp: percent, size, speed, ETA parsed live.
- Telegram-file fetch: real percent + speed + ETA (via the local Bot API server's temp store; see below).
- Archive extraction: bytes done/total + ETA; moving:
N/M files.
- Native media organization (Go, no shell scripts):
- sorts into
movies / series / anime / music / documents / archives / others; - episode detection (
S01E01,S1E1,1x01,s01.e01, …); Season Nfolders (no leading zero); single episodes never treated as season packs; smart folder matching (normalized + token overlap);- AniList lookup for anime confirmation (default on);
- clean names (
Some.Show.S02E04.1080p.x264.Hindi.mkv→Some.Show.S02E04.mkv); - duplicates get numeric suffixes instead of being overwritten.
- sorts into
- Archive handling — zip/rar/7z/tar/gz/bz2/xz are extracted (native Go first; 7z/unrar/unzip fallback), then re-run through the organizer. Stages on real disk with a free-space pre-flight check; optional delete-after-extract.
- yt-dlp — any site yt-dlp supports, at 1080p + best audio, metadata and
thumbnail embedded, into
YouTube/<Channel>/[<Playlist>/]<video>.mp4or<Service>/<Uploader>/…. - Multi-user — users request access via
/start; the admin approves or denies. Regular users only ever see their own tasks; the admin sees everyone's. The admin has a 👥 Users button to list approved users and revoke access. - Telegram file forwarding — forward any document/video/audio/photo to the bot and it downloads it (through the local server, up to 2 GB), then runs it through the same organizer.
- Crash-safe — orphaned extraction stages are swept on startup; completed-but-unorganized archives are resumed; stale progress messages are cleaned up.
- Resilient — Telegram API errors (including rate-limit
429) never take the bot down; progress views refresh every 5 s.
Telegram ──► AriaFlow ──► aria2c (RPC) ──► downloads/ ──► organizer ──► media library
│ ▲
├────────► yt-dlp ───────────────────────────────────────────────────┤
│ │
└────────► local Bot API server (optional) ──► temp store ──────────┘
- The bot talks to aria2 over its websocket RPC (
aria2-server+aria2-key). - yt-dlp is shelled out for supported video sites.
- For files forwarded to the bot, an optional self-hosted Bot API server removes Telegram's 20 MB bot download cap and enables up to 2 GB.
| Component | Why | Mandatory |
|---|---|---|
aria2c |
the actual downloader (RPC) | yes |
| Telegram bot token | bot login | yes |
| Numeric Telegram user id | the bot admin | yes |
yt-dlp |
video-page URLs (YouTube, …) | no |
ffmpeg |
yt-dlp merge/embed | no (needed for yt-dlp) |
7z / unrar / unzip |
archive extraction fallbacks | no |
telegram-bot-api + api-id/api-hash |
forwarded files > 20 MB | no |
Supported platforms: Linux amd64 and arm64 (prebuilt binaries and CI).
Recommended: deploy with Docker Compose — a full stack (
docker-compose.ymlin the repo) runs the bot,aria2c, and the optional local Bot API server in containers, sharing one media volume. It needs no manual binary or dependency installation and is the simplest, most reproducible way to run AriaFlow in production.
A Docker Compose stack ships with the repo (docker-compose.yml + docker/).
It runs the bot, an aria2c daemon, and (optionally) the local Bot API server
as containers sharing one media volume — no manual binary/tool installation.
cp .env.example .env
# edit .env: set ARIA2_SECRET, BOT_TOKEN, ADMIN_ID (required)
docker compose up -d --build
# with the local Bot API server (forwarded files up to 2 GB):
docker compose --profile api-server up -d --buildExtensive guidance (all variables, storage layout, troubleshooting) is in
docker/README.md. The bot reads config.json, which is
generated from environment variables at container start, so no config editing
is needed.
Prefer a standalone binary (no Docker)? Grab the release for your arch:
-
Download the binary for your arch from the Releases:
File Platform ariaflow-linux-amd64Linux x86-64 (most VPS/NAS) ariaflow-linux-arm64Linux ARM 64-bit (Raspberry Pi 4/5 64-bit OS, ARM VPS) -
Install the runtime dependencies (the binary shells out to these — it fails without them):
sudo apt update sudo apt install -y aria2 ffmpeg unzip p7zip-full sudo apt install -y unrar || true # non-free/multiverse; skip if unavailable # yt-dlp (standalone release; the apt package is outdated): # amd64: sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux \ -o /usr/local/bin/yt-dlp # arm64 (instead of the above): # sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux_aarch64 \ # -o /usr/local/bin/yt-dlp sudo chmod +x /usr/local/bin/yt-dlp
-
Start aria2 and write
config.json— follow Full setup steps 2–6. -
Run:
chmod +x ariaflow-linux-amd64 ./ariaflow-linux-amd64 -c ./config.json
The binary embeds its English translations — no sidecar files required.
Debian / Ubuntu / Raspberry Pi OS:
sudo apt update
sudo apt install -y aria2 ffmpeg unzip p7zip-full
# unrar is in non-free (Debian) / multiverse (Ubuntu):
sudo apt install -y unrar || trueInstall yt-dlp from its standalone release (the apt package is outdated):
# amd64
sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux \
-o /usr/local/bin/yt-dlp
# arm64
sudo curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux_aarch64 \
-o /usr/local/bin/yt-dlp
sudo chmod +x /usr/local/bin/yt-dlparia2c is the downloader. It must expose the RPC interface the bot uses:
aria2c \
--enable-rpc --rpc-listen-all --rpc-listen-port=6800 \
--rpc-secret=CHANGE_ME_SECRET \
--dir=/mnt/nas/downloads \
--continue=true --max-concurrent-downloads=5 \
--max-connection-per-server=16 --split=16 \
--file-allocation=none--dirmust equaldownloadFolderinconfig.json.- Remember
--rpc-secret→ it becomesaria2-key.
systemd unit (/etc/systemd/system/aria2c.service):
[Unit]
Description=Aria2 download daemon
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/bin/aria2c --enable-rpc --rpc-listen-all --rpc-listen-port=6800 \
--rpc-secret=CHANGE_ME_SECRET --dir=/mnt/nas/downloads \
--continue=true --max-concurrent-downloads=5 \
--max-connection-per-server=16 --split=16 \
--file-allocation=none --enable-dht=true --seed-time=0
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable --now aria2c
curl -s -X POST http://127.0.0.1:6800/jsonrpc \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":"1","method":"aria2.getVersion","params":["token:CHANGE_ME_SECRET"]}'- Talk to @BotFather →
/newbot→ copy the token (123456789:AA…). - Get your numeric user id from @userinfobot.
- That id is the admin (
output.telegram.user-id) — exactly one id, enforced at startup.
Telegram's cloud Bot API only lets a bot download files up to 20 MB. Running a local Bot API server removes that cap (up to 2 GB) and is required for forwarding big files to the bot.
You need api-id + api-hash from https://my.telegram.org → API
development tools. They identify your application — they do not have
to come from the account that created the bot; any valid app credentials work.
Build telegram-bot-api (on a machine with a toolchain):
sudo apt install -y cmake g++ make libssl-dev zlib1g-dev gperf
git clone --recursive https://github.com/tdlib/telegram-bot-api.git
cd telegram-bot-api
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX:PATH=.. ..
cmake --build . --target install -j"$(nproc)"
# binary lands at ../bin/telegram-bot-apiPut the binary next to the bot as telegram-bot-api, and use the bundled
launcher run-tg-api.sh (reads credentials from config.json, passes them
via environment, runs in --local mode):
install -m755 telegram-bot-api /opt/ariaflow/telegram-bot-api
install -m755 run-tg-api.sh /opt/ariaflow/run-tg-api.shAdd to config.json (see next step): api-id, api-hash,
api-base: "http://127.0.0.1:8081", api-dir: "/mnt/nas/.tg-bot-api".
Important: each running server needs its own
api-dir. If two machines share the same storage, give each a distinct directory (/mnt/nas/.tg-bot-apivs/mnt/nas/.tg-bot-api-prod), otherwise the server aborts with “Can't lock file … tqueue.binlog”.Tip: use a directory on the same filesystem as
downloadFolderso the bot can hardlink fetched files (instant, no extra disk).
cp default.config.json config.jsonMinimal config (cloud Bot API, ≤ 20 MB forwarded files):
{
"input": {
"aria2": {
"aria2-server": "ws://127.0.0.1:6800/jsonrpc",
"aria2-key": "CHANGE_ME_SECRET"
}
},
"output": {
"telegram": {
"bot-key": "123456789:AA-your-bot-token",
"user-id": "123456789"
}
},
"max-index": 10,
"language": "en",
"downloadFolder": "/mnt/nas/downloads",
"organize": {
"enabled": true,
"anilist": true,
"deleteArchive": false,
"movies": "/mnt/nas/movies",
"series": "/mnt/nas/series",
"anime": "/mnt/nas/anime",
"music": "/mnt/nas/music",
"documents": "/mnt/nas/documents",
"archives": "/mnt/nas/archives",
"others": "/mnt/nas/others",
"torrents": "/mnt/nas/torrents",
"keepTorrent": true,
"youtube": "/mnt/nas/YouTube",
"services": "/mnt/nas/Services",
"ytdlpPath": "yt-dlp",
"ytdlpQuality": "bestvideo[height<=1080]+bestaudio/best[height<=1080]/best",
"ytdlpEmbed": true
},
"log": { "logPath": "", "errPath": "", "level": "info" }
}With the local Bot API server enabled (files up to 2 GB), add:
"api-id": 1234567,
"api-hash": "0123456789abcdef0123456789abcdef",
"api-base": "http://127.0.0.1:8081",
"api-dir": "/mnt/nas/.tg-bot-api"api-id/api-hash— from my.telegram.org (see step 4).api-base— leave empty to use Telegram cloud (20 MB cap).api-dir— the local server's data directory; required for byte-level progress while fetching forwarded files.
config.json,users.json,tasks.jsonand the binaries are git-ignored — never commit them (they contain secrets).
sudo mkdir -p /mnt/nas/downloads \
/mnt/nas/{movies,series,anime,music,documents,archives,others,torrents,YouTube,Services}Adjust to the paths used in config.json.
Use a release binary (ariaflow-linux-amd64 / -arm64) or build from
source — see Building from source.
Install it (example layout /opt/ariaflow):
sudo install -m755 ariaflow-linux-amd64 /opt/ariaflow/ariaflow
sudo cp config.json /opt/ariaflow/config.jsonBot API server (/etc/systemd/system/tg-bot-api.service) — only if using
the local server from step 4:
[Unit]
Description=Telegram Bot API server (local mode, AriaFlow backend)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/ariaflow
ExecStart=/opt/ariaflow/run-tg-api.sh
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetThe bot (/etc/systemd/system/downloadbot.service):
[Unit]
Description=AriaFlow (Telegram Aria2 bot)
After=network-online.target aria2c.service tg-bot-api.service
Wants=network-online.target
Requires=aria2c.service tg-bot-api.service
[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/opt/ariaflow
ExecStart=/opt/ariaflow/ariaflow -c /opt/ariaflow/config.json
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetDrop the tg-bot-api.service entries from After=/Requires= if you didn't
set up the local server. Then:
sudo systemctl daemon-reload
sudo systemctl enable --now aria2c tg-bot-api downloadbot # omit tg-bot-api if unused
systemctl is-active aria2c downloadbot
journalctl -u downloadbot -n 20 --no-pagerHealthy log:
Configuration information loading completed!
using local Bot API server at http://127.0.0.1:8081 # only with api-base
Connecting to ws://127.0.0.1:6800/jsonrpc
Aria2 websocket connection established! Aria2 version 1.37.0
Login to authorized account YourBot
- Open your bot, send
/start. - Admin: the control keyboard appears (see below).
- Others: they get a “request sent” message and the admin receives
✅ Approve / ⛔ Deny buttons. After approval they
/startagain. - Send a link, a magnet/torrent, a video-page URL, or forward a file.
Keyboard:
⬇️ Downloading ⌛️ Waiting ✅ Finished/Stopped
⏸️ Pause task ▶️ Resume task ❌ Remove task
👥 Users (admin only)
- ⬇️ Downloading opens a live list of your active tasks (admin: all).
- ❌ Remove task lists only active items (aria2 + in-progress Telegram fetches); finished items live in ✅ Finished/Stopped.
- 👥 Users (admin) lists approved users with a ❌ Remove button that revokes access.
| Key | Description |
|---|---|
input.aria2.aria2-server |
aria2 websocket RPC endpoint, e.g. ws://127.0.0.1:6800/jsonrpc |
input.aria2.aria2-key |
aria2 --rpc-secret |
output.telegram.bot-key |
bot token from @BotFather |
output.telegram.user-id |
admin's numeric Telegram id (exactly one) |
output.telegram.api-id |
my.telegram.org app id (local Bot API server; 0 = cloud) |
output.telegram.api-hash |
my.telegram.org app hash |
output.telegram.api-base |
local server base URL, e.g. http://127.0.0.1:8081; empty = cloud (20 MB cap) |
output.telegram.api-dir |
local server data dir (enables byte-level progress for forwarded files) |
max-index |
max items shown in waiting/finished lists |
language |
UI language (en) |
downloadFolder |
aria2 download directory (must match --dir) |
organize.enabled |
enable post-download organizing |
organize.anilist |
AniList anime confirmation (default on) |
organize.deleteArchive |
delete archives after successful extraction |
organize.movies … organize.others |
media library roots |
organize.torrents |
.torrent storage (default <downloadFolder>/torrents) |
organize.keepTorrent |
keep .torrent after its content is organized (default true) |
organize.youtube |
base for YouTube downloads (default <downloadFolder>/YouTube) |
organize.services |
base for other yt-dlp services (default <downloadFolder>/Services) |
organize.ytdlpPath |
yt-dlp binary path/name |
organize.ytdlpQuality |
yt-dlp format selector |
organize.ytdlpCookies |
cookies.txt path for yt-dlp |
organize.ytdlpProxy |
proxy URL for yt-dlp |
organize.ytdlpEmbed |
embed metadata + thumbnail (needs ffmpeg) |
organize.refreshJellyfin |
trigger a Jellyfin library scan after organizing (default false) |
organize.jellyfinUrl |
Jellyfin server base URL, e.g. http://jellyfin:8096 |
organize.jellyfinApiKey |
Jellyfin API key (X-Emby-Token) |
organize.refreshPlex |
trigger a Plex library scan after organizing (default false) |
organize.plexUrl |
Plex server base URL, e.g. http://plex:32400 |
organize.plexToken |
Plex token (X-Plex-Token) |
log.logPath / log.errPath |
log files (empty = stdout only) |
log.level |
debug | info | warn | error |
movies
├── Some Movie (2024).mkv
└── Title (Year)/
└── Title (Year).mkv ← torrent/release folder grouping
series
└── Some Show
└── Season 2
└── Some.Show.S02E04.mkv
anime
└── Anime Show
└── Season 3
└── Anime.Show.S03E01.mkv
music / documents / archives / others / torrents
YouTube
└── Channel Name
├── video.mp4
└── Playlist Name
├── video1.mp4
└── video2.mp4
Services
└── ServiceName
└── Uploader
└── video.mp4
The bot is a natural front-end for a self-hosted media server. Because every finished download lands in a clean, named library folder, you can point Jellyfin or Plex at the organize directories and the downloads show up as library items — the classic *arr-style workflow, but driven from Telegram.
Point your libraries at the organize `:
| Bot folder | Jellyfin/Plex library type |
|---|---|
movies |
Movies |
series |
TV Shows |
anime |
TV Shows (separate library if you prefer) |
music |
Music |
Add each as a library in the media server, then any download the bot organizes appears there (after the server's next periodic scan).
Instant refresh (optional). To skip the wait for the periodic scan, the bot
can trigger a library rescan right after it moves files. Enable it per server in
config.json (or the matching env vars in Docker):
"organize": {
...
"refreshJellyfin": true,
"jellyfinUrl": "http://jellyfin:8096",
"jellyfinApiKey": "YOUR_API_KEY",
"refreshPlex": true,
"plexUrl": "http://plex:32400",
"plexToken": "YOUR_PLEX_TOKEN"
}- Jellyfin uses your API key (
X-Emby-Token) and posts to/Library/Refresh. - Plex uses your Plex token and hits
/library/sections/all/refresh. - A refresh runs only when something moved into a media-library category
(
movies/series/anime/music); YouTube/Services downloads, documents, archives and others are skipped. - The result is shown as a short line on the final "All done!" summary, e.g.
🔄 Jellyfin + Plex refreshed. Refresh failures are reported, never fatal.
The bot never talks to the media server's web UI — it only calls the scan endpoints, so no extra configuration is needed beyond the backend key/token.
All progress uses the same 13-segment bar and refreshes every 5 seconds.
aria2 download (auto-appears / via ⬇️ Downloading):
⬇️ Downloading
[●●●●●●●○○○○○○] 42.69 %
Downloaded: 607.06 MB of 1.39 GB
Speed: 8.80 MB/s
ETA: 4m 12s
Threads: 16
GID: d829eeca5dc91475
Forwarded Telegram file:
⬇️ Downloading
Filename: Some.Movie.2024.1080p.mkv
[●●○○○○○○○○○○○] 18.40 %
Downloaded: 229.50 MB of 1.22 GB
Speed: 5.85 MB/s ETA: 3m 22s
GID: tg1789384326596837985
Organize / extraction:
🗂 Organizing...
📦 Extracting
[●●●○○○○○○○○○○] 23.49 %
Extracted: 2.23 GB of 9.50 GB
ETA: 6m 41s
3/11 files
Final summary:
✅ All done!
📦 Archive processed
Some.Show.S01.720p.zip
🗂 Result
• 10 episodes moved
Series
└── Some Show
└── Season 1
├── Some.Show.S01E01.mkv
└── Some.Show.S01E10.mkv
📦 Size: 2.59 GB
⏱ Time taken: 2m 0s
output.telegram.user-idis the admin.- New user →
/start→ admin gets Approve / Deny buttons. - Approved users can download; denied users may
/startagain to re-request. - Visibility: regular users see only their own tasks and progress; the admin sees all users' tasks.
- Control: users can pause/resume/remove only their own tasks; the admin can control everything. Bulk pause/resume affects only your own tasks (admin: all).
- Users management: admin 👥 Users → lists approved users → ❌ Remove revokes access (they can re-request later).
State lives in users.json (roles) and tasks.json (ownership), both
git-ignored.
Forward/send any document, video, audio, GIF, voice note, video note or photo to the bot and it downloads through the same pipeline as links.
| Setup | Max file size |
|---|---|
Telegram cloud Bot API (no api-base) |
20 MB (Telegram's bot limit) |
Local Bot API server (api-base set) |
up to 2 GB |
With the local server, the bot:
- asks the server to fetch the file (progress is read from the server's
*/temp/store — real percent, speed and ETA); - hardlinks the finished file into
downloadFolder(instant, no extra disk); - deletes the server-side cache copy;
- runs the normal organizer.
Notes
- Repeatedly forwarding the same file is instant (server cache).
- Byte-level progress requires
api-dirto point at the server's data dir; without it you get an indeterminate “Fetching…” spinner, never wrong numbers. - Cancelling a fetch stops the bot's processing; the server may still finish fetching in the background.
Requirements: Go 1.24+, vendored deps (-mod=vendor).
git clone https://github.com/spaulll/AriaFlow.git
cd AriaFlow
go build -mod=vendor -trimpath -ldflags "-s -w" -o ariaflow ./cmd/DownloadBot
./ariaflow -c ./config.jsonCross-compile (supported targets):
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=vendor -trimpath -ldflags "-s -w" -o ariaflow-linux-amd64 ./cmd/DownloadBot
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -mod=vendor -trimpath -ldflags "-s -w" -o ariaflow-linux-arm64 ./cmd/DownloadBotTests:
go test ./internal/... ./tool/output/...Releases are produced by CI on v* tags — see
.github/workflows/build.yml (only
linux/amd64 and linux/arm64 are built).
# from source
git pull
go build -mod=vendor -trimpath -ldflags "-s -w" -o ariaflow ./cmd/DownloadBot
sudo systemctl restart downloadbot # add tg-bot-api if you self-host the API serverIf the config or an aria2-side setting changed, restart the affected units:
sudo systemctl restart aria2c downloadbot| Symptom | Cause / fix |
|---|---|
Bot exits with Aria2 RPC connection failed |
aria2 not running / wrong aria2-server or aria2-key |
output.telegram.user-id must be a single numeric Telegram ID |
set exactly one numeric id |
| Telegram errors, bot restarts | now non-fatal; check journalctl -u downloadbot for details |
429 Too Many Requests |
rate limit; the bot backs off automatically (updates every 5 s) |
| Forwarded file > 20 MB fails | set up the local Bot API server |
Server log: Can't lock file … tqueue.binlog |
another instance uses the same api-dir; give each its own |
| Forwarded-file progress stuck on “Fetching…” | api-dir wrong/missing — must point at the server's data dir |
| yt-dlp failures | install yt-dlp + ffmpeg, or set organize.ytdlpPath |
| Archives fail | install 7z/unrar; zip/tar are native |
| Nothing organizes | organize.enabled false, or library paths not writable |
| Torrent picker / magnet silent | dead magnet or no metadata; check journalctl -u downloadbot |
Logs:
journalctl -u downloadbot -n 50 --no-pager
journalctl -u tg-bot-api -n 50 --no-pager # if self-hosting the API server
journalctl -u aria2c -n 50 --no-pagerconfig.jsonholds the bot token and API credentials — keep it0600, never commit it.users.json/tasks.jsoncontain user ids — also git-ignored.- If a token ever leaks (logs, screenshots, chat), revoke it via @BotFather
and update
bot-key, thensystemctl restart downloadbot. - The local Bot API server runs as root by default in the sample units; use a dedicated user with access to the download/library directories if you prefer.
- Original project: DownloadBot by gaowanliang (Gaowan Liang) — Apache-2.0.
- AriaFlow is the Go evolution of an earlier shell-script media organizer
(
spaulll/AriaFlow-script). - This repository is a heavily reworked derivative: English-only UI, native Go media organization, archive extraction, yt-dlp support, multi-user management, Telegram-file forwarding and a self-hosted Bot API integration.
- Optional self-hosted API server uses tdlib/telegram-bot-api.
- Licensed under Apache-2.0 — see LICENSE and NOTICE.