feat(biliup-rs): 新增 BCut Android 投稿接口 (#1402)
* remove: blive hack feature * feat(stream-gears): 支持直播流下载的多条件分割 * 新增 BCut Android 投稿接口 * chore: 优化实现方法 * chore: 完成 Python 实例 * chore: 完善测试用例 * chore: 复用代码 * cleanup
This commit is contained in:
1 parent
ce732944f4
commit
dba7780c3d
19 files changed
+385
-444
No files matched your search
Generated
+5
-4
@@ -779,9 +779,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.41"
|
||||
version = "4.5.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9"
|
||||
checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -789,9 +789,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.41"
|
||||
version = "4.5.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d"
|
||||
checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -5293,6 +5293,7 @@ version = "1.0.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"biliup",
|
||||
"clap",
|
||||
"futures",
|
||||
"pyo3",
|
||||
"serde",
|
||||
|
||||
@@ -291,8 +291,9 @@ const Global: React.FC = () => {
|
||||
}}
|
||||
showClear={true}
|
||||
>
|
||||
<Form.Select.Option value="web">网页端(web)</Form.Select.Option>
|
||||
<Form.Select.Option value="client">客户端(client)</Form.Select.Option>
|
||||
<Form.Select.Option value="app">安卓APP(app)</Form.Select.Option>
|
||||
<Form.Select.Option value="b-cut-android">BCut安卓APP(b-cut-android)</Form.Select.Option>
|
||||
<Form.Select.Option value="web">网页(web)</Form.Select.Option>
|
||||
</Form.Select>
|
||||
<Form.Select
|
||||
field="uploader"
|
||||
|
||||
@@ -536,7 +536,6 @@ def stream_gears_download(url, headers, file_name, segment_time=None, file_size=
|
||||
segment.size = file_size
|
||||
if file_size is None and segment_time is None:
|
||||
segment.size = 8 * 1024 * 1024 * 1024
|
||||
print('Python segment.__dict__:', segment.__dict__)
|
||||
# FIXME: 下载时如出现403,这里不会回到上层方法获取新链接
|
||||
if file_name_callback:
|
||||
stream_gears.download_with_callback(
|
||||
|
||||
@@ -62,6 +62,10 @@ def generate_fake_buvid3():
|
||||
formatted = f"{u_str[0:8]}-{u_str[8:12]}-{u_str[12:16]}-{u_str[16:20]}-{u_str[20:]}infoc"
|
||||
return formatted
|
||||
|
||||
class SubmitOption:
|
||||
App = "app"
|
||||
BCutAndroid = "b-cut-android"
|
||||
|
||||
class Wbi:
|
||||
WTS = "wts"
|
||||
W_RID = "w_rid"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import multiprocessing as mp
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
import stream_gears
|
||||
|
||||
from typing import List, Optional, Union
|
||||
from . import SubmitOption
|
||||
from ..engine import Plugin
|
||||
from ..engine.upload import UploadBase, logger
|
||||
|
||||
@@ -11,7 +12,8 @@ from ..engine.upload import UploadBase, logger
|
||||
@Plugin.upload(platform="biliup-rs")
|
||||
class BiliWeb(UploadBase):
|
||||
def __init__(
|
||||
self, principal, data, submit_api=None, copyright=2, postprocessor=None, dtime=None,
|
||||
self, principal, data, submit_api: Optional[Union[SubmitOption, str]] = None,
|
||||
copyright=2, postprocessor=None, dtime=None,
|
||||
dynamic='', lines='AUTO', threads=3, tid=122, tags=None, cover_path=None, description='',
|
||||
dolby=0, hires=0, no_reprint=0, is_only_self=0, open_elec=0, credits=None,
|
||||
user_cookie='cookies.json', copyright_source=None, extra_fields = ""
|
||||
@@ -137,6 +139,6 @@ def stream_gears_upload(ex_conn, lines, *args, **kwargs):
|
||||
elif lines == 'bldsa':
|
||||
kwargs['line'] = stream_gears.UploadLine.Bldsa
|
||||
|
||||
stream_gears.upload_by_app(*args, **kwargs)
|
||||
stream_gears.upload(*args, **kwargs)
|
||||
except Exception as e:
|
||||
ex_conn.send(e)
|
||||
@@ -1,8 +1,9 @@
|
||||
import time
|
||||
|
||||
import stream_gears
|
||||
from typing import List
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from . import SubmitOption
|
||||
from ..engine import Plugin
|
||||
from ..engine.upload import UploadBase, logger
|
||||
|
||||
@@ -10,7 +11,8 @@ from ..engine.upload import UploadBase, logger
|
||||
@Plugin.upload(platform="stream_gears")
|
||||
class BiliWeb(UploadBase):
|
||||
def __init__(
|
||||
self, principal, data, submit_api=None, copyright=2, postprocessor=None, dtime=None,
|
||||
self, principal, data, submit_api: Optional[Union[SubmitOption, str]] = None,
|
||||
copyright=2, postprocessor=None, dtime=None,
|
||||
dynamic='', lines='AUTO', threads=3, tid=122, tags=None, cover_path=None, description='',
|
||||
dolby=0, hires=0, no_reprint=0, open_elec=0, credits=None,
|
||||
user_cookie='cookies.json', copyright_source=None, extra_fields = ""
|
||||
@@ -78,7 +80,7 @@ class BiliWeb(UploadBase):
|
||||
dtime = None
|
||||
if self.dtime:
|
||||
dtime = int(time.time() + self.dtime)
|
||||
stream_gears.upload_by_app(
|
||||
stream_gears.upload(
|
||||
video_path=file_list,
|
||||
cookie_file=self.user_cookie,
|
||||
title=self.data["format_title"][:80],
|
||||
@@ -97,7 +99,8 @@ class BiliWeb(UploadBase):
|
||||
desc_v2=desc_v2,
|
||||
dtime=dtime,
|
||||
line=line,
|
||||
extra_fields=self.extra_fields
|
||||
extra_fields=self.extra_fields,
|
||||
submit=self.submit_api,
|
||||
)
|
||||
logger.info(f"上传成功: {self.principal}")
|
||||
return file_list
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use biliup::uploader::bilibili::{Studio, Vid};
|
||||
use biliup::uploader::util::SubmitOption;
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
|
||||
use std::path::PathBuf;
|
||||
@@ -151,11 +152,7 @@ pub enum Commands {
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
|
||||
pub enum UploadLine {
|
||||
Bda2,
|
||||
Ws,
|
||||
Qn,
|
||||
// Kodo,
|
||||
// Cos,
|
||||
// CosInternal,
|
||||
Bldsa,
|
||||
Tx,
|
||||
Txa,
|
||||
@@ -163,13 +160,6 @@ pub enum UploadLine {
|
||||
Alia,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, ValueEnum)]
|
||||
pub enum SubmitOption {
|
||||
Client,
|
||||
App,
|
||||
Web,
|
||||
}
|
||||
|
||||
fn human_size(s: &str) -> Result<u64, String> {
|
||||
let ret = match s.as_bytes() {
|
||||
[init @ .., b'K'] => parse_u8(init)? * 1000.0,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::cli::{SubmitOption, UploadLine};
|
||||
use crate::cli::UploadLine;
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use biliup::client::StatelessClient;
|
||||
use biliup::error::Kind;
|
||||
use biliup::uploader::bilibili::{BiliBili, Studio, Vid, Video};
|
||||
use biliup::uploader::credential::{Credential, LoginInfo};
|
||||
use biliup::uploader::line::Probe;
|
||||
use biliup::uploader::util::SubmitOption;
|
||||
use biliup::uploader::{VideoFile, credential, line, load_config};
|
||||
use bytes::{Buf, Bytes};
|
||||
use clap::ValueEnum;
|
||||
@@ -84,16 +85,9 @@ pub async fn upload_by_command(
|
||||
cover_up(&mut studio, &bili).await?;
|
||||
studio.videos = upload(&video_path, &bili, line, limit).await?;
|
||||
|
||||
// if studio.submit_by_app {
|
||||
// bili.submit_by_app(&studio).await?;
|
||||
// }
|
||||
// else {
|
||||
// bili.submit(&studio).await?;
|
||||
// }
|
||||
// 说不定会适配 web 呢...?
|
||||
match submit {
|
||||
SubmitOption::App => bili.submit_by_app(&studio, proxy).await?,
|
||||
_ => bili.submit(&studio, proxy).await?,
|
||||
SubmitOption::BCutAndroid => bili.submit_by_bcut_android(&studio, proxy).await?,
|
||||
_ => bili.submit_by_app(&studio, proxy).await?,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
@@ -139,7 +133,7 @@ pub async fn append(
|
||||
video_path: Vec<PathBuf>,
|
||||
line: Option<UploadLine>,
|
||||
limit: usize,
|
||||
submit: SubmitOption ,
|
||||
submit: SubmitOption,
|
||||
proxy: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let bilibili = login_by_cookies(user_cookie, proxy).await?;
|
||||
@@ -234,17 +228,13 @@ pub async fn upload(
|
||||
let client = StatelessClient::default();
|
||||
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::Bldsa) => line::bldsa(),
|
||||
Some(UploadLine::Tx) => line::tx(),
|
||||
Some(UploadLine::Txa) => line::txa(),
|
||||
Some(UploadLine::Bda) => line::bda(),
|
||||
Some(UploadLine::Alia) => line::alia(),
|
||||
None => Probe::probe(&client.client).await.unwrap_or_default(),
|
||||
_ => Probe::probe(&client.client).await.unwrap_or_default(),
|
||||
};
|
||||
// let line = line::kodo();
|
||||
for video_path in video_path {
|
||||
|
||||
@@ -50,46 +50,13 @@ impl Segmentable {
|
||||
|
||||
/// 检查是否需要分割 - 只要时间或大小任一条件满足就返回 true
|
||||
pub fn needed(&self) -> bool {
|
||||
// 检查时间条件
|
||||
let time_exceeded = if let Some(expected_time) = self.time.expected {
|
||||
(self.time.current - self.time.start) >= expected_time
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// 检查大小条件
|
||||
let size_exceeded = if let Some(expected_size) = self.size.expected {
|
||||
self.size.current >= expected_size
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// 任一条件满足就需要分割
|
||||
let time_exceeded = self.time_needed();
|
||||
let size_exceeded = self.size_needed();
|
||||
let result = time_exceeded || size_exceeded;
|
||||
|
||||
// 添加调试信息
|
||||
if result {
|
||||
if time_exceeded && size_exceeded {
|
||||
tracing::debug!(
|
||||
"Segmentation needed: Both time ({:?} >= {:?}) and size ({} >= {}) conditions met",
|
||||
self.time.current - self.time.start,
|
||||
self.time.expected.unwrap(),
|
||||
self.size.current,
|
||||
self.size.expected.unwrap()
|
||||
);
|
||||
} else if time_exceeded {
|
||||
tracing::debug!(
|
||||
"Segmentation needed: Time condition met ({:?} >= {:?})",
|
||||
self.time.current - self.time.start,
|
||||
self.time.expected.unwrap()
|
||||
);
|
||||
} else if size_exceeded {
|
||||
tracing::debug!(
|
||||
"Segmentation needed: Size condition met ({} >= {})",
|
||||
self.size.current,
|
||||
self.size.expected.unwrap()
|
||||
);
|
||||
}
|
||||
self.log_segmentation_reason(time_exceeded, size_exceeded);
|
||||
}
|
||||
|
||||
result
|
||||
@@ -113,6 +80,36 @@ impl Segmentable {
|
||||
}
|
||||
}
|
||||
|
||||
/// 记录分割原因的调试信息
|
||||
fn log_segmentation_reason(&self, time_exceeded: bool, size_exceeded: bool) {
|
||||
match (time_exceeded, size_exceeded) {
|
||||
(true, true) => {
|
||||
tracing::info!(
|
||||
"Segmentation needed: Both time ({:?} >= {:?}) and size ({} >= {}) conditions met",
|
||||
self.time.current - self.time.start,
|
||||
self.time.expected.unwrap(),
|
||||
self.size.current,
|
||||
self.size.expected.unwrap()
|
||||
);
|
||||
}
|
||||
(true, false) => {
|
||||
tracing::info!(
|
||||
"Segmentation needed: Time condition met ({:?} >= {:?})",
|
||||
self.time.current - self.time.start,
|
||||
self.time.expected.unwrap()
|
||||
);
|
||||
}
|
||||
(false, true) => {
|
||||
tracing::info!(
|
||||
"Segmentation needed: Size condition met ({} >= {})",
|
||||
self.size.current,
|
||||
self.size.expected.unwrap()
|
||||
);
|
||||
}
|
||||
(false, false) => {} // 不应该到达这里,因为只有在需要分割时才调用
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取分割原因的描述
|
||||
pub fn get_segment_reason(&self) -> String {
|
||||
let time_exceeded = self.time_needed();
|
||||
@@ -159,29 +156,51 @@ impl Segmentable {
|
||||
self.time.start = Duration::ZERO;
|
||||
}
|
||||
|
||||
/// 格式化进度信息的通用方法
|
||||
fn format_progress<T>(
|
||||
label: &str,
|
||||
current: T,
|
||||
expected: Option<T>,
|
||||
unit: &str,
|
||||
format_fn: impl Fn(T) -> String,
|
||||
) -> String
|
||||
where
|
||||
T: Copy + Into<f64>,
|
||||
{
|
||||
if let Some(expected_val) = expected {
|
||||
let current_f64 = current.into();
|
||||
let expected_f64 = expected_val.into();
|
||||
let percentage = (current_f64 / expected_f64 * 100.0).min(100.0);
|
||||
format!(
|
||||
"{}: {}/{} {} ({:.1}%)",
|
||||
label,
|
||||
format_fn(current),
|
||||
format_fn(expected_val),
|
||||
unit,
|
||||
percentage
|
||||
)
|
||||
} else {
|
||||
format!("{}: No limit", label)
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取当前状态信息
|
||||
pub fn get_status(&self) -> String {
|
||||
let time_info = if let Some(expected) = self.time.expected {
|
||||
format!(
|
||||
"Time: {:.1}s/{:.1}s ({:.1}%)",
|
||||
(self.time.current - self.time.start).as_secs_f64(),
|
||||
expected.as_secs_f64(),
|
||||
((self.time.current - self.time.start).as_secs_f64() / expected.as_secs_f64() * 100.0).min(100.0)
|
||||
)
|
||||
} else {
|
||||
"Time: No limit".to_string()
|
||||
};
|
||||
let time_info = Self::format_progress(
|
||||
"Time",
|
||||
(self.time.current - self.time.start).as_secs_f64(),
|
||||
self.time.expected.map(|d| d.as_secs_f64()),
|
||||
"s",
|
||||
|t| format!("{:.1}", t),
|
||||
);
|
||||
|
||||
let size_info = if let Some(expected) = self.size.expected {
|
||||
format!(
|
||||
"Size: {}/{} bytes ({:.1}%)",
|
||||
self.size.current,
|
||||
expected,
|
||||
(self.size.current as f64 / expected as f64 * 100.0).min(100.0)
|
||||
)
|
||||
} else {
|
||||
"Size: No limit".to_string()
|
||||
};
|
||||
let size_info = Self::format_progress(
|
||||
"Size",
|
||||
self.size.current as f64,
|
||||
self.size.expected.map(|s| s as f64),
|
||||
"bytes",
|
||||
|s| format!("{}", s as u64),
|
||||
);
|
||||
|
||||
format!("{}, {}", time_info, size_info)
|
||||
}
|
||||
@@ -227,17 +246,22 @@ impl LifecycleFile {
|
||||
}
|
||||
|
||||
pub fn create(&mut self) -> Result<&Path, std::io::Error> {
|
||||
// 构建最终文件名
|
||||
self.file_name = format!(
|
||||
"{}.{}",
|
||||
format_filename(&self.fmt_file_name),
|
||||
self.extension
|
||||
);
|
||||
|
||||
// 构建临时文件路径(带 .part 后缀)
|
||||
self.path = PathBuf::from(&self.file_name);
|
||||
if let Some(parent) = self.path.parent() {
|
||||
fs::create_dir_all(parent)?
|
||||
}
|
||||
// path.set_extension(&self.extension);
|
||||
self.path.set_extension(format!("{}.part", self.extension));
|
||||
|
||||
// 确保父目录存在
|
||||
if let Some(parent) = self.path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
info!("Save to {}", self.path.display());
|
||||
Ok(self.path.as_path())
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use std::task::{Context, Poll};
|
||||
pub mod bilibili;
|
||||
pub mod credential;
|
||||
pub mod line;
|
||||
pub mod util;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
|
||||
@@ -241,6 +241,57 @@ impl BiliBili {
|
||||
self.submit_by_app(studio, proxy).await
|
||||
}
|
||||
|
||||
/// 使用必剪接口投稿
|
||||
pub async fn submit_by_bcut_android(
|
||||
&self,
|
||||
studio: &Studio,
|
||||
proxy: Option<&str>,
|
||||
) -> Result<ResponseData> {
|
||||
let payload = {
|
||||
let mut payload = json!({
|
||||
"access_key": self.login_info.token_info.access_token,
|
||||
"appkey": crate::credential::AppKeyStore::BCutAndroid.app_key(),
|
||||
"aurora_version": "2.39.0",
|
||||
"build": 2800030,
|
||||
"c_locale": "zh-Hans_CN",
|
||||
"channel": "master",
|
||||
"mobi_app": "android_bbs",
|
||||
"montage_version": "1.42.1.0",
|
||||
"platform": "android",
|
||||
"s_locale": "zh-Hans_CN",
|
||||
"sdk_type": "mon",
|
||||
"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::BCutAndroid.appsec(),
|
||||
);
|
||||
payload["sign"] = Value::from(sign);
|
||||
payload
|
||||
};
|
||||
|
||||
let ret: ResponseData = reqwest::Client::proxy_builder(proxy)
|
||||
.user_agent("Mozilla/5.0 os/android model/Mi 10 Pro mobi_app/android_bbs build/2800030 channel/master osVer/13 kernel_version/V14.0.4.0.TJACNXM BiliDroid/5.6.0 (bbcallen@gmail.com)")
|
||||
.timeout(Duration::new(60, 0))
|
||||
.build()?
|
||||
.post("https://member.bilibili.com/x/vu/mvp/add")
|
||||
.query(&payload)
|
||||
.json(studio)
|
||||
.send()
|
||||
.await?
|
||||
.json()
|
||||
.await?;
|
||||
info!("{:?}", ret);
|
||||
if ret.code == 0 {
|
||||
info!("BCUT接口投稿成功");
|
||||
Ok(ret)
|
||||
} else {
|
||||
Err(Kind::Custom(format!("{:?}", ret)))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn submit_by_app(
|
||||
&self,
|
||||
studio: &Studio,
|
||||
|
||||
@@ -33,6 +33,7 @@ use url::Url;
|
||||
pub(crate) enum AppKeyStore {
|
||||
BiliTV,
|
||||
Android,
|
||||
BCutAndroid,
|
||||
}
|
||||
|
||||
impl AppKeyStore {
|
||||
@@ -40,6 +41,7 @@ impl AppKeyStore {
|
||||
match self {
|
||||
AppKeyStore::BiliTV => "4409e2ce8ffd12b8",
|
||||
AppKeyStore::Android => "783bbb7264451d82",
|
||||
AppKeyStore::BCutAndroid => "5dce947fe22167f9",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +49,7 @@ impl AppKeyStore {
|
||||
match self {
|
||||
AppKeyStore::BiliTV => "59b43e04ad6965f34319062b478f83dd",
|
||||
AppKeyStore::Android => "2653583c8873dea268ab9386918b1d65",
|
||||
AppKeyStore::BCutAndroid => "5491a31c6bc11fb764a9b1f8d4acf092",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
#[cfg(feature = "cli")]
|
||||
use clap::ValueEnum;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "cli", derive(ValueEnum))]
|
||||
pub enum SubmitOption {
|
||||
// Client,
|
||||
App,
|
||||
// Web,
|
||||
BCutAndroid,
|
||||
}
|
||||
|
||||
impl SubmitOption {
|
||||
/// Parse a string into SubmitOption, compatible with both clap and manual parsing
|
||||
pub fn parse_str(s: &str) -> Result<Self, String> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"app" => Ok(SubmitOption::App),
|
||||
"bcutandroid" | "b-cut-android" | "bcut_android" => Ok(SubmitOption::BCutAndroid),
|
||||
_ => Err(format!("Unknown submit option: {}", s))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ futures = "0.3.31"
|
||||
biliup = { path = "../biliup"}
|
||||
time = "0.3"
|
||||
typed-builder = "0.20.0"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
# [target.'cfg(all(target_arch="aarch64", target_os="linux"))'.dependencies]
|
||||
# downloader = { path = "../downloader", default-features = false , features = ["rustls-tls"] }
|
||||
|
||||
@@ -14,6 +14,7 @@ use biliup::credential::Credential;
|
||||
use biliup::downloader::construct_headers;
|
||||
use biliup::downloader::extractor::CallbackFn;
|
||||
use biliup::downloader::util::Segmentable;
|
||||
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
|
||||
#[derive(FromPyObject, Debug)]
|
||||
@@ -75,8 +76,7 @@ fn download_with_callback(
|
||||
|
||||
let segmentable = match (segment.time, segment.size) {
|
||||
(Some(time), Some(size)) => {
|
||||
// 如果同时有时间和大小,可以选择优先使用哪个,或者创建支持两者的逻辑
|
||||
// 这里假设优先使用时间分割
|
||||
// 已支持同时创建时间和大小
|
||||
Segmentable::new(Some(Duration::from_secs(time)), Some(size))
|
||||
}
|
||||
(Some(time), None) => {
|
||||
@@ -91,8 +91,6 @@ fn download_with_callback(
|
||||
}
|
||||
};
|
||||
|
||||
tracing::debug!("Segmentable: {:?}", segmentable);
|
||||
|
||||
let file_name_hook = file_name_callback_fn.map(|callback_fn| -> CallbackFn {
|
||||
Box::new(move |fmt_file_name| {
|
||||
Python::with_gil(|py| match callback_fn.call1(py, (fmt_file_name,)) {
|
||||
@@ -231,96 +229,8 @@ fn login_by_web_qrcode(
|
||||
|
||||
#[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, limit=3, desc_v2=vec![], dtime=None, line=None, extra_fields="".to_string(), proxy=None))]
|
||||
#[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(), submit=None, proxy=None))]
|
||||
fn upload(
|
||||
py: Python<'_>,
|
||||
video_path: Vec<PathBuf>,
|
||||
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,
|
||||
limit: usize,
|
||||
desc_v2: Vec<PyCredit>,
|
||||
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()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
// 输出到控制台中
|
||||
// use of deprecated function `time::util::local_offset::set_soundness`: no longer needed; TZ is refreshed manually
|
||||
// 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)
|
||||
.desc_v2_credit(desc_v2)
|
||||
.extra_fields(Some(parse_extra_fields(extra_fields)))
|
||||
.build();
|
||||
|
||||
match rt.block_on(uploader::upload(studio_pre, proxy.as_deref())) {
|
||||
Ok(_) => Ok(()),
|
||||
// Ok(_) => { },
|
||||
Err(err) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!(
|
||||
"{}, {}",
|
||||
err.root_cause(),
|
||||
err
|
||||
))),
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[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(), proxy=None))]
|
||||
fn upload_by_app(
|
||||
py: Python<'_>,
|
||||
video_path: Vec<PathBuf>,
|
||||
cookie_file: PathBuf,
|
||||
@@ -344,6 +254,7 @@ fn upload_by_app(
|
||||
dtime: Option<u32>,
|
||||
line: Option<UploadLine>,
|
||||
extra_fields: Option<String>,
|
||||
submit: Option<String>,
|
||||
proxy: Option<String>,
|
||||
) -> PyResult<()> {
|
||||
py.allow_threads(|| {
|
||||
@@ -398,7 +309,12 @@ fn upload_by_app(
|
||||
.extra_fields(Some(parse_extra_fields(extra_fields)))
|
||||
.build();
|
||||
|
||||
match rt.block_on(uploader::upload_by_app(studio_pre, proxy.as_deref())) {
|
||||
// let submit = match submit {
|
||||
// Some(value) => SubmitOption::from_str(&value, true).unwrap(),
|
||||
// None => SubmitOption::App,
|
||||
// };
|
||||
|
||||
match rt.block_on(uploader::upload(studio_pre, submit.as_deref(), proxy.as_deref())) {
|
||||
Ok(_) => Ok(()),
|
||||
// Ok(_) => { },
|
||||
Err(err) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!(
|
||||
@@ -420,7 +336,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!(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)?)?;
|
||||
|
||||
@@ -4,6 +4,7 @@ use biliup::error::Kind;
|
||||
use biliup::uploader::bilibili::{Credit, ResponseData, Studio};
|
||||
use biliup::uploader::credential::login_by_cookies;
|
||||
use biliup::uploader::line::Probe;
|
||||
use biliup::uploader::util::SubmitOption;
|
||||
use biliup::uploader::{VideoFile, line};
|
||||
use futures::StreamExt;
|
||||
use pyo3::prelude::*;
|
||||
@@ -20,7 +21,6 @@ use typed_builder::TypedBuilder;
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum UploadLine {
|
||||
Bda2,
|
||||
Ws,
|
||||
Qn,
|
||||
Bldsa,
|
||||
Tx,
|
||||
@@ -69,129 +69,7 @@ pub struct StudioPre {
|
||||
extra_fields: Option<HashMap<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
pub async fn upload(studio_pre: StudioPre, proxy: Option<&str>) -> Result<ResponseData> {
|
||||
// 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,
|
||||
desc_v2_credit,
|
||||
extra_fields,
|
||||
..
|
||||
} = studio_pre;
|
||||
|
||||
let bilibili = login_by_cookies(&cookie_file, proxy).await;
|
||||
let bilibili = match bilibili {
|
||||
Err(Kind::IO(_)) => bilibili.with_context(|| {
|
||||
String::from("open cookies file: ") + &cookie_file.to_string_lossy()
|
||||
})?,
|
||||
_ => 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(),
|
||||
Some(UploadLine::Alia) => line::alia(),
|
||||
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)
|
||||
.desc_v2(Some(desc_v2))
|
||||
.extra_fields(extra_fields)
|
||||
.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(&studio, proxy).await?)
|
||||
}
|
||||
|
||||
pub async fn upload_by_app(studio_pre: StudioPre, proxy: Option<&str>) -> Result<ResponseData> {
|
||||
pub async fn upload(studio_pre: StudioPre, submit: Option<&str>, proxy: Option<&str>) -> Result<ResponseData> {
|
||||
// let file = std::fs::File::options()
|
||||
// .read(true)
|
||||
// .write(true)
|
||||
@@ -233,17 +111,13 @@ pub async fn upload_by_app(studio_pre: StudioPre, proxy: Option<&str>) -> Result
|
||||
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(),
|
||||
Some(UploadLine::Alia) => line::alia(),
|
||||
None => Probe::probe(&client.client).await.unwrap_or_default(),
|
||||
_ => Probe::probe(&client.client).await.unwrap_or_default(),
|
||||
};
|
||||
for video_path in video_path {
|
||||
println!("{:?}", video_path.canonicalize()?.to_str());
|
||||
@@ -315,5 +189,14 @@ pub async fn upload_by_app(studio_pre: StudioPre, proxy: Option<&str>) -> Result
|
||||
studio.cover = url;
|
||||
}
|
||||
|
||||
Ok(bilibili.submit_by_app(&studio, proxy).await?)
|
||||
}
|
||||
let submit = match submit {
|
||||
Some(submit) => SubmitOption::parse_str(submit).unwrap_or(SubmitOption::App),
|
||||
_ => SubmitOption::App,
|
||||
};
|
||||
|
||||
let submit_result = match submit {
|
||||
SubmitOption::BCutAndroid => bilibili.submit_by_bcut_android(&studio, proxy).await?,
|
||||
_ => bilibili.submit_by_app(&studio, proxy).await?,
|
||||
};
|
||||
Ok(submit_result)
|
||||
}
|
||||
@@ -114,28 +114,25 @@ class UploadLine(Enum):
|
||||
"""上传线路"""
|
||||
|
||||
Bda2 = 1
|
||||
"""百度upos"""
|
||||
"""百度云"""
|
||||
|
||||
Ws = 2
|
||||
"""网宿upos"""
|
||||
Qn = 2
|
||||
"""七牛"""
|
||||
|
||||
Qn = 3
|
||||
"""七牛upos"""
|
||||
|
||||
Bda = 4
|
||||
Bda = 3
|
||||
"""百度云海外"""
|
||||
|
||||
Tx = 5
|
||||
Tx = 4
|
||||
"""腾讯云EO"""
|
||||
|
||||
Txa = 6
|
||||
Txa = 5
|
||||
"""腾讯云EO海外"""
|
||||
|
||||
Bldsa = 7
|
||||
"""Bldsa"""
|
||||
Bldsa = 6
|
||||
"""Bilibili大陆动态加速"""
|
||||
|
||||
Alia = 8
|
||||
"""阿里云upos"""
|
||||
Alia = 7
|
||||
"""阿里云海外"""
|
||||
|
||||
|
||||
def upload(video_path: List[str],
|
||||
@@ -157,6 +154,7 @@ def upload(video_path: List[str],
|
||||
dtime: Optional[int],
|
||||
line: Optional[UploadLine],
|
||||
extra_fields: Optional[str],
|
||||
submit: Optional[str],
|
||||
proxy: Optional[str]) -> None:
|
||||
|
||||
"""
|
||||
@@ -181,56 +179,6 @@ 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],
|
||||
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],
|
||||
extra_fields: Optional[str],
|
||||
proxy: Optional[str]) -> 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: 上传线路
|
||||
:param Optional[ExtraFields] line: 上传额外参数
|
||||
:param Optional[str] submit: 提交接口, 可选值: BCutAndroid, App(默认)
|
||||
:param Optional[str] proxy: 代理
|
||||
"""
|
||||
@@ -1,19 +1,73 @@
|
||||
import stream_gears
|
||||
import sys
|
||||
import multiprocessing
|
||||
import time
|
||||
|
||||
|
||||
class Segment:
|
||||
pass
|
||||
|
||||
|
||||
def download_worker():
|
||||
"""在子进程中执行下载任务"""
|
||||
try:
|
||||
segment = Segment()
|
||||
segment.time = 60 * 1 # secs(1 min)
|
||||
# segment.size = 6000 * 1024 * 1024
|
||||
segment.size = 1000 * 1000 * 10 # bytes(10MB)
|
||||
url = ""
|
||||
|
||||
stream_gears.download(
|
||||
url = url,
|
||||
header_map = {
|
||||
"referer": "https://live.bilibili.com",
|
||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
|
||||
},
|
||||
file_name = "new_test%Y-%m-%dT%H_%M_%S",
|
||||
segment = segment,
|
||||
proxy = None
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"下载过程中发生错误: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def download_with_signal_handling():
|
||||
"""使用多进程包装下载函数,支持 Ctrl+C 中断"""
|
||||
print("开始下载,按 Ctrl+C 可以退出...")
|
||||
|
||||
# 创建下载进程
|
||||
download_process = multiprocessing.Process(target=download_worker)
|
||||
download_process.start()
|
||||
|
||||
try:
|
||||
# 主进程等待下载进程完成,同时监听信号
|
||||
while download_process.is_alive():
|
||||
time.sleep(0.1) # 短暂休眠,避免占用过多 CPU
|
||||
|
||||
# 检查下载进程的退出状态
|
||||
download_process.join()
|
||||
if download_process.exitcode == 0:
|
||||
print("下载完成")
|
||||
else:
|
||||
print("下载进程异常退出")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n收到中断信号,正在终止下载...")
|
||||
# 终止下载进程
|
||||
download_process.terminate()
|
||||
# 等待进程真正结束
|
||||
download_process.join(timeout=5)
|
||||
if download_process.is_alive():
|
||||
print("强制杀死下载进程...")
|
||||
download_process.kill()
|
||||
download_process.join()
|
||||
print("下载已被用户中断")
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
segment = Segment()
|
||||
# segment.time = 60
|
||||
# segment.size = 6000 * 1024 * 1024
|
||||
segment.size = 60 * 1024 * 1024
|
||||
stream_gears.download(
|
||||
"",
|
||||
{"referer": "https://live.bilibili.com"},
|
||||
# {},
|
||||
"new_test%Y-%m-%dT%H_%M_%S",
|
||||
segment
|
||||
)
|
||||
# Windows 上需要这个来支持 multiprocessing
|
||||
multiprocessing.freeze_support()
|
||||
download_with_signal_handling()
|
||||
@@ -1,48 +1,96 @@
|
||||
"""
|
||||
stream_gears 上传功能测试
|
||||
|
||||
这个测试文件展示了如何使用 stream_gears.upload() 函数的各种功能:
|
||||
1. 基础上传 - 使用关键字参数让代码更清晰
|
||||
2. 多文件上传 - 上传多P视频
|
||||
3. 定时发布 - 设置未来时间发布
|
||||
4. 高级功能 - 使用富文本简介和额外字段
|
||||
5. 最小参数 - 只使用必需参数,其他使用默认值
|
||||
|
||||
注意:运行前请确保:
|
||||
- 存在有效的 cookies.json 文件
|
||||
- 视频文件路径正确
|
||||
- 网络连接正常
|
||||
"""
|
||||
|
||||
import stream_gears
|
||||
from typing import List, Dict, Any
|
||||
|
||||
video_path = ["E:\\Projects\\biliup\\target\\x86_64-pc-windows-msvc\\debug\\test.flv"]
|
||||
cookie_file = "E:\\Projects\\biliup\\target\\x86_64-pc-windows-msvc\\debug\\cookies.json"
|
||||
# submit_api = "BCutAndroid"
|
||||
|
||||
if __name__ == '__main__':
|
||||
# full kwargs 上传测试
|
||||
# stream_gears.upload(
|
||||
# video_path=["examples/test.mp4"],
|
||||
# cookie_file="cookies.json",
|
||||
# title="测试视频标题",
|
||||
# tid=171, # 电子竞技分区
|
||||
# tag="测试,上传,演示",
|
||||
# copyright=1, # 自制
|
||||
# source="",
|
||||
# desc="这是一个测试视频的描述",
|
||||
# dynamic="发布动态内容",
|
||||
# cover="", # 封面路径,空表示不设置封面
|
||||
# dolby=0, # 不开启杜比音效
|
||||
# lossless_music=0, # 不开启Hi-Res
|
||||
# no_reprint=0, # 允许转载
|
||||
# open_elec=0, # 不开启充电
|
||||
# up_close_reply=False, # 不关闭评论
|
||||
# up_selection_reply=False, # 不开启精选评论
|
||||
# up_close_danmu=False, # 不关闭弹幕
|
||||
# limit=3, # 并发数
|
||||
# desc_v2=[], # 视频简介v2
|
||||
# dtime=None, # 立即发布
|
||||
# line=stream_gears.UploadLine.Bda2, # 指定上传线路
|
||||
# extra_fields=None, # 额外字段
|
||||
# submit="BCutAndroid", # 使用必剪安卓版接口提交
|
||||
# proxy=None # 不使用代理
|
||||
# )
|
||||
|
||||
# 视频简介v2
|
||||
def creditsToDesc_v2(desc: str, credits: List[Dict[str, Any]]):
|
||||
desc_v2 = []
|
||||
desc_v2_tmp = desc
|
||||
for credit in credits:
|
||||
try:
|
||||
num = desc_v2_tmp.index("@credit")
|
||||
desc_v2.append({
|
||||
"raw_text": " " + desc_v2_tmp[:num],
|
||||
"biz_id": "",
|
||||
"type": 1
|
||||
})
|
||||
desc_v2.append({
|
||||
"raw_text": credit["username"],
|
||||
"biz_id": str(credit["uid"]),
|
||||
"type": 2
|
||||
})
|
||||
desc = desc.replace(
|
||||
"@credit", "@" + credit["username"] + " ", 1)
|
||||
desc_v2_tmp = desc_v2_tmp[num + 7:]
|
||||
except IndexError:
|
||||
print('简介中的@credit占位符少于credits的数量,替换失败')
|
||||
desc_v2.append({
|
||||
"raw_text": str(desc_v2_tmp),
|
||||
"biz_id": "",
|
||||
"type": 1
|
||||
})
|
||||
# desc_v2[0]["raw_text"] = desc_v2[0]["raw_text"][1:] # 开头空格会导致识别简介过长
|
||||
return desc_v2
|
||||
|
||||
# 额外字段(JSON格式)
|
||||
extra_fields_json = '{"is_self_only": "1"}'
|
||||
|
||||
# 测试
|
||||
stream_gears.upload(
|
||||
["examples/test.mp4"],
|
||||
"cookies.json",
|
||||
"title",
|
||||
171,
|
||||
"tag",
|
||||
1,
|
||||
"source",
|
||||
"desc",
|
||||
"dynamic",
|
||||
"",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
3,
|
||||
[],
|
||||
None,
|
||||
stream_gears.UploadLine.Bda2,
|
||||
video_path=video_path,
|
||||
cookie_file=cookie_file,
|
||||
copyright=1,
|
||||
extra_fields=extra_fields_json,
|
||||
submit="b-cut-android",
|
||||
title="必剪投稿接口测试"
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
print("所有测试用例已完成!")
|
||||
Reference in new issue
Block a user