用官方 Python 库 PyVimeo 上传视频:库内部用 tus 断点续传协议,脚本在创建视频时就设成“仅自己可见”(
privacy.view = nobody),上传后轮询转码状态,完成后输出播放链接。测试时用本地的假 Vimeo 服务跑通了 PyVimeo 的完整上传流程。
先看清成品
本机写了一个只实现 POST / HEAD / PATCH / GET 四个接口的本地假服务,让 PyVimeo 1.1.2 实际把一个 5000 字节的文件通过 tus 传了上去:服务端收到的字节和原文件完全一致,创建请求里带着
privacy.view = nobody和upload.approach = tus,轮询 3 次后转码状态变成 complete。pytest 2 个测试通过。用假 token 请求真实接口/me,返回401、error_code 8003(见调试示例)。没有用真实账号上传。
注意:Vimeo 各会员等级的价格、存储和上传额度以官方价格页为准,本文不列数字(官方上传指南里有额度表)。只上传和嵌入你有权使用的视频;不要共享会员账号或 access token;不要绕过隐私设置、密码或禁止嵌入的限制。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
vimeo_upload.py |
上传、设置隐私、等待转码 |
fake_vimeo.py |
本地假服务,离线测试用 |
| 隐私校验 | 只允许官方文档列出的几种 privacy.view 值 |
需要准备哪些环境?
| 项目 | 要求 | 说明(来自官方上传指南) |
|---|---|---|
| Vimeo 账号和 API 应用 | 在 developer.vimeo.com 创建应用 | Free 会员需要先提交工单,申请 API 上传权限 |
| 上传权限 | 应用要申请 upload access | 见 Getting Started: Uploading Videos |
| Access token | 需要 upload 和 edit 两个 scope |
在应用页面生成,只给自己用 |
| Python | 3.9 以上(本机 3.13) | pip install -r requirements.txt |
文件结构
30-vimeo-A-src/
├── vimeo_upload.py
├── fake_vimeo.py
├── test_upload.py
└── requirements.txt
第 1 步:上传流程(官方指南)
| 步骤 | 请求 | 说明 |
|---|---|---|
| 1 | POST /me/videos,upload.approach=tus,upload.size=文件大小 |
返回 uri 和 upload.upload_link;name、privacy 也可以在这一步一起设置 |
| 2 | PATCH upload_link,带 Tus-Resumable: 1.0.0、Upload-Offset |
传文件内容 |
| 3 | HEAD upload_link |
比较 Upload-Length 和 Upload-Offset,判断是否传完 |
PyVimeo 的 client.upload() 帮你完成这三步。看过源码:步骤 1 要求返回 200,否则抛出 UploadAttemptCreationFailure;tus 失败时最多重试 3 次。
privacy.view 的取值,官方表格列的是 anybody、contacts、disable、nobody、password、unlisted、users;其中 contacts 和 users 在指南里被标为已弃用。部分取值还需要额外参数(比如 password)。脚本只允许常用的 5 个。
第 2 步:脚本
vimeo_upload.py:
"""用 PyVimeo(tus 协议)上传视频,默认仅自己可见,然后等待转码完成。
用法:VIMEO_TOKEN=... python vimeo_upload.py video.mp4 "标题" "描述" """
import os, sys, time
import vimeo
def upload_data(title, description, view="nobody"):
# privacy.view:nobody=仅自己,unlisted=知道链接的人,其他取值见 API 参考
if view not in {"nobody", "unlisted", "password", "disable", "anybody"}:
raise ValueError(f"不支持的 privacy.view: {view}")
return {"name": title[:128], "description": description, "privacy": {"view": view}}
def wait_transcode(client, uri, timeout=1800, every=15, sleep=time.sleep, log=print):
waited = 0
while waited <= timeout:
r = client.get(uri + "?fields=transcode.status,link")
r.raise_for_status()
body = r.json()
status = body["transcode"]["status"]
log(f"转码状态:{status}")
if status == "complete": return body["link"]
if status == "error": raise RuntimeError("转码失败,请在 Vimeo 后台查看")
sleep(every); waited += every
raise TimeoutError("等待转码超时")
def main(path, title, desc, client=None, **kw):
client = client or vimeo.VimeoClient(token=os.environ["VIMEO_TOKEN"])
uri = client.upload(path, data=upload_data(title, desc))
print("已上传:", uri)
link = wait_transcode(client, uri, **kw)
print("可以播放了:", link)
return uri, link
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2], sys.argv[3] if len(sys.argv) > 3 else "")
第 3 步:本地假服务和测试
fake_vimeo.py:
"""本地假 Vimeo:只实现上传所需的 3 个接口,用来验证 PyVimeo 的调用流程。"""
import json, threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class State:
def __init__(self): self.received = bytearray(); self.created = None; self.polls = 0
def make_server(state):
class H(BaseHTTPRequestHandler):
def log_message(self, *a): pass
def _send(self, code, body=None, headers=None):
self.send_response(code)
for k, v in (headers or {}).items(): self.send_header(k, v)
data = json.dumps(body).encode() if body is not None else b""
self.send_header("Content-Length", str(len(data))); self.end_headers(); self.wfile.write(data)
def do_POST(self): # 创建上传
n = int(self.headers["Content-Length"]); state.created = json.loads(self.rfile.read(n))
port = self.server.server_address[1]
self._send(200, {"uri": "/videos/900001", "upload": {"upload_link": f"http://127.0.0.1:{port}/tus/900001"}})
def do_HEAD(self): # tus:查询偏移量
self._send(200, None, {"Upload-Offset": str(len(state.received)), "Tus-Resumable": "1.0.0",
"Upload-Length": str(state.created["upload"]["size"])})
def do_PATCH(self): # tus:上传一块
n = int(self.headers["Content-Length"]); state.received += self.rfile.read(n)
self._send(204, None, {"Upload-Offset": str(len(state.received)), "Tus-Resumable": "1.0.0"})
def do_GET(self): # 查询转码状态
state.polls += 1
st = "in_progress" if state.polls < 3 else "complete"
self._send(200, {"transcode": {"status": st}, "link": "https://vimeo.com/900001"})
srv = ThreadingHTTPServer(("127.0.0.1", 0), H)
threading.Thread(target=srv.serve_forever, daemon=True).start()
return srv
test_upload.py:
import pytest, vimeo
from fake_vimeo import State, make_server
import vimeo_upload as vu
def test_full_flow(tmp_path):
st = State(); srv = make_server(st)
class Local(vimeo.VimeoClient):
API_ROOT = f"http://127.0.0.1:{srv.server_address[1]}"
video = tmp_path / "v.mp4"; video.write_bytes(b"x" * 5000)
logs = []
uri, link = vu.main(str(video), "测试", "描述", client=Local(token="fake"), sleep=lambda s: None, log=logs.append)
assert uri == "/videos/900001" and link == "https://vimeo.com/900001"
assert bytes(st.received) == video.read_bytes()
assert st.created["privacy"] == {"view": "nobody"} and st.created["upload"]["approach"] == "tus"
assert logs == ["转码状态:in_progress", "转码状态:in_progress", "转码状态:complete"]
srv.shutdown()
def test_bad_view():
with pytest.raises(ValueError):
vu.upload_data("t", "d", view="public")
pip install -r requirements.txt
python -m pytest -q # 2 passed
测试里把 API_ROOT 指向本地,假服务返回的 upload_link 也指向本地,所以 PyVimeo 和 tuspy 的真实代码都会执行,只是不连接 Vimeo。
第 4 步:真实上传(本机未执行)
export VIMEO_TOKEN=你的token
python vimeo_upload.py demo.mp4 "测试上传" "仅自己可见"
预期依次输出 已上传: /videos/…、若干行 转码状态:in_progress,最后输出 可以播放了: https://vimeo.com/…。
调试示例
token 无效
401 {"error":"Something strange occurred. Please get in touch with the app's creator.",
"developer_message":"The app didn't receive the user's credentials.","error_code":8003}
这是本机用一个随便写的 token 请求 /me 得到的结果。error 字段是写给最终用户看的,内容很笼统;排查问题要看 developer_message 和 error_code。官方上传指南把“无法识别的 access token”列为 8002。实际遇到 8002 还是 8003,都要回到应用页面检查 token 是否有效,以及 scope 是否包含 upload 和 edit。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
UploadAttemptCreationFailure |
没有上传权限,或 token 缺少 scope | 申请上传权限,重新生成带 upload/edit 的 token |
| 403,错误码 4101/4102/4104 | 账号达到了上传额度上限 | 看官方指南里的额度说明 |
| 一直是 in_progress | 视频还在转码 | 加大 timeout;超时后到后台查看 |
| 后台出现很多空视频 | 先创建了视频,然后上传失败 | 指南建议文件准备好之后再创建 |
模板规则
- 创建视频时就设置好隐私,不要先公开再改。
- token 放在环境变量里,只申请需要的 scope。
- 用本地假服务测试完整流程,不用真实账号做测试。
项目资源
| 用途 | 地址 |
|---|---|
| 上传指南(tus、隐私字段、错误码) | https://developer.vimeo.com/api/upload/videos |
| 认证与 scope | https://developer.vimeo.com/api/authentication |
| 隐私设置说明 | https://developer.vimeo.com/api/guides/videos/interact |
| PyVimeo | https://github.com/vimeo/vimeo.py |
| tus 协议 | https://tus.io/protocols/resumable-upload |
| Vimeo 服务条款 | https://vimeo.com/terms |
Vimeo 的上传分成两部分:先创建视频占位,再用 tus 上传文件。把隐私设置写进创建请求,再用本地假服务把整个流程跑通,真实上传时出问题的概率就小很多。