跳转到内容

下载、资料库与关注列表

这一篇讲的是「留下来」而不只是「看一眼」:把视频和图片存到你自己的磁盘上、几个月后还能找回来、以及按定时反复采集同一个目标。读完之后你会清楚这三个页面各自的代价、字节到底落在哪里、容量上限会拿走什么,以及怎样保住那个你真正舍不得丢的作品。

控制台把「我见过什么」「我手上留着什么」「我还在持续采集什么」拆成了三个页面,这个拆分是有原因的:第一件事免费,第二件事会占满磁盘,第三件事会一直消耗身份池。

页面 路径 存的是什么 代价
资料库 /library 归档:本实例解析过的每一个作品各一行——标题、作者、标签、媒体清单 没有代价。它只查 Postgres,不消耗身份、不发起任何请求
下载 /downloads 下载索引:每一次「保存某作品媒体」的请求各一行,以及实际落盘的文件 磁盘;如果作品还没抓过,还要额外一次走身份池的请求
关注列表 /watchlist 长期指令:每隔 N 小时采集这个作者或这个作品,永远执行下去 只要条目还在,就按计划持续消耗身份池

第四个页面 视频转写(/transcriptions)存放开启可选的转写服务后由已存视频生成的文字;它消耗本机 CPU,不向平台发任何请求。见把已存的视频转成文字。

这些页面都在侧边栏的 内容 分组下。一个作品可以只在归档里而媒体不在磁盘上(这是常态);反过来,下载记录也可以比归档行活得更久——即使作品后来被从资料库里删了,下载表里的那一行描述的仍然是媒体卷上一个真实的目录。

真正搬运字节的是一个独立容器:downloader sidecar。它是一个需要显式开启的 Compose profile;没有它的实例是「配置正确」而不是「有问题」——归档、API、控制台和关注列表都照常工作,缺的只有抓取文件这一半。

在仓库根目录启动它:

终端窗口
echo 'DTK_DOWNLOADER_URL=http://downloader:9100' >> .env
docker compose -p dtk -f docker/compose.yml --profile downloader up -d --build

没有设置 DTK_DOWNLOADER_URL 时,POST /api/v1/downloads 会返回 501 与 NOT_CONFIGURED,消息是 “this instance has no media downloader; start the downloader compose profile and set DTK_DOWNLOADER_URL”。「下载」页面顶部会显示同样的提示卡片,页面本身仍可作为此前已存内容的只读索引使用。

sidecar 是整个栈里唯一会去连接任意 CDN 主机的容器,所以它被刻意做成了最「空」的一个:scratch 镜像里只有一个静态编译的 Go 二进制,没有 shell、没有包管理器,根文件系统只读,丢弃全部 capabilities,no-new-privileges,并且只有一个可写路径。它只接在 edge 网络上——没有任何通往 Postgres 或 Redis 的路由——而且即便共享的 env 文件里带着这些值,Compose 也会把它的 DTK_SECRET_KEY、DTK_DATABASE_URL、DTK_REDIS_URL 置空。它从来拿不到 Cookie、代理、API Key 或数据库地址;它收到的只有一份镜像 URL 列表、对应平台的主机白名单,和一个字节上限。

它自己的参数,全部从 .env 读取:

变量 默认值 作用
DTK_DOWNLOADER_URL (空) 由 api 和 worker 读取。留空即完全关闭媒体下载
DTK_DOWNLOADER_TOKEN (空) 共享密钥。留空表示 sidecar 不做校验;它监听在一个别的容器都不在的内部网络上,所以这属于纵深防御而不是边界本身
DTK_DOWNLOADER_WORKERS 4 并行处理的任务数
DTK_DOWNLOADER_ITEM_WORKERS 4 单个任务内部并行下载的文件数
DTK_DOWNLOADER_QUEUE 64 队列容量,超出后回答「下载队列已满」
DTK_DOWNLOADER_TIMEOUT_SECONDS 900 单次传输的时间上限
DTK_DOWNLOADER_HISTORY 500 内存中保留多少条已完成任务,用于汇报进度
DTK_DOWNLOADER_MAX_REDIRECTS 5 一个镜像允许的重定向跳数

并发传输的上限是这两个 worker 数的乘积——默认配置下是 16。带宽好可以调高,代价只是每个传输一个 goroutine 加一个 32 KiB 缓冲区,此外没什么开销。容器被限制在 512 MB 内存和 2 个 CPU,因为传输是流式的,常驻内存取决于缓冲区数量而不是文件大小。

任务历史保存在内存里,重启即丢失。这带来一个可见的后果:重启 sidecar 之后,它已经忘掉的那些任务的实时进度条会消失,那一行就不画进度条了。持久记录是在任务结束时写入的,所以除了进度条本身,什么都不会丢。

文件写入名为 media-data 的 Docker 命名卷,挂载在 /var/lib/dtk/media。下载器以读写方式挂载它,并且是唯一会写入的组件;api 容器以只读方式挂载,这正是控制台能把已存文件交给浏览器、而下载器又不会变成中继的原因。容器内的路径是固定的——如果你想把媒体放到宿主机的其他位置,改的是 docker/compose.yml 里的卷定义,而不是某个设置项。

目录结构是「一个作品一个目录」:

/var/lib/dtk/media/<platform>/<author_uid>/<content_id>/
├── video.mp4 # 也可能是 .mov、.webm、.m4v
├── cover.jpg # 只有一个,不会有两个
├── image-01.jpg # 图文作品:一张图一个文件
├── image-01.mp4 # 实况图的视频,和它的静图同名
├── image-02.jpg
└── meta.json

meta.json 写在文件旁边,正是它让两年后翻出来的这个文件夹仍然说得清自己是什么。它包含平台、作品 ID、类型、web_url、标题、描述、发布时间、时长、作者 uid 与昵称、音乐、标签、位置以及文件列表。它刻意不包含任何带签名的 CDN 链接——那些几小时内就会过期,会是整个文件里唯一会误导人的部分。

在抓取任何东西之前,规划器就已经决定好的事:

  • 每个作品只存一个视频。 平台给出的所有码率都被当作同一个文件的镜像——先落地的那个赢。把同一段视频存四份不是功能,是把磁盘塞满。解析器给出的顺序会被保留,而它把无水印的流排在最前。B 站视频是一个渐进式 MP4,而不是分开的音轨和画面轨,所以也只有一个文件;多 P 视频算一个作品,存下来的是第一个分 P,除非你指定别的分 P(见多 P 视频的某一 P)。用访客身份实测,这个文件在较早的投稿上最高 1080p,在当下的投稿上是 720p。
  • 实况图连视频一起存。 抖音的实况图每张都带一段几秒的 MP4,存在它的静图后面,文件名相同——image-01.webp 旁边就是 image-01.mp4。一个图集里可以混着普通静图,静图没有视频。这段视频不是作品的视频:资料库不会把它当作品播放,转写也不会读它。5.1.6 之前归档的图集,存下的媒体清单里没有这些视频,所以升级后第一次下载它时,不管链接多新都会先重新解析一次。
  • 只存一张封面,最后添加,且仅在作品还没达到文件数上限时才加。
  • 每个作品最多 128 个文件。抖音图文上限大约是 35 张,如果每张都是实况图,加上封面就是 71 个文件;这个上限是防止畸形数据变成上千项的任务,而不是给正常内容设的门槛。
  • 图片单个上限 32 MiB,若 media.max_file_bytes 更小则取更小的那个。一张 500 MB 的「图片」说明出了问题,而不是照片很大。实况图的视频也用这个上限:实测 720p 约 0.6 MB。
  • 只在认识的时候才从 URL 取扩展名——视频认 mp4、mov、webm、m4v,图片认 jpg、jpeg、png、webp、heic、gif——否则用该类型的默认扩展名。一个以 .php 结尾的查询串不可能变成你磁盘上的文件名。
  • 每个镜像都要先过媒体主机白名单,任务才会存在;被丢掉的镜像会作为 skipped 原因回报给你。

正在传输的文件写作 <name>.part,完成后才改名就位,所以一个名字出现时,它背后的文件一定是完整的。.part 存在期间也计入媒体卷总量。

「下载」页面顶部是一排模式标签。每个模式接受的输入形状不同,这正是它们不能共用一个提交按钮的原因——作品链接和主页链接是两回事,一个会猜的表单会悄悄下载错东西。

模式 接受什么 结果
单个作品 一条分享链接、包含链接的整段分享文案,或纯作品 ID 该作品的媒体存到服务器的媒体卷上
批量作品 一次粘贴很多条链接 每条链接一条解析结果,之后可一次性全部保存
评论 作品链接或 ID 一个 JSON 文件,下载到你的浏览器——评论是数据,不是媒体
作者的作品 主页链接或纯作者 ID 为该作者 feed 中的每个作品各排一次下载

跳过已下载的内容 这个勾选框由所有「可能作用于多个对象」的模式共用(除评论外都算)。它会直接返回本实例已有的那一份,而不是重新抓取——重跑一次 feed 时想要的正是这个:作者新发了三个作品,另外四十个早就在本地了。已被清理掉的副本不算,那里已经没有字节可跳过。

粘贴链接或作品 ID。纯数字以及 B 站的 BV…、av… 号按 ID 处理,其他一律按链接处理。旁边的平台菜单只在输入纯数字时才起作用,输入链接时会被禁用,因为链接自带平台信息——而且如果两者同时给出且互相矛盾,API 会直接拒绝而不是去猜。BV… 和 av… 号自带平台信息,所以会发往 B 站,菜单也会显示 B 站;选中 B 站时输入的纯数字会被当作 av 号并换算成 bvid。

会发生三种情况,提示条会告诉你是哪一种:

  • 已开始下载。 作品本来就在归档里,直接用已存的镜像生成计划并排队。响应是 202,带 download_id 和执行它的 task_id。
  • 正在先抓取再下载。 本实例从没见过这个作品。worker 会先通过和其他读取完全一样的身份池、调度器与请求日志把它抓下来,再下载抓回来的内容。首次响应会慢一些,好处是你不必先自己解析一遍。
  • 已在下载中 / 已经下载过。 请求被一条已存在的下载记录接住了(reused: "in_flight" 或 "already_stored",HTTP 200)。合流到进行中的下载永远不是可选项,无论 skip_existing 怎么设:同一个作品的两次下载写进同一个目录,竞争中输的那一方会去重命名赢家已经挪走的文件,最后变成缺文件的 partial。

短链(v.douyin.com/...、vm.tiktok.com/...、b23.tv/...)在这里会被拒绝,这是设计如此:解开短链意味着要去访问它,而这个接口不发起任何网络请求。把短链交给 POST /api/v1/parse——它会在后台展开——等你回来时作品已经归档好了。批量作品 模式可以直接接受短链,因为它走的是解析流水线。

如果作品已经归档但没有任何可下载的媒体,你会立刻拿到一个带原因的 400,而不是一分钟后失败的任务。

B 站的投稿可以分成好几 P。普通下载存的是第一 P,也就是作品链接播放的那一 P。要存别的,给 POST /api/v1/downloads 传 part(从 1 开始数),或者直接粘贴播放器所在的链接——B 站链接里的 ?p=3 就是第 3 P,会按同样的方式读取。part: 1 就是作品本身的下载,不管怎么写都一样。请求的分 P 超过最后一 P 时会失败,并说明这个视频有几 P。抖音和 TikTok 的作品没有分 P,传 part 会返回 400。

在下载页的 单个作品 模式里,B 站作品的输入框下方会多出一个 分P 菜单。它默认停在第 1 P,粘贴的链接带 ?p= 时则停在链接指定的那一 P;点 列出分P 才会把其余各 P 连同标题和时长一起列出来——要你点了才列,而不是边输入边查,因为列一次就是一次走身份池的真实请求,而大多数视频只有一 P。分 P 的下载在表格行和抽屉里都会标成 P3 这样。

每一 P 都是独立的下载,有自己的 download_id,存在作品目录旁边的单独目录里:bilibili/<mid>/<bvid>-p3/。放在旁边而不是里面,是因为删除下载会删除它的目录,嵌在作品目录里的分 P 会在第一 P 的文件被删时一起消失。同一个作品的两个分 P 可以同时下载;同一分 P 的第二个请求会加入正在进行的那个,和其他作品一样。保存到你自己的电脑时,分 P 的文件名会带上分 P(dtk-bilibili-<bvid>-p3-video.mp4),所以存到同一个文件夹里的不同分 P 不会互相覆盖。

资料库展示的是作品链接播放的内容,所以只把第一 P 的下载算作作品已保存的媒体,不算后面的分 P。转写跟着下载走,所以转写第 3 P 就是转写第 3 P 的那次下载。GET /api/v1/bilibili/video/parts 会列出每一 P 的序号、标题和时长(API)。

这个模式会把单行表单整个替换掉,换成一个文本框、它自己的结果表格和自己的工具栏。一次最多粘贴 500 条链接,提交时按每批 50 条分块送入解析流水线。

它是围绕「你手上真实拥有的输入」设计的。一段抖音分享文案长这样:2.84 nqe:/ <标题> https://v.douyin.com/L4FJNR3/ <一句话>——一条链接被散文包着。按空白切分会得到四行,其中三行是垃圾。所以这里的做法是:你输入时(停顿 300 毫秒后)整段文本会被送到 POST /api/v1/tools/parse-batch,由它用系统其他部分同一份主机白名单抽取 URL 并报告识别结果。这个接口不碰网络、不消耗身份;它最多接受 512 KiB 文本、最多返回 1000 项,被截断时会用 truncated 说明。

每一行会被判定为其中之一:

类型 可提交 含义
link 是 一条能识别的抖音、TikTok 或 B 站链接
short_link 是 需要跟随跳转的分享链接;解析流水线会展开它
content_id 否 一个不带平台信息的纯作品 ID——或一个裸的 B 站 bvid,它带着平台信息,但仍然不是链接;这个模式只提交链接
bad_id 否 看着像作品 ID 但不可能是——抖音和 TikTok 生成的 ID 高位就是它被签发时的那一秒
unknown 否 这一行里没有抖音、TikTok 或 B 站链接

被拒绝的那几类在本地就给出答案,不会消耗任务,会以失败行的形式出现在表格里并附上原因。

提交是每块一次请求,而跟踪是逐行的。每一行从此各过各的日子——自己的状态、自己的错误、自己的重试——因为一个同步的批量接口会把一条失效链接抹成整批的一个错误。轮询间隔 1.5 秒,一次 8 行,轮转推进,这样长列表的尾部不会被饿死。

结果表格的列是:状态、链接、标题、作者、平台、类型、内容 ID、播放、点赞、错误码。评论数、分享数和发布时间也有,但默认隐藏——用表格的列选择器打开。点击一条已完成的行会打开抽屉,里面有播放器、图片、各项数据、逐文件的下载按钮和原始结果载荷。实况图在静图的位置直接播放它的视频,并且单独有一个下载这段视频的按钮。

工具栏:

  • 保存到本实例 — 对每一行发出和单作品模式完全相同的请求,所以这些行同样享有进度、重试、去重和「跳过已有」这一整套行为。一条失败不会放弃其余的;提示条会报告排队了多少、已经在本地的有多少、无法排队的有多少。
  • 导出 CSV — 每条解析结果一行,保存为 dtk-parse-<时间戳>.csv。列为 url、state、error_code、platform、kind、content_id、web_url、title、author、author_uid、created_at、duration_ms、play_count、digg_count、comment_count、share_count、collect_count、video_url。含逗号、引号或换行的单元格会加引号,并且以 =、+、-、@ 开头的值前面会补一个单引号,防止表格软件把它当公式执行。
  • 导出 JSON — 同样这些行,但带完整结果载荷,保存为 dtk-parse-<时间戳>.json。
  • 重试失败项(N) — 只重试错误码不属于永久性错误的行。一条死链不值得再消耗一个身份。
  • 清空 — 清空表格。服务器上什么都不会变;这些行是浏览器里的状态,刷新页面就没了。

抽屉里的下载按钮和「保存到本实例」是两回事:它们从平台 CDN 直接抓到你的浏览器,文件落在你自己的下载文件夹里,服务器全程不中继媒体。镜像会按顺序逐个尝试。有些 CDN 会拒绝跨域读取,当所有镜像都拒绝时,第一个镜像会在新标签页打开并给出提示——那是 CORS 或防盗链,不是链接失效。

粘贴主页链接,或者纯作者 ID:抖音是资料库和调试台给你的 sec_user_id,TikTok 是 secUid(两者都以 MS4wLjABAAAA 开头),B 站是数字 mid。TikTok 的主页链接里是 @handle,作品列表接口不接受它,所以页面会先读一次主页拿到 secUid。下载多少个 菜单提供最新 20、50、100、200 个,或「feed 给多少取多少」。这里刻意用菜单而不是数字输入框:每个作品都是一次下载,feed 每翻一页都是一次走身份池的读取,而自由输入的深度会诱使人不假思索地敲一个「99999」。选「全部」时会先确认一次。

页面会以每页 20 个的节奏翻 feed 直到取够,最多翻 40 页,然后为每个作品排一次下载,并在表单下方实时显示进度:先是正在读第几页,然后是已排队、本地已有、失败各多少,以及第一个失败的原因。停止 会在任意两次请求之间结束这次运行。个别作品失败不会放弃其余的。

抖音只对已登录身份提供作者作品的第二页及以后:没有的话,第 2 页会返回 IDENTITY_LOGIN_REQUIRED。这时会保留前面几页取到的作品并为它们排队,同时提示后面的页需要已登录身份,并附上该平台已登录身份的链接。游客应答的第一页也可能漏掉作者最新的几个作品,发生时报告里会写明。

有一个坑值得知道:纯作者 ID 看不出属于哪个平台——MS4wLjABAAAA… 在抖音和 TikTok 上长得一样,mid 又是纯数字——所以会按菜单当前显示的平台去查。平台菜单正是为这种情况才启用的;框里是主页链接时它会被禁用,因为链接自带平台信息。

如果需要比 feed 一次能给出的更深的历史,请用资料库作品抽屉里的 回溯这位作者的历史,那是另一件事、另一种代价。

通过普通读取接口最多翻 5 页评论,平台允许时每页 50 条(TikTok 为 35 条;B 站的每页条数由它自己决定),然后把一个名为 comments-<platform>-<id>.json 的 JSON 文件交给你的浏览器。每一页都是一次走身份池的真实请求。评论是数据而不是媒体,所以服务器的媒体卷上不会写入任何东西,下载器也不参与。

B 站只给游客看最热的三条评论,而且没有第二页。如果是游客应答的,或者后面的页需要已登录身份,已经读到的评论照样会保存,页面会说明为什么没有更多。

列 含义
作品 已存封面的缩略图、归档里的标题,下方是作者与平台。归档对该作品一无所知时退回显示内容 ID
状态 queued、running、done、partial、failed 或 cancelled,运行中还会带一条实时进度条
平台 douyin、tiktok 或 bilibili
文件数 成功完成的文件数量
大小 磁盘上的字节数;文件已被删除的行(手动删除或容量上限清理)显示 已清理
完成时间 结束的时间
保留 置顶开关
重试 只在值得重试的已结束行上出现

partial 是一个独立状态,而不算成功也不算失败:视频落地了、第三张图 404 了的作品哪一头都不是——报成 done 会掩盖缺口,报成 failed 会掩盖磁盘上确实存在的那个视频。

按状态和平台的筛选发生在服务端,所以「最近的 TikTok 下载」指的是全部里最近的,而不是碰巧加载的那一页里最近的。排序则相反,是在浏览器里对已加载的行做的——默认按完成时间倒序。表格每页 100 行,带上一页、下一页和当前页在全部中的位置(第 101-200 条,共 340 条);上方的搜索框按归档里的作品标题、作者或作品 ID 匹配(GET /api/v1/downloads 的 q)。筛选、搜索和打开的抽屉都记在地址里,所以 /downloads?id=<下载 ID> 从任何地方都能直接打开那条下载。

有任务进行中时表格每 2 秒轮询一次,否则每 10 秒一次。进度条最多同时为 8 条进行中的行向 sidecar 实时读取。它显示已写入的字节数与总大小、百分比、最近几秒的速度和剩余时间,例如 38.2 MB / 96.7 MB · 39%,下一行是 1.4 MB/s · 剩余 42 秒。总大小来自各文件服务器在 Content-Length 里声明的大小。sidecar 在执行容量上限时从不采信这个数字,上限是按实际写入的字节强制的,但拿来画进度条足够了。还没开始下载的文件还没有声明大小,所以在一个下载的所有文件都开始之前(比如图集后面的几张图),进度条改为按文件数计算,旁边显示已写入的字节数。

点击一行会打开以作品标题命名的抽屉,里面有播放器、逐文件列表、每个文件的 SHA-256,以及每个已完成文件的保存链接。归档里有这个作品时,抽屉会链接到资料库中的它;视频已经有转写时,转写视频 会变成 查看转写。抽屉会随表格刷新跟着这一行更新:在下载进行中打开它,能看到每个文件各自的进度和速度,下载完成后会显示已保存的文件。

在 API 里,这条进度条就是 progress:GET /api/v1/downloads 的每一行和 GET /api/v1/downloads/{download_id} 都带着它。它是实时向 sidecar 读的,从不落库,所以以下情况它是 null:已结束的行;下载器没配置或没应答;sidecar 已经忘了这个任务(它只保留有限的历史,重启就全丢);以及一页里排在前 8 条之后的进行中行。null 的意思是“没有进度条”,不是“没有进展”。

字段 类型 含义
bytes 整数 目前已写入的字节数,所有文件相加
bytes_expected 整数或 null 整个下载最终的大小:已完成的文件按实际大小,进行中的文件按它声明的 Content-Length,失败的文件不计。只要有一个文件还没开始、或者它的服务器没有声明长度,就是 null——这时请像控制台那样改按文件数计算
bytes_per_second 整数 最近几秒的速度,进行中的文件相加
files_done、files_total 整数 已完成的文件数,以及任务里的文件总数
state 字符串 sidecar 自己记录的任务状态:queued、running、done、partial、failed 或 cancelled。它可能比这一行早一点结束
items 数组 每个文件一项,按任务里的顺序:name、kind(video、image 或 cover)、state(queued、running、done 或 failed)、bytes、bytes_expected(服务器没声明时为 null)和 bytes_per_second(不在下载中时为 0)

bytes_expected 只用于显示:容量上限在 sidecar 里按实际写入的字节强制执行,从不采信服务器声明的大小。

重试,以及为什么两次尝试之间没有断点续传

Section titled “重试,以及为什么两次尝试之间没有断点续传”

重试出现在 failed、partial 和 cancelled 的行上。它调用 POST /api/v1/downloads/retry 并带上 download_id,从头把同一个作品再走一遍;如果已存镜像太旧(超过 media.mirror_max_age_seconds,默认 600 秒),会先重新解析作品拿新链接。

两次尝试之间没有断点续传,而且做出来也没有意义。sidecar 写入 <name>.part 并在每次尝试时清空它,原因在上游:媒体 URL 带签名且会过期,一小时前抓下来的字节接不到一条现在返回 403 的链接上。重新请求才是有效的做法。

同一次传输之内则不同。有些 CDN 节点会提前结束传输:2026 年 10 月在 B 站实测,Akamai 节点在还没缓存某个视频时,每次都在恰好 2 MiB 处以 “unexpected EOF” 断开,一分钟后同样的请求就能完整返回。所以当镜像在声明的长度之前就断开时,sidecar 会向同一个镜像请求剩下的部分——从断开的那个字节开始发 Range 请求——最多三次,间隔只有几秒,用的还是同一条链接。只接受恰好覆盖同一个文件剩余部分的 206(If-Range 带上第一次响应的校验值),所以中途被替换的文件会被拒绝,而不是拼接上去。如果节点对「剩下的部分」这个请求返回服务器错误——实测是它还在从源站拉取文件时返回的 504——会在 1、2、4 秒后再问。下载器日志里 item stored 那一行会写明一次传输续传了几次。

下载还在进行时重试会被拒绝——第二次尝试会和第一次在同一个目录里打架。卡住的下载会在创建 两小时 后被维护任务判为失败(维护每五分钟跑一次),之后这一行就可以重试了。这个过程不会动磁盘上的任何东西:已经落地的文件原样保留。

这是会删东西的部分,值得在需要它之前先读一遍。

已存媒体的总量由 media.max_bytes 限制,默认 2 GiB。当媒体卷超过它时,维护任务会从最旧的开始删除已存下载,直到回到上限以下。这个任务具体怎么做:

  • 总量取自媒体卷本身,由 sidecar 测量,而不是取自数据库。因此手动删掉的文件、从备份恢复回来的文件都会被算进去;一个只相信自己账本的清理任务,最终会为了一个别处都不认同的数字去删文件。
  • 置顶的下载永远跳过。 这是唯一的豁免方式。
  • 只有文件会消失。记录保留它的文件列表、大小和摘要,并汇报 on_disk: false,所以控制台仍能告诉你当时收了什么、以及它后来被清理了。这个区分很重要:「收过后来被清理」可以靠重新请求撤销,「从来没抓过」则怎么都撤销不了。
  • 每一次真的删了东西的清理都会触发 media_evicted 告警(六小时最多一次)。这是你唯一能得到的「磁盘策略刚刚执行过」的通知——配置通知渠道见运维。
  • 如果超出上限的部分全是置顶内容,则什么都不删,并记录一条「没有可清理项」的警告。
  • media.max_bytes = 0 完全关闭清理。这是你可以做的选择,代价是从此不会自动清理任何东西。

置顶 在「保留」列操作,也可以选中若干行后批量操作。它让整条下载永久免于清理。任何你丢了会难受的内容都该置顶,尤其是平台已经下架的作品——那个再也抓不回来了。

和媒体上限相互独立的还有一道容量护栏,它盯的是本实例能看到的卷的实际磁盘占用。超过 capacity.warn_percent(80)会发警告;超过 capacity.hard_stop_percent(92)时,新的下载任务会被以 Retry-After: 300 拒绝、定时采集也会停下来,而所有读取照常。护栏本身不删任何东西。磁盘满了应该让实例降级,而不是让它下线。

页面顶部的四块指标分别是:已占用字节(对照上限,75% 变黄、90% 变红)、下载总数与进行中数量、置顶数量、已清理数量。

重复项、取消,以及到底什么才会真的删掉字节

Section titled “重复项、取消,以及到底什么才会真的删掉字节”

重复下载。 重复下载同一个作品是常事——第一次失败了、链接过期了、文件被清理了——每次尝试都会留下一条记录,而成功的那几次会留下完整的第二份副本。重复下载 卡片会先跑一次 dry run,把数量放进确认对话框里,因为这个数字就是整个决策本身。确认之后,每个作品保留最新的、且文件还在的那一份,其余删除。只有当没有任何保留下来的记录还在用某个目录时,该目录才会被删除——所以在两次尝试写到同一位置这种常见情形下,记录会删掉、文件会留下,这是正确结果:磁盘上本来就只有一份。如果需要删除目录而下载器不可达,它会拒绝执行,而不是留下无人指向的字节。

取消。 DELETE /api/v1/downloads/{id} 会停止一个尚未结束的传输:通知 sidecar 取消,并把记录标为 cancelled。它不删除任何东西。已经完成的文件原样保留,而对一条已经结束的下载,它会原样返回状态。

删除下载的文件。 「下载」页面的批量 删除 会对每个仍有文件在磁盘上的选中行调用 DELETE /api/v1/downloads/{id}/files。它删掉文件、保留记录,记录随后显示 已清理,并计入那块指标——和容量上限清理掉的效果一样,重新请求这次下载就能把文件取回来。同一作品的多条下载共用一个目录,所以会删掉这个目录并标记其中所有记录。已置顶或仍在进行中的下载不会被选入,接口也会拒绝:置顶是「文件会留着」的承诺,进行中的下载还在往里写。在有这个接口之前,这个按钮调用的是上面的取消接口,对已完成的行提示删除成功、实际什么都没删。

因此,会删掉已存文件的有四条路:

  1. 上面说的下载页的删除:文件删除,记录保留并显示为已清理。
  2. 在资料库里删除作品(它的批量删除),会一并删掉归档行、合集归属、下载记录和磁盘上的目录。因为记录也一起删了,它们不会出现在 已清理 里——已经没有东西可计数了。
  3. 容量上限清理,针对一切未置顶的内容。
  4. 上面说的重复项清理,针对冗余副本。

已存文件可以交给你的浏览器。在下载抽屉里,每个已完成文件都有一个 保存这个文件 链接;在资料库抽屉里,则是 导出视频文件 以及每张已存图片(和实况图的视频)、封面各一个链接。

这走的是 GET /api/v1/downloads/{download_id}/files/{name},需要 media:read 权限范围。这个接口不抓取任何东西:它用已存记录重建路径——该下载自己的目录,加上一个必须出现在这行文件列表里的名字——解析路径后再校验结果仍落在媒体根目录内。记录里没有的名字一律 404,无论磁盘上是否真的存在同名文件。文件已被清理的下载会返回 404 并带 reason: "evicted",而不会回头去平台抓;想恢复就重新请求一次这个下载。

文件会带着下载器记录的 content type,以及一个把它重命名的 Content-Disposition,其中 <stored name> 是这个文件存在卷上时的文件名:

dtk-<platform>-<content_id>-<stored name>

例如 dtk-douyin-7408915107113127220-video.mp4。在媒体卷上,每个作品独占一个目录,所以文件叫 video.mp4 也永远不会撞名。你的下载文件夹没有这样的目录——存五十个作品就是五十个 video.mp4,第二个变成 video (1).mp4,而两个名字都说不出它来自哪个作品。A-Za-z0-9._- 以外的字符会变成连字符,每一段最多 64 个字符。名字由服务端生成,所以 curl、控制台和任何其他客户端拿到的是同一个。

要注意它不是什么:服务器不会按需代理媒体。保存一个文件读的是已经落盘的那一份,而且要通过带认证的权限校验。「保存到本实例」和「保存到我的电脑」是两个不同的目的地,这也是控制台把它们分开命名的原因。

已完成下载的视频可以在本机转成文字。transcriber 边车运行 FunASR 模型——Paraformer 识别语音,FSMN-VAD 找出哪里有人在说话,CT-Transformer 补上标点——而且它所在的网络没有任何出口,所以音频不会离开这台主机。它是可选的,默认关闭:用 transcriber profile 开启,需要的约 500 MB 模型按准备 FunASR ONNX 模型获取。用一键安装脚本时,回答“开启转写”这两件事会一起做完。

批量排队之前先了解它擅长什么:模型是通用普通话模型,中文语音效果好,其他语言效果差。结果是每个视频一整段文字——没有时间戳,也不区分说话人。B 站的下载和抖音、TikTok 的一样可以转写;多 P 视频转写的是那次下载存下来的分 P——默认是第一个,除非你下载的是别的分 P。

从哪里 怎么做
控制台 下载 → 打开一个已完成的下载 → 转写视频;或者 资料库 → 打开一个已保存的视频作品 → 转写视频
REST POST /api/v1/transcriptions,请求体 {"download_id": "<uuid>"};加 ?wait=30 可以挂住连接直到完成
MCP transcribe_video(download_id)——从一个链接开始的话,顺序是 parse_url → download_media → transcribe_video

下载必须已完成,而且视频还在磁盘上。被容量上限清理掉的会被拒绝并说明原因:重新下载一次再转写。

重复请求不浪费。同一个文件已完成的转写会直接返回(200,reused 会说明),正在进行的会被合并(202),而不是重新开始。refresh: true 强制重新转写。normalize(默认开启)把口语里的数字改写成阿拉伯数字;如果这最后一步失败,由 DTK_TRANSCRIBER_NORMALIZATION_FAILURE_POLICY 决定是拿到未规范化的文字(fallback,默认),还是让这次转写失败(fail)。

/transcriptions 同样在 内容 分组里,按时间倒序列出所有转写,显示状态、文字预览、视频时长、模型,以及数字是否已规范化。进行中的会显示当前阶段——解码音频、检测语音片段、识别语音、添加标点、规范数字写法——详情抽屉里显示目前已识别的文字,在最后加上标点和数字格式之前都标为临时文本。已完成的可以导出为 .txt。

列表每页 100 条,可以按转写文字、作品标题或作品 ID 搜索;筛选和打开的抽屉都记在地址里(/transcriptions?id=<转写 ID>)。转写的抽屉会链接到它所用的下载,以及资料库中的作品。转写组件关闭时(默认如此),各处的转写按钮会变灰并说明如何开启,这个页面为空时也会这样提示。

取消转写会停止排队中或进行中的任务。删除只移除已结束的记录——文字及其元数据——不会动它来源的视频。

只在任务运行时占用 CPU:模型常驻内存约 750 MiB,一个任务会占满两个核。2026-10-16 实测,一段 3 分 13 秒的视频在独占两个核时用了 3.7 秒。和浏览器容器铸造身份共用这些核时会变慢——见转写和浏览器在同一台主机上。

边车一次只跑一个任务(DTK_TRANSCRIBER_WORKERS),后面最多排八个(DTK_TRANSCRIBER_QUEUE);再多的转写会被标为失败并写明原因,稍后重新发起即可。转写和下载一样计入 worker 队列的上限,所以批量发起时可能在到达边车之前就收到 QUEUE_FULL。

/library 就是归档:本实例解析过的全部内容,完全由本地存储回答。这里不消耗身份、不触发上游限流,作品即使已经被平台删除,也仍然在这里,并由「可用性」字段说明它的状态。这正是保留一份归档的全部意义。

顶部的指标显示作品总数、作者数、被标为 已消失(上游已删除)的数量,以及每个平台各一块。每块下面都带一行「已下载 N / M」,它同时是个链接:点击会把列表筛成媒体在本磁盘上的那些作品。「见过」和「留下了」是两个不同的数字,把前者当后者读,正是 66 个作品的归档被误以为是卷上 66 个视频的原因。

两种视图:

  • 封面(默认)— 封面墙,带时长;已保存的作品还带一块体积标签。封面优先取本实例磁盘上的那份,没有才回退到平台的,这个顺序有讲究:归档里的 cover_url 是会过期、还可能有防盗链的 CDN 链接。网格可以不分组、按作者分组,或按保存日期分组。
  • 表格 — 可排序的列,用来回答网格答不了的问题:作品、类型、时长、可用性、发布时间、最后见到。

翻页用游标而不是页码,因为你翻的同时这张表还在被写入,用 offset 会静默地跳过和重复行。「上一页」是在你已经走过的游标里回退。每页 50 个作品。

选中状态按作品键保存,并且翻页不会丢失——这里选几个、那里选几个、然后一起处理,正是设计意图。只要有选中项,视口底部就会出现批量操作条。除了合集和 删除,它还能 保存媒体(每个作品发起一次下载;已经保存过的只计数,不会重新抓取)、转写(选中项中媒体已保存在本地的视频;图集和还没保存的作品会被跳过并计数)和 导出选中项(通过 POST /api/v1/archive/export 导出为 NDJSON)。保存和转写会逐个进行,带实时计数和停止按钮,结束后报告完成、跳过和失败各多少。

筛选 取值 生效时机
搜索 标题或描述的子串 提交时
平台 douyin、tiktok、bilibili 改变即生效
类型 视频、图文 改变即生效
时长 一分钟以内(< 60 秒)、三分钟以内(< 180 秒)、长视频(≥ 180 秒)、未知 改变即生效
可用性 正常、已删除、已私密、未知 改变即生效
合集 你创建的任意合集 改变即生效
本地文件 下载与否不限 / 仅已下载 / 仅未下载 改变即生效
作者 点击卡片、表格或作品抽屉里的作者名 立即生效,显示为可移除的标签
标签 点击作品抽屉里的 #标签 立即生效,显示为可移除的标签

多个筛选条件可以叠加,而且每一个——连同打开的作品——都记在地址里:/library?author=<uid>、/library?tag=<标签> 和 /library?platform=<平台>&content_id=<ID> 这些其他页面使用的链接,从任何地方都能直接打开。菜单类的一改就生效,因为从一个封闭列表里选中「douyin」这件事本身就是决定;而文本框要等按钮,因为在你停下来之前打字还不算决定,每敲一个键就发一次查询等于在拼一个词的路上把整个归档翻一遍。

搜索是按子串匹配而不是按词匹配,这是一个有意的取舍而非疏漏:Postgres 的全文检索不会切分中文,按词搜索会在主力平台上静默地匹配不到任何东西。代价是搜一个很短的串会命中比你想要的更多的内容。

「本地文件」描述的是留下了什么,而不是作品本身:已被清理的下载不算已下载,因为已经没有东西可播了。「清空」旁边的计数会告诉你有几个筛选条件正在收窄列表,而这通常就是「为什么 660 个作品的资料库只显示了 4 个」的答案。

合集是你手动分出来的一组作品。它是资料库里唯一不从作品本身推导出来的分组方式——作者、平台、时长、保存日期都来自记录本身,只有这一个来自你的决定。

点击「清空」旁边的 合集 可以创建、查看和删除它们。名称最长 80 个字符,且大小写不敏感地唯一(由数据库保证,所以两个控制台标签页不会竞争成功);可选的备注最长 500 个字符,且永远不会被解析。

选中作品后用批量条里的 加入合集 添加。从当前合集移出 只在你正按某个合集筛选时才出现——否则「从哪个合集移出」没有答案。把已经在合集里的作品再加一次是空操作而不是错误,不在归档里的作品会被跳过,所以一张过期的卡片不会连累另外十九张。单次调用最多 500 个作品。

删除合集只删掉这个标签。作品仍留在归档中,文件也仍在磁盘上。这是本页唯一一个不碰任何作品、任何字节的删除操作。

点开一个作品会打开抽屉,底部有三个操作:

  • 回溯这位作者的历史 — 翻过第一页,把这位作者的历史作品全部归档。它和关注列表刻意分开:关注列表关注的是「新增了什么」并且永远运行下去,而回溯是一次性的、代价完全不同的任务。控制台使用服务端默认的 5 页;API 接受 1–20,worker 硬性上限是 20 页、每页 20 个作品。遇到第一个空页、达到上限,或平台表示没有更多历史时停止。每页一次上游请求,需要对应平台的读取权限范围。
  • 保存到本实例 — 为这个作品发起一次下载。如果媒体已经存过,按钮会变成 已保存 · 重新抓取,提示里会写明已存副本多大;点它会重新抓一份替换掉现有的。(这个按钮不设置 skip_existing,所以它是重新抓取而不是什么都不做。)下载是排队执行的任务,所以提示条出现的那一刻文件还没落盘——页面会在几秒后重新查询一次,而下载记录会立刻出现在「下载」页面上。
  • 导出视频文件 — 把已存的视频交给你的浏览器(如果有视频的话)。图文作品没有视频,所以这个按钮不会出现;它们的每张图(实况图的视频紧跟在它的静图后面)在抽屉正文里各有一个链接,因为浏览器不会因为一次点击就发起三十个下载,假装可以只会静默地只存下三十分之一。

抽屉还会内嵌播放已存视频,播的是本实例磁盘上的文件而不是平台的:没有会过期的 CDN 链接、没有会失败的防盗链校验,也不会有本机以外的人知道有人在看什么。它只预加载元数据,所以打开抽屉不会让一个 240 MB 的视频开始传输。

播放器下方,视频类作品有一个 转写视频 按钮,和下载抽屉里的是同一个操作;已经有转写时它会变成 查看转写。它读取的是已保存的文件,而不是平台上的,所以作品还没保存时按钮是灰的,并提示先保存。已保存的作品还会链接到对应的下载。见把已存的视频转成文字。

页面标题栏的 导出 会把归档以换行分隔的 JSON(NDJSON)流式导出,保存为 archive-YYYY-MM-DD.ndjson。

它是一页一页流式发出的,而不是在服务端拼好再返回,所以不会出现「数据越多越导不出来」的情况。一行一个作品,形状和列表接口返回的一致,但不含媒体清单——需要镜像链接时请用列表接口或 GET /api/v1/archive/{platform}/{content_id}。单次导出上限 50 000 行,超出请用列表接口的 cursor 翻页。

依赖它之前有两点要知道:

  • 它需要 archive:export 权限范围,普通读取 Key 并不带这个权限范围。这是唯一一个能把读取 Key 变成一份数据库副本的调用,所以刻意单独划成一个权限范围。控制台会话带全部权限范围,因此任何已登录用户点这个按钮都能用。
  • 导出会应用页面上的所有筛选,包括合集和「本地文件」。以前这两个会被忽略,导出一个合集拿到的是整个归档。如果要导出手动挑选的一组作品,请选中它们后用 导出选中项。

要备份整个实例(包含身份和设置),见运维里的备份工具;这里的导出只涵盖归档。

页面标题栏的 重新检查 回答的是「我保存的东西里哪些已经没了」。每个作品都是一次走身份池的真实请求,所以这个过程作为后台任务运行,并且有明确边界。

默认值都是可改的设置项:每次检查 archive.recheck_batch 个作品(25,硬上限 200),只检查上次检查距今超过 archive.recheck_after_days(7)天的作品,每次请求之间间隔 archive.recheck_pause_seconds(3)秒。这个间隔是实测出来而不是拍出来的:设成 0.5 秒时,一次真实的扫描在 13 秒内发了 20 个 TikTok 请求,全部被拒,而一分钟后手动发的单个请求却成功了——调度器是按身份限速的,这对一个按地址计数的平台限制毫无作用。没有人在等这个扫描,所以慢是免费的。

已删除的作品会返回平台自己的 not-found,而那就是结论,不是失败:记录会保留并标记为 deleted,于是它仍然可搜索、可导出,并且带着真相。私密作品标记为 private。其他任何错误都不会改动这一行,因为一个风控响应说明不了作品是否还存在。如果扫描途中某个接口的熔断器打开,扫描会提前停止,而不是把剩下的都标成未检查。

worker 大约每六小时自己排一次重新检查,所以这个按钮是给「我现在就要答案」用的。把 archive.recheck_after_days 设为 0 会彻底关闭重新检查。

因为它会消耗身份池,这个接口要求的是平台读取权限范围(douyin:read、tiktok:read 或 bilibili:read)而不是 archive:read——读归档承诺不花任何代价,而重新检查是一次披着归档名字的平台读取。

出于同一个原因,重新检查和作者回溯抓取都像读请求一样排队。worker 队列到达 sched.queue_max(500)之后,它们会返回 503 和 QUEUE_FULL,并带 Retry-After,值为 sched.max_wait_seconds(10):等这么久再发一次。在一个相同的请求还在排队时再发,会合并到它上面,而不是再排一个。worker 自己定时排的重新检查是直接提交的,不受这个上限约束。

选中作品后点批量条里的 删除。这是归档里唯一一个真正销毁东西的调用:不可撤销、没有回收站。归档行会消失,合集归属随之消失;并且除非你在 API 里传 media: false,下载记录和磁盘上的目录也会被删除。

删除顺序是先删文件、后删记录。反过来会留下一条指向已不存在目录的记录——一个控制台提供得了、却交付不了的下载;而这个顺序最坏的结果是留下没有记录的字节,而存储面板本来就会汇报这一点。

如果作品有已存媒体而下载器不可达——没运行,或者没有响应——这个调用会拒绝执行并且什么都不删,而不是在无人能核对的卷上留下孤儿字节。如果你确实想删掉记录但保留文件,那就是对 POST /api/v1/archive/delete 传 media: false;控制台永远传 true。

单次调用最多 500 个作品,需要 media:write(或 admin):能发起下载和取消下载的那个权限范围,才能删除它产生的东西,比它更弱的都不行。

平台上的原作品不受影响。这里删的是你的那一份。

/watchlist 是控制台里唯一会创建长期工作的地方。其他一切都是做一次就结束;而这里的一个条目,只要还存在,就会每隔几小时消耗一次身份池。整个页面都是围绕「让这个代价可见」设计的:间隔是一等公民的列、失败次数直接可见、全部暂停 是一个按钮而不是让你用逐行开关拼出来的东西。

它服务的是一张表:快照历史。在有东西按计划采集之前,那张表里装的只是「谁碰巧在什么时刻解析过什么」——一堆互不相关的散点,而不是一个序列。是它让增长曲线成为真的。

这个页面自己不抓取任何东西。条目到期时,worker 会把它作为普通任务提交:同一个队列、同一个调度器、同一个身份池,结束时同样写归档和快照。因此定时采集永远排在交互式请求之后——那边有人在等——而且它也跑不过限流,因为限流只有一套。

列 含义
目标 名称或 ID,下方是类型和平台;链接到资料库里它采集到的内容(TikTok 作者除外:归档按数字 ID 记录 TikTok 作者,而关注的是 secUid)
间隔 多久运行一次
下次运行 相对时间,或 已暂停
运行次数 自加入以来采集到的观测次数
上次运行 相对时间,或一条红色的「连续失败 N 次」,鼠标悬停显示最后一次的错误
运行中 启用/暂停开关
(无标题) 编辑(名称和间隔)与 移除,移除前会先确认

三块指标显示:目标总数与其中运行中的数量、总运行次数、当前失败中的数量。

五个字段:平台、关注什么、目标、可选的名称、间隔。目标可以是主页或作品链接——平台以链接为准——也可以是纯 ID。在这里或之后在 编辑 里起的名称会一直保留;留空时首次成功运行会填上作者的昵称,在编辑里清空则把名称交还给平台。

类型 目标 ID 一次运行采集什么
一个作者 作者的稳定 ID——抖音是 sec_user_id,TikTok 是 secUid(两者都以 MS4wLjAB 开头),B 站是数字 mid 他作品列表的一页(最新 20 个)。那一页的每一项本来就带着作者记录,所以一次请求同时回答了「新增了什么」和「现在多少粉丝」
一个作品 作品 ID,和归档里保存的一致 该作品的详情,也就是它的数据随时间怎么变

TikTok 的主页链接里是 @handle,作品列表接口不接受它,所以页面会先读一次主页并保存 secUid。但直接在框里输入裸的 @handle 仍然不管用:添加时没有任何地方校验 ID 的形状,所以条目会被创建——之后每一次运行都会失败,因为作品列表接口没有任何能接受 handle 的参数,它会按名字拒绝,而不是把它送到一个根本用不了的地方。同一个「平台 + 类型 + 目标」重复添加会被当作重复项拒绝。

首次运行是立即到期的。刚添加完一个作者的人想马上看到它采集,把首次运行按间隔往后推只会让这个功能在六小时里看起来是坏的。

一个条目采集什么,在这个页面上不可配置,这是有意的——原因见上表:一次请求就同时回答了两个问题。API 里的 pages 字段(1–10,默认 1)会存在条目上、也会由列表接口返回,但一次定时运行提交的是一次「最新 20 个作品」的请求;更深的历史请用资料库的 回溯这位作者的历史。

间隔、下限,以及一个持续失败的条目

Section titled “间隔、下限,以及一个持续失败的条目”

控制台提供 15 分钟、1 小时、6 小时、12 小时和 24 小时,并隐藏低于服务端下限的选项。新条目默认使用 watchlist.default_interval_seconds(6 小时),菜单里也一定列着表单将要提交的那个值。

下限是 watchlist.min_interval_seconds,默认 900 秒,而且无论怎么改都不能低于 60 秒。低于下限的间隔会被拒绝,而不是被悄悄抬高到一个你没要求过的数字。理由不是客气:每几秒采集一次的目标不会产生更好的时间序列,只会把整个身份池烧在一个作者身上,把其他所有事饿死。哪个平台上都没有任何东西变化得快到需要短于 15 分钟。

代价,方便你估算:一个条目按 6 小时计,一天 4 次请求。一百个作者按 6 小时计,一天 400 次请求,每次一页。缩短间隔会立即生效,而不是等旧的间隔走完。

退避作用在条目上,而不是任务上。连续 2 次失败之后,每多失败一次间隔翻一倍,上限 8 小时。否则一个 ID 敲错的作者会永远每次运行都失败;这样一来它变成一天试几次,而那一行连同它的错误仍然可见,留给来修它的人。平台故障总会结束,所以它仍然会被重试。重新启用一个暂停的条目会清掉退避和错误并让它立刻到期——你在断言问题已经解决,而让你为了验证这一点再等八小时,本身就是个 bug。

还有两条边界值得知道:

  • watcher 每分钟走一轮,每轮最多排 watchlist.batch_size(10)个到期条目。其余仍然到期,留到下一轮,于是同一分钟到期的一百个条目会被摊到几分钟里,而不是一次性堵在正在用 API 的人前面。
  • 下次运行是从提交时刻起算的,而不是从完成时刻。否则一个永远跑不完的任务会让这个条目永远停住,而「每六小时采集一次」即使丢了一次运行也应该继续成立。
  • 超过容量硬停线后,定时采集完全停下。没有人在等它,而让这些写入方停下正是护栏存在的意义。

三个不同的开关,作用范围由小到大:

操作 效果 如何恢复
行上的 运行中 开关 暂停该条目。它的历史、运行次数和标签都保留 再打开即可;退避和最后一次错误会被清除,并立刻到期
标题栏的 全部暂停 一次性停用所有启用中的条目。这是「出事了,我要把身份池要回来」那个按钮 只能逐个恢复,这是刻意的——没有「全部恢复」
watchlist.enabled 设置 全局停止定时采集,所有条目的配置原样保留。页面会显示一条横幅说明 把设置改回打开即可,其他什么都不变

移除 只删除这条计划,别的什么都不删。已经采集到的内容全部留在归档和快照历史里——删掉指令不等于要求删掉它产出的东西。

修改、暂停、添加和移除条目都需要 operator 及以上角色。仅查看列表只需要一个已登录的控制台会话。

以下都是运行时设置,可在控制台的「设置」页面或设置 API 中修改;设置的作用域与修改方式见配置参考。

设置项 默认值 作用
media.enabled true 是否允许媒体下载。关闭会拒绝新任务,已存文件原样保留;关闭本身不删除任何东西
media.max_bytes 2147483648(2 GiB) 已存媒体可占用的磁盘量。超出后从最旧的未置顶下载开始删除,直到回到上限以下,并发出告警说明删了什么。0 表示完全关闭清理
media.max_file_bytes 536870912(512 MiB) 单文件上限,按实际写入的字节计算,而不是按服务器声明的大小。达到上限的传输会被拒绝且不留下任何东西。必须为 0 或至少 1 MiB
media.mirror_max_age_seconds 600 归档里的媒体链接多旧之后,下载会先重新解析作品拿新的。0 表示每次都重新解析
capacity.warn_percent 80 磁盘占用达到百分之多少时发出警告
capacity.hard_stop_percent 92 磁盘占用达到百分之多少时暂停新下载任务与定时采集。交互式读取永不暂停,也不会删除任何东西
archive.enabled true 任务结果过期后仍保留解析出的作品和作者。关闭后除日志外实例不保存状态
archive.store_raw false 同时保留各平台未加工的原始载荷。默认关闭:它是本实例可以选择存储的最大一块内容
archive.recheck_after_days 7 作品上次存在性检查距今多久之后需要重新检查。0 关闭重新检查
archive.recheck_batch 25 一次重新检查扫描验证多少个作品
archive.recheck_pause_seconds 3 重新检查或回溯抓取的各次请求之间间隔多少秒
watchlist.enabled true 是否运行定时采集。关闭会保留条目及其历史,只是不再提交运行
watchlist.min_interval_seconds 900 条目允许的最短间隔。不能低于 60
watchlist.default_interval_seconds 21600(6 小时) 未指定间隔时新条目使用的间隔
watchlist.batch_size 10 一轮最多排队多少个到期条目

这三个页面背后的 API 面是刻意分开授权的:看采集到了什么、看运维者文件系统上有什么、以及往上面放东西,是三个不同的问题。

权限范围 允许
archive:read 搜索归档、读取统计、读取单个作品
archive:export 批量 NDJSON 导出,仅此一项
media:read 列出下载、读取存储用量、获取已存文件
media:write 发起、重试、置顶和取消下载;删除下载的文件;去重;创建和修改合集;删除归档作品
douyin:read / tiktok:read / bilibili:read 可用性重新检查与作者历史回溯,因为两者都消耗身份池
transcription:write 发起、刷新、取消和删除转写
transcription:read 列出转写并读取文字
identity:manage 关注列表的各个接口(/api/v1/admin/watchlist/*);其中一切写入操作还要求 operator 及以上角色
admin 全部,关注列表的各个接口也在内

由此引出两点容易搞错的事:

  • 一个只带 archive:read 的 API Key 既不能导出归档,也看不到你磁盘上的任何一个字节。这正是设计意图。给 Key 授予刚好够用的最小一组权限范围——见用户与 API 密钥。
  • 控制台会话带有全部权限范围,其边界是账号的角色而不是权限范围。只有关注列表里做写入的那些接口会额外检查角色(operator 及以上)。也就是说,任何已登录控制台的用户——包括 viewer——都能从界面上发起下载或删除归档作品。如果你不希望这样,那就不要把控制台账号发给你不愿意授予 media:write 的人。
  • 配置参考 — 这里提到的每一个设置项,以及怎么修改。
  • 核心概念 — 这些页面所消耗的身份池、调度器和任务队列。
  • 控制台总览 — 其余页面以及它们之间的关系。
  • 调试台与工具 — 解析单条链接、给请求签名,以及本页输入框用到的 URL 识别器。
  • 用户与 API 密钥 — 用正确的权限范围签发一个 Key。
  • 运维 — 告警、通知渠道、磁盘与备份。
  • REST API 指南 — 这里每个按钮背后的接口。
  • 故障排查 — 下载失败、关注条目持续失败,或磁盘被占满时怎么办。