1use crate::client::Client;
2use crate::error::Result;
3use crate::uploader::cos::Cos;
4use crate::uploader::kodo::Kodo;
5use crate::uploader::upos::Upos;
6use crate::uploader::Uploader;
7use crate::{Video, VideoFile, VideoStream};
8use anyhow::Context;
9use futures::{Stream, TryStreamExt};
10use reqwest::Body;
11use serde::de::DeserializeOwned;
12use serde::{Deserialize, Serialize};
13use serde_json::json;
14use std::ffi::OsStr;
15
16use std::time::Instant;
17use tracing::info;
18
19pub struct Parcel<'a> {
20 line: &'a Line,
21 video_file: VideoFile,
22 params: serde_json::Value,
23}
24
25impl<'a> Parcel<'a> {
26 fn new(line: &'a Line, video_file: VideoFile) -> Parcel<'a> {
27 let total_size = video_file.total_size;
28 let file_name = video_file.file_name.clone();
29 let profile = if let Uploader::Upos = line.os {
30 "ugcupos/bup"
31 } else {
32 "ugcupos/bupfetch"
33 };
34 let params = json!({
35 "r": line.os,
36 "profile": profile,
37 "ssl": 0,
38 "version": "2.11.0",
39 "build": 2110000,
40 "name": file_name,
41 "size": total_size,
42 });
43 info!("pre_upload: {}", params);
44 Self {
45 line,
46 params,
47 video_file,
48 }
49 }
50
51 pub async fn pre_upload<T: DeserializeOwned>(&self, login: &Client) -> Result<T> {
52 Ok(login
53 .client
54 .get(format!(
55 "https://member.bilibili.com/preupload?{}",
56 self.line.query
57 ))
58 .query(&self.params)
59 .send()
60 .await?
61 .json()
62 .await
63 .with_context(|| "Failed to pre_upload from")?)
64 }
65
66 pub async fn upload<F, S, B>(&self, client: &Client, limit: usize, progress: F) -> Result<Video>
67 where
68 F: FnOnce(VideoStream) -> S,
69 S: Stream<Item = Result<(B, usize)>>,
70 B: Into<Body> + Clone,
71 {
72 let mut video = match self.line.os {
73 Uploader::Upos => {
74 let bucket: crate::uploader::upos::Bucket = self.pre_upload(client).await?;
75 let chunk_size = bucket.chunk_size;
76 let upos = Upos::from(bucket).await?;
77 let mut parts = Vec::new();
78 let stream = upos
79 .upload_stream(
80 progress(self.video_file.get_stream(chunk_size)?),
81 self.video_file.total_size,
82 limit,
83 )
84 .await?;
85 tokio::pin!(stream);
86 while let Some((part, _size)) = stream.try_next().await? {
87 parts.push(part);
88 }
89 upos.get_ret_video_info(&parts, &self.video_file.filepath)
90 .await?
91 }
92 Uploader::Kodo => {
93 let bucket = self.pre_upload(client).await?;
94 let chunk_size = 4194304;
95 Kodo::from(bucket)
96 .await?
97 .upload_stream(
98 progress(self.video_file.get_stream(chunk_size)?),
99 self.video_file.total_size,
100 limit,
101 )
102 .await?
103 }
104 Uploader::Bos => {
105 panic!()
106 }
107 Uploader::Gcs => {
108 panic!()
109 }
110 Uploader::Cos => {
111 let bucket = self.pre_upload(client).await?;
112 let cos_client = Cos::form_post(bucket).await?;
113 let chunk_size = 10485760;
114 let enable_internal = self.line.probe_url == "internal";
115 let parts = cos_client
116 .upload_stream(
117 progress(self.video_file.get_stream(chunk_size)?),
118 self.video_file.total_size,
119 limit,
120 enable_internal,
121 )
122 .await?;
123 cos_client.merge_files(parts).await?
124 }
125 };
126 if video.title == None {
127 video.title = self
128 .video_file
129 .filepath
130 .file_stem()
131 .and_then(OsStr::to_str)
132 .map(|s| s.to_string())
133 };
134 Ok(video)
135 }
136}
137
138#[derive(Deserialize, Serialize, Debug)]
139pub struct Probe {
140 #[serde(rename = "OK")]
141 ok: u8,
142 lines: Vec<Line>,
143 probe: serde_json::Value,
144}
145
146impl Probe {
147 pub async fn probe() -> Result<Line> {
148 let res: Self = reqwest::get("https://member.bilibili.com/preupload?r=probe")
149 .await?
150 .json()
151 .await?;
152 let client = if !res.probe["get"].is_null() {
153 |url| reqwest::Client::new().get(url)
154 } else {
155 |url| {
156 reqwest::Client::new()
157 .post(url)
158 .body(vec![0; (1024. * 0.1 * 1024.) as usize])
159 }
160 };
161 let mut choice_line: Line = Default::default();
162 for mut line in res.lines {
163 let instant = Instant::now();
164 if client(format!("https:{}", line.probe_url))
165 .send()
166 .await?
167 .status()
168 == 200
169 {
170 line.cost = instant.elapsed().as_millis();
171 info!("{}: {}", line.query, line.cost);
172 if choice_line.cost > line.cost {
173 choice_line = line
174 }
175 };
176 }
177 Ok(choice_line)
178 }
179}
180
181#[derive(Deserialize, Serialize, Debug)]
182pub struct Line {
183 os: Uploader,
184 probe_url: String,
185 query: String,
186 #[serde(skip)]
187 cost: u128,
188}
189
190impl Line {
191 pub fn to_uploader(&self, filepath: VideoFile) -> Parcel<'_> {
192 Parcel::new(self, filepath)
193 }
194}
195
196impl Default for Line {
197 fn default() -> Self {
198 Line {
199 os: Uploader::Upos,
200 probe_url: "//upos-sz-upcdnbda2.bilivideo.com/OK".to_string(),
201 query: "upcdn=bda2&probe_version=20211012".to_string(),
202 cost: u128::MAX,
203 }
204 }
205}
206
207pub fn kodo() -> Line {
208 Line {
209 os: Uploader::Kodo,
210 query: "bucket=bvcupcdnkodobm&probe_version=20211012".into(),
211 probe_url: "//up-na0.qbox.me/crossdomain.xml".into(),
212 cost: 0,
213 }
214}
215
216pub fn bda2() -> Line {
217 Line {
218 os: Uploader::Upos,
219 query: "upcdn=bda2&probe_version=20211012".into(),
220 probe_url: "//upos-sz-upcdnbda2.bilivideo.com/OK".into(),
221 cost: 0,
222 }
223}
224
225pub fn ws() -> Line {
226 Line {
227 os: Uploader::Upos,
228 query: "upcdn=ws&probe_version=20211012".into(),
229 probe_url: "//upos-sz-upcdnws.bilivideo.com/OK".into(),
230 cost: 0,
231 }
232}
233
234pub fn qn() -> Line {
235 Line {
236 os: Uploader::Upos,
237 query: "upcdn=qn&probe_version=20211012".into(),
238 probe_url: "//upos-sz-upcdnqn.bilivideo.com/OK".into(),
239 cost: 0,
240 }
241}
242
243pub fn cos() -> Line {
244 Line {
245 os: Uploader::Cos,
246 query: "&probe_version=20211012&r=cos&profile=ugcupos%2Fbupfetch&ssl=0&version=2.10.4.0&build=2100400&webVersion=2.0.0".into(),
247 probe_url: "".into(),
248 cost: 0,
249 }
250}
251
252pub fn cos_internal() -> Line {
253 Line {
254 os: Uploader::Cos,
255 query: "".into(),
256 probe_url: "internal".into(),
257 cost: 0,
258 }
259}