biliup/
line.rs

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}