diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 044f004..ed76bf3 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -100,7 +100,7 @@ jobs: release: name: Release runs-on: ubuntu-latest - if: "startsWith(github.ref, 'refs/tags/py-')" + if: startsWith(github.ref, 'refs/tags/py-') needs: [linux, windows, macos, sdist] steps: - uses: actions/download-artifact@v4 diff --git a/Cargo.lock b/Cargo.lock index 0efe7b8..d83dd8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -244,7 +244,7 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "biliup" -version = "0.2.1" +version = "0.2.2" dependencies = [ "anyhow", "async-trait", @@ -2704,7 +2704,7 @@ dependencies = [ [[package]] name = "stream-gears" -version = "0.2.1" +version = "0.2.2" dependencies = [ "anyhow", "biliup", diff --git a/README.md b/README.md index 049b370..8f25c18 100644 --- a/README.md +++ b/README.md @@ -47,29 +47,31 @@ Arguments: [VIDEO_PATH]... 需要上传的视频路径,若指定配置文件投稿不需要此参数 Options: - --submit 提交接口 [default: client] [possible values: client, app, web] - -c, --config Sets a custom config file - -l, --line 选择上传线路 [possible values: bda2, ws, qn, bldsa, tx, txa, bda] - --limit 单视频文件最大并发数 [default: 3] - --copyright 是否转载, 1-自制 2-转载 [default: 1] - --source 转载来源 [default: ] - --tid 投稿分区 [default: 171] - --cover 视频封面 [default: ] - --title 视频标题 [default: ] - --desc <DESC> 视频简介 [default: ] - --dynamic <DYNAMIC> 空间动态 [default: ] - --tag <TAG> 视频标签,逗号分隔多个tag [default: ] - --dtime <DTIME> 延时发布时间,距离提交大于4小时,格式为10位时间戳 - --interactive <INTERACTIVE> [default: 0] + --submit <SUBMIT> 提交接口 [default: client] [possible values: client, app, web] + -c, --config <FILE> Sets a custom config file + -l, --line <LINE> 选择上传线路 [possible values: bda2, ws, qn, bldsa, tx, txa, bda, alia] + --limit <LIMIT> 单视频文件最大并发数 [default: 3] + --copyright <COPYRIGHT> 是否转载, 1-自制 2-转载 [default: 1] + --source <SOURCE> 转载来源 [default: ] + --tid <TID> 投稿分区 [default: 171] + --cover <COVER> 视频封面 [default: ] + --title <TITLE> 视频标题 [default: ] + --desc <DESC> 视频简介 [default: ] + --dynamic <DYNAMIC> 空间动态 [default: ] + --tag <TAG> 视频标签,逗号分隔多个tag [default: ] + --topic-id <TOPIC_ID> 视频参与话题(需要自己获取topic_id配合填写mission_id) + --dtime <DTIME> 延时发布时间,距离提交大于4小时,格式为10位时间戳 + --interactive <INTERACTIVE> [default: 0] --mission-id <MISSION_ID> - --dolby <DOLBY> 是否开启杜比音效, 0-关闭 1-开启 [default: 0] - --hires <LOSSLESS_MUSIC> 是否开启 Hi-Res, 0-关闭 1-开启 [default: 0] - --no-reprint <NO_REPRINT> 0-允许转载,1-禁止转载 [default: 0] - --open-elec <OPEN_ELEC> 是否开启充电, 0-关闭 1-开启 [default: 0] - --up-selection-reply 是否开启精选评论,仅提交接口为app时可用 - --up-close-reply 是否关闭评论,仅提交接口为app时可用 - --up-close-danmu 是否关闭弹幕,仅提交接口为app时可用 - -h, --help Print help + --dolby <DOLBY> 是否开启杜比音效, 0-关闭 1-开启 [default: 0] + --hires <LOSSLESS_MUSIC> 是否开启 Hi-Res, 0-关闭 1-开启 [default: 0] + --no-reprint <NO_REPRINT> 0-允许转载,1-禁止转载 [default: 0] + --open-elec <OPEN_ELEC> 是否开启充电, 0-关闭 1-开启 [default: 0] + --up-selection-reply 是否开启精选评论,仅提交接口为app时可用 + --up-close-reply 是否关闭评论,仅提交接口为app时可用 + --up-close-danmu 是否关闭弹幕,仅提交接口为app时可用 + --extra-fields <EXTRA_FIELDS> 自定义提交参数 + -h, --help Print help ``` - 下载视频:`./biliup download https://xxxx` @@ -77,26 +79,28 @@ Options: - 查看完整用法命令行输入 `biliup -h` ```shell -biliup 0.1.14 +$ biliup help Upload video to bilibili. -USAGE: - biliup.exe [OPTIONS] <SUBCOMMAND> - -OPTIONS: - -h, --help Print help information - -u, --user-cookie <USER_COOKIE> 登录信息文件 [default: cookies.json] - -V, --version Print version information - -SUBCOMMANDS: - append 是否要对某稿件追加视频 - download 下载视频 - dump-flv 输出flv元数据 - help Print this message or the help of the given subcommand(s) - login 登录B站并保存登录信息 - renew 手动验证并刷新登录信息 - show 打印视频详情 - upload 上传视频 +Usage: biliup [OPTIONS] <COMMAND> + +Commands: + login 登录B站并保存登录信息 + renew 手动验证并刷新登录信息 + upload 上传视频 + append 是否要对某稿件追加视频 + show 打印视频详情 + dump-flv 输出flv元数据 + download 下载视频 + list 列出所有已上传的视频 + help Print this message or the help of the given subcommand(s) + +Options: + -p, --proxy <PROXY> 配置代理 + -u, --user-cookie <USER_COOKIE> 登录信息文件 [default: cookies.json] + --rust-log <RUST_LOG> [default: tower_http=debug,info] + -h, --help Print help + -V, --version Print version ``` ### 多账号支持 @@ -109,6 +113,13 @@ $biliup --user-cookie user2.json upload ... $biliup renew # ./cookies.json ``` +### 代理支持 + +请在子命令**之前**通过 `-p` 或者 `--proxy` 参数传入 代理 的地址。例如: +```powershell +.\biliup.exe -p http://username:password@proxy.example.com:8080 upload +``` + ### Windows 演示 登录: diff --git a/crates/biliup/Cargo.toml b/crates/biliup/Cargo.toml index b714177..b368dd0 100644 --- a/crates/biliup/Cargo.toml +++ b/crates/biliup/Cargo.toml @@ -1,10 +1,11 @@ [package] name = "biliup" -version = "0.2.1" +version = "0.2.2" edition = "2021" description = "Upload video to bilibili." license = "MIT OR Apache-2.0" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [[bin]] diff --git a/crates/biliup/src/client.rs b/crates/biliup/src/client.rs index 331cbab..23316ed 100644 --- a/crates/biliup/src/client.rs +++ b/crates/biliup/src/client.rs @@ -1,4 +1,4 @@ -use crate::retry; +use crate::{retry, ReqwestClientBuilderExt}; use rand::Rng; use reqwest::header::HeaderMap; use reqwest::{header, Response}; @@ -17,8 +17,8 @@ pub struct StatelessClient { } impl StatelessClient { - pub fn new(headers: HeaderMap) -> Self { - let client = reqwest::Client::builder() + pub fn new(headers: HeaderMap, proxy: Option<&str>) -> Self { + let client = reqwest::Client::proxy_builder(proxy) .user_agent("Mozilla/5.0 (X11; Linux x86_64; rv:60.1) Gecko/20100101 Firefox/60.1") .default_headers(headers) // .timeout(Duration::new(60, 0)) @@ -64,12 +64,12 @@ pub struct StatefulClient { } impl StatefulClient { - pub fn new(headers: HeaderMap) -> Self { + pub fn new(headers: HeaderMap, proxy: Option<&str>) -> Self { let cookie_store = reqwest_cookie_store::CookieStore::default(); let cookie_store = CookieStoreMutex::new(cookie_store); let cookie_store = Arc::new(cookie_store); StatefulClient { - client: reqwest::Client::builder() + client: reqwest::Client::proxy_builder(proxy) .cookie_provider(std::sync::Arc::clone(&cookie_store)) .user_agent( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/63.0.3239.108", @@ -87,7 +87,7 @@ impl StatefulClient { impl Default for StatelessClient { fn default() -> Self { - Self::new(header::HeaderMap::new()) + Self::new(header::HeaderMap::new(), None) } } diff --git a/crates/biliup/src/downloader.rs b/crates/biliup/src/downloader.rs index b057547..b87e663 100644 --- a/crates/biliup/src/downloader.rs +++ b/crates/biliup/src/downloader.rs @@ -8,8 +8,8 @@ use tracing::{debug, error, info}; use crate::downloader::util::{LifecycleFile, Segmentable}; use crate::client::StatelessClient; -use std::str::FromStr; use crate::downloader::extractor::CallbackFn; +use std::str::FromStr; pub mod error; pub mod extractor; @@ -26,8 +26,9 @@ pub async fn download( file_name: &str, segment: Segmentable, file_name_hook: Option<CallbackFn>, + proxy: Option<&str>, ) -> anyhow::Result<()> { - let client = StatelessClient::new(headers); + let client = StatelessClient::new(headers, proxy); let response = client.retryable(url).await?; let mut connection = Connection::new(response); // let buf = &mut [0u8; 9]; @@ -90,7 +91,7 @@ pub fn construct_headers(hash_map: HashMap<String, String>) -> HeaderMap { #[cfg(test)] mod tests { use crate::downloader::download; - use crate::downloader::util::{Segmentable}; + use crate::downloader::util::Segmentable; use anyhow::Result; use reqwest::header::{HeaderMap, HeaderValue, REFERER}; @@ -111,6 +112,7 @@ mod tests { // Segment::Size(20 * 1024 * 1024, 0), Segmentable::new(Some(std::time::Duration::from_secs(6000)), None), None, + None, )?; Ok(()) } diff --git a/crates/biliup/src/lib.rs b/crates/biliup/src/lib.rs index d89ef58..0937ad8 100644 --- a/crates/biliup/src/lib.rs +++ b/crates/biliup/src/lib.rs @@ -43,6 +43,22 @@ where } } +trait ReqwestClientBuilderExt { + fn proxy_builder<U: reqwest::IntoUrl>(proxy: Option<U>) -> reqwest::ClientBuilder; +} + +impl ReqwestClientBuilderExt for reqwest::Client { + fn proxy_builder<U: reqwest::IntoUrl>(proxy: Option<U>) -> reqwest::ClientBuilder { + match proxy { + Some(proxy) => { + tracing::debug!("使用代理: {}", proxy.as_str()); + Self::builder().proxy(reqwest::Proxy::all(proxy).unwrap()) + } + None => Self::builder(), + } + } +} + #[cfg(test)] mod tests { use crate::uploader::bilibili::Vid; diff --git a/crates/biliup/src/uploader/bilibili.rs b/crates/biliup/src/uploader/bilibili.rs index 884d243..c80eaed 100644 --- a/crates/biliup/src/uploader/bilibili.rs +++ b/crates/biliup/src/uploader/bilibili.rs @@ -1,5 +1,6 @@ use crate::error::{Kind, Result}; use crate::uploader::credential::LoginInfo; +use crate::ReqwestClientBuilderExt; use serde::ser::Error; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; @@ -64,6 +65,11 @@ pub struct Studio { #[clap(long, default_value_t)] pub tag: String, + /// 视频参与话题(需要自己获取topic_id配合填写mission_id) + #[clap(long)] + #[serde(default)] + pub topic_id: Option<u32>, + #[serde(default)] #[builder(!default)] #[clap(skip)] @@ -235,8 +241,8 @@ pub struct BiliBili { } impl BiliBili { - pub async fn submit(&self, studio: &Studio) -> Result<ResponseData> { - let ret: ResponseData = reqwest::Client::builder() + pub async fn submit(&self, studio: &Studio, proxy: Option<&str>) -> Result<ResponseData> { + let ret: ResponseData = reqwest::Client::proxy_builder(proxy) .user_agent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/63.0.3239.108") .timeout(Duration::new(60, 0)) .build()? @@ -258,7 +264,11 @@ impl BiliBili { } } - pub async fn submit_by_app(&self, studio: &Studio) -> Result<ResponseData> { + pub async fn submit_by_app( + &self, + studio: &Studio, + proxy: Option<&str>, + ) -> Result<ResponseData> { let payload = { let mut payload = json!({ "access_key": self.login_info.token_info.access_token, @@ -283,7 +293,7 @@ impl BiliBili { payload }; - let ret: ResponseData = reqwest::Client::builder() + let ret: ResponseData = reqwest::Client::proxy_builder(proxy) .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()? @@ -303,8 +313,8 @@ impl BiliBili { } } - pub async fn edit(&self, studio: &Studio) -> Result<serde_json::Value> { - let ret: serde_json::Value = reqwest::Client::builder() + pub async fn edit(&self, studio: &Studio, proxy: Option<&str>) -> Result<serde_json::Value> { + let ret: serde_json::Value = reqwest::Client::proxy_builder(proxy) .user_agent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/63.0.3239.108") .timeout(Duration::new(60, 0)) .build()? @@ -327,8 +337,8 @@ impl BiliBili { } /// 查询视频的 json 信息 - pub async fn video_data(&self, vid: &Vid) -> Result<Value> { - let res: ResponseData = reqwest::Client::builder() + pub async fn video_data(&self, vid: &Vid, proxy: Option<&str>) -> Result<Value> { + let res: ResponseData = reqwest::Client::proxy_builder(proxy) .user_agent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/63.0.3239.108") .timeout(Duration::new(60, 0)) .build()? @@ -354,8 +364,8 @@ impl BiliBili { } } - pub async fn studio_data(&self, vid: &Vid) -> Result<Studio> { - let mut video_info = self.video_data(vid).await?; + pub async fn studio_data(&self, vid: &Vid, proxy: Option<&str>) -> Result<Studio> { + let mut video_info = self.video_data(vid, proxy).await?; let mut studio: Studio = serde_json::from_value(video_info["archive"].take())?; let videos: Vec<Video> = serde_json::from_value(video_info["videos"].take())?; diff --git a/crates/biliup/src/uploader/credential.rs b/crates/biliup/src/uploader/credential.rs index b1fa8c7..877ae33 100644 --- a/crates/biliup/src/uploader/credential.rs +++ b/crates/biliup/src/uploader/credential.rs @@ -51,8 +51,8 @@ impl AppKeyStore { } } -pub async fn login_by_cookies(file: impl AsRef<Path>) -> Result<BiliBili> { - let client = Credential::new(); +pub async fn login_by_cookies(file: impl AsRef<Path>, proxy: Option<&str>) -> Result<BiliBili> { + let client = Credential::new(proxy); // let path = file.as_ref(); let mut file = std::fs::File::options().read(true).write(true).open(file)?; let login_info: LoginInfo = serde_json::from_reader(std::io::BufReader::new(&file))?; @@ -127,13 +127,13 @@ pub struct OAuthInfo { pub struct Credential(StatefulClient); impl Credential { - pub fn new() -> Self { + pub fn new(proxy: Option<&str>) -> Self { let mut headers = header::HeaderMap::new(); headers.insert( "Referer", header::HeaderValue::from_static("https://www.bilibili.com/"), ); - Self(StatefulClient::new(headers)) + Self(StatefulClient::new(headers, proxy)) } async fn validate_tokens(&self, login_info: &LoginInfo) -> Result<ResponseData<ResponseValue>> { @@ -424,7 +424,12 @@ impl Credential { .error_for_status()?; let full = raw.bytes().await?; - let res: ResponseData<ResponseValue> = serde_json::from_slice(&full).map_err(|_| Kind::Custom(format!("error decoding response body, content: {:#?}", String::from_utf8_lossy(&full))))?; + let res: ResponseData<ResponseValue> = serde_json::from_slice(&full).map_err(|_| { + Kind::Custom(format!( + "error decoding response body, content: {:#?}", + String::from_utf8_lossy(&full) + )) + })?; match res { ResponseData { code: 0, @@ -609,6 +614,6 @@ impl Credential { impl Default for Credential { fn default() -> Self { - Self::new() + Self::new(None) } } diff --git a/crates/bin/cli.rs b/crates/bin/cli.rs index b006cb9..e470fd0 100644 --- a/crates/bin/cli.rs +++ b/crates/bin/cli.rs @@ -13,6 +13,10 @@ pub struct Cli { #[clap(subcommand)] pub command: Commands, + /// 配置代理 + #[arg(short, long, default_value = None)] + pub proxy: Option<String>, + /// 登录信息文件 #[arg(short, long, default_value = "cookies.json")] pub user_cookie: PathBuf, @@ -36,7 +40,6 @@ pub enum Commands { // Optional name to operate on // name: Option<String>, - /// 需要上传的视频路径,若指定配置文件投稿不需要此参数 #[arg()] video_path: Vec<PathBuf>, @@ -55,7 +58,6 @@ pub enum Commands { #[command(flatten)] studio: Studio, - // #[arg(required = false, last = true, default_value = "client")] // submit: Option<String>, }, @@ -147,7 +149,7 @@ pub enum UploadLine { Tx, Txa, Bda, - Alia + Alia, } #[derive(Debug, Clone, ValueEnum)] @@ -157,7 +159,6 @@ pub enum SubmitOption { Web, } - fn human_size(s: &str) -> Result<u64, String> { let ret = match s.as_bytes() { [init @ .., b'K'] => parse_u8(init)? * 1000.0, diff --git a/crates/bin/main.rs b/crates/bin/main.rs index fe6c267..a5fc28d 100644 --- a/crates/bin/main.rs +++ b/crates/bin/main.rs @@ -43,9 +43,9 @@ async fn main() -> Result<()> { .init(); match cli.command { - Commands::Login => login(cli.user_cookie).await?, + Commands::Login => login(cli.user_cookie, cli.proxy.as_deref()).await?, Commands::Renew => { - renew(cli.user_cookie).await?; + renew(cli.user_cookie, cli.proxy.as_deref()).await?; } Commands::Upload { video_path, @@ -54,20 +54,41 @@ async fn main() -> Result<()> { limit, studio, submit, - } => upload_by_command(studio, cli.user_cookie, video_path, line, limit, submit).await?, + } => { + upload_by_command( + studio, + cli.user_cookie, + video_path, + line, + limit, + submit, + cli.proxy.as_deref(), + ) + .await? + } Commands::Upload { video_path: _, config: Some(config), .. - } => upload_by_config(config, cli.user_cookie).await?, + } => upload_by_config(config, cli.user_cookie, cli.proxy.as_deref()).await?, Commands::Append { video_path, vid, line, limit, studio: _, - } => append(cli.user_cookie, vid, video_path, line, limit).await?, - Commands::Show { vid } => show(cli.user_cookie, vid).await?, + } => { + append( + cli.user_cookie, + vid, + video_path, + line, + limit, + cli.proxy.as_deref(), + ) + .await? + } + Commands::Show { vid } => show(cli.user_cookie, vid, cli.proxy.as_deref()).await?, Commands::DumpFlv { file_name } => generate_json(file_name)?, Commands::Download { url, @@ -81,7 +102,16 @@ async fn main() -> Result<()> { is_pubing, pubed, not_pubed, - } => list(cli.user_cookie, is_pubing, pubed, not_pubed).await?, + } => { + list( + cli.user_cookie, + is_pubing, + pubed, + not_pubed, + cli.proxy.as_deref(), + ) + .await? + } }; Ok(()) } diff --git a/crates/bin/uploader.rs b/crates/bin/uploader.rs index 2115ade..a58f805 100644 --- a/crates/bin/uploader.rs +++ b/crates/bin/uploader.rs @@ -25,8 +25,8 @@ use std::task::Poll; use std::time::Instant; use tracing::{info, warn}; -pub async fn login(user_cookie: PathBuf) -> Result<()> { - let client = Credential::new(); +pub async fn login(user_cookie: PathBuf, proxy: Option<&str>) -> Result<()> { + let client = Credential::new(proxy); let selection = Select::with_theme(&ColorfulTheme::default()) .with_prompt("选择一种登录方式") .default(1) @@ -52,8 +52,8 @@ pub async fn login(user_cookie: PathBuf) -> Result<()> { Ok(()) } -pub async fn renew(user_cookie: PathBuf) -> Result<()> { - let client = Credential::new(); +pub async fn renew(user_cookie: PathBuf, proxy: Option<&str>) -> Result<()> { + let client = Credential::new(proxy); let mut file = fopen_rw(user_cookie)?; let login_info: LoginInfo = serde_json::from_reader(&file)?; let new_info = client.renew_tokens(login_info).await?; @@ -71,8 +71,9 @@ pub async fn upload_by_command( line: Option<UploadLine>, limit: usize, submit: SubmitOption, + proxy: Option<&str>, ) -> Result<()> { - let bili = login_by_cookies(user_cookie).await?; + let bili = login_by_cookies(user_cookie, proxy).await?; if studio.title.is_empty() { studio.title = video_path[0] .file_stem() @@ -91,16 +92,20 @@ pub async fn upload_by_command( // } // 说不定会适配 web 呢...? match submit { - SubmitOption::App => bili.submit_by_app(&studio).await?, - _ => bili.submit(&studio).await?, + SubmitOption::App => bili.submit_by_app(&studio, proxy).await?, + _ => bili.submit(&studio, proxy).await?, }; Ok(()) } -pub async fn upload_by_config(config: PathBuf, user_cookie: PathBuf) -> Result<()> { +pub async fn upload_by_config( + config: PathBuf, + user_cookie: PathBuf, + proxy: Option<&str>, +) -> Result<()> { // println!("number of concurrent futures: {limit}"); - let bilibili = login_by_cookies(user_cookie).await?; + let bilibili = login_by_cookies(user_cookie, proxy).await?; let config = load_config(&config)?; for (filename_patterns, mut studio) in config.streamers { let mut paths = Vec::new(); @@ -123,7 +128,7 @@ pub async fn upload_by_config(config: PathBuf, user_cookie: PathBuf) -> Result<( config.limit, ) .await?; - bilibili.submit(&studio).await?; + bilibili.submit(&studio, proxy).await?; } Ok(()) } @@ -134,19 +139,20 @@ pub async fn append( video_path: Vec<PathBuf>, line: Option<UploadLine>, limit: usize, + proxy: Option<&str>, ) -> Result<()> { - let bilibili = login_by_cookies(user_cookie).await?; + let bilibili = login_by_cookies(user_cookie, proxy).await?; let mut uploaded_videos = upload(&video_path, &bilibili, line, limit).await?; - let mut studio = bilibili.studio_data(&vid).await?; + let mut studio = bilibili.studio_data(&vid, proxy).await?; studio.videos.append(&mut uploaded_videos); - bilibili.edit(&studio).await?; + bilibili.edit(&studio, proxy).await?; // studio.edit(&login_info).await?; Ok(()) } -pub async fn show(user_cookie: PathBuf, vid: Vid) -> Result<()> { - let bilibili = login_by_cookies(user_cookie).await?; - let video_info = bilibili.video_data(&vid).await?; +pub async fn show(user_cookie: PathBuf, vid: Vid, proxy: Option<&str>) -> Result<()> { + let bilibili = login_by_cookies(user_cookie, proxy).await?; + let video_info = bilibili.video_data(&vid, proxy).await?; println!("{}", serde_json::to_string_pretty(&video_info)?); Ok(()) } @@ -156,6 +162,7 @@ pub async fn list( is_pubing: bool, pubed: bool, not_pubed: bool, + proxy: Option<&str>, ) -> Result<()> { let status = match (is_pubing, pubed, not_pubed) { (true, false, false) => "is_pubing", @@ -168,7 +175,7 @@ pub async fn list( } }; - let bilibili = login_by_cookies(user_cookie).await?; + let bilibili = login_by_cookies(user_cookie, proxy).await?; bilibili .all_archives(status) .await? @@ -177,8 +184,8 @@ pub async fn list( Ok(()) } -async fn login_by_cookies(user_cookie: PathBuf) -> Result<BiliBili> { - let result = credential::login_by_cookies(&user_cookie).await; +async fn login_by_cookies(user_cookie: PathBuf, proxy: Option<&str>) -> Result<BiliBili> { + let result = credential::login_by_cookies(&user_cookie, proxy).await; Ok(if let Err(Kind::IO(_)) = result { result .with_context(|| String::from("open cookies file: ") + &user_cookie.to_string_lossy())? diff --git a/crates/stream-gears/Cargo.toml b/crates/stream-gears/Cargo.toml index dcbfc08..399921b 100644 --- a/crates/stream-gears/Cargo.toml +++ b/crates/stream-gears/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "stream-gears" -version = "0.2.1" +version = "0.2.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/crates/stream-gears/src/lib.rs b/crates/stream-gears/src/lib.rs index 1a892df..50b4556 100644 --- a/crates/stream-gears/src/lib.rs +++ b/crates/stream-gears/src/lib.rs @@ -35,8 +35,9 @@ fn download( header_map: HashMap<String, String>, file_name: &str, segment: PySegment, + proxy: Option<String>, ) -> PyResult<()> { - download_with_callback(py, url, header_map, file_name, segment, None) + download_with_callback(py, url, header_map, file_name, segment, None, proxy) } #[pyfunction] @@ -47,6 +48,7 @@ fn download_with_callback( file_name: &str, segment: PySegment, file_name_callback_fn: Option<PyObject>, + proxy: Option<String>, ) -> PyResult<()> { py.allow_threads(|| { let map = construct_headers(header_map); @@ -87,7 +89,14 @@ fn download_with_callback( let collector = formatting_layer.with(file_layer); tracing::subscriber::with_default(collector, || -> PyResult<()> { - match biliup::downloader::download(url, map, file_name, segment, file_name_hook) { + match biliup::downloader::download( + url, + map, + file_name, + segment, + file_name_hook, + proxy.as_deref(), + ) { Ok(res) => Ok(res), Err(err) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!( "{}, {}", @@ -100,9 +109,9 @@ fn download_with_callback( } #[pyfunction] -fn login_by_cookies(file: String) -> PyResult<bool> { +fn login_by_cookies(file: String, proxy: Option<String>) -> PyResult<bool> { let rt = tokio::runtime::Runtime::new().unwrap(); - let result = rt.block_on(async { login::login_by_cookies(&file).await }); + let result = rt.block_on(async { login::login_by_cookies(&file, proxy.as_deref()).await }); match result { Ok(_) => Ok(true), Err(err) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!( @@ -114,9 +123,10 @@ fn login_by_cookies(file: String) -> PyResult<bool> { } #[pyfunction] -fn send_sms(country_code: u32, phone: u64) -> PyResult<String> { +fn send_sms(country_code: u32, phone: u64, proxy: Option<String>) -> PyResult<String> { let rt = tokio::runtime::Runtime::new().unwrap(); - let result = rt.block_on(async { login::send_sms(country_code, phone).await }); + let result = + rt.block_on(async { login::send_sms(country_code, phone, proxy.as_deref()).await }); match result { Ok(res) => Ok(res.to_string()), Err(err) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!( @@ -127,10 +137,11 @@ fn send_sms(country_code: u32, phone: u64) -> PyResult<String> { } #[pyfunction] -fn login_by_sms(code: u32, ret: String) -> PyResult<bool> { +fn login_by_sms(code: u32, ret: String, proxy: Option<String>) -> PyResult<bool> { let rt = tokio::runtime::Runtime::new().unwrap(); - let result = - rt.block_on(async { login::login_by_sms(code, serde_json::from_str(&ret).unwrap()).await }); + let result = rt.block_on(async { + login::login_by_sms(code, serde_json::from_str(&ret).unwrap(), proxy.as_deref()).await + }); match result { Ok(_) => Ok(true), Err(_) => Ok(false), @@ -138,9 +149,9 @@ fn login_by_sms(code: u32, ret: String) -> PyResult<bool> { } #[pyfunction] -fn get_qrcode() -> PyResult<String> { +fn get_qrcode(proxy: Option<String>) -> PyResult<String> { let rt = tokio::runtime::Runtime::new().unwrap(); - let result = rt.block_on(async { login::get_qrcode().await }); + let result = rt.block_on(async { login::get_qrcode(proxy.as_deref()).await }); match result { Ok(res) => Ok(res.to_string()), Err(err) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!( @@ -151,10 +162,10 @@ fn get_qrcode() -> PyResult<String> { } #[pyfunction] -fn login_by_qrcode(ret: String) -> PyResult<String> { +fn login_by_qrcode(ret: String, proxy: Option<String>) -> PyResult<String> { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { - let info = Credential::new() + let info = Credential::new(proxy.as_deref()) .login_by_qrcode(serde_json::from_str(&ret).unwrap()) .await?; let res = serde_json::to_string_pretty(&info)?; @@ -164,9 +175,15 @@ fn login_by_qrcode(ret: String) -> PyResult<String> { } #[pyfunction] -fn login_by_web_cookies(sess_data: String, bili_jct: String) -> PyResult<bool> { +fn login_by_web_cookies( + sess_data: String, + bili_jct: String, + proxy: Option<String>, +) -> PyResult<bool> { let rt = tokio::runtime::Runtime::new().unwrap(); - let result = rt.block_on(async { login::login_by_web_cookies(&sess_data, &bili_jct).await }); + let result = rt.block_on(async { + login::login_by_web_cookies(&sess_data, &bili_jct, proxy.as_deref()).await + }); match result { Ok(_) => Ok(true), Err(err) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!( @@ -177,9 +194,15 @@ fn login_by_web_cookies(sess_data: String, bili_jct: String) -> PyResult<bool> { } #[pyfunction] -fn login_by_web_qrcode(sess_data: String, dede_user_id: String) -> PyResult<bool> { +fn login_by_web_qrcode( + sess_data: String, + dede_user_id: String, + proxy: Option<String>, +) -> PyResult<bool> { let rt = tokio::runtime::Runtime::new().unwrap(); - let result = rt.block_on(async { login::login_by_web_qrcode(&sess_data, &dede_user_id).await }); + let result = rt.block_on(async { + login::login_by_web_qrcode(&sess_data, &dede_user_id, proxy.as_deref()).await + }); match result { Ok(_) => Ok(true), Err(err) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!( @@ -191,6 +214,7 @@ fn login_by_web_qrcode(sess_data: String, dede_user_id: String) -> PyResult<bool #[allow(clippy::too_many_arguments)] #[pyfunction] +#[pyo3(signature = (video_path, cookie_file, title, tid=171, tag="".to_string(), topic_id=None, copyright=2, source="".to_string(), desc="".to_string(), dynamic="".to_string(), cover="".to_string(), dolby=0, lossless_music=0, no_reprint=0, open_elec=0, limit=3, desc_v2=vec![], dtime=None, line=None, extra_fields="".to_string(), proxy=None))] fn upload( py: Python<'_>, video_path: Vec<PathBuf>, @@ -198,6 +222,7 @@ fn upload( title: String, tid: u16, tag: String, + topic_id: Option<u32>, copyright: u8, source: String, desc: String, @@ -212,6 +237,7 @@ fn upload( dtime: Option<u32>, line: Option<UploadLine>, extra_fields: Option<String>, + proxy: Option<String>, ) -> PyResult<()> { py.allow_threads(|| { let rt = tokio::runtime::Builder::new_current_thread() @@ -247,6 +273,7 @@ fn upload( .title(title) .tid(tid) .tag(tag) + .topic_id(topic_id) .copyright(copyright) .source(source) .desc(desc) @@ -261,7 +288,7 @@ fn upload( .extra_fields(Some(parse_extra_fields(extra_fields))) .build(); - match rt.block_on(uploader::upload(studio_pre)) { + match rt.block_on(uploader::upload(studio_pre, proxy.as_deref())) { Ok(_) => Ok(()), // Ok(_) => { }, Err(err) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!( @@ -276,7 +303,7 @@ fn upload( #[allow(clippy::too_many_arguments)] #[pyfunction] -#[pyo3(signature = (video_path, cookie_file, title, tid=171, tag="".to_string(), copyright=2, source="".to_string(), desc="".to_string(), dynamic="".to_string(), cover="".to_string(), dolby=0, lossless_music=0, no_reprint=0, open_elec=0, up_close_reply=false, up_selection_reply=false, up_close_danmu=false, limit=3, desc_v2=vec![], dtime=None, line=None, extra_fields="".to_string()))] +#[pyo3(signature = (video_path, cookie_file, title, tid=171, tag="".to_string(), topic_id=None, copyright=2, source="".to_string(), desc="".to_string(), dynamic="".to_string(), cover="".to_string(), dolby=0, lossless_music=0, no_reprint=0, open_elec=0, up_close_reply=false, up_selection_reply=false, up_close_danmu=false, limit=3, desc_v2=vec![], dtime=None, line=None, extra_fields="".to_string(), proxy=None))] fn upload_by_app( py: Python<'_>, video_path: Vec<PathBuf>, @@ -284,6 +311,7 @@ fn upload_by_app( title: String, tid: u16, tag: String, + topic_id: Option<u32>, copyright: u8, source: String, desc: String, @@ -301,6 +329,7 @@ fn upload_by_app( dtime: Option<u32>, line: Option<UploadLine>, extra_fields: Option<String>, + proxy: Option<String>, ) -> PyResult<()> { py.allow_threads(|| { let rt = tokio::runtime::Builder::new_current_thread() @@ -336,6 +365,7 @@ fn upload_by_app( .title(title) .tid(tid) .tag(tag) + .topic_id(topic_id) .copyright(copyright) .source(source) .desc(desc) @@ -353,7 +383,7 @@ fn upload_by_app( .extra_fields(Some(parse_extra_fields(extra_fields))) .build(); - match rt.block_on(uploader::upload_by_app(studio_pre)) { + match rt.block_on(uploader::upload_by_app(studio_pre, proxy.as_deref())) { Ok(_) => Ok(()), // Ok(_) => { }, Err(err) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!( diff --git a/crates/stream-gears/src/login.rs b/crates/stream-gears/src/login.rs index 64e190c..e59ddea 100644 --- a/crates/stream-gears/src/login.rs +++ b/crates/stream-gears/src/login.rs @@ -2,30 +2,42 @@ use anyhow::Result; use biliup::uploader::bilibili::BiliBili; use biliup::uploader::credential::Credential; -pub async fn login_by_cookies(file: &str) -> Result<BiliBili> { - let login_info = biliup::uploader::credential::login_by_cookies(file).await?; +pub async fn login_by_cookies(file: &str, proxy: Option<&str>) -> Result<BiliBili> { + let login_info = biliup::uploader::credential::login_by_cookies(file, proxy).await?; Ok(login_info) } -pub async fn send_sms(country_code: u32, phone: u64) -> Result<serde_json::Value> { - let ret = Credential::new().send_sms(phone, country_code).await?; +pub async fn send_sms( + country_code: u32, + phone: u64, + proxy: Option<&str>, +) -> Result<serde_json::Value> { + let ret = Credential::new(proxy).send_sms(phone, country_code).await?; Ok(ret) } -pub async fn login_by_sms(code: u32, res: serde_json::Value) -> Result<bool> { - let info = Credential::new().login_by_sms(code, res).await?; +pub async fn login_by_sms( + code: u32, + res: serde_json::Value, + proxy: Option<&str>, +) -> Result<bool> { + let info = Credential::new(proxy).login_by_sms(code, res).await?; let file = std::fs::File::create("cookies.json")?; serde_json::to_writer_pretty(&file, &info)?; Ok(true) } -pub async fn get_qrcode() -> Result<serde_json::Value> { - let qrcode = Credential::new().get_qrcode().await?; +pub async fn get_qrcode(proxy: Option<&str>) -> Result<serde_json::Value> { + let qrcode = Credential::new(proxy).get_qrcode().await?; Ok(qrcode) } -pub async fn login_by_web_cookies(sess_data: &str, bili_jct: &str) -> Result<bool> { - let info = Credential::new() +pub async fn login_by_web_cookies( + sess_data: &str, + bili_jct: &str, + proxy: Option<&str>, +) -> Result<bool> { + let info = Credential::new(proxy) .login_by_web_cookies(sess_data, bili_jct) .await?; let file = std::fs::File::create("cookies.json")?; @@ -33,8 +45,12 @@ pub async fn login_by_web_cookies(sess_data: &str, bili_jct: &str) -> Result<boo Ok(true) } -pub async fn login_by_web_qrcode(sess_data: &str, dede_user_id: &str) -> Result<bool> { - let info = Credential::new() +pub async fn login_by_web_qrcode( + sess_data: &str, + dede_user_id: &str, + proxy: Option<&str>, +) -> Result<bool> { + let info = Credential::new(proxy) .login_by_web_qrcode(sess_data, dede_user_id) .await?; let file = std::fs::File::create("cookies.json")?; diff --git a/crates/stream-gears/src/uploader.rs b/crates/stream-gears/src/uploader.rs index 57f025d..07e48d8 100644 --- a/crates/stream-gears/src/uploader.rs +++ b/crates/stream-gears/src/uploader.rs @@ -51,6 +51,7 @@ pub struct StudioPre { title: String, tid: u16, tag: String, + topic_id: Option<u32>, copyright: u8, source: String, desc: String, @@ -72,7 +73,7 @@ pub struct StudioPre { extra_fields: Option<HashMap<String, serde_json::Value>>, } -pub async fn upload(studio_pre: StudioPre) -> Result<ResponseData> { +pub async fn upload(studio_pre: StudioPre, proxy: Option<&str>) -> Result<ResponseData> { // let file = std::fs::File::options() // .read(true) // .write(true) @@ -85,6 +86,7 @@ pub async fn upload(studio_pre: StudioPre) -> Result<ResponseData> { title, tid, tag, + topic_id, copyright, source, desc, @@ -100,7 +102,7 @@ pub async fn upload(studio_pre: StudioPre) -> Result<ResponseData> { .. } = studio_pre; - let bilibili = login_by_cookies(&cookie_file).await; + let bilibili = login_by_cookies(&cookie_file, proxy).await; let bilibili = if let Err(Kind::IO(_)) = bilibili { bilibili .with_context(|| String::from("open cookies file: ") + &cookie_file.to_string_lossy())? @@ -169,6 +171,7 @@ pub async fn upload(studio_pre: StudioPre) -> Result<ResponseData> { .dynamic(dynamic) .source(source) .tag(tag) + .topic_id(topic_id) .tid(tid) .title(title) .videos(videos) @@ -191,10 +194,10 @@ pub async fn upload(studio_pre: StudioPre) -> Result<ResponseData> { studio.cover = url; } - Ok(bilibili.submit(&studio).await?) + Ok(bilibili.submit(&studio, proxy).await?) } -pub async fn upload_by_app(studio_pre: StudioPre) -> Result<ResponseData> { +pub async fn upload_by_app(studio_pre: StudioPre, proxy: Option<&str>) -> Result<ResponseData> { // let file = std::fs::File::options() // .read(true) // .write(true) @@ -207,6 +210,7 @@ pub async fn upload_by_app(studio_pre: StudioPre) -> Result<ResponseData> { title, tid, tag, + topic_id, copyright, source, desc, @@ -224,7 +228,7 @@ pub async fn upload_by_app(studio_pre: StudioPre) -> Result<ResponseData> { extra_fields, } = studio_pre; - let bilibili = login_by_cookies(&cookie_file).await; + let bilibili = login_by_cookies(&cookie_file, proxy).await; let bilibili = if let Err(Kind::IO(_)) = bilibili { bilibili .with_context(|| String::from("open cookies file: ") + &cookie_file.to_string_lossy())? @@ -293,6 +297,7 @@ pub async fn upload_by_app(studio_pre: StudioPre) -> Result<ResponseData> { .dynamic(dynamic) .source(source) .tag(tag) + .topic_id(topic_id) .tid(tid) .title(title) .videos(videos) @@ -318,5 +323,5 @@ pub async fn upload_by_app(studio_pre: StudioPre) -> Result<ResponseData> { studio.cover = url; } - Ok(bilibili.submit_by_app(&studio).await?) + Ok(bilibili.submit_by_app(&studio, proxy).await?) } diff --git a/crates/stream-gears/stream_gears/stream_gears.pyi b/crates/stream-gears/stream_gears/stream_gears.pyi index 798530d..5591cc8 100644 --- a/crates/stream-gears/stream_gears/stream_gears.pyi +++ b/crates/stream-gears/stream_gears/stream_gears.pyi @@ -7,7 +7,8 @@ from .pyobject import Segment, Credit def download(url: str, header_map: Dict[str, str], file_name: str, - segment: Segment) -> None: + segment: Segment, + proxy: Optional[str]) -> None: """ 下载视频 @@ -15,6 +16,7 @@ def download(url: str, :param Dict[str, str] header_map: HTTP请求头 :param str file_name: 文件名格式 :param Segment segment: 视频分段设置 + :param Optional[str] proxy: 代理 """ @@ -22,7 +24,8 @@ def download_with_callback(url: str, header_map: Dict[str, str], file_name: str, segment: Segment, - file_name_callback_fn: Callable[[str], None]) -> None: + file_name_callback_fn: Callable[[str], None], + proxy: Optional[str]) -> None: """ 下载视频 @@ -31,70 +34,78 @@ def download_with_callback(url: str, :param str file_name: 文件名格式 :param Segment segment: 视频分段设置 :param Callable[[str], None] file_name_callback_fn: 回调已下载完成文件名 + :param Optional[str] proxy: 代理 """ -def login_by_cookies() -> bool: +def login_by_cookies(proxy: Optional[str]) -> bool: """ cookie登录 + :param Optional[str] proxy: 代理 :return: 是否登录成功 """ -def send_sms(country_code: int, phone: int) -> str: +def send_sms(country_code: int, phone: int, proxy: Optional[str]) -> str: """ 发送短信验证码 :param int country_code: 国家/地区代码 :param int phone: 手机号 + :param Optional[str] proxy: 代理 :return: 短信登录JSON信息 """ -def login_by_sms(code: int, ret: str) -> bool: +def login_by_sms(code: int, ret: str, proxy: Optional[str]) -> bool: """ 短信登录 :param int code: 验证码 :param str ret: 短信登录JSON信息 + :param Optional[str] proxy: 代理 :return: 是否登录成功 """ -def get_qrcode() -> str: +def get_qrcode(proxy: Optional[str]) -> str: """ 获取二维码 + :param Optional[str] proxy: 代理 :return: 二维码登录JSON信息 """ -def login_by_qrcode(ret: str) -> bool: +def login_by_qrcode(ret: str, proxy: Optional[str]) -> bool: """ 二维码登录 :param str ret: 二维码登录JSON信息 + :param Optional[str] proxy: 代理 :return: 是否登录成功 """ -def login_by_web_cookies(sess_data: str, bili_jct: str) -> bool: +def login_by_web_cookies(sess_data: str, bili_jct: str, proxy: Optional[str]) -> bool: """ 网页Cookie登录1 :param str sess_data: SESSDATA :param str bili_jct: bili_jct + :param Optional[str] proxy: 代理 :return: 是否登录成功 """ -def login_by_web_qrcode(sess_data: str, dede_user_id: str) -> bool: +def login_by_web_qrcode(sess_data: str, dede_user_id: str, proxy: Optional[str]) -> bool: """ 网页Cookie登录2 :param str sess_data: SESSDATA :param str dede_user_id: DedeUserID + :param Optional[str] proxy: 代理 :return: 是否登录成功 """ @@ -141,6 +152,7 @@ def upload(video_path: List[str], title: str, tid: int, tag: str, + topic_id: Optional[int], copyright: int, source: str, desc: str, @@ -154,7 +166,8 @@ def upload(video_path: List[str], desc_v2: List[Credit], dtime: Optional[int], line: Optional[UploadLine], - extra_fields: Optional[str]) -> None: + extra_fields: Optional[str], + proxy: Optional[str]) -> None: """ 上传视频稿件 @@ -164,6 +177,7 @@ def upload(video_path: List[str], :param str title: 视频标题 :param int tid: 投稿分区 :param str tag: 视频标签, 英文逗号分隔多个tag + :param Optional[int] topic_id: 话题ID :param int copyright: 是否转载, 1-自制 2-转载 :param str source: 转载来源 :param str desc: 视频简介 @@ -178,6 +192,7 @@ def upload(video_path: List[str], :param Optional[dtime] int dtime: 定时发布时间, 距离提交大于2小时小于15天, 格式为10位时间戳 :param Optional[UploadLine] line: 上传线路 :param Optional[ExtraFields] line: 上传额外参数 + :param Optional[str] proxy: 代理 """ def upload_by_app(video_path: List[str], @@ -185,6 +200,7 @@ def upload_by_app(video_path: List[str], title: str, tid: int, tag: str, + topic_id: Optional[int], copyright: int, source: str, desc: str, @@ -201,7 +217,8 @@ def upload_by_app(video_path: List[str], desc_v2: List[Credit], dtime: Optional[int], line: Optional[UploadLine], - extra_fields: Optional[str]) -> None: + extra_fields: Optional[str], + proxy: Optional[str]) -> None: """ 上传视频稿件 @@ -210,6 +227,7 @@ def upload_by_app(video_path: List[str], :param str title: 视频标题 :param int tid: 投稿分区 :param str tag: 视频标签, 英文逗号分隔多个tag + :param Optional[int] topic_id: 话题ID :param int copyright: 是否转载, 1-自制 2-转载 :param str source: 转载来源 :param str desc: 视频简介 @@ -227,4 +245,5 @@ def upload_by_app(video_path: List[str], :param Optional[dtime] int dtime: 定时发布时间, 距离提交大于2小时小于15天, 格式为10位时间戳 :param Optional[UploadLine] line: 上传线路 :param Optional[ExtraFields] line: 上传额外参数 - """ \ No newline at end of file + :param Optional[str] proxy: 代理 + """