Telegram 频道视频自动发布

给频道做视频自动发布时踩到的两个硬限制:Bot API 单请求 50MB(实测 46MB 过、62MB 413),以及 HLS 下载静默截断。换 MTProto 用户账号后单文件 2GB 原样直发,文中附方案对比与实测数据。

技术 · 2026-10-24 · 约 7 分钟阅读 · 标签:Telegram、MTProto、工程实践、Bot API、FFmpeg

起因:一条 68 分钟的视频,只发出去了 2 分 19 秒

流水线本身很简单:定时抓取内容源 → 入库去重 → 下载视频 → 发到自己的 Telegram 频道。这套东西内部叫 呱呱 Guagua(最早的代号是 xclib,刚改的名),跑在一台 arm64 小主机上的一只容器里。跑了几天都正常,直到频道里出现几条「没有视频、只有文字和链接」的帖子。

排查下来不是抓取的问题,是出口的体积限制——而且它不会报错给你看,只会悄悄降级。

这一篇把踩过的四个坑和最后换出口的方案记下来,都是实测数据,不是文档抄来的。


坑一:Bot API 的 50MB,比你想的更紧

Bot API 上传单文件上限 50MB,这是常识。但真正的限制是一次 HTTP 请求的体积,也就是整个 multipart body。

实测(同一个 bot,同一个频道,2 段视频的媒体组):

| 请求总体积 | 结果 | | --- | --- | | 31 MB | ✅ 成功 | | 46 MB | ✅ 成功 | | 62 MB | ❌ 413 Request Entity Too Large | | 78 MB | ❌ 同上 | | 94 MB | ❌ 同上 |

所以阈值大约在 50MB 上下:「一条消息里放几个文件」不影响额度,总字节数才影响。

这直接否决了一个看似优雅的方案:

超过 48MB 就无损切片,几段用 sendMediaGroup 塞进同一条消息,画质一点都不掉。

一次请求多段 → 413;改成逐段 sendVideo → 请求能过,但一条帖子变成三四条消息,体验直接崩掉。

而「一条消息」这个目标一旦锁死,可用的字节预算就是 48MB(50MB 留 2MB 余量)。一个 48MB 的单文件,和两个 24MB 的文件放在同一条消息里,画质完全一样——总字节一样,切片只是多了一个文件、多了一次点击。

结论:只要出口是 Bot API,「一条消息」和「48MB」是绑死的,画质没有操作空间。


坑二:下载截断,静默失败

画质问题还没解决,又发现新问题:某条 5 分钟的视频,发出去只有 2 分 19 秒。

原因在 HLS:ffmpeg 拉 m3u8 时如果中途断流,它不会报错,会老老实实把已经下载的部分封装成一个完整可播放的 mp4。文件能播、时长正常、ffprobe 也没意见——只是内容少了一半。

修法是加一道完整性校验:

  1. 先请求 m3u8 播放列表,把 #EXTINF 全部加起来,得到「应有的总时长」
  2. 下载完成后 ffprobe 拿实际时长
  3. 实际 < 应有 × 97% → 删掉重下(最多 3 次)
  4. 3 次还不完整 → 直接报错,宁可不发,也不发半截

这段代码只有二十来行,但它是「发出去的东西完整」这条底线的唯一保证。


坑三:压缩救不了画质,只会二次伤害

中间试过按目标体积反推码率的自适应压缩:

  • 由 目标体积 / 时长 反推可用视频码率
  • 按每像素比特数 bpp = 码率 / (宽 × 高 × 25) 判断,bpp ≥ 0.06 才算不糊
  • 从这个条件倒推该用 1080p 还是 720p、540p、480p、360p
  • 编码用 CRF 22 + maxrate 双约束,压完核对体积,超了就降一档再来

结果很现实:8 分钟片子压到 48MB 还能看,68 分钟的片子压到 48MB,就是 360p 以下的马赛克。源片本身码率就不高的情况下,二次压缩等于二次伤害。

所以压缩只应该当兜底,不能当方案。


换出口:把「发帖」从 Bot API 换成 MTProto

问题的根不在流水线,在出口。同一件事换条路走,限制就消失了:

  • Bot API:单文件 50MB,一次请求 50MB
  • MTProto(用户账号):单文件 2GB(Premium 4GB),一条消息发完,服务端不重编码

很多人以为「用账号发」意味着帖子署名变成个人。其实不会:频道帖永远署名频道本身,只有账号在群组里发言才显示个人名字。所以在读者眼里,换出口是零感知的。

实现上用 telethon:

  • 独立 session 文件(落在容器数据卷),不和其他 Telegram 客户端抢登录
  • 通过 HTTP 代理连 Telegram 数据中心(python-socks)
  • 发视频用 send_file + DocumentAttributeVideo(显式带时长、分辨率、supports_streaming),缩略图单独生成后一起传
  • 超时按体积估算:max(1200s, 体积MB × 8 + 600)

两个实现细节值得记一笔

1. 数字 chat_id 需要 access_hash

-100xxxxxxxxxx 这种 channel id 不能直接丢给 send_file,会报 Cannot find any entity corresponding to ...。解决办法是先用可见的用户名把频道解析成 InputPeer:

peer = await client.get_input_entity("@channel_username")

拿不到用户名时再退化成 PeerChannel(abs(id) - 10**12)。

2. 登录不需要碰服务器终端

一次性登录做成了面板里的一个页面:填手机号 → 收验证码(发到 Telegram 客户端,不是短信)→ 有二步验证就再填一次 → session 落盘。之后全自动,掉线了在面板里能直接看到状态、重新登录。

密码和 API 凭据只存在本机的 env_file(600 权限),不进仓库、不进数据库、不打日志。


结果

  • 一条 68 分钟 / 1.17GB 的视频,一条消息发完,时长和源完全一致
  • 一条 46 分钟的视频,定时任务自动发的,同样一条消息
  • 两条都没有切片、没有重编码
  • 本地不存副本:传完即删,磁盘占用从 3.2GB 归档 + 0.5GB 残留清到接近 0

整条链路现在是:抓取 → 完整性校验 → 原文件直发 → 删除本地,中间的切片、相册、压缩逻辑全部退居兜底。

顺带把项目从内部代号 xclib 正式改成了 呱呱 Guagua——名字跟频道调性对得上,也终于好念了。


顺手清掉的噪音

  • 分类只有第一页时,源站返回 410 Gone。这本来算正常情况,但之前被当成错误写进了状态栏,面板上一直挂着一条红字。改成静默跳过,并且在抓取成功时清空 last_error——错误提示要能代表真实故障,否则就是狼来了。
  • 面板加了 Basic Auth,密码单独放 env_file,不写进镜像也不写进代码。
  • 删掉两条曾经用到的容器挂载(媒体库方案放弃了),重建后健康检查恢复正常。

取舍

| 方案 | 单文件上限 | 一条消息 | 画质 | | --- | --- | --- | --- | | Bot API | 50MB | 受请求体积限制 | 长片必须压缩 | | 自建 Bot API server(--local) | 2000MB | ✅ | 原样 | | MTProto 用户账号 | 2GB | ✅ | 原样 |

自建 telegram-bot-api --local 也能解锁 2GB,还能保留 bot 身份,但它必须直连 Telegram 数据中心(--proxy 只管 webhook,不管 MTProto),在网络受限的环境里很别扭。MTProto 这条路直接复用现有代理,代价只有一个:需要一次账号登录,以及 session 掉线时得重登。

对一个「画质不能妥协、一条消息必须发完」的场景,这个代价很划算。


呱呱 Guagua 为私有自建工具,不公开仓库地址与部署信息。本文只记录工程结论与实测数据。

全部文章 · 返回 OneMJJ 首页

OneMJJ · VPS、自托管与 AI 工具与历史维护笔记。少踩坑,多留传家宝。

部分工具由第三方运营;本站命令仅供复制,不会自动执行。运行前请核对环境、阅读源码并备份重要数据。本站可能展示 Google AdSense 广告,详见隐私政策。