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!\t bvid:{ bvid } ' )
0 commit comments