feat: 后端支持获取普通视频信息

This commit is contained in:
lanyeeee
2025-07-13 05:09:30 +08:00
parent d0c0a86977
commit 2c6497daa4
8 changed files with 440 additions and 3 deletions
+51 -1
View File
@@ -12,7 +12,13 @@ use tauri::{
AppHandle,
};
use crate::types::{qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, user_info::UserInfo};
use crate::{
extensions::AppHandleExt,
types::{
get_normal_info_params::GetNormalInfoParams, normal_info::NormalInfo,
qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, user_info::UserInfo,
},
};
const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36";
const REFERRER: &str = "https://www.bilibili.com/";
@@ -132,6 +138,50 @@ impl BiliClient {
Ok(user_info)
}
pub async fn get_normal_info(&self, params: GetNormalInfoParams) -> anyhow::Result<NormalInfo> {
use GetNormalInfoParams::{Aid, Bvid};
let params = match params {
Bvid(bvid) => json!({"bvid": bvid}),
Aid(aid) => json!({"aid": aid}),
};
// 发送获取普通视频信息的请求
let request = self
.api_client
.read()
.get("https://api.bilibili.com/x/web-interface/view")
.query(&params)
.header("cookie", self.get_cookie());
let http_resp = request.send().await?;
// 检查http响应状态码
let status = http_resp.status();
let body = http_resp.text().await?;
if status != StatusCode::OK {
return Err(anyhow!("预料之外的状态码({status}): {body}"));
}
// 尝试将body解析为BiliResp
let bili_resp: BiliResp =
serde_json::from_str(&body).context(format!("将body解析为BiliResp失败: {body}"))?;
// 检查BiliResp的code字段
if bili_resp.code != 0 {
return Err(anyhow!("预料之外的code: {bili_resp:?}"));
}
// 检查BiliResp的data是否存在
let Some(data) = bili_resp.data else {
return Err(anyhow!("BiliResp中不存在data字段: {bili_resp:?}"));
};
// 尝试将data解析为NormalInfo
let data_str = data.to_string();
let normal_info: NormalInfo = serde_json::from_str(&data_str)
.context(format!("将data解析为NormalInfo失败: {data_str}"))?;
Ok(normal_info)
}
fn get_cookie(&self) -> String {
let sessdata = self.app.get_config().read().sessdata.clone();
format!("SESSDATA={sessdata}")
}
}
fn create_api_client(_app: &AppHandle) -> ClientWithMiddleware {
+18 -1
View File
@@ -6,7 +6,10 @@ use crate::{
errors::{CommandError, CommandResult},
extensions::AppHandleExt,
logger,
types::{qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, user_info::UserInfo},
types::{
get_normal_info_params::GetNormalInfoParams, normal_info::NormalInfo,
qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, user_info::UserInfo,
},
};
#[tauri::command]
@@ -91,3 +94,17 @@ pub async fn get_user_info(app: AppHandle, sessdata: String) -> CommandResult<Us
.map_err(|err| CommandError::from("获取用户信息失败", err))?;
Ok(user_info)
}
#[tauri::command(async)]
#[specta::specta]
pub async fn get_normal_info(
app: AppHandle,
params: GetNormalInfoParams,
) -> CommandResult<NormalInfo> {
let bili_client = app.get_bili_client();
let normal_info = bili_client
.get_normal_info(params)
.await
.map_err(|err| CommandError::from("获取普通视频信息失败", err))?;
Ok(normal_info)
}
+2
View File
@@ -9,6 +9,7 @@ use tauri::{AppHandle, Manager};
pub struct Config {
pub download_dir: PathBuf,
pub enable_file_logger: bool,
pub sessdata: String,
}
impl Config {
@@ -66,6 +67,7 @@ impl Config {
Config {
download_dir: app_data_dir.join("视频下载"),
enable_file_logger: true,
sessdata: String::new(),
}
}
}
+1
View File
@@ -30,6 +30,7 @@ pub fn run() {
generate_qrcode,
get_qrcode_status,
get_user_info,
get_normal_info,
])
.events(tauri_specta::collect_events![LogEvent]);
@@ -0,0 +1,8 @@
use serde::{Deserialize, Serialize};
use specta::Type;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub enum GetNormalInfoParams {
Bvid(String),
Aid(i64),
}
+2
View File
@@ -1,4 +1,6 @@
pub mod get_normal_info_params;
pub mod log_level;
pub mod normal_info;
pub mod qrcode_data;
pub mod qrcode_status;
pub mod user_info;
+325
View File
@@ -0,0 +1,325 @@
use serde::{Deserialize, Serialize};
use specta::Type;
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
#[allow(clippy::struct_excessive_bools)]
pub struct NormalInfo {
pub bvid: String,
pub aid: i64,
pub videos: i64,
pub tid: i64,
pub tid_v2: i64,
pub tname: String,
pub tname_v2: String,
pub copyright: i64,
pub pic: String,
pub title: String,
pub pubdate: i64,
pub ctime: i64,
pub desc: String,
pub desc_v2: Option<Vec<DescV2>>,
pub state: i64,
pub duration: u64,
pub rights: Rights,
pub owner: OwnerInNormal,
pub stat: StatInNormal,
pub argue_info: ArgueInfo,
pub dynamic: String,
pub cid: i64,
pub dimension: Dimension,
pub teenage_mode: i64,
pub is_chargeable_season: bool,
pub is_story: bool,
pub is_upower_exclusive: bool,
pub is_upower_play: bool,
pub is_upower_preview: bool,
pub enable_vt: i64,
pub vt_display: String,
pub is_upower_exclusive_with_qa: bool,
pub no_cache: bool,
pub pages: Vec<PageInNormal>,
pub subtitle: SubtitleInNormal,
pub staff: Option<Vec<Staff>>,
pub ugc_season: Option<UgcSeason>,
pub is_season_display: bool,
pub user_garb: UserGarb,
pub honor_reply: HonorReply,
pub like_icon: String,
pub need_jump_bv: bool,
pub disable_show_up_info: bool,
pub is_story_play: i64,
pub is_view_self: bool,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct DescV2 {
pub raw_text: String,
#[serde(rename = "type")]
pub type_field: i64,
pub biz_id: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct Rights {
pub bp: i64,
pub elec: i64,
pub download: i64,
pub movie: i64,
pub pay: i64,
pub hd5: i64,
pub no_reprint: i64,
pub autoplay: i64,
pub ugc_pay: i64,
pub is_cooperation: i64,
pub ugc_pay_preview: i64,
pub no_background: i64,
pub clean_mode: i64,
pub is_stein_gate: i64,
pub is_360: i64,
pub no_share: i64,
pub arc_pay: i64,
pub free_watch: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct OwnerInNormal {
pub mid: i64,
pub name: String,
pub face: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct StatInNormal {
pub aid: i64,
pub view: i64,
pub danmaku: i64,
pub reply: i64,
pub favorite: i64,
pub coin: i64,
pub share: i64,
pub now_rank: i64,
pub his_rank: i64,
pub like: i64,
pub dislike: i64,
pub evaluation: String,
pub vt: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
#[allow(clippy::struct_field_names)]
pub struct ArgueInfo {
pub argue_msg: String,
pub argue_type: i64,
pub argue_link: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct Dimension {
pub width: i64,
pub height: i64,
pub rotate: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
#[allow(clippy::struct_field_names)]
pub struct PageInNormal {
pub cid: i64,
pub page: i64,
pub from: String,
pub part: String,
pub duration: u64,
pub vid: String,
pub weblink: String,
pub dimension: Dimension,
pub ctime: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct SubtitleInNormal {
pub allow_submit: bool,
pub list: Vec<SubtitleDetailInNormal>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct SubtitleDetailInNormal {
pub id: i64,
pub lan: String,
pub lan_doc: String,
pub is_lock: bool,
pub subtitle_url: String,
#[serde(rename = "type")]
pub type_field: i64,
pub id_str: String,
pub ai_type: i64,
pub ai_status: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct UserGarb {
pub url_image_ani_cut: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct HonorReply {
pub honor: Option<Vec<Honor>>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct Honor {
pub aid: i64,
#[serde(rename = "type")]
pub type_field: i64,
pub desc: String,
pub weekly_recommend_num: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct UgcSeason {
pub id: i64,
pub title: String,
pub cover: String,
pub mid: i64,
pub intro: String,
pub sign_state: i64,
pub attribute: i64,
pub sections: Vec<SectionInNormal>,
pub stat: StatInNormalSeason,
pub ep_count: i64,
pub season_type: i64,
pub is_pay_season: bool,
pub enable_vt: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct SectionInNormal {
pub season_id: i64,
pub id: i64,
pub title: String,
#[serde(rename = "type")]
pub type_field: i64,
pub episodes: Vec<EpInNormal>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct EpInNormal {
pub season_id: i64,
pub section_id: i64,
pub id: i64,
pub aid: i64,
pub cid: i64,
pub title: String,
pub attribute: i64,
pub arc: Arc,
pub page: PageInNormalEp,
pub bvid: String,
pub pages: Vec<PageInNormalEp>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct Arc {
pub aid: i64,
pub videos: i64,
pub type_id: i64,
pub type_name: String,
pub copyright: i64,
pub pic: String,
pub title: String,
pub pubdate: i64,
pub ctime: i64,
pub desc: String,
pub state: i64,
pub duration: u64,
pub rights: RightsInNormalEp,
pub author: Author,
pub stat: StatInNormalEp,
pub dynamic: String,
pub dimension: Dimension,
pub is_chargeable_season: bool,
pub is_blooper: bool,
pub enable_vt: i64,
pub vt_display: String,
pub type_id_v2: i64,
pub type_name_v2: String,
pub is_lesson_video: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct Author {
pub mid: i64,
pub name: String,
pub face: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct StatInNormalEp {
pub aid: i64,
pub view: i64,
pub danmaku: i64,
pub reply: i64,
pub fav: i64,
pub coin: i64,
pub share: i64,
pub now_rank: i64,
pub his_rank: i64,
pub like: i64,
pub dislike: i64,
pub evaluation: String,
pub argue_msg: String,
pub vt: i64,
pub vv: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct StatInNormalSeason {
pub season_id: i64,
pub view: i64,
pub danmaku: i64,
pub reply: i64,
pub fav: i64,
pub coin: i64,
pub share: i64,
pub now_rank: i64,
pub his_rank: i64,
pub like: i64,
pub vt: i64,
pub vv: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct PageInNormalEp {
pub cid: i64,
pub page: i64,
pub from: String,
pub part: String,
pub duration: u64,
pub vid: String,
pub weblink: String,
pub dimension: Dimension,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct RightsInNormalEp {
pub bp: i64,
pub elec: i64,
pub download: i64,
pub movie: i64,
pub pay: i64,
pub hd5: i64,
pub no_reprint: i64,
pub autoplay: i64,
pub ugc_pay: i64,
pub is_cooperation: i64,
pub ugc_pay_preview: i64,
pub arc_pay: i64,
pub free_watch: i64,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct Staff {
pub mid: i64,
pub title: String,
pub name: String,
pub face: String,
pub follower: i64,
pub label_style: i64,
}
+33 -1
View File
@@ -42,6 +42,14 @@ async getUserInfo(sessdata: string) : Promise<Result<UserInfo, CommandError>> {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getNormalInfo(params: GetNormalInfoParams) : Promise<Result<NormalInfo, CommandError>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_normal_info", { params }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
}
}
@@ -60,18 +68,42 @@ logEvent: "log-event"
/** user-defined types **/
export type Arc = { aid: number; videos: number; type_id: number; type_name: string; copyright: number; pic: string; title: string; pubdate: number; ctime: number; desc: string; state: number; duration: number; rights: RightsInNormalEp; author: Author; stat: StatInNormalEp; dynamic: string; dimension: Dimension; is_chargeable_season: boolean; is_blooper: boolean; enable_vt: number; vt_display: string; type_id_v2: number; type_name_v2: string; is_lesson_video: number }
export type ArgueInfo = { argue_msg: string; argue_type: number; argue_link: string }
export type Author = { mid: number; name: string; face: string }
export type CommandError = { err_title: string; err_message: string }
export type Config = { downloadDir: string; enableFileLogger: boolean }
export type Config = { downloadDir: string; enableFileLogger: boolean; sessdata: string }
export type DescV2 = { raw_text: string; type: number; biz_id: number }
export type Dimension = { width: number; height: number; rotate: number }
export type EpInNormal = { season_id: number; section_id: number; id: number; aid: number; cid: number; title: string; attribute: number; arc: Arc; page: PageInNormalEp; bvid: string; pages: PageInNormalEp[] }
export type GetNormalInfoParams = { Bvid: string } | { Aid: number }
export type Honor = { aid: number; type: number; desc: string; weekly_recommend_num: number }
export type HonorReply = { honor: Honor[] | null }
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key in string]: JsonValue }
export type LabelInUserInfo = { path: string; text: string; label_theme: string; text_color: string; bg_style: number; bg_color: string; border_color: string; use_img_label: boolean; img_label_uri_hans: string; img_label_uri_hant: string; img_label_uri_hans_static: string; img_label_uri_hant_static: string }
export type LevelInfoInUserInfo = { current_level: number; current_min: number; current_exp: number }
export type LogEvent = { timestamp: string; level: LogLevel; fields: { [key in string]: JsonValue }; target: string; filename: string; line_number: number }
export type LogLevel = "TRACE" | "DEBUG" | "INFO" | "WARN" | "ERROR"
export type NormalInfo = { bvid: string; aid: number; videos: number; tid: number; tid_v2: number; tname: string; tname_v2: string; copyright: number; pic: string; title: string; pubdate: number; ctime: number; desc: string; desc_v2: DescV2[] | null; state: number; duration: number; rights: Rights; owner: OwnerInNormal; stat: StatInNormal; argue_info: ArgueInfo; dynamic: string; cid: number; dimension: Dimension; teenage_mode: number; is_chargeable_season: boolean; is_story: boolean; is_upower_exclusive: boolean; is_upower_play: boolean; is_upower_preview: boolean; enable_vt: number; vt_display: string; is_upower_exclusive_with_qa: boolean; no_cache: boolean; pages: PageInNormal[]; subtitle: SubtitleInNormal; staff: Staff[] | null; ugc_season: UgcSeason | null; is_season_display: boolean; user_garb: UserGarb; honor_reply: HonorReply; like_icon: string; need_jump_bv: boolean; disable_show_up_info: boolean; is_story_play: number; is_view_self: boolean }
export type Official = { role: number; title: string; desc: string; type: number }
export type OfficialVerify = { type: number; desc: string }
export type OwnerInNormal = { mid: number; name: string; face: string }
export type PageInNormal = { cid: number; page: number; from: string; part: string; duration: number; vid: string; weblink: string; dimension: Dimension; ctime: number }
export type PageInNormalEp = { cid: number; page: number; from: string; part: string; duration: number; vid: string; weblink: string; dimension: Dimension }
export type PendantInUserInfo = { pid: number; name: string; image: string; expire: number; image_enhance: string; image_enhance_frame: string; n_pid: number }
export type QrcodeData = { url: string; qrcode_key: string }
export type QrcodeStatus = { url: string; refresh_token: string; timestamp: number; code: number; message: string }
export type Rights = { bp: number; elec: number; download: number; movie: number; pay: number; hd5: number; no_reprint: number; autoplay: number; ugc_pay: number; is_cooperation: number; ugc_pay_preview: number; no_background: number; clean_mode: number; is_stein_gate: number; is_360: number; no_share: number; arc_pay: number; free_watch: number }
export type RightsInNormalEp = { bp: number; elec: number; download: number; movie: number; pay: number; hd5: number; no_reprint: number; autoplay: number; ugc_pay: number; is_cooperation: number; ugc_pay_preview: number; arc_pay: number; free_watch: number }
export type SectionInNormal = { season_id: number; id: number; title: string; type: number; episodes: EpInNormal[] }
export type Staff = { mid: number; title: string; name: string; face: string; follower: number; label_style: number }
export type StatInNormal = { aid: number; view: number; danmaku: number; reply: number; favorite: number; coin: number; share: number; now_rank: number; his_rank: number; like: number; dislike: number; evaluation: string; vt: number }
export type StatInNormalEp = { aid: number; view: number; danmaku: number; reply: number; fav: number; coin: number; share: number; now_rank: number; his_rank: number; like: number; dislike: number; evaluation: string; argue_msg: string; vt: number; vv: number }
export type StatInNormalSeason = { season_id: number; view: number; danmaku: number; reply: number; fav: number; coin: number; share: number; now_rank: number; his_rank: number; like: number; vt: number; vv: number }
export type SubtitleDetailInNormal = { id: number; lan: string; lan_doc: string; is_lock: boolean; subtitle_url: string; type: number; id_str: string; ai_type: number; ai_status: number }
export type SubtitleInNormal = { allow_submit: boolean; list: SubtitleDetailInNormal[] }
export type UgcSeason = { id: number; title: string; cover: string; mid: number; intro: string; sign_state: number; attribute: number; sections: SectionInNormal[]; stat: StatInNormalSeason; ep_count: number; season_type: number; is_pay_season: boolean; enable_vt: number }
export type UserGarb = { url_image_ani_cut: string }
export type UserInfo = { isLogin: boolean; email_verified: number; face: string; face_nft: number; face_nft_type: number; level_info: LevelInfoInUserInfo; mid: number; mobile_verified: number; money: number; moral: number; official: Official; officialVerify: OfficialVerify; pendant: PendantInUserInfo; scores: number; uname: string; vipDueDate: number; vipStatus: number; vipType: number; vip_pay_type: number; vip_theme_type: number; vip_label: VipLabel; vip_avatar_subscript: number; vip_nickname_color: string; vip: VipInUserInfo; wallet: Wallet | null; has_shop: boolean; shop_url: string; answer_status: number; is_senior_member: number; wbi_img: WbiImg; is_jury: boolean }
export type VipInUserInfo = { type: number; status: number; due_date: number; vip_pay_type: number; theme_type: number; label: LabelInUserInfo; avatar_subscript: number; nickname_color: string; role: number; avatar_subscript_url: string; tv_vip_status: number; tv_vip_pay_type: number; tv_due_date: number }
export type VipLabel = { path: string; text: string; label_theme: string; text_color: string; bg_style: number; bg_color: string; border_color: string; use_img_label: boolean; img_label_uri_hans: string; img_label_uri_hant: string; img_label_uri_hans_static: string; img_label_uri_hant_static: string }