效率工具

用官方 Python 库 PyVimeo 上传视频

用官方 Python 库 PyVimeo 上传视频:库内部用 tus 断点续传协议,脚本在创建视频时就设成“仅自己可见”(privacy.view = nobody),上传后轮询转码状态,完成后输出播放链接。测试时用本地的假 Vimeo 服务跑通了 PyVimeo 的完整上传流程。

BINBHO · 图文教程

用官方 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 上传文件。把隐私设置写进创建请求,再用本地假服务把整个流程跑通,真实上传时出问题的概率就小很多。