Douyin_TikTok_Download_API

Evil0ctalApache-2.0活跃维护

自托管的抖音、TikTok、B站数据采集与无水印视频下载 API,提供异步 REST 接口、MCP 服务、CLI 和网页控制台,支持离线语音转文字,Docker 一键部署。

🚀 自托管的抖音、TikTok、B 站数据采集与无水印视频下载 API,支持 AI agent 通过 MCP 调用与离线语音转文字,Docker 一键部署。| Self-hosted TikTok, Douyin & Bilibili scraper and no-watermark video downloader — async REST API, MCP server for AI agents, CLI and web console, plus offline speech-to-text for downloaded videos. Self-healing identity pool, PostgreSQL archive, one docker compose up.

v5.1.11发布于 2026-11-08 07:42 UTC21,335star +73 / 7 天2,961fork同步于 2026-11-10 23:25 UTC

怎么上手

由 README 提炼,执行前请确认
docker compose up
硬件要求按 README 说明

README 未说明显存或 GPU 要求;语音转文字由 FunASR 模型在无外网的 sidecar 中运行,但未给出具体硬件门槛。

纯 CPU 可运行支持 Apple Silicon有 Docker 镜像
下载:源码 zip·GitHub Releases(安装包)本站不镜像安装包

项目说明

由 README 自动翻译提炼

自托管抖音、TikTok、B站数据采集与无水印下载 API,一条 docker compose 起服务

自托管的抖音、TikTok、B站数据采集与无水印视频下载 API,提供异步 REST 接口、MCP 服务、CLI 和网页控制台,支持离线语音转文字,Docker 一键部署。

核心能力

  • 用 REST API、MCP 服务与 Web 控制台统一采集三家平台数据
  • 下载视频与图集时直接取平台已发布的干净流,不做去水印处理
  • 自维护身份池,可导入登录 Cookie 扩大可见范围
  • 采集结果存入自建 PostgreSQL,含计数快照与内容归档
  • 内置 FunASR 离线语音转文字,音频不出本机

容易踩的坑

部分接口(如抖音关注列表、TikTok 帖子搜索、B站完整评论)需导入登录 Cookie 才能用;MCP 客户端需指向实例的 /mcp 路径并带 API key;公开 demo 实例为只读且有速率限制,不能当生产用。

内容由模型根据 README 生成,可能不完整或不准确,以官方文档为准。看原文 README

最近发版

5 个版本v5.1.11
  1. v5.1.11最新正式版2026-11-08 · 2026-11-08 07:42 UTC

    What's new

    TikTok lists and search that get past the posts TikTok will not play. One post sent without a media address used to turn a whole page of an author's posts or a search into a 502. Those posts are TikTok Shop videos, whose media TikTok's web API gives to nobody, guest or signed in; they now come back with their details and no streams, and asking for one directly says so with its own error code. Page two of a TikTok post search, empty until now, pages again. Also: the console's single-post form takes the share text the apps copy, retention stops rewriting the same finished tasks on every pass, and A-Bogus signing is about 9% faster.

    Upgrading

    Installed with the script on 5.1.6 or later, in the install directory:

    ./dtkctl upgrade
    

    On 5.1.5 or earlier, dtkctl has no upgrade yet, so this once:

    cd /opt/dtk && git pull           # or ~/dtk - wherever you installed it
    bash install/install.sh --upgrade # install/install.zh.sh for the Chinese one
    

    It checks out v5.1.11, pulls 5.1.11, backs the database up, migrates, restarts and writes DTK_IMAGE_TAG=5.1.11 into .env itself; there is nothing to edit. CloakBrowser and the browser image are unchanged.

    By hand:

    cd /opt/dtk && git pull
    export COMPOSE_ENV_FILES=.env   # without this, `image:` ignores the root .env
    # DTK_IMAGE_TAG=latest needs no edit; a pinned one becomes 5.1.11 - no `v`, see the tag table
    docker compose -p dtk -f docker/compose.yml pull api worker downloader transcriber
    docker compose -p dtk -f docker/compose.yml build browser-rpc   # with --profile browser
    docker compose -p dtk -f docker/compose.yml run --rm migrate
    docker compose -p dtk -f docker/compose.yml up -d
    

    No new migrations, no new settings, and API keys need nothing. Your data is untouched.

    Added

    • MEDIA_WITHHELD (403, not retryable): the post was read in full, but the platform gives out no media for it - today, a TikTok Shop video. Asking for such a post's details, or downloading it, fails with this instead of UPSTREAM_CHANGED. If your client handles error codes, treat it like CONTENT_PRIVATE: the post exists, and no retry or login gets its media.

    Changed

    • A TikTok list can hold posts without media. Author posts, hashtag, music, related posts and post search return a TikTok Shop video with its details, media.video: null and no streams, instead of failing the page. If you download from a list, skip the posts whose media is empty. A page on which no post has media at all still answers UPSTREAM_CHANGED: that is the platform changing its format, not a few posts.
    • The TikTok post search cursor carries a search id, as in 20:2026110807205434CD61E448A521C8F318. Pass it back as you got it and nothing changes for you; if your code read it as a number, stop - the cursor has always been documented as opaque. A bare offset is still accepted, and TikTok answers it with an empty page, as before.
    • A-Bogus signing is about 9% faster. SM3's message expansion was rewritten; the signatures are byte for byte the same. Nothing to do. Thanks to @dexhunter (#821).

    Fixed

    • A TikTok author's posts or a post search failed with 502 UPSTREAM_CHANGED when one post on the page had no media address, and every other post on the page was lost with it (#819). See Changed. Thanks to @talaon2 and @codersjj.
    • Page two of a TikTok post search was always empty. TikTok answers a later page only to a request that names the search it continues, and no request did; the cursor carries it now. Searching posts still needs a signed-in TikTok identity. Thanks to @codersjj.
    • A TikTok Shop video's details were a 502 asking you to report a bug, and downloading one failed with "retry the download", which never helps. Both say MEDIA_WITHHELD now.
    • **The conso
  2. v5.1.102026-11-02 · 2026-11-02 07:43 UTC
    查看更新说明

    What's new

    Fixes for what the demo's traffic turned up. Under load an instance could run out of database connections and answer with 500s; guest identities the platforms had flagged were never retired, and the risk-control answers they kept collecting tripped the circuit breakers for everyone; comments on a post hidden from all but its author came back as a 502; and TikTok's post search, which now refuses guests, answered "not found". All four are fixed, and the console's overview no longer polls an endpoint the demo account cannot read.

    Upgrading

    Installed with the script on 5.1.6 or later, in the install directory:

    ./dtkctl upgrade
    

    On 5.1.5 or earlier, dtkctl has no upgrade yet, so this once:

    cd /opt/dtk && git pull           # or ~/dtk - wherever you installed it
    bash install/install.sh --upgrade # install/install.zh.sh for the Chinese one
    

    It checks out v5.1.10, pulls 5.1.10, backs the database up, migrates, restarts and writes DTK_IMAGE_TAG=5.1.10 into .env itself; there is nothing to edit. The postgres container is recreated once, because it now starts with max_connections=100; its data volume is untouched. CloakBrowser and the browser image are unchanged.

    By hand:

    cd /opt/dtk && git pull
    export COMPOSE_ENV_FILES=.env   # without this, `image:` ignores the root .env
    # DTK_IMAGE_TAG=latest needs no edit; a pinned one becomes 5.1.10 - no `v`, see the tag table
    docker compose -p dtk -f docker/compose.yml pull api worker downloader transcriber
    docker compose -p dtk -f docker/compose.yml build browser-rpc   # with --profile browser
    docker compose -p dtk -f docker/compose.yml run --rm migrate
    docker compose -p dtk -f docker/compose.yml up -d
    

    One thing to know. Within five minutes of starting, the worker retires the minted guest identities that have failed ten or more times in a row - on a busy pool that can be hundreds at once - and the pool refills with fresh ones. If you would rather keep every identity until you retire it yourself, set pool.retire_fail_streak to 0 on the Scheduler page. Imported identities are never retired this way.

    No new migrations, and API keys need nothing. Your data is untouched.

    Added

    • DTK_DB_POOL_SIZE and DTK_DB_MAX_OVERFLOW (5 and 5): how many database connections each of the api and the worker keeps open, and how many more it may open under load before a request waits for one. Defaults suit every install; change them only if the diagnosis tells you to.
    • A connection check in the diagnosis. Step 1 now compares what the api and the worker can open between them with Postgres' max_connections, and warns with both numbers when they do not fit.
    • pool.retire_fail_streak (10): the failure streak at which a minted guest identity is retired automatically. See Changed.

    Changed

    • Minted guests a platform keeps refusing are retired. Nothing retired an identity on its own: one the platform had flagged cooled, came back, failed and cooled again for ever. On the demo, 1273 of 1288 live Douyin identities had failed three or more times in a row and about 600 were still used each day, and their risk-control answers tripped the circuit breakers for the requests healthy identities could have served. The worker now retires minted guests at a streak of pool.retire_fail_streak, every five minutes - never an imported identity, and never on a platform with no healthy identity left, since then the platform is refusing everyone. 0 turns it off.
    • TikTok's post search needs a signed-in identity. Since 2026-11-02 it refuses a guest outright (status_code 2483, "Please login your account first"). With no login in rotation, /api/v1/tiktok/search/posts answers IDENTITY_LOGIN_REQUIRED at once instead of spending a request. The account search still answers guests and is unchanged. To search posts, import a logged-in TikTok identity.

    Fixed

    • **"too many clients alrea
  3. v5.1.92026-11-02 · 2026-11-02 06:01 UTC
    查看更新说明

    What's new

    A pool with proxies now refills past one identity per platform, and the browser image can be built where GitHub is out of reach. Adding a proxy no longer takes the machine's own IP out of minting: it counts as one more exit, and the refill card stops drawing a wait for a free exit as a row of failures. TikTok author counters now come from the exact copy TikTok sends. The browser image's Chromium download can go through a mirror you name - with what that costs spelled out - and the install README now says how to run an install day to day.

    Upgrading

    Installed with the script on 5.1.6 or later, in the install directory:

    ./dtkctl upgrade
    

    On 5.1.5 or earlier, dtkctl has no upgrade yet, so this once:

    cd /opt/dtk && git pull           # or ~/dtk - wherever you installed it
    bash install/install.sh --upgrade # install/install.zh.sh for the Chinese one
    

    It checks out v5.1.9, pulls 5.1.9, backs the database up, migrates, restarts and writes DTK_IMAGE_TAG=5.1.9 into .env itself; there is nothing to edit. CloakBrowser stays at the version pinned since 5.1.7. If rebuilding the browser image stops at Downloading from https://cloakbrowser.dev/..., see the mirror option under Added.

    By hand:

    cd /opt/dtk && git pull
    export COMPOSE_ENV_FILES=.env   # without this, `image:` ignores the root .env
    # DTK_IMAGE_TAG=latest needs no edit; a pinned one becomes 5.1.9 - no `v`, see the tag table
    docker compose -p dtk -f docker/compose.yml pull api worker downloader transcriber
    docker compose -p dtk -f docker/compose.yml build browser-rpc   # with --profile browser
    docker compose -p dtk -f docker/compose.yml run --rm migrate
    docker compose -p dtk -f docker/compose.yml up -d
    

    One thing to check. If you have proxies configured and this machine cannot reach a platform directly - TikTok from mainland China is the usual case - turn that platform's pool.<platform>.direct_egress off on the Scheduler page after upgrading. It is on by default, and without a direct route every direct mint fails and puts automatic refill into backoff. It takes effect without a restart.

    No new migrations, and API keys need nothing. Your data is untouched.

    Added

    • A download mirror for the browser's Chromium. Building the browser image downloads Chromium from cloakbrowser.dev, which redirects to a GitHub release; a build host that reaches neither stopped there (#803), and DTK_GH_PROXY does not cover that step. A build-time proxy is still the first choice. Where there is none, CLOAKBROWSER_DOWNLOAD_MIRROR in .env points the download at a mirror - but CloakBrowser does not check the release signature on a mirror, so a mirror can hand over any browser. Use only one you run or trust. The installation guide's new section, "A download mirror for the browser binary, and what it risks", gives the value to set.
    • Day-to-day dtkctl in the install README: the everyday commands, why a bare docker compose gets them wrong, what comes back on its own after a reboot, and how to reach the console from another machine (#784).

    Changed

    • With proxies configured, the machine's own IP is one more exit. Up to 5.1.8, adding a single proxy took the direct egress out of minting altogether, so automatic refill stopped at one identity per platform per proxy (#807). Now, once every proxy carries an identity of a platform, refill mints one more on the machine's IP; like a proxy it carries only one, and an identity imported with "No proxy" already occupies it. It is switched per platform by pool.douyin.direct_egress, pool.tiktok.direct_egress and pool.bilibili.direct_egress on the Scheduler page, all on by default. With no proxies at all, nothing changes.
    • Dependencies: FastAPI 0.162, Pydantic 2.14, SQLAlchemy 2.1.6, wreq 0.13 and the MCP SDK 2.3; the console now builds with TypeScript 6. Nothing to do.

    Fixed

    • **TikTok author counters come fr
  4. v5.1.82026-10-27 · 2026-10-27 11:31 UTC
    查看更新说明

    What's new

    Three more live reads, and an installer that can clone through a mirror you choose. A live room can now be asked for by its broadcast's own id, and a Douyin app share link to a live room is understood; up to 100 rooms can be checked for being on air in one request on Douyin and TikTok; and a Douyin room's audience ranking can be read. For servers that cannot reach GitHub, the installer takes an https:// mirror prefix - GitHub stays the default, and it asks before cloning from anywhere else. The console also says, before a login is imported, that the account could be banned.

    Upgrading

    Installed with the script on 5.1.6 or 5.1.7, in the install directory:

    ./dtkctl upgrade
    

    On 5.1.5 or earlier, dtkctl has no upgrade yet, so this once:

    cd /opt/dtk && git pull           # or ~/dtk - wherever you installed it
    bash install/install.sh --upgrade # install/install.zh.sh for the Chinese one
    

    It checks out v5.1.8, pulls 5.1.8, backs the database up, migrates, restarts and writes DTK_IMAGE_TAG=5.1.8 into .env itself; there is nothing to edit. CloakBrowser stays at the version 5.1.7 pinned.

    By hand:

    cd /opt/dtk && git pull
    export COMPOSE_ENV_FILES=.env   # without this, `image:` ignores the root .env
    # DTK_IMAGE_TAG=latest needs no edit; a pinned one becomes 5.1.8 - no `v`, see the tag table
    docker compose -p dtk -f docker/compose.yml pull api worker downloader transcriber
    docker compose -p dtk -f docker/compose.yml build browser-rpc   # with --profile browser
    docker compose -p dtk -f docker/compose.yml run --rm migrate
    docker compose -p dtk -f docker/compose.yml up -d
    

    No new migrations, no new settings, and API keys need nothing: the new reads are under each platform's existing <platform>:read scope. Your data is untouched.

    Added

    • A live room by its broadcast's own id, and Douyin app share links. GET /api/v1/{platform}/live takes live_id beside url and room_id, on Douyin and TikTok. A Douyin app share link to a live room - the v.douyin.com form, or what it expands to - is answered by /live, /parse, dtk fetch and MCP parse_url. Every live room answer that finds the room on air now carries its live_id, which is what the two reads below take. Bilibili numbers a room only once and answers live_id with UNSUPPORTED_CONTENT.
    • Whether up to 100 rooms are on air, in one request. GET /api/v1/{platform}/live/status?live_ids=<a>,<b>,... on Douyin and TikTok answers a {live_id, is_live} for each broadcast, each in its own place in the order asked. A broadcast that has ended and an id that never was one are both is_live: false. It costs one request however many are asked about: poll it, and ask /live for the room that has just come on.
    • A Douyin room's audience ranking. GET /api/v1/douyin/live/ranking?live_id= answers up to 200 viewers, top first, as {rank, author}. No score is returned: a guest is sent 0 for every one, and a 0 would read as a fact. A viewer who chose not to be listed, or whom the platform names to a guest only as a masked nickname, is a place with a null author.
    • All three answer a guest identity (measured 2026-10-26) and are in the console's Playground. Their per-identity rate limits are in the identities guide.
    • DTK_GH_PROXY for the installer, for servers in mainland China that cannot reach GitHub: one https:// prefix put in front of the GitHub addresses the installer uses. GitHub stays the default and no mirror is built in. The script you download first does not go through it - prefix that download by hand. The installation guide has a new section, "Cloning from a mirror, and what it risks", on what a mirror can change.

    Changed

    • The installer asks before cloning from anywhere but GitHub. A mirror, whether set with DTK_GH_PROXY or DTK_REPO_URL, hands over all of the code, docker/compose.yml and so which images run, and
  5. v5.1.72026-10-21 · 2026-10-21 11:08 UTC
    查看更新说明

    What's new

    Ten new ways to read the platforms, and an upgrade that follows the release. Beside a post and its author you can now ask for the posts recommended next to it, a sound and the posts that use it, a hashtag, keyword search, an author's own series, a live room and the trending boards - on the platforms that answer a guest for each. ./dtkctl upgrade now moves the install to the release it upgrades to, backs up before migrating, and puts everything back if a step fails. The release also stops the browser service spending a CPU core on autoplaying video nobody watches, and answers a deleted Douyin post with not-found instead of risk control (#795).

    Upgrading

    Installed with the script on 5.1.6, in the install directory:

    ./dtkctl upgrade
    

    On 5.1.5 or earlier, dtkctl has no upgrade yet, so this once:

    cd /opt/dtk && git pull           # or ~/dtk - wherever you installed it
    bash install/install.sh --upgrade # install/install.zh.sh for the Chinese one
    

    It checks out v5.1.7, pulls 5.1.7, rebuilds the browser image, backs the database up, migrates, restarts and writes DTK_IMAGE_TAG=5.1.7 into .env itself; there is nothing to edit. If any step before the restart fails, the checkout and .env go back to what they were and the running version keeps running.

    With the browser profile, the browser image is rebuilt on CloakBrowser 0.6.3, which downloads its Chromium - a few minutes on a small machine, so pick a quiet moment. Earlier installers wrote CloakBrowser's version into .env, where it stayed when the code moved on: an install from 5.1.3 or earlier that upgraded to 5.1.6 with the script was built on CloakBrowser 0.5.10 against the Playwright 1.64 that only 0.6.3 is verified with. The upgrade comments out the two values installers wrote (f04c23da… and fbf005bc…), so the build takes the version docker/compose.yml pins. A value you set yourself is kept, and the upgrade says so.

    By hand:

    cd /opt/dtk && git pull
    export COMPOSE_ENV_FILES=.env   # without this, `image:` ignores the root .env
    # DTK_IMAGE_TAG=latest needs no edit. A pinned one becomes 5.1.7 - no `v`, see the tag table.
    # If .env has CLOAKBROWSER_COMMIT=f04c23da... or =fbf005bc..., written by an
    # older installer, delete that line: the version docker/compose.yml pins is the
    # one the browser service is tested with.
    docker compose -p dtk -f docker/compose.yml pull api worker downloader transcriber
    docker compose -p dtk -f docker/compose.yml build browser-rpc   # with --profile browser
    docker compose -p dtk -f docker/compose.yml run --rm migrate
    docker compose -p dtk -f docker/compose.yml up -d
    

    No new migrations, and API keys need nothing: the new reads are under each platform's existing <platform>:read scope. Your data is untouched.

    Added

    • Ten discovery reads, on REST, the console's Playground and dtk fetch. Each was measured with a guest identity on 2026-10-20, and a platform that does not answer a guest is not offered rather than guessed at:
      • video/related - the posts each platform's own player lists next to a post, on all three. One batch, not a page.
      • music and music/posts - a sound by id or link, with how many posts use it and who published it, and the posts made with it. Douyin and TikTok.
      • hashtag - a hashtag by link or id, and on TikTok by name: its posts and views and the id its posts are listed by. hashtag/posts pages them on TikTok; Douyin answers a guest with nothing, so it is not offered there.
      • search/posts and search/users - keyword search, on TikTok and Bilibili. TikTok gives a guest a thin first page of posts and no second page of either, so post search prefers a signed-in identity and both need one to page. Douyin refuses guests search outright, so it is not offered.
      • user/mixes - the series an author published: Douyin's mixes, TikTok's playlists, Bilibili's seasons. mix/posts reads a Bilibili s

用户点评

登录用户的真实使用记录,可按版本和硬件筛选
共 0 条

    还没有人点评。第一个说出真实体验的人最有价值。

    README 原文

    未翻译

    English | 简体中文

    🚀 A self-hosted data API for Douyin, TikTok and Bilibili. One docker compose up, an identity pool that maintains itself, and a REST API, MCP server and web console on top.

    Open source, free, and it runs on your own machine — no signup, no quota, nobody else in the path. It fetches posts, authors and comments, sounds, hashtags, live rooms, trending boards and search results, downloads video and image albums without a watermark (it picks the clean stream the platform already publishes rather than stripping anything), and keeps what it collects in your own PostgreSQL. A video it has downloaded can be turned into text on the same machine, offline, and an AI agent can drive all of it over MCP.

    GitHub license Release Version GitHub Star GitHub forks GitHub issues CI CodeQL Last commit Website Live demo Python MCP Docker Pulls Docker Image Size

    💖 Sponsors

    These sponsors paid to be here, and Douyin_TikTok_Download_API stays free and open because of it. To sponsor the project, see my GitHub Sponsors page.

    🧩 REER — a forum for reverse engineering

    Somewhere to put what you work out. This project exists because people wrote down how a signature was built; the forum is for the next round of that — signatures, protocols, packers, anything taken apart.

    Free, no advertising, and close to no rules beyond the obvious. Bilingual, English and Chinese. Register and post; nothing is gated.

    🎬 What it looks like

    One real call: paste a link, send it, get the normalised result back. The identity pool, the scheduler and the API reference it passed through on the way are all in the same console. The interface follows the browser's language, and both are written by hand rather than machine-translated. 中文界面

    Try it without installing anything

    https://demo.douyin.wtf is a live instance, open to everyone. Sign in — the login page fills the demo account in for you — and use the console: the playground, the scheduler, the library, the API reference. The same account comes with an API key you can call the scraping endpoints with, shown in plaintext on the API keys page.

    It is read-only. Demo requests are not written to the request log or the archive, so the database of a public instance cannot be filled by strangers, and switching demo mode off ends every demo session and kills the key immediately.

    Rate limit: 30 requests per 10 seconds. Go over it and you are cooled down for 10 seconds, during which every request is refused. It is a shared instance, and the identity pool behind it is the ceiling for everybody using it at once.

    What the demo is for is deciding whether to run your own. It is not a service to build on — it can go down, be reset, or be switched off, and none of that will be announced.

    🚀 v4 vs v5

    v5 is a rewrite. It started from an empty branch and inherits no v4 code.

    v4's real problem was never a shortage of features — it was that the API would die quietly and nobody would know. A cookie expires, a signature algorithm changes, an endpoint gets rate-limited, and you find out when someone files an issue. v5 puts "you can see it" and "it heals itself" ahead of features.

    v4v5
    Where identities come fromYou copy cookies out of a browser into config.yamlA headless browser mints guest identities, and the pool tops itself up when usable ones run low
    How requests go outStraight out, as they arriveHealth tiers, quantised LRU rotation, one in-flight lock per identity, a token bucket per (identity, endpoint), a circuit breaker per endpoint
    When something breaksYou wait for a bug reportOne structured record per request, live health for every identity and endpoint, visible in the console
    Call styleSynchronous — send and waitAsynchronous by default (202 + task_id); add ?wait= to go back to synchronous
    What is keptNothing; parsed and discardedPostgreSQL + Redis. Everything parsed is archived, so a post deleted upstream is still here
    Access controlNone; anyone can call itAPI keys with scopes and roles, managed in the console
    InterfaceA single PyWebIO pageA React console: identity pool, scheduler, library, downloads, logs, diagnostics
    Ways inRESTREST, MCP and a CLI, all over the same service layer
    SigningX-Bogus, A_Bogusa_bogus, X-Bogus, X-Gnarly, X-Dynosaur and Bilibili's WBI in pure Python, with a browser fallback
    Deploymentpip install -r requirements.txt + python start.pydocker compose up, three images
    PlatformsDouyin, TikTok, BilibiliDouyin, TikTok, Bilibili

    Bilibili was the one thing that went backwards in 5.0, because it shares neither the signing nor the identity machinery with Douyin and TikTok. It is back in 5.1.6, with the two pieces of its own that needed writing: its WBI signature in pure Python, and guest identities minted over plain HTTP, so a Bilibili pool needs no browser container.

    Still on v4?

    v4's code stays on the v4 branch, the image is still published, and one command brings it up:

    docker run -d --name dtk-v4 --restart unless-stopped \
      -p 8080:80 evil0ctal/douyin_tiktok_download_api:V4.1.2
    

    Then open http://localhost:8080; the API reference is at /docs. Port 8080 is deliberate — v5 binds 8000, so the two versions can run side by side on one machine.

    Replace the Douyin cookie before you use it. v4 has no identity pool, and the cookie baked into the image expired long ago, so Douyin endpoints answer 400 until you put your own in:

    docker cp dtk-v4:/app/crawlers/douyin/web/config.yaml ./douyin-web.yaml
    # replace the Cookie line with one from your browser, then save
    docker rm -f dtk-v4
    docker run -d --name dtk-v4 --restart unless-stopped -p 8080:80 \
      -v "$PWD/douyin-web.yaml:/app/crawlers/douyin/web/config.yaml" \
      evil0ctal/douyin_tiktok_download_api:V4.1.2
    

    TikTok's two config files are at /app/crawlers/tiktok/web/config.yaml and /app/crawlers/tiktok/app/config.yaml, and are mounted the same way.

    To keep it under Compose instead, one file is enough — with douyin-web.yaml already copied out next to it, or Docker will create a directory under that name and the app will fail to read its config:

    # compose.yml
    services:
      app:
        image: evil0ctal/douyin_tiktok_download_api:V4.1.2
        container_name: dtk-v4
        restart: unless-stopped
        ports:
          - "8080:80"
        environment:
          TZ: Asia/Shanghai
        volumes:
          - ./douyin-web.yaml:/app/crawlers/douyin/web/config.yaml
    
    docker rm -f dtk-v4   # the container from the run above holds the name
    docker compose -p dtk-v4 up -d
    

    main is v5 now and latest is the newest v5 release — it moves when a release is cut, never on a push to main — so pin the tag to stay on v4. V4.1.2 is the last v4 release; the branch merged a few PRs after it (Bilibili downloads, cookie hot-reload), and that code is published under the commit tag 5be4838.

    There is no guided installer for v4 — the script in Quick start below installs v5. What is above is all v4 gets: the branch still takes PRs, but nothing new is built on it, and a fresh install should start on v5.

    Building this together

    There are a few group chats around my open-source projects. If you want to work on this one, or just talk shop, add me on WeChat at Evil0ctal with the note github 交流 and I will add you.

    The groups are for learning from each other. No advertising and nothing illegal — they are for making friends and talking about the work.

    📦 What it can fetch

    CapabilityDouyinTikTokBilibili
    One post (video or image album)✅✅✅
    Author profile✅✅✅
    An author's posts✅✅✅
    An author's liked posts✅✅❌
    Mixes / playlists / seasons, and an author's list of them✅✅✅
    Comments✅✅✅
    Comment replies✅✅✅
    Public folders and what is in them❌✅✅
    A video's parts, for one uploaded in several——✅
    The site's popular boards: hot, weekly, all-time picks, top 100❌❌✅
    Followers❌✅❌
    Following❌✅❌
    Posts recommended beside a post✅✅✅
    A sound, and the posts that use it✅✅—
    A hashtag✅✅—
    The posts under a hashtag❌✅—
    Keyword search: posts and accounts❌✅✅
    A live room: whether it is on, and its pull URLs✅✅✅
    A live room by its broadcast's own id, and Douyin app share links✅✅—
    Whether many live rooms are on air, in one call✅✅❌
    A live room's audience ranking✅❌❌
    Trending boards✅❌✅

    Douyin serves its follower and following lists only to a signed-in session, so those two endpoints are not registered at all: an endpoint that always returns an empty page is worth nothing. Search and the posts under a hashtag are the same story: Douyin refuses both to a guest, and no signed-in answer has been measured yet, so neither is offered there. On TikTok the account search answers a guest, while the post search refuses one outright (measured 2026-11-02), so an imported login is what it takes. Importing your own logged-in cookies widens what the rest can see, too. On Bilibili that matters most for comments: a guest is shown the three hottest and no more, so an imported login is what turns that into the full thread. On Douyin it decides an author's liked posts outright: a guest is shown none, public or not, so that list needs a logged-in identity in the pool. An imported login is used at your own risk: platforms rarely ban an account for reading data with it, but a ban cannot be ruled out (what to know before importing one).

    The dashes are not gaps: a Douyin or TikTok post is never more than one video, so it has no parts to list, and a Bilibili live room has one number for good rather than an id per broadcast.

    Media downloads, the content archive, counter snapshots, collections and a watchlist are built in; none of them needs another service.

    🤖 Let an AI agent use it: MCP

    Point Claude Code, Claude Desktop, Codex CLI, Cherry Studio or any other MCP client at /mcp on your own instance, and the agent can read posts, authors and comments, store a post's media and transcribe it. It goes through the same service layer, identity pool and scheduler as the REST API, and an API key's scopes bound what it may do.

    claude mcp add --transport http dtk http://127.0.0.1:8000/mcp/ \
      --header "Authorization: Bearer dtk_YOUR_API_KEY"
    
    ToolWhat it does
    parse_urlFetch whatever a link points at — a post, an author, a short link, pasted share text
    get_video · get_userOne post, or one author profile, by id
    list_user_posts · list_commentsAn author's posts or a post's comments, a page at a time
    get_content_historyThe recorded counter history of a post or an author, from the local database
    pool_statusIdentity-pool and endpoint health, so an agent can tell "try later" from "broken"
    download_mediaStore a post's media on this instance
    transcribe_videoTurn a stored video into text
    get_task_resultCollect a result that did not finish within the call

    Ten tools, and few on purpose: every extra tool makes an agent worse at picking the right one, so a new capability arrives as an argument to an existing tool. Setup for each client, and what every tool returns: MCP and AI agents. The console shows the same guide at /mcp-guide.

    🎙 Speech to text, on your own machine

    A downloaded video can be transcribed where it is stored. FunASR models — Paraformer for the speech, FSMN-VAD to find where someone is talking, CT-Transformer for punctuation — run in a sidecar on a network with no route out, so the audio never leaves the host and no API is called.

    • Where: the Transcriptions page in the console, a Transcribe video button on every finished download, the Library (one stored post, or a whole selection at once), POST /api/v1/transcriptions, or the transcribe_video MCP tool
    • What you get: one whole text per video, with numbers written as digits, exportable as .txt
    • How fast: a 3 min 13 s clip took 3.7 seconds on two CPU cores; no GPU needed
    • What it is good at: Mandarin. The models are the general Chinese ones, so other languages come out poorly

    It is off by default. The guided installer asks, then downloads the ~500 MB of models and checks each file against its SHA-256; by hand, see Preparing the FunASR ONNX models and Turning a stored video into text.

    🔗 Which links it accepts

    Paste whatever you have — a short link, a post URL, or the whole caption a platform app puts on your clipboard:

    https://v.douyin.com/L4NpDJ6/
    https://www.douyin.com/video/7126745726494821640
    https://www.douyin.com/jingxuan?modal_id=7660875690212492466
    https://www.tiktok.com/@evil0ctal/video/7156033831819037994
    https://www.tiktok.com/t/ZTR9nkkmL/
    2.84 nqe:/ <caption> https://v.douyin.com/L4FJNR3/ <sentence telling you to open the app>
    https://www.bilibili.com/video/BV1xx411c7mD
    https://www.bilibili.com/video/av170001
    https://b23.tv/<code>
    https://space.bilibili.com/2
    

    Short links are followed and a link buried in a caption is extracted. A post id is also checked against the platform's own encoding first, so an id that cannot exist is refused here rather than costing an upstream request. A Bilibili av number is turned into the bvid it stands for, so either spelling reaches the same post.

    ⚗️ Built with

    ServicePython 3.12 · FastAPI · SQLAlchemy 2.0 (async) · Alembic · Typer · structlog
    Transportwreq (browser TLS fingerprint emulation) · httpx
    DataPostgreSQL + TimescaleDB · Redis
    ConsoleReact 19 · TypeScript · Vite · TanStack Query · wouter · i18next
    Signinga_bogus, X-Bogus, X-Gnarly, X-Dynosaur and Bilibili's WBI in pure Python
    Identity mintingCloakBrowser, headless, in a container of its own, called over HTTP; Bilibili guests over plain HTTP, no browser
    DownloaderGo 1.23, standard library only, statically linked into a scratch image
    TranscriptionFunASR ONNX (Paraformer, FSMN-VAD, CT-Transformer), offline, in a container with no network route
    Authargon2id password hashing · API keys with scopes
    ProtocolsREST (OpenAPI) · MCP (streamable-http) · CLI
    Toolinguv · ruff · mypy · pytest · Docker Compose

    Nothing beyond Postgres and Redis is required. No Kafka, no Elasticsearch, no object store, no Kubernetes.

    CloakBrowser is pinned to a specific commit. That pin is a security control — see docker/Dockerfile.browser.

    🗂 Project layout

    First, what dtk is. Douyin_TikTok_Download_API is a mouthful, so the code says DTK instead: Douyin + TiKTok. The two platforms, and not the download or API halves of the name.

    It is the Python package (src/dtk/), the command (dtk --help), the prefix on every environment variable (DTK_SECRET_KEY and friends), the Compose project name (-p dtk), and the three letters in the console's title bar. Where you see dtk, it means Douyin_TikTok_Download_API.

    Douyin_TikTok_Download_API/
    ├── src/dtk/                the service; all of it lives here
    │   ├── api/                FastAPI routes, auth, OpenAPI localisation
    │   ├── platforms/          Douyin, TikTok and Bilibili adapters: endpoints, params, parsers
    │   ├── signing/            a_bogus / X-Bogus / X-Gnarly / X-Dynosaur / WBI
    │   ├── transport/          outbound requests, response classification
    │   ├── identity/           identity minting and health
    │   ├── scheduler/          identity selection, token buckets, circuit breakers
    │   ├── services/           the business layer, shared by REST, MCP and the CLI
    │   ├── worker/             async tasks, callbacks, scheduled collection
    │   ├── db/                 SQLAlchemy models and Alembic migrations
    │   ├── ops/                diagnostics, backups, health checks, alerting
    │   ├── media/              downloader client
    │   ├── models/             one content model across every platform
    │   ├── urls/               link recognition, short-link expansion, id validation
    │   ├── mcp/                MCP server
    │   ├── cli/                the dtk command line
    │   ├── i18n/               server-side English and Chinese strings
    │   └── core/               settings, logging, error types
    ├── web/                    the React console, built into the app image
    │   └── src/
    │       ├── pages/          one file per console page
    │       ├── components/     design system and shared components
    │       └── locales/        console English and Chinese strings
    ├── docker/                 four Dockerfiles, compose, and three sidecars
    │   ├── browser_rpc/        Python, wrapping CloakBrowser
    │   ├── downloader/         Go, the media download sidecar
    │   ├── transcriber/        Python, offline FunASR transcription
    │   └── compose.yml
    ├── documents/              user documentation, 18 pages in each language
    ├── tests/                  unit / integration / contract / replay
    ├── scripts/                smoke.sh
    ├── .github/workflows/      CI and Docker image publishing
    ├── alembic.ini
    ├── pyproject.toml
    └── Makefile
    

    ⚡️ Quick start: self-host it with Docker

    If you would rather not think about it, the guided script asks a few questions and brings the stack up — it works out your distribution, checks for Docker, and scales the resource limits to the machine:

    curl -fsSL https://raw.githubusercontent.com/Evil0ctal/Douyin_TikTok_Download_API/main/install/install.sh -o install.sh
    less install.sh    # reading it first is a good habit
    bash install.sh
    

    Afterwards the same script is the operations tool. Run it again and it finds the install and opens a menu: status, upgrade (compared against the latest GitHub release), passwords, an extra administrator, backup and restore, runtime settings, disk cleanup, stop or uninstall. --manage goes straight there.

    Details in install/README.md. The manual route follows.

    There are two ways to install this; below is the recommended one. For the by-hand route, see Without Docker.

    HowWho forFull steps
    Docker Compose (recommended)Almost everyone, production includedInstallation · First install
    By handDocker is not an option, or you are changing the codeFor development · On bare metal

    Installing from mainland China? Switch your mirrors before you start, or the pull will most likely time out: Network preparation in mainland China.

    You need Docker and Docker Compose. Start by cloning the repository — the compose file, the Dockerfiles and the migrations all live in it, and the default branch is v5:

    git clone https://github.com/Evil0ctal/Douyin_TikTok_Download_API.git
    cd Douyin_TikTok_Download_API
    

    Nothing in the repository ships a default password or key, so write .env next:

    POSTGRES_PASSWORD=$(openssl rand -hex 24)
    REDIS_PASSWORD=$(openssl rand -hex 24)
    cat > .env <<EOF
    DTK_SECRET_KEY=$(openssl rand -base64 48)
    POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    REDIS_PASSWORD=${REDIS_PASSWORD}
    DTK_DATABASE_URL=postgresql+asyncpg://dtk:${POSTGRES_PASSWORD}@postgres:5432/dtk
    DTK_REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379/0
    EOF
    
    docker compose -p dtk -f docker/compose.yml up -d
    docker compose -p dtk -f docker/compose.yml logs api   # prints the setup token
    

    Open http://127.0.0.1:8000 and use the token from the log to create the first administrator.

    That starts no browser container, and without it (--profile browser, see the optional profiles) Douyin and TikTok identities can only be imported, not minted: both hand a visitor its cookies from inside their page scripts. Bilibili guests are minted over plain HTTP either way.

    Where the image comes from

    By default it is built locally: the first up compiles the application image from the Dockerfile in this repository, which takes a few minutes. To skip that, point compose at the published image instead, in the same .env:

    cat >> .env <<'EOF'
    DTK_IMAGE=evil0ctal/douyin_tiktok_download_api
    DTK_IMAGE_TAG=latest
    EOF
    export COMPOSE_ENV_FILES=.env
    docker compose -p dtk -f docker/compose.yml pull
    docker compose -p dtk -f docker/compose.yml up -d
    

    These two are read by compose itself rather than by the containers, and compose looks for them in docker/.env unless COMPOSE_ENV_FILES says otherwise. Without the export it finds nothing, falls back to the local names and quietly builds from source again, so it belongs in every shell you run compose from. The script's dtkctl carries it for you.

    latest is the newest release and moves only when one is cut, so updating later needs nothing edited here. The pull is not optional: these services declare both image and build, so compose builds from the local Dockerfile whenever the image is not already on the machine rather than reaching for a registry.

    Images are published for linux/amd64 and linux/arm64, so Apple Silicon and a Raspberry Pi both run natively. The browser container is not published: it installs CloakBrowser from a pinned commit, and that pin is a security control that should be yours to choose, so it stays a local build.

    Without Docker

    Supported, and it is how the project itself is developed. You provide PostgreSQL 17 (with the TimescaleDB extension — a plain postgres:17 will not do), Redis 8, Python 3.12 and uv, plus Node 22 if you build the console. Two sets of steps:

    What the containers were doing for you — a non-root user, memory and CPU ceilings, a read-only root filesystem, process supervision — you put back yourself. That section lists them one by one.

    Where to look next

    What you want to doWhere
    Size a machine, change the container limitsThree sizes · Changing the limits
    Turn on the browser container, the downloader or the transcriber sidecarThe optional profiles
    Put TLS and a reverse proxy in frontBehind a reverse proxy
    Understand how the environment is actually readEnvironment variables
    Confirm an install is genuinely healthyVerifying an install
    Read the compose file line by linedocker/README.md

    🖥 What you get: console, REST API, MCP and CLI

    Entry pointWhereWhat it is
    Web console/Identity pool, scheduler, library, downloads, transcriptions, logs, diagnostics
    API reference/docsSwagger UI inside the console, English and Chinese
    Bare reference/swagger, /redocNo login required
    REST API/api/v1/...128 operations
    MCP/mcpShares the service layer with REST; client setup at /mcp-guide in the console
    CLIdtk --helpSame

    The main capabilities:

    • Parse a link, share text, short link, or a bare post id
    • Archive everything parsed, so a post deleted upstream is still here
    • Download media to your own disk, in bulk by author, skipping what you have, with duplicate cleanup
    • Transcribe a stored video to text, offline
    • Watch an author or a post and re-collect it on a timer
    • iOS Shortcut support at /api/v1/ios/shortcut

    🔄 Updating to a new release

    The console keeps an eye out for you: system.check_updates is on by default, and if a newer release exists you get one notice after signing in, at most once a day. That request goes from your browser to GitHub — the server never sends anything outward, so it does not tell anyone this instance exists. Turn it off in Settings if you would rather it did not.

    If you installed with the script, it is one command in the install directory:

    cd /opt/dtk           # or ~/dtk - wherever you installed it
    ./dtkctl upgrade      # the latest release; ./dtkctl upgrade 5.1.6 for a given one
    

    It moves the checkout to the release's tag, pulls the new images (or rebuilds them, on an install built from source), rebuilds the browser image, takes a backup, migrates, and only then writes the version it moved to into .env and restarts. Anything that fails before the restart puts the checkout and .env back, and the old version keeps running. There is nothing to edit by hand. An install from before 5.1.6 does not have upgrade yet; there, once, run git pull && bash install/install.sh --upgrade in the same directory, and from then on it is ./dtkctl upgrade. Running bash install.sh --manage and picking 2 does the same from the menu.

    By hand it is:

    cd /opt/dtk && git pull
    export COMPOSE_ENV_FILES=.env
    
    # Running the published images (recommended). Name the services you run:
    # add downloader and transcriber if you enabled them, with the same --profile flags.
    docker compose -p dtk -f docker/compose.yml pull api worker
    docker compose -p dtk -f docker/compose.yml build browser-rpc   # only with --profile browser
    docker compose -p dtk -f docker/compose.yml run --rm migrate
    docker compose -p dtk -f docker/compose.yml up -d
    

    With DTK_IMAGE_TAG=latest there is nothing to change first. A pinned version or sha- build is the one thing a pin costs: move it in .env before you pull, which is the step the script does for you. A checkout the script made sits on a release tag, where git pull has no branch to pull: there, use git fetch origin tag v5.1.7 && git checkout --detach v5.1.7. Building locally instead? Swap pull for build. migrate runs on every start and Alembic is idempotent, so running it separately is only about getting the schema up before the containers switch over.

    Your data stays put. The named volumes (postgres-data, redis-data, media-data) survive a rebuild, so the identity pool, the archive, the settings and the API keys are all where you left them.

    The browser image is built from this checkout, never pulled, so rebuild it on every update: under a tag that stays the same - latest, or a local build's dev - the old one keeps running the old code. Cached layers keep it short.

    To go back, ./dtkctl upgrade 5.1.5 with the script, or set DTK_IMAGE_TAG to the previous version or sha- by hand and up -d again. Migrations have no automatic downgrade, so the script refuses, before pulling anything, a release that lacks the database's latest migration; past one, the way back is a new install of the old version with a backup taken on it restored. Back the database up before upgrading — the script does — which is the only rollback that always works.

    📖 Documentation

    Read it online at douyin.wtf — the same 18 pages, rendered, searchable and in both languages. The markdown source is in documents/ and is what the site is built from, so the two never disagree.

    New here: Quick start · Concepts · Console overview

    Running it: Installation and deployment · Configuration reference · Operations · Troubleshooting · Security

    Building against it: REST API guide · MCP and AI agents · CLI reference · Contributing

    The endpoint reference is not in there: it is generated from the code that serves the requests, so your own instance is the copy that is never out of date. Find it at /docs inside the console, or at /swagger, /redoc and /openapi.json without a login. Both languages.

    中文文档:documents/README.zh-CN.md

    Reading this with an LLM? llms.txt is the whole documentation set as one annotated index, in the llmstxt.org format.

    📮 Contact

    IssuesGitHub Issues — public, keeps its history, and anyone who has hit the same thing can answer
    Email[email protected] — reaches one person; best for anything that does not belong in public
    Author@Evil0ctal

    Before asking, read Troubleshooting and include the output of the Diagnose page or dtk diagnose. It answers most of what a maintainer would otherwise have to ask you.

    ⭐️ Star history

    Star History Chart

    Started 2021/11/06 · GitHub @Evil0ctal

    📄 Licence

    Apache License 2.0.

    You may use, modify and distribute this project, including commercially and inside closed-source products. The grant is irrevocable. In return the licence asks you to:

    • Keep the licence text and the NOTICE file, which carries the copyright notice, with any copy you distribute
    • State what you changed, in files you modified
    • Accept that it comes with no warranty

    A request from the author

    This project is given away, and it stays free because sponsors pay for it rather than users. If you are making money from it, please consider sponsoring instead of only taking.

    This is a request, not a licence condition — Apache 2.0 permits commercial use, and nothing above takes that back.

    ☕️ Support the author

    The sponsors above pay for the project. This section is for the person who maintains it, and is entirely optional.

    NetworkAddress
    SolanaHvtkxmDERbNXfCoojpdFAYN5mSWowjpXgedsG9eF7y9z
    Tron (TRC20)TQwSM2vjcnrdRU7gY7KNp2tCgMnK33azkT
    Ethereum (ERC20)0x2f210FdfD981B59eC130370E5b1Aa8A6a06fb5Ad
    BNB Smart Chain (BEP20)0x2f210FdfD981B59eC130370E5b1Aa8A6a06fb5Ad
    Bitcoinbc1q785j55cxlnjqe8lkwy8cq57t8t9vn3ak9tlsfy

    These networks carry the usual major tokens. USDT on Tron (TRC20) or Solana is the easiest to receive, and the cheapest to send.

    Send only on the network an address is listed under. A transfer on the wrong chain cannot be recovered by anybody. Ethereum and BNB Smart Chain share one address on purpose: both are EVM chains, and the same key controls it.

    GitHub Sponsors works too.

    What you are responsible for

    This fetches data from platforms that have their own terms, and it runs on your machine under your control. Respect those terms and applicable law, respect the people whose content you collect, do not use it to harass anyone, and do not redistribute work that is not yours. Nobody else can enforce any of that for you.