投稿

上传封面

https://member.bilibili.com/x/vu/web/cover/up

请求方式: POST

认证方式:Cookie(SESSDATA)

注: 目前看来上传的图片似乎不会自动删除

URL参数:

参数名类型内容必要性备注
tsnum当前时间不必要UNIX 毫秒时间戳

正文参数(application/x-www-form-urlencoded):

参数名类型内容必要性备注
csrfstrCSRF Token (位于 Cookie 中 bili_jct)必要
coverbase64视频封面必要经过 base64 编码的图片数据

JSON回复:

根对象:

字段类型内容备注
codenum返回值0: 成功
-400: 请求错误
-111: csrf 校验失败
-101: 账号未登录
messagestr错误信息默认为 0
ttlnum1
dataobj信息本体

data 对象:

字段类型内容备注
urlstr封面 URL

示例:

假设已经把需要发送的数据进行编码存放在文件 ./b64 中:

csrf=xxxxxxxxxxxx&cover=data%3Aimage%2Fjpeg%3Bbase64%2C%2F9j%2F4AAQSkZJRgABA...

发送请求:

curl -X POST --url "https://member.bilibili.com/x/vu/web/cover/up" \
--url-query "ts=$(date +%s%3N)" \
--data-binary @b64 \
-b "SESSDATA=xxxxxx; bili_jct=xxxxxx"

JavaScript (Node.js) 请求示例open in new window

查看响应示例:
{
  "code": 0,
  "message": "0",
  "ttl": 1,
  "data": {
    "url": "https://archive.biliimg.com/bfs/archive/77906db03b1eefac02613de184afad03f7bc58d7.jpg"
  }
}

预测稿件类型

https://member.bilibili.com/x/vupre/web/archive/types/predict

请求方式: POST

认证方式: Cookie(SESSDATA)

URL参数:

参数名类型内容必要性备注
tsnum当前时间不必要UNIX 毫秒时间戳
csrfstrCSRF Token (位于 Cookie 中 bili_jct)必要

正文参数(multipart/form-data):

参数名类型内容必要性备注
filenamestr视频文件名必要从视频上传接口获取, 无后缀名, 可为空
titlestr视频标题不必要
upload_idstr上传 ID不必要616368979_1723455540876_8794

JSON回复:

根对象:

字段类型内容备注
codenum返回值0: 成功
-400: 请求错误
-111: csrf 校验失败
-101: 账号未登录
messagestr错误信息默认为 0
ttlnum1
dataarray信息本体

data 数组:

类型内容备注
0obj视频类型 1
1obj视频类型 2
……obj……
nobj视频类型 (n+1)

data 数组中的对象:

字段类型内容备注
idnum子分区 ID
parentnum总分区 ID
parent_namestr总分区名称
namestr子分区名称
descriptionstr子分区描述
descstr子分区描述description
intro_originalstr原创简介说明
intro_copystr转载简介说明
noticestr注意事项
copy_rightnum版权信息?0
showbool是否显示?true
ranknum排序权重?
max_video_countnum最大视频数量?
request_idstr

示例:

curl -X POST --url 'https://member.bilibili.com/x/vupre/web/archive/types/predict' \
--url-query 'csrf=d51eadf05ba3bc6c5f76def7fbcc0185' \
--data-urlencode 'filename=' \
-b 'SESSDATA=xxx; bili_jct=xxx'
查看响应示例:
{
  "code": 0,
  "message": "0",
  "ttl": 1,
  "data": [
    {
      "id": 122,
      "parent": 36,
      "parent_name": "知识",
      "name": "野生技能协会",
      "description": "技能展示或技能教学分享类视频",
      "desc": "技能展示或技能教学分享类视频",
      "intro_original": "可对视频内容进行补充说明,并对所使用的视频素材进行标明。\n如是系列,也可附带上期视频地址。\n请勿加入涉政或具较大争议性的文字简介,否则将做打回处理。",
      "intro_copy": "转载稿件需标明出处,请注明原作者、原作者频道名或原作者投稿地址。\n可对相关内容进行补充说明。\n请勿加入涉政或具较大争议性的文字简介,否则将做打回处理。\n如是系列,也可附带上期视频地址。",
      "notice": "清晰明了表明内容亮点的标题会更受观众欢迎哟!",
      "copy_right": 0,
      "show": true,
      "rank": 75,
      "max_video_count": 100,
      "request_id": ""
    },
    {
      "id": 21,
      "parent": 160,
      "parent_name": "生活",
      "name": "日常",
      "description": "一般日常向的生活类视频",
      "desc": "一般日常向的生活类视频",
      "intro_original": "能够选择自制的必须是up主个人或工作室自己制作剪辑的视频,除此之外的搬运视频字幕制作,对于视频进行加速、慢放等简易二次创作,在视频中添加前后贴片或者打水印等行为均不被认作自制",
      "intro_copy": "转载需写明请注明转载作品详细信息原作者、原标题及出处(需为该视频最原始出处,如所标注明显为非原始出处的话会被打回)",
      "notice": "",
      "copy_right": 0,
      "show": true,
      "rank": 4,
      "max_video_count": 50,
      "request_id": ""
    },
    {
      "id": 242,
      "parent": 5,
      "parent_name": "娱乐",
      "name": "娱乐粉丝创作",
      "description": "粉丝向创作视频",
      "desc": "粉丝向创作视频",
      "intro_original": "",
      "intro_copy": "",
      "notice": "清晰明了表明内容亮点的标题会更受观众欢迎哟!",
      "copy_right": 0,
      "show": true,
      "rank": 40,
      "max_video_count": 50,
      "request_id": ""
    },
    {
      "id": 65,
      "parent": 4,
      "parent_name": "游戏",
      "name": "网络游戏",
      "description": "多人在线游戏为主要内容的相关视频",
      "desc": "多人在线游戏为主要内容的相关视频",
      "intro_original": "建议在简介和TAG中添加正确的游戏名,以便在分区和搜索中得到更好的展示。\n录制他人直播(包括授权转载、授权录制)不属于自制内容,请选转载。",
      "intro_copy": "建议在简介和TAG中添加正确的游戏名。\n搬运转载内容请添加原作者、原链接地址信息。录制他人直播内容请添加原主播信息、直播时间。\n未添加正确转载、录播信息的稿件可能被打回。",
      "notice": "【UP主/节目名】+《游戏名》+主要标题+期号",
      "copy_right": 0,
      "show": true,
      "rank": 30,
      "max_video_count": 50,
      "request_id": ""
    },
    {
      "id": 138,
      "parent": 160,
      "parent_name": "生活",
      "name": "搞笑",
      "description": "搞笑挑战、剪辑、表演、配音以及各类日常沙雕视频",
      "desc": "搞笑挑战、剪辑、表演、配音以及各类日常沙雕视频",
      "intro_original": "能够选择自制的必须是up主个人或工作室自己制作剪辑的视频,除此之外的搬运视频字幕制作,对于视频进行加速、慢放等简易二次创作,在视频中添加前后贴片或者打水印等行为均不被认作自制",
      "intro_copy": "转载需写明请注明转载作品详细信息原作者、原标题及出处(需为该视频最原始出处,如所标注明显为非原始出处的话会被打回)",
      "notice": "",
      "copy_right": 0,
      "show": true,
      "rank": 30,
      "max_video_count": 50,
      "request_id": ""
    }
  ]
}

预测稿件标签

https://member.bilibili.com/x/vupre/web/tag/recommend

请求方式: GET

认证方式: Cookie(SESSDATA)

URL参数:

参数名类型内容必要性备注
upload_idstr预测稿件类型upload_id不必要
subtype_idint子分区 ID不必要
titlestr视频标题不必要
filenamestr预测稿件类型filename不必要
descriptionstr视频简介不必要
cover_urlstr视频封面 URL不必要不含 https:http: 字串
tint当前 UNIX 毫秒时间戳不必要

JSON回复:

根对象:

字段类型内容备注
codeint返回值0: 成功
-101: 账号未登录
dataarray标签信息
messagestr错误信息默认为 0
request_idstr请求 ID

data 数组:

类型内容备注
0obj标签 1
1obj标签 2
……obj……
nobj标签 (n+1)

data 数组中的对象:

字段类型内容备注
tagstr标签名称
checkedint0
request_idstr请求 ID同根对象

示例:

curl -G 'https://member.bilibili.com/x/vupre/web/tag/recommend' \
--url-query 'subtype_id=122' \
--url-query 'title=Telnet手打HTTP' \
--url-query 'description=测试用 Telnet 手打 HTTP/1.x 协议访问本地服务器, 无 SSL/TLS 支持'
-b 'SESSDATA=xxx'
查看响应示例:
{
  "code": 0,
  "data": [
    {
      "tag": "学习",
      "checked": 0,
      "request_id": "TAG_1723543336295_3371"
    },
    {
      "tag": "编程",
      "checked": 0,
      "request_id": ""
    },
    {
      "tag": "课程",
      "checked": 0,
      "request_id": ""
    },
    {
      "tag": "学习心得",
      "checked": 0,
      "request_id": ""
    },
    {
      "tag": "经验分享",
      "checked": 0,
      "request_id": ""
    }
  ],
  "message": "0",
  "request_id": "TAG_1723543336295_3371"
}

投递视频稿件

https://member.bilibili.com/x/vu/web/add/v3

请求方式: POST

认证方式:Cookie(SESSDATA)

URL参数:

参数名类型内容必要性备注
tsnum当前时间不必要UNIX 毫秒时间戳
csrfstrCSRF Token (位于 Cookie 中 bili_jct)必要

正文参数(application/json):

根对象:

参数名类型内容必要性备注
videosarray视频信息必要若为分 P 视频, 请注意数组元素顺序
coverstr视频封面 URL必要参见上传视频封面
cover43str视频封面 URL (比例为 4:3)必要可为空
titlestr视频标题必要最多 80 字
copyrightnum1: 自制
2: 转载
必要
tidnum分类 ID必要
tagstr视频标签必要多个标签用 , 分隔, 最多 10 个
desc_format_idnum简介格式 ID?必要9999: 纯文本
descstr视频简介必要最多 2000 字
recreatenum是否允许二创必要-1: 允许(默认)
1: 不允许
dynamicstr粉丝动态必要
interactivenum互动视频?必要0: 否
act_reserve_createnum活动预约?必要0: 否
no_disturbancenum勿扰模式?必要0: 否
no_reprintnum是否允许转载必要1: 允许
0: 不允许
subtitleobj字幕信息必要
dolbynum杜比音效必要0: 否(默认)
1: 是
lossless_musicnum无损音乐必要0: 否(默认)
1: 是
up_selection_replybool精选评论必要
up_close_replybool关闭评论必要
up_close_danmubool关闭弹幕必要
web_osnum平台类型?必要3

videos 数组中的对象:

参数名类型内容必要性备注
filenamestr视频文件名必要从视频上传接口获取, 无后缀名
titlestr分 P 标题必要
descstr分 P 简介必要
cidnum分 P cid必要从视频上传接口获取, 即 biz_id

subtitle 对象:

参数名类型内容必要性备注
opennum是否启用字幕投稿必要0: 启用(默认)
1: 不启用
lanstr字幕投稿语言必要可为空

示例:

假设已经把需要发送的数据存放在文件 ./data.json 中:

{
  "videos": [
    {
      "filename": "n240728ad33h52yqhxbtw51cb06sq9gx",
      "title": "Telnet手打HTTP",
      "desc": "",
      "cid": 500001629877726
    }
  ],
  "cover": "https://archive.biliimg.com/bfs/archive/85447ea20431ef799382c403c84b4bfb82a41053.jpg",
  "cover43": "",
  "title": "Telnet手打HTTP",
  "copyright": 1,
  "tid": 122,
  "tag": "telnet,socket,tcp,linux,http",
  "desc_format_id": 9999,
  "desc": "测试用 Telnet 手打 HTTP/1.x 协议访问本地服务器, 无 SSL/TLS 支持",
  "recreate": -1,
  "dynamic": "for testing",
  "interactive": 0,
  "act_reserve_create": 0,
  "no_disturbance": 0,
  "no_reprint": 1,
  "subtitle": {
    "open": 0,
    "lan": ""
  },
  "dolby": 0,
  "lossless_music": 0,
  "up_selection_reply": false,
  "up_close_reply": false,
  "up_close_danmu": false,
  "web_os": 3,
  "csrf": "xxxxxxxxxxxxxxxxxxxxxxxx"
}

发送请求:

curl -X POST --url "https://member.bilibili.com/x/vu/web/add/v3" \
--url-query "ts=$(date +%s%3N)" \
--url-query "csrf=xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json; charset=utf-8" \
--data @data.json \
-b "SESSDATA=xxxxxx; bili_jct=xxxxxxxxxxxxxxxxxxxxxxxx"
查看响应示例:
{
  "code": 0,
  "message": "0",
  "ttl": 1,
  "data": {
    "aid": 112861976201494,
    "bvid": "BV181vnexEmB"
  }
}

上传视频文件

注: 目前看来上传的视频文件似乎不会自动删除, 而且似乎不是视频也可以上传的样子, 但是下载认证字段有效期只有 5 天

上传流程

整个上传流程较为复杂, 详细参见Demo

  1. GET preupload 接口, 获取上传元数据

  2. POST 第 1 步得到的地址, 上传视频元数据

  3. PUT 第 1 步得到的地址, 分片上传视频文件

  4. POST 第 1 步得到的地址, 结束上传视频文件

  5. GET 第 1 步得到的地址, 下载已上传的视频文件 , 确认上传成功 (可选)

上传接口

获取上传元数据 (预上传)

https://member.bilibili.com/preupload

请求方式: GET

认证方式:Cookie(SESSDATA)

URL参数:

参数名类型内容必要性备注
namestr文件名必要会影响返回的上传地址
rstr上传区域?必要upos
profilestr上传配置?必要普通视频: ugcfx/bup
提交反馈: feedback/bup
probe_versionnum上传版本?不必要20221109
upcdnstr上传 CDN?不必要txa
zonestr上传区域?不必要cs
sslnum是否使用 SSL?不必要0
versionstr上传版本?不必要2.14.0.0
buildstr上传版本?不必要2140000
sizenum文件大小不必要视频文件大小, 单位 字节
webVersionstr上传版本?不必要2.13.0

JSON回复:

根对象:

字段类型内容备注
OKnum1
authstr上传凭证作为后面请求中请求头, 有效期 5 天
biz_idnum业务 ID?
chunk_retrynum重试次数?
chunk_retry_delaynum重试延迟?
chunk_sizenum分块大小后面要用
endpointstr上传节点后面要用
endpointsarray上传节点列表
expose_paramsnull
put_querystr上传参数?
threadsnum上传线程数
timeoutnum超时时间?
uipstr你的 IP
upos_uristr上传地址后面要用

endpoints 数组:

类型内容备注
0str上传节点1
……str……
nstr上传节点n

示例:

假设视频文件名为 2024-07-28_15-37-50.mkv, 视频大小为 305333744 字节

curl -G "https://member.bilibili.com/preupload" \
--data-urlencode "name=2024-07-28_15-37-50.mkv" \
--data-urlencode "r=upos" \
--data-urlencode "profile=ugcfx/bup" \
-b "SESSDATA=xxxxxxxxxxx"
查看响应示例:
{
  "OK": 1,
  "auth": "ak=1494471752&cdn=%2F%2Fupos-cs-upcdntxa.bilivideo.com&os=upos&sign=b6c5cc520a281200906aea97e190b098&timestamp=1722155211.324&uid=616368979&uip=108.181.24.77&uport=52096&use_dqp=0",
  "biz_id": 500001630152509,
  "chunk_retry": 10,
  "chunk_retry_delay": 3,
  "chunk_size": 10485760,
  "endpoint": "//upos-cs-upcdntxa.bilivideo.com",
  "endpoints": [
    "//upos-cs-upcdntxa.bilivideo.com",
    "//upos-cs-upcdnalia.bilivideo.com"
  ],
  "expose_params": null,
  "put_query": "os=upos&profile=ugcfx%2Fbup",
  "threads": 3,
  "timeout": 1200,
  "uip": "108.181.24.77",
  "upos_uri": "upos://ugcfx2lf/n240728ad1p51if4g3ke4s3o95sznogy.mkv"
}

上传视频元数据

URL 拼接格式: "https" + 上一个接口endpoint + 上一个接口的upos_uri去掉协议名
JavaScript 模板字符串: https:${preupload.endpoint}/${endpoint.upos_uri.replace("upos://", "")}

请求方式: POST

认证方式:请求头 X-Upos-Auth 为上一接口得到的 auth

URL参数:

参数名类型内容必要性备注
uploadsstr留空必要留空
outputstr输出格式不必要默认为 json(推荐), 留空为 xml
profilestr上传配置?必要与上一个接口保持相同
filesizenum文件大小必要视频文件大小, 单位 字节
feedback/bup 不必要
partsizenum分块大小必要上一个接口返回, 且后面要用
feedback/bup 不必要
biz_idnum业务 ID?必要上一个接口返回, 且后面要用
feedback/bup 不必要

JSON回复:

根对象:

字段类型内容备注
OKnum1
bucketstr空间名?
keystr文件名?
upload_idstr上传 ID后面要用

示例:

假设上一接口返回的 authak=1494471752&cdn=%2F%2Fupos-cs-upcdntxa.bilivideo.com&os=upos&sign=4004b35628e982bc90b59cec86f8c441&timestamp=1722173443.298&uid=616368979&uip=104.28.153.18&uport=44282&use_dqp=0, biz_id500001630454700, endpoint//upos-cs-upcdntxa.bilivideo.com, upos_uriupos://ugcfx2lf/n240728adhejliqv0kqyg2s5n6huv501.mkv, chunk_size10485760. 视频文件大小为 305333744 字节.

curl -X POST --url "https://upos-cs-upcdntxa.bilivideo.com/ugcfx2lf/n240728adhejliqv0kqyg2s5n6huv501.mkv` \
--url-query "uploads=" \
--url-query "output=json" \
--url-query "profile=ugcfx/bup" \
--url-query "filesize=305333744" \
--url-query "partsize=10485760" \
--url-query "biz_id=500001630454700" \
-H "X-Upos-Auth: ak=1494471752&cdn=%2F%2Fupos-cs-upcdntxa.bilivideo.com&os=upos&sign=4004b35628e982bc90b59cec86f8c441&timestamp=1722173443.298&uid=616368979&uip=104.28.153.18&uport=44282&use_dqp=0" \
-b "SESSDATA=xxxxxxxxx"
查看响应示例:
{
  "OK": 1,
  "bucket": "ugcfx2lf",
  "key": "/n240728adhejliqv0kqyg2s5n6huv501.mkv",
  "upload_id": "26c674b4-0dce-45f5-a9cd-a199d9c982bf"
}

分片上传视频文件

URL 同 上一个接口

请求方式: PUT

认证方式:请求头 X-Upos-Auth 为上上一接口得到的 auth

URL参数:

参数名类型内容必要性备注
partNumbernum分块序号必要从 1 开始
uploadIdstr上传 ID必要上一个接口返回
chunknum分块序号必要从 0 开始
chunksnum分块总数必要自行计算: 文件大小除以分块大小并向上取整
sizenum该分块大小必要该实际上传字节数
startnum该分块开始位置必要已实际上传字节数
endnum该分块结束位置必要该分块上传结束后实际上传总字节数
totalnum总大小必要视频文件大小, 单位 字节

正文参数(application/octet-stream):

视频文件在该分块的字节流

纯文本回复:

MULTIPART_PUT_SUCCESS

示例:

假设上上一接口返回的 authak=1494471752&cdn=%2F%2Fupos-cs-upcdntxa.bilivideo.com&os=upos&sign=911dd5b995895805d785aa607b4153b6&timestamp=1722212776.333&uid=616368979&uip=108.181.24.77&uport=36044&use_dqp=0, endpoint//upos-cs-upcdntxa.bilivideo.com, upos_uriupos://ugcfx2lf/n240729ad7gxi43yaoml312h2nbt2pnf.xz, chunk_size10485760.

上一接口返回的 upload_id8130090a-16f7-4fe6-8a29-198f5abce913.

视频文件名为 20240724-remove-linux-then-install.tar.xz, 文件大小为 278255704 字节.

假设您要上传的分块序号为 1, 该分块大小为 10485760, 该分块开始位置为 0, 该分块结束位置为 10485760, 该分块实际上传字节数为 10485760, 您已将文件分块存放至 part01.tar.xz, part02.tar.xz, ..., part27.tar.xz.

curl -X PUT --url "https://upos-cs-upcdntxa.bilivideo.com/ugcfx2lf/n240729ad7gxi43yaoml312h2nbt2pnf.xz" \
--url-query "partNumber=1" \
--url-query "uploadId=8130090a-16f7-4fe6-8a29-198f5abce913" \
--url-query "chunk=0" \
--url-query "chunks=27" \
--url-query "size=10485760" \
--url-query "start=0" \
--url-query "end=10485760" \
--url-query "total=278255704" \
-H "X-Upos-Auth: ak=1494471752&cdn=%2F%2Fupos-cs-upcdntxa.bilivideo.com&os=upos&sign=911dd5b995895805d785aa607b4153b6&timestamp=1722212776.333&uid=616368979&uip=108.181.24.77&uport=36044&use_dqp=0" \
-H "Content-Type: application/octet-stream" \
--data-binary @part01.tar.xz \
-b "SESSDATA=xxxxxxxxx"
查看响应示例:
MULTIPART_PUT_SUCCESS

结束上传视频文件

URL 同 上一个接口

请求方式: POST

认证方式:请求头 X-Upos-Auth 为上上上一接口得到的 auth

URL参数:

参数名类型内容必要性备注
outputstr输出格式不必要默认为 json(推荐), 留空为 xml
namestr文件名必要视频文件名
profilestr上传配置?必要与上一个接口相同, 普通视频: ugcfx/bup
uploadIdstr上传 ID必要与上一个接口相同
biz_idnum业务 ID?必要与上上一个接口相同

正文参数(application/json):

根对象:

参数名类型内容必要性备注
partsarray各分块信息必要按实际上传顺序而不是分块序号顺序

parts 数组:

类型内容必要性备注
0obj分块信息1必要按实际上传顺序而不是分块序号顺序
1obj分块信息2必要
……obj……
nobj分块信息n必要

parts 数组中的对象:

参数名类型内容必要性备注
partNumbernum分块序号必要从 1 开始
eTagstretag必要

JSON回复:

上上一个接口 相同

示例:

假设上上上一接口返回的 authak=1494471752&cdn=%2F%2Fupos-cs-upcdntxa.bilivideo.com&os=upos&sign=911dd5b995895805d785aa607b4153b6&timestamp=1722212776.333&uid=616368979&uip=108.181.24.77&uport=36044&use_dqp=0, endpoint//upos-cs-upcdntxa.bilivideo.com, upos_uriupos://ugcfx2lf/n240729ad7gxi43yaoml312h2nbt2pnf.xz, biz_id500001630826789.

上上一接口返回的 upload_id8130090a-16f7-4fe6-8a29-198f5abce913.

视频文件名为 20240724-remove-linux-then-install.tar.xz, 文件大小为 278255704 字节.

假设您已经全部上传完毕, 共上传 27 个分块, 本次请求上传的的内容存放在 body.json 文件中.

curl -X PUT --url "https://upos-cs-upcdntxa.bilivideo.com/ugcfx2lf/n240729ad7gxi43yaoml312h2nbt2pnf.xz" \
--url-query "output=json" \
--url-query "name=20240724-remove-linux-then-install.tar.xz" \
--url-query "profile=ugcfx%2Fbup" \
--url-query "uploadId=8130090a-16f7-4fe6-8a29-198f5abce913" \
--url-query "biz_id=500001630826789" \
-H "X-Upos-Auth: ak=1494471752&cdn=%2F%2Fupos-cs-upcdntxa.bilivideo.com&os=upos&sign=911dd5b995895805d785aa607b4153b6&timestamp=1722212776.333&uid=616368979&uip=108.181.24.77&uport=36044&use_dqp=0" \
-H "Content-Type: application/json" \
--data-binary @body.json \
-b "SESSDATA=xxxxxxxxx"
查看响应示例:
{
  "OK": 1,
  "location": "ugcfx2lf/n240729ad7gxi43yaoml312h2nbt2pnf.xz",
  "bucket": "ugcfx2lf",
  "key": "/n240729ad7gxi43yaoml312h2nbt2pnf.xz"
}

下载已上传的视频文件

URL 同 上一个接口

请求方式: GET

认证方式:请求头 X-Upos-Auth 为上上上上一接口得到的 auth

注: 由于 X-Upos-Auth 有效期只有 5 天, 过期请求将返回 HTTP 403 如下

HTTP/1.1 403 Forbidden
Bili-Trace-Id: 3e3f2db61366adbf
Server: upos@hcsgw@jscs-bvc-hcsgw-public-02
X-Bili-Trace-Id: 0d8ca1af6d3510253e3f2db61366adbf
X-Upos-Auth: AUTH_TS_GT_5DAY AUTH=ak=1494471752&cdn=%2F%2Fupos-cs-upcdntxa.bilivideo.com&os=upos&sign=911dd5b995895805d785aa607b4153b6&timestamp=1722212776.333&uid=616368979&uip=108.181.24.77&uport=36044&use_dqp=0 Now=1722662669 DURATION=449893
Content-Length: 0
Connection: keep-alive
Date: Sat, 03 Aug 2024 05:24:29 GMT
EO-LOG-UUID: 4296647794590631154
EO-Cache-Status: MISS

字节流回复:

视频文件字节流

示例:

假设请求上一接口时的 URL 为 https://upos-cs-upcdntxa.bilivideo.com/ugcfx2lf/n240729ad7gxi43yaoml312h2nbt2pnf.xz, 请求头的 X-Upos-Authak=1494471752&cdn=%2F%2Fupos-cs-upcdntxa.bilivideo.com&os=upos&sign=911dd5b995895805d785aa607b4153b6&timestamp=1722212776.333&uid=616368979&uip=108.181.24.77&uport=36044&use_dqp=0, 您想要下载到运行目录下的 file.tar.xz 文件

curl -G "https://upos-cs-upcdntxa.bilivideo.com/ugcfx2lf/n240729ad7gxi43yaoml312h2nbt2pnf.xz" \
-H "X-Upos-Auth: ak=1494471752&cdn=%2F%2Fupos-cs-upcdntxa.bilivideo.com&os=upos&sign=911dd5b995895805d785aa607b4153b6&timestamp=1722212776.333&uid=616368979&uip=108.181.24.77&uport=36044&use_dqp=0" \
--output file.tar.xz
查看检查示例:
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  265M  100  265M    0     0  9493k      0  0:00:28  0:00:28 --:--:-- 10.3M
$ sha512sum file.tar.xz
abfbedf1ac4f251c81103beb4d5406af1e0b64b9d54e99bfc77d2a8a9c4913a9fd2f1751828ace8aac036f6385609d99e251437b07a0491caca2ad7069a57003  file.tar.xz
$ sha512sum ~/Documents/video-proj/20240724-remove-linux-then-install.tar.xz
abfbedf1ac4f251c81103beb4d5406af1e0b64b9d54e99bfc77d2a8a9c4913a9fd2f1751828ace8aac036f6385609d99e251437b07a0491caca2ad7069a57003  /home/sess/Documents/video-proj/20240724-remove-linux-then-install.tar.xz

Demo

Java

注: 需要 Gson 依赖, Java 8+, 单线程上传, 无异常处理, 仅供参考

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.StringJoiner;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;

/**
 * A demo class for uploading videos to Bilibili.
 * 
 * @author SessX6cf
 */
public class BiliVideoUploader {

  private static String SESSDATA;
  private static File VIDEO_FILE;

  public static void main(String[] args) throws IOException {
    long ts = System.currentTimeMillis();
    if (args.length < 2) {
      System.out.println("Usage: java BiliVideoUploader <video_file> <sessdata>");
      return;
    } else {
      VIDEO_FILE = new File(args[0]);
      if (!VIDEO_FILE.isFile()) {
        System.out.println("It is not a file!");
        return;
      } else if (!VIDEO_FILE.canRead()) {
        System.out.println("Cannot read the file!");
        return;
      } else if (VIDEO_FILE.isDirectory()) {
        System.out.println("You can play a directory?!");
        return;
      }
      SESSDATA = args[1];
    }
    // step 1: preupload video
    System.out.println("step 1: preupload video");
    JsonObject preuploadVideo = preuploadVideo();
    // step 2: post video meta
    System.out.println("step 2: post video meta");
    JsonObject postVideoMeta = postVideoMeta(preuploadVideo);
    // step 3: upload video
    System.out.println("step 3: upload video");
    int chunks = uploadVideo(preuploadVideo, postVideoMeta);
    // step 4: end upload
    System.out.println("step 4: end upload");
    endupload(preuploadVideo, postVideoMeta, chunks);
    // finished
    System.out.println("finished (" + (System.currentTimeMillis() - ts) + "ms)");
  }

  private static String querypart(String key, String value) throws IOException {
    return key + "=" + URLEncoder.encode(value, "UTF-8");
  }

  private static HttpURLConnection conn(String url, String method) throws IOException {
    HttpURLConnection conn;
    try {
      conn = (HttpURLConnection) new URI(url).toURL().openConnection();
    } catch (java.net.URISyntaxException e) {
      throw new IOException(e);
    }
    conn.setRequestMethod(method);
    // conn.setRequestProperty("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0");
    if (url.contains("bilibili.com")) conn.setRequestProperty("Cookie", "SESSDATA=" + SESSDATA);
    return conn;
  }

  private static byte[] inputStreamToString(HttpURLConnection conn) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    InputStream in;
    in = conn.getInputStream();
    int b;
    while ((b = in.read()) != -1) {
      baos.write(b);
    }
    in.close();
    return baos.toByteArray();
  }

  private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();

  private static JsonObject preuploadVideo() throws IOException {
    StringJoiner url = new StringJoiner("&", "https://member.bilibili.com/preupload?", "");
    url.add(querypart("name", VIDEO_FILE.getName()));
    // url.add(querypart("size", String.valueOf(VIDEO_FILE.length())));
    url.add(querypart("r", "upos"));
    url.add(querypart("profile", "ugcfx/bup"));
    HttpURLConnection conn = conn(url.toString(), "GET");
    System.out.println("GET " + url.toString());
    String response = new String(inputStreamToString(conn), StandardCharsets.UTF_8);
    try {
      JsonObject json = GSON.fromJson(response, JsonObject.class);
      System.out.println(GSON.toJson(json));
      return json;
    } catch (JsonSyntaxException e) {
      System.out.println(response);
      throw e;
    }
  }

  private static JsonObject postVideoMeta(JsonObject preuploadVideo) throws IOException {
    String schemeandhost = "https:" + preuploadVideo.get("endpoint").getAsString();
    String path = preuploadVideo.get("upos_uri").getAsString().replaceFirst("upos:/", "");
    StringJoiner url = new StringJoiner("&", schemeandhost + path + "?", "");
    url.add(querypart("uploads", "")); // WARNING: this is not a typo, it's required, or 404
    url.add(querypart("output", "json"));
    url.add(querypart("profile", "ugcfx/bup"));
    url.add(querypart("filesize", String.valueOf(VIDEO_FILE.length())));
    url.add(querypart("partsize", preuploadVideo.get("chunk_size").getAsString()));
    url.add(querypart("biz_id", preuploadVideo.get("biz_id").getAsString()));
    HttpURLConnection conn = conn(url.toString(), "POST");
    conn.setRequestProperty("X-Upos-Auth", preuploadVideo.get("auth").getAsString()); // 403 without it
    System.out.println("POST " + url.toString());
    String response = new String(inputStreamToString(conn), StandardCharsets.UTF_8);
    try {
      JsonObject json = GSON.fromJson(response, JsonObject.class);
      System.out.println(GSON.toJson(json));
      return json;
    } catch (JsonSyntaxException e) {
      System.out.println(response);
      throw e;
    }
  }

  private static int uploadVideo(JsonObject preuploadVideo, JsonObject postVideoMeta) throws IOException {
    long startts = System.currentTimeMillis() - 1;
    String schemeandhost = "https:" + preuploadVideo.get("endpoint").getAsString();
    String path = preuploadVideo.get("upos_uri").getAsString().replaceFirst("upos:/", "");
    String urlp = schemeandhost + path + "?";
    long length = VIDEO_FILE.length();
    byte[] buffer = new byte[preuploadVideo.get("chunk_size").getAsInt()];
    int size = 0;
    int chunks = (int) Math.ceil(length / (double) buffer.length);
    InputStream in = new FileInputStream(VIDEO_FILE);
    for (int chunk = 0; chunk < chunks; chunk++) {
      System.out.println("speed: " + (chunk * buffer.length) / (System.currentTimeMillis() - startts) + "bytes/s");
      System.out.println("chunk: " + (chunk + 1) + "/" + chunks);
      size = in.read(buffer, 0, buffer.length);
      if (size == -1) {
        break;
      }
      StringJoiner url = new StringJoiner("&", urlp, "");
      url.add(querypart("partNumber", String.valueOf(chunk + 1)));
      url.add(querypart("uploadId", postVideoMeta.get("upload_id").getAsString()));
      url.add(querypart("chunk", String.valueOf(chunk)));
      url.add(querypart("chunks", String.valueOf(chunks)));
      url.add(querypart("size", String.valueOf(size)));
      url.add(querypart("start", String.valueOf(chunk * buffer.length)));
      url.add(querypart("end", String.valueOf((chunk) * buffer.length + size)));
      url.add(querypart("total", String.valueOf(length)));
      HttpURLConnection conn = conn(url.toString(), "PUT");
      conn.setRequestProperty("X-Upos-Auth", preuploadVideo.get("auth").getAsString());
      conn.setRequestProperty("Content-Type", "application/octet-stream");
      conn.setRequestProperty("Content-Length", String.valueOf(size));
      conn.setDoOutput(true);
      conn.getOutputStream().write(buffer, 0, size);
      System.out.println("PUT " + url.toString());
      String response = new String(inputStreamToString(conn), StandardCharsets.UTF_8);
      System.out.println(response);
    }
    in.close();
    return chunks;
  }

  private static void endupload(JsonObject preuploadVideo, JsonObject postVideoMeta, int chunks) throws IOException {
    String schemeandhost = "https:" + preuploadVideo.get("endpoint").getAsString();
    String path = preuploadVideo.get("upos_uri").getAsString().replaceFirst("upos:/", "");
    StringJoiner url = new StringJoiner("&", schemeandhost + path + "?", "");
    url.add(querypart("output", "json"));
    url.add(querypart("name", VIDEO_FILE.getName()));
    url.add(querypart("profile", "ugcfx/bup"));
    url.add(querypart("uploadId", postVideoMeta.get("upload_id").getAsString()));
    url.add(querypart("biz_id", preuploadVideo.get("biz_id").getAsString()));
    JsonArray parts = new JsonArray();
    for (int i = 1; i <= chunks; i++) {
      JsonObject part = new JsonObject();
      part.addProperty("partNumber", i);
      part.addProperty("eTag", "etag");
      parts.add(part);
    }
    JsonObject body = new JsonObject();
    body.add("parts", parts);
    HttpURLConnection conn = conn(url.toString(), "POST");
    conn.setRequestProperty("X-Upos-Auth", preuploadVideo.get("auth").getAsString());
    conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
    conn.setDoOutput(true);
    conn.getOutputStream().write(body.toString().getBytes(StandardCharsets.UTF_8));
    System.out.println("POST " + url.toString());
    String response = new String(inputStreamToString(conn), StandardCharsets.UTF_8);
    try {
      JsonObject json = GSON.fromJson(response, JsonObject.class);
      System.out.println(GSON.toJson(json));
    } catch (JsonSyntaxException e) {
      System.out.println(response);
      throw e;
    }
  }

}