From 2c6497daa429e35e39c677dbf5fb76c233769e0c Mon Sep 17 00:00:00 2001 From: lanyeeee Date: Sun, 13 Jul 2025 05:09:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=8E=E7=AB=AF=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E8=8E=B7=E5=8F=96=E6=99=AE=E9=80=9A=E8=A7=86=E9=A2=91=E4=BF=A1?= =?UTF-8?q?=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/bili_client.rs | 52 ++- src-tauri/src/commands.rs | 19 +- src-tauri/src/config.rs | 2 + src-tauri/src/lib.rs | 1 + src-tauri/src/types/get_normal_info_params.rs | 8 + src-tauri/src/types/mod.rs | 2 + src-tauri/src/types/normal_info.rs | 325 ++++++++++++++++++ src/bindings.ts | 34 +- 8 files changed, 440 insertions(+), 3 deletions(-) create mode 100644 src-tauri/src/types/get_normal_info_params.rs create mode 100644 src-tauri/src/types/normal_info.rs diff --git a/src-tauri/src/bili_client.rs b/src-tauri/src/bili_client.rs index c8c232f..ddaafb5 100644 --- a/src-tauri/src/bili_client.rs +++ b/src-tauri/src/bili_client.rs @@ -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 { + 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(¶ms) + .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 { diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 7b6b900..45de8e8 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -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 CommandResult { + 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) +} diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 0993dae..055b2ac 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -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(), } } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ebf2013..12ae7bd 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -30,6 +30,7 @@ pub fn run() { generate_qrcode, get_qrcode_status, get_user_info, + get_normal_info, ]) .events(tauri_specta::collect_events![LogEvent]); diff --git a/src-tauri/src/types/get_normal_info_params.rs b/src-tauri/src/types/get_normal_info_params.rs new file mode 100644 index 0000000..9387677 --- /dev/null +++ b/src-tauri/src/types/get_normal_info_params.rs @@ -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), +} diff --git a/src-tauri/src/types/mod.rs b/src-tauri/src/types/mod.rs index d05d719..7706426 100644 --- a/src-tauri/src/types/mod.rs +++ b/src-tauri/src/types/mod.rs @@ -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; diff --git a/src-tauri/src/types/normal_info.rs b/src-tauri/src/types/normal_info.rs new file mode 100644 index 0000000..fb42f38 --- /dev/null +++ b/src-tauri/src/types/normal_info.rs @@ -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>, + 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, + pub subtitle: SubtitleInNormal, + pub staff: Option>, + pub ugc_season: Option, + 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, +} + +#[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>, +} + +#[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, + 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, +} + +#[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, +} + +#[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, +} diff --git a/src/bindings.ts b/src/bindings.ts index d6cafe2..926d9be 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -42,6 +42,14 @@ async getUserInfo(sessdata: string) : Promise> { if(e instanceof Error) throw e; else return { status: "error", error: e as any }; } +}, +async getNormalInfo(params: GetNormalInfoParams) : Promise> { + 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 }