Skip to content

Commit 55c4aab

Browse files
committed
feat: implement upload module
1 parent c92d593 commit 55c4aab

File tree

10 files changed

+410
-0
lines changed

10 files changed

+410
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,7 @@
22
node_modules/
33
package-lock.json
44
package.json
5+
__pycache__/
6+
cookie.json
7+
*.mp4
8+
*.yaml

cli.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
2+
# Copyright (c) 2024 biliup-py.
3+
4+
import argparse
5+
import sys
6+
import os
7+
import logging
8+
from login.login_bili import login_bili
9+
from utils.parse_cookies import parse_cookies
10+
from upload.bili_upload import BiliUploader
11+
from utils.parse_yaml import parse_yaml
12+
13+
def cli():
14+
logging.basicConfig(
15+
format='[%(levelname)s] - [%(asctime)s %(name)s] - %(message)s',
16+
level=logging.INFO
17+
)
18+
parser = argparse.ArgumentParser(description='Python implementation of biliup')
19+
parser.add_argument('-V', '--version', action='version', version='biliup-py 1.0', help='Print version information')
20+
21+
subparsers = parser.add_subparsers(dest='subcommand', help='Subcommands')
22+
23+
# Login subcommand
24+
subparsers.add_parser('login', help='login and save the cookies')
25+
26+
# Upload subcommand
27+
upload_parser = subparsers.add_parser('upload', help='upload the video')
28+
upload_parser.add_argument('video_path', help='(required) the path to video file')
29+
upload_parser.add_argument('-c', '--cookies', required=True, help='The path to cookies')
30+
upload_parser.add_argument('-y', '--yaml', help='The path to yaml file(if yaml file is provided, the arguments below will be ignored)')
31+
upload_parser.add_argument('--copyright', type=int, default=2, help='(default is 2) 1 for original, 2 for reprint')
32+
upload_parser.add_argument('--title', help='(default is video name) The title of video')
33+
upload_parser.add_argument('--desc', default='', help='(default is empty) The description of video')
34+
upload_parser.add_argument('--tid', type=int, help='(default is 138) For more info to the type id, refer to https://biliup.github.io/tid-ref.html')
35+
upload_parser.add_argument('--tags', help='(default is biliup-py) video tags, separated by comma')
36+
upload_parser.add_argument('--line', default='bda2', help='(default is bda2) line refer to https://biliup.github.io/upload-systems-analysis.html')
37+
38+
args = parser.parse_args()
39+
40+
sessdata, bili_jct = parse_cookies(args.cookies)
41+
# Check if no subcommand is provided
42+
if args.subcommand is None:
43+
print("No subcommand provided. Please specify a subcommand.")
44+
parser.print_help()
45+
sys.exit()
46+
47+
if args.subcommand == 'login':
48+
login_bili()
49+
50+
if args.subcommand == 'upload':
51+
if (args.yaml):
52+
line, copyright, tid, title, desc, tags = parse_yaml(args.yaml)
53+
BiliUploader(
54+
sessdata,
55+
bili_jct,
56+
line
57+
).upload_and_publish_video(
58+
args.video_path,
59+
title=title,
60+
desc=desc,
61+
copyright=copyright,
62+
tid=tid,
63+
tags=tags
64+
)
65+
else:
66+
BiliUploader(
67+
sessdata,
68+
bili_jct,
69+
args.line
70+
).upload_and_publish_video(
71+
args.video_path,
72+
title=args.title,
73+
desc=args.desc,
74+
copyright=args.copyright,
75+
tid=args.tid,
76+
tags=args.tags
77+
)
78+
79+
if __name__ == '__main__':
80+
cli()

login/__init__.py

Whitespace-only changes.
File renamed without changes.

requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
PyYAML==6.0.2
2+
qrcode==8.0
3+
Requests==2.32.3
4+
requests_html==0.10.0

upload/__init__.py

Whitespace-only changes.

upload/bili_upload.py

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
# Copyright (c) 2024 biliup-py.
2+
3+
import re
4+
import sys
5+
import logging
6+
import argparse
7+
from math import ceil
8+
from json import dumps
9+
from pathlib import Path
10+
from time import sleep
11+
from requests_html import HTMLSession
12+
from requests.utils import cookiejar_from_dict
13+
from utils.parse_cookies import parse_cookies
14+
15+
# you can test your best cdn line https://member.bilibili.com/preupload?r=ping
16+
cdn_lines = {
17+
'qn': 'upos-sz-upcdnqn.bilivideo.com',
18+
'bda2': 'upos-sz-upcdnbda2.bilivideo.com',
19+
}
20+
21+
class BiliUploader(object):
22+
ua = {
23+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:71.0) Gecko/20100101 Firefox/71.0',
24+
}
25+
26+
def __init__(self, sessdata, bili_jct, line):
27+
self.logger = logging.getLogger('biliup-py')
28+
self.SESSDATA = sessdata
29+
self.bili_jct = bili_jct
30+
self.auth_cookies = {
31+
'SESSDATA': sessdata,
32+
'bili_jct': bili_jct
33+
}
34+
self.session = HTMLSession()
35+
self.session.cookies = cookiejar_from_dict(self.auth_cookies)
36+
self.session.headers = self.ua
37+
self.line = line
38+
39+
def preupload(self, *, filename, filesize):
40+
"""The preupload process to get `upos_uri` and `auth` information.
41+
Parameters
42+
----------
43+
filename : str
44+
the name of the video to be uploaded
45+
filesize : int
46+
the size of the video to be uploaded
47+
biz_id : num
48+
the business id
49+
50+
Returns
51+
-------
52+
- upos_uri: str
53+
the uri of the video will be stored in server
54+
- auth: str
55+
the auth information
56+
57+
[Easter egg] Sometimes I'm also confused why it is called `upos`
58+
So I ask a question on the V2EX: https://v2ex.com/t/1103152
59+
Finally, the netizens reckon that may be the translation style of bilibili.
60+
"""
61+
url = 'https://member.bilibili.com/preupload'
62+
params = {
63+
'name': filename,
64+
'size': filesize,
65+
# The parameters below are fixed
66+
'r': 'upos',
67+
'profile': 'ugcupos/bup',
68+
'ssl': 0,
69+
'version': '2.8.9',
70+
'build': '2080900',
71+
'upcdn': self.line,
72+
'probe_version': '20200810'
73+
}
74+
res_json = self.session.get(
75+
url,
76+
params=params,
77+
headers={'TE': 'Trailers'}
78+
).json()
79+
assert res_json['OK'] == 1
80+
self.logger.info('Completed preupload phase')
81+
return res_json
82+
83+
def get_upload_video_id(self, *, upos_uri, auth):
84+
"""Get the `upload_id` of video.
85+
86+
Parameters
87+
----------
88+
- upos_uri: str
89+
get from `preupload`
90+
- auth: str
91+
get from `preupload`
92+
Returns
93+
-------
94+
- upload_id: str
95+
the id of the video to be uploaded
96+
"""
97+
url = f'https://{cdn_lines[self.line]}/{upos_uri}?uploads&output=json'
98+
res_json = self.session.post(url, headers={'X-Upos-Auth': auth}).json()
99+
assert res_json['OK'] == 1
100+
self.logger.info('Completed upload_id obtaining phase')
101+
return res_json
102+
103+
def upload_video_in_chunks(self, *, upos_uri, auth, upload_id, fileio, filesize, chunk_size, chunks):
104+
"""Upload the video in chunks.
105+
106+
Parameters
107+
----------
108+
- upos_uri: str
109+
get from `preupload`
110+
- auth: str
111+
get from `preupload`
112+
- upload_id: str
113+
get from `get_upload_video_id`
114+
- fileio: io.BufferedReader
115+
the io stream of the video to be uploaded
116+
- filesize: int
117+
the size of the video to be uploaded
118+
- chunk_size: int
119+
the size of each chunk to be uploaded
120+
- chunks: int
121+
the number of chunks to be uploaded
122+
"""
123+
url = f'https://{cdn_lines[self.line]}/{upos_uri}'
124+
params = {
125+
'partNumber': None, # start from 1
126+
'uploadId': upload_id,
127+
'chunk': None, # start from 0
128+
'chunks': chunks,
129+
'size': None, # current batch size
130+
'start': None,
131+
'end': None,
132+
'total': filesize,
133+
}
134+
# Single thread upload
135+
for chunknum in range(chunks):
136+
start = fileio.tell()
137+
batchbytes = fileio.read(chunk_size)
138+
params['partNumber'] = chunknum + 1
139+
params['chunk'] = chunknum
140+
params['size'] = len(batchbytes)
141+
params['start'] = start
142+
params['end'] = fileio.tell()
143+
res = self.session.put(url, params=params, data=batchbytes, headers={
144+
'X-Upos-Auth': auth})
145+
assert res.status_code == 200
146+
self.logger.debug(f'Completed chunk{chunknum+1} uploading')
147+
148+
def finish_upload(self, *, upos_uri, auth, filename, upload_id, biz_id, chunks):
149+
"""Notify the all chunks have been uploaded.
150+
151+
Parameters
152+
----------
153+
- upos_uri: str
154+
get from `preupload`
155+
- auth: str
156+
get from `preupload`
157+
- filename: str
158+
the name of the video to be uploaded
159+
- upload_id: str
160+
get from `get_upload_video_id`
161+
- biz_id: num
162+
get from `preupload`
163+
- chunks: int
164+
the number of chunks to be uploaded
165+
"""
166+
url = f'https://{cdn_lines[self.line]}/{upos_uri}'
167+
params = {
168+
'output': 'json',
169+
'name': filename,
170+
'profile' : 'ugcupos/bup',
171+
'uploadId': upload_id,
172+
'biz_id': biz_id
173+
}
174+
data = {"parts": [{"partNumber": i, "eTag": "etag"}
175+
for i in range(chunks, 1)]}
176+
res_json = self.session.post(url, params=params, json=data,
177+
headers={'X-Upos-Auth': auth}).json()
178+
assert res_json['OK'] == 1
179+
180+
def publish_video(self, bilibili_filename, title, tid, tags, source='来源于网络', copyright=2, desc='', cover_url=''):
181+
"""publish the uploaded video"""
182+
url = f'https://member.bilibili.com/x/vu/web/add?csrf={self.bili_jct}'
183+
data = {'copyright': copyright,
184+
'videos': [{'filename': bilibili_filename,
185+
'title': title,
186+
'desc': desc}],
187+
'source': source,
188+
'tid': tid,
189+
'cover': cover_url,
190+
'title': title,
191+
'tag': tags,
192+
'desc_format_id': 0,
193+
'desc': desc,
194+
'dynamic': '',
195+
'subtitle': {'open': 0, 'lan': ''}}
196+
if copyright != 2:
197+
del data['source']
198+
# copyright: 1 original 2 reprint
199+
data['copyright'] = 1
200+
# interactive: 0 no 1 yes
201+
data['interactive'] = 0
202+
# no_reprint: 0 no 1 yes
203+
data['no_reprint'] = 1
204+
res_json = self.session.post(url, json=data, headers={'TE': 'Trailers'}).json()
205+
return res_json
206+
207+
def upload_and_publish_video(self, file, *, title=None, desc='', copyright=2, tid=None, tags=None):
208+
"""upload and publish video on bilibili"""
209+
file = Path(file)
210+
assert file.exists(), f'The file {file} does not exist'
211+
filename = file.name
212+
title = title or file.stem
213+
filesize = file.stat().st_size
214+
self.logger.info(f'The {title} to be uploaded')
215+
216+
# upload video
217+
self.logger.info('Start preuploading the video')
218+
pre_upload_response = self.preupload(filename=filename, filesize=filesize)
219+
upos_uri = pre_upload_response['upos_uri'].split('//')[-1]
220+
auth = pre_upload_response['auth']
221+
biz_id = pre_upload_response['biz_id']
222+
chunk_size = pre_upload_response['chunk_size']
223+
chunks = ceil(filesize/chunk_size)
224+
225+
self.logger.info('Start uploading the video')
226+
upload_video_id_response = self.get_upload_video_id(upos_uri=upos_uri, auth=auth)
227+
upload_id = upload_video_id_response['upload_id']
228+
key = upload_video_id_response['key']
229+
230+
bilibili_filename = re.search(r'/(.*)\.', key).group(1)
231+
232+
self.logger.info(f'Uploading the video in {chunks} batches')
233+
fileio = file.open(mode='rb')
234+
self.upload_video_in_chunks(
235+
upos_uri=upos_uri,
236+
auth=auth,
237+
upload_id=upload_id,
238+
fileio=fileio,
239+
filesize=filesize,
240+
chunk_size=chunk_size,
241+
chunks=chunks
242+
)
243+
fileio.close()
244+
245+
# notify the all chunks have been uploaded
246+
self.finish_upload(upos_uri=upos_uri, auth=auth, filename=filename,
247+
upload_id=upload_id, biz_id=biz_id, chunks=chunks)
248+
249+
# select tid
250+
tid = 138
251+
252+
# customize tags
253+
if not tags:
254+
tags_text = 'biliuppy'
255+
else:
256+
tags_text = tags
257+
258+
# customize video cover
259+
# cover_url =
260+
261+
# publish video
262+
publish_video_response = self.publish_video(bilibili_filename=bilibili_filename, title=title,
263+
tid=tid, tags=tags_text, copyright=copyright, desc=desc)
264+
bvid = publish_video_response['data']['bvid']
265+
self.logger.info(f'[{title}]upload success!\tbvid:{bvid}')

utils/__init__.py

Whitespace-only changes.

utils/parse_cookies.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Copyright (c) 2024 biliup-py.
2+
3+
import json
4+
import os
5+
import sys
6+
7+
def parse_cookies(cookies_path):
8+
try:
9+
with open(cookies_path, 'r') as file:
10+
data = json.load(file)
11+
except FileNotFoundError:
12+
return "Error: Cookies file not found."
13+
except json.JSONDecodeError:
14+
return "Error: Failed to decode JSON from cookies file."
15+
16+
cookies = data.get('data', {}).get('cookie_info', {}).get('cookies', [])
17+
18+
sessdata_value = None
19+
bili_jct_value = None
20+
21+
for cookie in cookies:
22+
if cookie['name'] == 'SESSDATA':
23+
sessdata_value = cookie['value']
24+
elif cookie['name'] == 'bili_jct':
25+
bili_jct_value = cookie['value']
26+
27+
if not sessdata_value or not bili_jct_value:
28+
return "Error: Required cookies not found."
29+
30+
return sessdata_value, bili_jct_value
31+
32+
if __name__ == "__main__":
33+
sessdata, bili_jct = parse_cookies('')

0 commit comments

Comments
 (0)