diff --git a/crates/biliup/src/uploader/bilibili.rs b/crates/biliup/src/uploader/bilibili.rs index a02c521..77dab0f 100644 --- a/crates/biliup/src/uploader/bilibili.rs +++ b/crates/biliup/src/uploader/bilibili.rs @@ -221,14 +221,56 @@ pub struct BiliBili { impl BiliBili { pub async fn submit(&self, studio: &Studio) -> Result { + let ret: ResponseData = reqwest::Client::builder() + .user_agent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/63.0.3239.108") + .timeout(Duration::new(60, 0)) + .build()? + .post(format!( + "http://member.bilibili.com/x/vu/client/add?access_key={}", + self.login_info.token_info.access_token + )) + .json(studio) + .send() + .await? + .json() + .await?; + info!("{:?}", ret); + if ret.code == 0 { + info!("投稿成功"); + Ok(ret) + } else { + Err(Kind::Custom(format!("{:?}", ret))) + } + } + + pub async fn submit_by_app(&self, studio: &Studio) -> Result { + let payload = { + let mut payload = json!({ + "access_key": self.login_info.token_info.access_token, + "appkey": crate::credential::AppKeyStore::BiliTV.app_key(), + "build": 7800300, + "c_locale": "zh-Hans_CN", + "channel": "bili", + "disable_rcmd": 0, + "mobi_app": "android", + "platform": "android", + "s_locale": "zh-Hans_CN", + "statistics": "\"appId\":1,\"platform\":3,\"version\":\"7.80.0\",\"abtest\":\"\"", + "ts": std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(), + }); + + let urlencoded = serde_urlencoded::to_string(&payload)?; + let sign = crate::credential::Credential::sign(&urlencoded, crate::credential::AppKeyStore::BiliTV.appsec()); + payload["sign"] = Value::from(sign); + payload + }; + let ret: ResponseData = reqwest::Client::builder() - .user_agent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/63.0.3239.108") + .user_agent("Mozilla/5.0 BiliDroid/7.80.0 (bbcallen@gmail.com) os/android model/MI 6 mobi_app/android build/7800300 channel/bili innerVer/7800310 osVer/13 network/2") .timeout(Duration::new(60, 0)) .build()? - .post(format!( - "http://member.bilibili.com/x/vu/client/add?access_key={}", - self.login_info.token_info.access_token - )) + .post("https://member.bilibili.com/x/vu/app/add") + .query(&payload) .json(studio) .send() .await? @@ -236,7 +278,6 @@ impl BiliBili { .await?; info!("{:?}", ret); if ret.code == 0 { - info!("投稿成功"); Ok(ret) } else { Err(Kind::Custom(format!("{:?}", ret))) diff --git a/crates/biliup/src/uploader/credential.rs b/crates/biliup/src/uploader/credential.rs index 086f329..b1fa8c7 100644 --- a/crates/biliup/src/uploader/credential.rs +++ b/crates/biliup/src/uploader/credential.rs @@ -36,14 +36,14 @@ pub(crate) enum AppKeyStore { } impl AppKeyStore { - fn app_key(&self) -> &'static str { + pub fn app_key(&self) -> &'static str { match self { AppKeyStore::BiliTV => "4409e2ce8ffd12b8", AppKeyStore::Android => "783bbb7264451d82", } } - fn appsec(&self) -> &'static str { + pub fn appsec(&self) -> &'static str { match self { AppKeyStore::BiliTV => "59b43e04ad6965f34319062b478f83dd", AppKeyStore::Android => "2653583c8873dea268ab9386918b1d65", @@ -571,7 +571,7 @@ impl Credential { Ok(()) } - fn sign(param: &str, app_sec: &str) -> String { + pub fn sign(param: &str, app_sec: &str) -> String { let mut hasher = Md5::new(); // process input message hasher.update(format!("{}{}", param, app_sec)); diff --git a/crates/stream-gears/src/lib.rs b/crates/stream-gears/src/lib.rs index 377be96..fe1647e 100644 --- a/crates/stream-gears/src/lib.rs +++ b/crates/stream-gears/src/lib.rs @@ -192,6 +192,7 @@ fn login_by_web_qrcode(sess_data: String, dede_user_id: String) -> PyResult, video_path: Vec, @@ -273,6 +274,95 @@ fn upload( }) } +#[allow(clippy::too_many_arguments)] +#[pyfunction] +fn upload_by_app( + py: Python<'_>, + video_path: Vec, + cookie_file: PathBuf, + title: String, + tid: u16, + tag: String, + copyright: u8, + source: String, + desc: String, + dynamic: String, + cover: String, + dolby: u8, + lossless_music: u8, + no_reprint: u8, + open_elec: u8, + up_close_reply: bool, + up_selection_reply: bool, + up_close_danmu: bool, + limit: usize, + desc_v2: Vec, + dtime: Option, + line: Option, +) -> PyResult<()> { + py.allow_threads(|| { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()?; + // 输出到控制台中 + unsafe { + time::util::local_offset::set_soundness(time::util::local_offset::Soundness::Unsound); + } + let local_time = tracing_subscriber::fmt::time::LocalTime::new(format_description!( + "[year]-[month]-[day] [hour]:[minute]:[second]" + )); + let formatting_layer = tracing_subscriber::FmtSubscriber::builder() + // will be written to stdout. + // builds the subscriber. + .with_timer(local_time.clone()) + .finish(); + let file_appender = tracing_appender::rolling::never("", "upload.log"); + let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); + let file_layer = tracing_subscriber::fmt::layer() + .with_ansi(false) + .with_timer(local_time) + .with_writer(non_blocking); + + let collector = formatting_layer.with(file_layer); + + tracing::subscriber::with_default(collector, || -> PyResult<()> { + let studio_pre = StudioPre::builder() + .video_path(video_path) + .cookie_file(cookie_file) + .line(line) + .limit(limit) + .title(title) + .tid(tid) + .tag(tag) + .copyright(copyright) + .source(source) + .desc(desc) + .dynamic(dynamic) + .cover(cover) + .dtime(dtime) + .dolby(dolby) + .lossless_music(lossless_music) + .no_reprint(no_reprint) + .open_elec(open_elec) + .up_close_reply(up_close_reply) + .up_selection_reply(up_selection_reply) + .up_close_danmu(up_close_danmu) + .desc_v2_credit(desc_v2) + .build(); + + match rt.block_on(uploader::upload_by_app(studio_pre)) { + Ok(_) => Ok(()), + // Ok(_) => { }, + Err(err) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!( + "{}, {}", + err.root_cause(), + err + ))), + } + }) + }) +} + /// A Python module implemented in Rust. #[pymodule] fn stream_gears(m: &Bound<'_, PyModule>) -> PyResult<()> { @@ -282,6 +372,7 @@ fn stream_gears(m: &Bound<'_, PyModule>) -> PyResult<()> { // .with_writer(non_blocking) // .init(); m.add_function(wrap_pyfunction!(upload, m)?)?; + m.add_function(wrap_pyfunction!(upload_by_app, m)?)?; m.add_function(wrap_pyfunction!(download, m)?)?; m.add_function(wrap_pyfunction!(download_with_callback, m)?)?; m.add_function(wrap_pyfunction!(login_by_cookies, m)?)?; diff --git a/crates/stream-gears/src/uploader.rs b/crates/stream-gears/src/uploader.rs index bf1d186..a4d9e4e 100644 --- a/crates/stream-gears/src/uploader.rs +++ b/crates/stream-gears/src/uploader.rs @@ -59,6 +59,12 @@ pub struct StudioPre { lossless_music: u8, no_reprint: u8, open_elec: u8, + #[builder(default=false)] + up_close_reply: bool, + #[builder(default=false)] + up_selection_reply: bool, + #[builder(default=false)] + up_close_danmu: bool, desc_v2_credit: Vec, } @@ -86,6 +92,7 @@ pub async fn upload(studio_pre: StudioPre) -> Result { no_reprint, open_elec, desc_v2_credit, + .. } = studio_pre; let bilibili = login_by_cookies(&cookie_file).await; @@ -179,3 +186,127 @@ pub async fn upload(studio_pre: StudioPre) -> Result { Ok(bilibili.submit(&studio).await?) } + +pub async fn upload_by_app(studio_pre: StudioPre) -> Result { + // let file = std::fs::File::options() + // .read(true) + // .write(true) + // .open(&cookie_file); + let StudioPre { + video_path, + cookie_file, + line, + limit, + title, + tid, + tag, + copyright, + source, + desc, + dynamic, + cover, + dtime, + dolby, + lossless_music, + no_reprint, + open_elec, + up_close_reply, + up_selection_reply, + up_close_danmu, + desc_v2_credit, + } = studio_pre; + + let bilibili = login_by_cookies(&cookie_file).await; + let bilibili = if let Err(Kind::IO(_)) = bilibili { + bilibili + .with_context(|| String::from("open cookies file: ") + &cookie_file.to_string_lossy())? + } else { + bilibili? + }; + + let client = StatelessClient::default(); + let mut videos = Vec::new(); + let line = match line { + Some(UploadLine::Bda2) => line::bda2(), + Some(UploadLine::Ws) => line::ws(), + Some(UploadLine::Qn) => line::qn(), + // Some(UploadLine::Kodo) => line::kodo(), + // Some(UploadLine::Cos) => line::cos(), + // Some(UploadLine::CosInternal) => line::cos_internal(), + Some(UploadLine::Bda) => line::bda(), + Some(UploadLine::Tx) => line::tx(), + Some(UploadLine::Txa) => line::txa(), + Some(UploadLine::Bldsa) => line::bldsa(), + None => Probe::probe(&client.client).await.unwrap_or_default(), + }; + for video_path in video_path { + println!("{:?}", video_path.canonicalize()?.to_str()); + info!("{line:?}"); + let video_file = VideoFile::new(&video_path)?; + let total_size = video_file.total_size; + let file_name = video_file.file_name.clone(); + let uploader = line.pre_upload(&bilibili, video_file).await?; + + let instant = Instant::now(); + + let video = uploader + .upload(client.clone(), limit, |vs| { + vs.map(|vs| { + let chunk = vs?; + let len = chunk.len(); + Ok((chunk, len)) + }) + }) + .await?; + let t = instant.elapsed().as_millis(); + info!( + "Upload completed: {file_name} => cost {:.2}s, {:.2} MB/s.", + t as f64 / 1000., + total_size as f64 / 1000. / t as f64 + ); + videos.push(video); + } + + let mut desc_v2 = Vec::new(); + for credit in desc_v2_credit { + desc_v2.push(Credit { + type_id: credit.type_id, + raw_text: credit.raw_text, + biz_id: credit.biz_id, + }); + } + + let mut studio: Studio = Studio::builder() + .desc(desc) + .dtime(dtime) + .copyright(copyright) + .cover(cover) + .dynamic(dynamic) + .source(source) + .tag(tag) + .tid(tid) + .title(title) + .videos(videos) + .dolby(dolby) + .lossless_music(lossless_music) + .no_reprint(no_reprint) + .open_elec(open_elec) + .up_close_reply(up_close_reply) + .up_selection_reply(up_selection_reply) + .up_close_danmu(up_close_danmu) + .desc_v2(Some(desc_v2)) + .build(); + + if !studio.cover.is_empty() { + let url = bilibili + .cover_up( + &std::fs::read(&studio.cover) + .with_context(|| format!("cover: {}", studio.cover))?, + ) + .await?; + println!("{url}"); + studio.cover = url; + } + + Ok(bilibili.submit_by_app(&studio).await?) +} \ No newline at end of file diff --git a/crates/stream-gears/stream_gears/stream_gears.pyi b/crates/stream-gears/stream_gears/stream_gears.pyi index 51c37df..16ef1b8 100644 --- a/crates/stream-gears/stream_gears/stream_gears.pyi +++ b/crates/stream-gears/stream_gears/stream_gears.pyi @@ -173,3 +173,50 @@ def upload(video_path: List[str], :param Optional[dtime] int dtime: 定时发布时间, 距离提交大于2小时小于15天, 格式为10位时间戳 :param Optional[UploadLine] line: 上传线路 """ + +def upload_by_app(video_path: List[str], + cookie_file: str, + title: str, + tid: int, + tag: str, + copyright: int, + source: str, + desc: str, + dynamic: str, + cover: str, + dolby: int, + lossless_music: int, + no_reprint: int, + open_elec: int, + up_close_reply: bool, + up_selection_reply: bool, + up_close_danmu:bool, + limit: int, + desc_v2: List[Credit], + dtime: Optional[int], + line: Optional[UploadLine]) -> None: + """ + 上传视频稿件 + + :param List[str] video_path: 视频文件路径 + :param str cookie_file: cookie文件路径 + :param str title: 视频标题 + :param int tid: 投稿分区 + :param str tag: 视频标签, 英文逗号分隔多个tag + :param int copyright: 是否转载, 1-自制 2-转载 + :param str source: 转载来源 + :param str desc: 视频简介 + :param str dynamic: 空间动态 + :param str cover: 视频封面 + :param int dolby: 是否开启杜比音效, 0-关闭 1-开启 + :param int lossless_music: 是否开启Hi-Res, 0-关闭 1-开启 + :param int no_reprint: 是否禁止转载, 0-允许 1-禁止 + :param int open_elec: 是否开启充电, 0-关闭 1-开启 + :param bool up_close_reply: 是否禁止评论, false-关闭 true-开启 + :param bool up_selection_reply: 是否精选评论, false-关闭 true-开启 + :param bool up_close_danmu: 是否禁止弹幕, false-关闭 true-开启 + :param int limit: 单视频文件最大并发数 + :param List[Credit] desc_v2: 视频简介v2 + :param Optional[dtime] int dtime: 定时发布时间, 距离提交大于2小时小于15天, 格式为10位时间戳 + :param Optional[UploadLine] line: 上传线路 + """ \ No newline at end of file diff --git a/crates/stream-gears/tests/test_upload.py b/crates/stream-gears/tests/test_upload.py index 96f4bb5..2a693e9 100644 --- a/crates/stream-gears/tests/test_upload.py +++ b/crates/stream-gears/tests/test_upload.py @@ -2,15 +2,16 @@ if __name__ == '__main__': stream_gears.upload( - ["examples/test.mp4"], + ["examples/test.mp4", + "examples/test2.mp4"], "cookies.json", - "title", + "dad424324", 171, - "tag", + "演示", 1, - "source", - "desc", - "dynamic", + "", + "", + "", "", 0, 0, @@ -19,5 +20,30 @@ 3, [], None, - stream_gears.UploadLine.Bda2, + stream_gears.UploadLine.Qn, ) + + # stream_gears.upload_by_app( + # ["examples/test.mp4", + # "examples/test2.mp4"], + # "cookies.json", + # "dadad", + # 171, + # "演示", + # 1, + # "", + # "", + # "", + # "", + # 0, + # 0, + # 0, + # 0, + # True, + # False, + # False, + # 3, + # [], + # None, + # stream_gears.UploadLine.Qn, + # )