use crate::client::{Client, LoginInfo};
use crate::error::{CustomError, Result};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::fmt::{Display, Formatter};
use std::num::ParseIntError;
use std::str::FromStr;
use std::time::Duration;
use tracing::info;
use typed_builder::TypedBuilder;
#[derive(Serialize, Deserialize, Debug, TypedBuilder)]
#[builder(field_defaults(default))]
#[derive(clap::Args)]
pub struct Studio {
#[clap(long, default_value = "1")]
#[builder(default = 1)]
pub copyright: u8,
#[clap(long, default_value_t)]
pub source: String,
#[clap(long, default_value = "171")]
#[builder(default = 171)]
pub tid: u16,
#[clap(long, default_value_t)]
#[clap(long)]
pub cover: String,
#[clap(long, default_value_t)]
#[builder(!default, setter(into))]
pub title: String,
#[clap(skip)]
pub desc_format_id: u32,
#[clap(long, default_value_t)]
pub desc: String,
#[clap(long, default_value_t)]
pub dynamic: String,
#[clap(skip)]
#[serde(default)]
#[builder(default, setter(skip))]
pub subtitle: Subtitle,
#[clap(long, default_value_t)]
pub tag: String,
#[serde(default)]
#[builder(!default)]
#[clap(skip)]
pub videos: Vec<Video>,
#[clap(long)]
pub dtime: Option<u32>,
#[clap(skip)]
#[serde(default)]
pub open_subtitle: bool,
#[clap(long, default_value = "0")]
#[serde(default)]
pub interactive: u8,
#[clap(long)]
pub mission_id: Option<u32>,
#[clap(long, default_value = "0")]
#[serde(default)]
pub dolby: u8,
#[clap(long)]
pub no_reprint: Option<u8>,
#[clap(skip)]
pub aid: Option<u64>,
#[clap(long)]
#[serde(default)]
pub up_selection_reply: bool,
#[clap(long)]
#[serde(default)]
pub up_close_reply: bool,
#[clap(long)]
#[serde(default)]
pub up_close_danmu: bool,
#[clap(long)]
pub open_elec: Option<u8>,
}
impl Studio {
pub async fn submit(&mut self, login_info: &LoginInfo) -> Result<serde_json::Value> {
let ret: serde_json::Value = 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={}",
login_info.token_info.access_token
))
.json(self)
.send()
.await?
.json()
.await?;
info!("{}", ret);
if ret["code"] == 0 {
info!("投稿成功");
Ok(ret)
} else {
Err(CustomError::Custom(ret.to_string()))
}
}
pub async fn edit(&mut self, login_info: &LoginInfo) -> Result<serde_json::Value> {
let ret: serde_json::Value = 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/edit?access_key={}",
login_info.token_info.access_token
))
.json(self)
.send()
.await?
.json()
.await?;
info!("{}", ret);
if ret["code"] == 0 {
info!("稿件修改成功");
Ok(ret)
} else {
Err(CustomError::Custom(ret.to_string()))
}
}
}
#[derive(Serialize, Deserialize, Debug, Default)]
pub struct Subtitle {
open: i8,
lan: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Video {
pub title: Option<String>,
pub filename: String,
pub desc: String,
}
impl Video {
pub fn new(filename: &str) -> Video {
Video {
title: None,
filename: filename.into(),
desc: "".into(),
}
}
}
#[derive(PartialEq, Debug)]
pub enum Vid {
Aid(u64),
Bvid(String),
}
impl FromStr for Vid {
type Err = ParseIntError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let s = s.trim();
match &s[..2] {
"BV" => Ok(Vid::Bvid(s.to_string())),
"av" => Ok(Vid::Aid(s[2..].parse()?)),
_ => Ok(Vid::Aid(s.parse()?)),
}
}
}
impl Display for Vid {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Vid::Aid(aid) => write!(f, "aid={}", aid),
Vid::Bvid(bvid) => write!(f, "bvid={}", bvid),
}
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Response {
pub code: i32,
pub data: Option<Value>,
message: String,
ttl: u8,
}
pub struct BiliBili<'a, 'b> {
client: &'a reqwest::Client,
login_info: &'b LoginInfo,
}
impl BiliBili<'_, '_> {
pub fn new<'a, 'b>(login_info: &'b LoginInfo, login: &'a Client) -> BiliBili<'a, 'b> {
BiliBili {
client: &login.client,
login_info,
}
}
pub async fn video_data(&self, vid: Vid) -> Result<Value> {
let res: Response = 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()?
.get(format!(
"http://member.bilibili.com/x/client/archive/view?access_key={}&{vid}",
self.login_info.token_info.access_token
))
.send()
.await?
.json()
.await?;
match res {
res @ Response {
code: _,
data: None,
..
} => Err(CustomError::Custom(format!("{res:?}"))),
Response {
code: _,
data: Some(v),
..
} => Ok(v),
}
}
pub async fn studio_data(&self, vid: Vid) -> Result<Studio> {
let mut video_info = self.video_data(vid).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())?;
studio.videos = videos;
Ok(studio)
}
pub async fn archive_pre(&self) -> Result<Value> {
Ok(self
.client
.get("https://member.bilibili.com/x/vupre/web/archive/pre")
.send()
.await?
.json()
.await?)
}
pub async fn cover_up(&self, input: &[u8]) -> Result<String> {
let csrf = self
.login_info
.cookie_info
.get("cookies")
.and_then(|c| c.as_array())
.ok_or("cover_up cookie error")?
.iter()
.filter_map(|c| c.as_object())
.find(|c| c["name"] == "bili_jct")
.ok_or("cover_up jct error")?;
let response = self
.client
.post("https://member.bilibili.com/x/vu/web/cover/up")
.form(&json!({
"cover": format!("data:image/jpeg;base64,{}", base64::encode(input)),
"csrf": csrf["value"]
}))
.send()
.await?;
let res: Response = if !response.status().is_success() {
return Err(CustomError::Custom(response.text().await?));
} else {
response.json().await?
};
if let Response {
code: _,
data: Some(value),
..
} = res
{
Ok(value["url"].as_str().ok_or("cover_up error")?.into())
} else {
return Err(CustomError::Custom(format!("{res:?}")));
}
}
}