From 24dba38a968255a0faa05eb7f9998a78f285871f Mon Sep 17 00:00:00 2001 From: lanyeeee Date: Sat, 19 Jul 2025 05:16:07 +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=92=AD=E6=94=BE=E5=99=A8=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/bili_client.rs | 42 ++++++- src-tauri/src/commands.rs | 17 ++- src-tauri/src/lib.rs | 1 + src-tauri/src/types/mod.rs | 1 + src-tauri/src/types/player_info.rs | 183 +++++++++++++++++++++++++++++ src/bindings.ts | 23 ++++ 6 files changed, 264 insertions(+), 3 deletions(-) create mode 100644 src-tauri/src/types/player_info.rs diff --git a/src-tauri/src/bili_client.rs b/src-tauri/src/bili_client.rs index 882d416..4913aac 100644 --- a/src-tauri/src/bili_client.rs +++ b/src-tauri/src/bili_client.rs @@ -18,8 +18,8 @@ use crate::{ bangumi_info::BangumiInfo, bangumi_media_url::BangumiMediaUrl, cheese_info::CheeseInfo, cheese_media_url::CheeseMediaUrl, get_bangumi_info_params::GetBangumiInfoParams, get_cheese_info_params::GetCheeseInfoParams, get_normal_info_params::GetNormalInfoParams, - normal_info::NormalInfo, normal_media_url::NormalMediaUrl, qrcode_data::QrcodeData, - qrcode_status::QrcodeStatus, user_info::UserInfo, + normal_info::NormalInfo, normal_media_url::NormalMediaUrl, player_info::PlayerInfo, + qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, user_info::UserInfo, }, }; @@ -386,6 +386,44 @@ impl BiliClient { Ok(media_url) } + pub async fn get_player_info(&self, aid: i64, cid: i64) -> anyhow::Result { + let params = json!({ + "aid": aid, + "cid": cid, + }); + // 发送获取播放器信息的请求 + let request = self + .api_client + .read() + .get("https://api.bilibili.com/x/player/wbi/v2") + .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解析为PlayerInfo + let data_str = data.to_string(); + let player_info: PlayerInfo = serde_json::from_str(&data_str) + .context(format!("将data解析为PlayerInfo失败: {data_str}"))?; + + Ok(player_info) + } + fn get_cookie(&self) -> String { let sessdata = self.app.get_config().read().sessdata.clone(); format!("SESSDATA={sessdata}") diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 4db30b5..02ae7cf 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -7,7 +7,11 @@ use crate::{ extensions::AppHandleExt, logger, types::{ - bangumi_info::BangumiInfo, bangumi_media_url::BangumiMediaUrl, cheese_info::CheeseInfo, cheese_media_url::CheeseMediaUrl, get_bangumi_info_params::GetBangumiInfoParams, get_cheese_info_params::GetCheeseInfoParams, get_normal_info_params::GetNormalInfoParams, normal_info::NormalInfo, normal_media_url::NormalMediaUrl, qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, user_info::UserInfo + bangumi_info::BangumiInfo, bangumi_media_url::BangumiMediaUrl, cheese_info::CheeseInfo, + cheese_media_url::CheeseMediaUrl, get_bangumi_info_params::GetBangumiInfoParams, + get_cheese_info_params::GetCheeseInfoParams, get_normal_info_params::GetNormalInfoParams, + normal_info::NormalInfo, normal_media_url::NormalMediaUrl, player_info::PlayerInfo, + qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, user_info::UserInfo, }, }; @@ -172,3 +176,14 @@ pub async fn get_cheese_url(app: AppHandle, ep_id: i64) -> CommandResult CommandResult { + let bili_client = app.get_bili_client(); + let player_info = bili_client + .get_player_info(aid, cid) + .await + .map_err(|err| CommandError::from("获取播放器信息失败", err))?; + Ok(player_info) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4e872b3..42fa0f7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -36,6 +36,7 @@ pub fn run() { get_normal_url, get_bangumi_url, get_cheese_url, + get_player_info, ]) .events(tauri_specta::collect_events![LogEvent]); diff --git a/src-tauri/src/types/mod.rs b/src-tauri/src/types/mod.rs index 003aa45..0b47972 100644 --- a/src-tauri/src/types/mod.rs +++ b/src-tauri/src/types/mod.rs @@ -8,6 +8,7 @@ pub mod get_normal_info_params; pub mod log_level; pub mod normal_info; pub mod normal_media_url; +pub mod player_info; pub mod qrcode_data; pub mod qrcode_status; pub mod user_info; diff --git a/src-tauri/src/types/player_info.rs b/src-tauri/src/types/player_info.rs new file mode 100644 index 0000000..7f76731 --- /dev/null +++ b/src-tauri/src/types/player_info.rs @@ -0,0 +1,183 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +#[allow(clippy::struct_excessive_bools)] +pub struct PlayerInfo { + pub aid: i64, + pub bvid: String, + pub allow_bp: bool, + pub no_share: bool, + pub cid: i64, + pub max_limit: i64, + pub page_no: i64, + pub has_next: bool, + pub ip_info: IpInfo, + pub login_mid: i64, + pub login_mid_hash: String, + pub is_owner: bool, + pub name: String, + pub permission: String, + pub level_info: LevelInfoInPlayerInfo, + pub vip: VipInPlayerInfo, + pub answer_status: i64, + pub block_time: i64, + pub role: String, + pub last_play_time: i64, + pub last_play_cid: i64, + pub now_time: i64, + pub online_count: i64, + pub need_login_subtitle: bool, + pub subtitle: SubtitleInPlayerInfo, + pub view_points: Vec, + pub preview_toast: String, + pub options: Options, + pub online_switch: OnlineSwitch, + pub fawkes: Fawkes, + pub show_switch: ShowSwitch, + pub toast_block: bool, + pub is_upower_exclusive: bool, + pub is_upower_play: bool, + pub is_ugc_pay_preview: bool, + pub elec_high_level: ElecHighLevel, + pub disable_show_up_info: bool, + pub is_upower_exclusive_with_qa: bool, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct IpInfo { + pub ip: String, + pub zone_ip: String, + pub zone_id: i64, + pub country: String, + pub province: String, + pub city: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct LevelInfoInPlayerInfo { + pub current_level: i64, + pub current_min: i64, + pub current_exp: i64, + pub next_exp: i64, + pub level_up: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +#[allow(clippy::struct_field_names)] +pub struct VipInPlayerInfo { + #[serde(rename = "type")] + pub type_field: i64, + pub status: i64, + pub due_date: i64, + pub vip_pay_type: i64, + pub theme_type: i64, + pub label: LabelInPlayerInfo, + pub avatar_subscript: i64, + pub nickname_color: String, + pub role: i64, + pub avatar_subscript_url: String, + pub tv_vip_status: i64, + pub tv_vip_pay_type: i64, + pub tv_due_date: i64, + pub avatar_icon: AvatarIcon, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +#[allow(clippy::struct_field_names)] +pub struct LabelInPlayerInfo { + pub path: String, + pub text: String, + pub label_theme: String, + pub text_color: String, + pub bg_style: i64, + pub bg_color: String, + pub border_color: String, + pub use_img_label: bool, + pub img_label_uri_hans: String, + pub img_label_uri_hant: String, + pub img_label_uri_hans_static: String, + pub img_label_uri_hant_static: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct AvatarIcon { + pub icon_resource: IconResource, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct IconResource {} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct SubtitleInPlayerInfo { + pub allow_submit: bool, + pub lan: String, + pub lan_doc: String, + pub subtitles: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct SubtitleDetailInPlayerInfo { + 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 ViewPoint { + #[serde(rename = "type")] + pub type_field: i64, + pub from: i64, + pub to: i64, + pub content: String, + pub img_url: Option, + pub logo_url: Option, + pub team_type: String, + pub team_name: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct Options { + pub is_360: bool, + pub without_vip: bool, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct OnlineSwitch { + pub enable_gray_dash_playback: String, + pub new_broadcast: String, + pub realtime_dm: String, + pub subtitle_submit_switch: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct Fawkes { + pub config_version: i64, + pub ff_version: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct ShowSwitch { + pub long_progress: bool, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct ElecHighLevel { + pub privilege_type: i64, + pub title: String, + pub sub_title: String, + pub show_button: bool, + pub button_text: String, + pub jump_url: String, + pub intro: String, + pub new: bool, + pub question_text: String, + pub qa_title: String, +} diff --git a/src/bindings.ts b/src/bindings.ts index c2786a4..d97e1f3 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -90,6 +90,14 @@ async getCheeseUrl(epId: number) : Promise> if(e instanceof Error) throw e; else return { status: "error", error: e as any }; } +}, +async getPlayerInfo(aid: number, cid: number) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_player_info", { aid, cid }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} } } @@ -114,6 +122,7 @@ export type Arc = { aid: number; videos: number; type_id: number; type_name: str export type Area = { id: number; name: string } export type ArgueInfo = { argue_msg: string; argue_type: number; argue_link: string } export type Author = { mid: number; name: string; face: string } +export type AvatarIcon = { icon_resource: IconResource } export type BadgeInfo = { bg_color: string; bg_color_night: string; text: string } export type BangumiMediaUrl = { accept_format: string; code: number; seek_param: string; is_preview: number; fnval: number; video_project: boolean; fnver: number; type: string; bp: number; seek_type: string; result: string; vip_type: number | null; from: string; video_codecid: number; record_info: RecordInfo | null; is_drm: boolean; no_rexcode: number; format: string; support_formats: SupportFormatInBangumi[]; message: string; accept_quality: number[]; quality: number; timelength: number; durls: DurlInBangumi[]; has_paid: boolean; vip_status: number | null; error_code: number; dash: DashInBangumi | null; clip_info_list: ClipInfoList[]; accept_description: string[]; status: number } export type BangumiInfo = { activity: Activity; actors: string; alias: string; areas: Area[]; bkg_cover: string; cover: string; delivery_fragment_video: boolean; enable_vt: boolean; episodes: EpInBangumi[]; evaluate: string; hide_ep_vv_vt_dm: number; icon_font: IconFont; jp_title: string; link: string; media_id: number; mode: number; new_ep: NewEp; payment: PaymentInBangumi | null; play_strategy: PlayStrategy | null; positive: Positive; publish: Publish; rating: Rating | null; record: string; rights: RightsInBangumi; season_id: number; season_title: string; seasons: Season[]; section: SectionInBangumi[] | null; series: Series; share_copy: string; share_sub_title: string; share_url: string; show: Show; show_season_type: number; square_cover: string; staff: string; stat: StatInBangumi; status: number; styles: string[]; subtitle: string; title: string; total: number; type: number; up_info: UpInfoInBangumi | null; user_status: UserStatusInBangumi } @@ -138,6 +147,7 @@ export type DurlDetailInCheese = { size: number; ahead: string; length: number; export type DurlInBangumi = { durl: DurlDetailInBangumi[]; quality: number } export type DurlInCheese = { durl: DurlDetailInCheese[]; quality: number } export type Ed = { end: number; start: number } +export type ElecHighLevel = { privilege_type: number; title: string; sub_title: string; show_button: boolean; button_text: string; jump_url: string; intro: string; new: boolean; question_text: string; qa_title: string } export type EpInBangumi = { aid: number; badge: string; badge_info: BadgeInfo; badge_type: number | null; bvid: string | null; cid: number; cover: string; dimension: DimensionInBangumi | null; duration: number | null; enable_vt: boolean; ep_id: number; from: string | null; id: number; is_view_hide: boolean; link: string; link_type: string | null; long_title: string | null; pub_time: number; pv: number; release_date: string | null; rights: RightsInBangumiEp | null; section_type: number; share_copy: string | null; share_url: string | null; short_link: string | null; showDrmLoginDialog: boolean; show_title: string | null; skip: Skip | null; status: number; subtitle: string | null; title: string; vid: string | null; icon_font: IconFont | null } export type EpInCheese = { aid: number; catalogue_index: number; cid: number; cover: string; duration: number; ep_status: number; episode_can_view: boolean; from: string; id: number; index: number; label: string | null; page: number; play: number; play_way: number; playable: boolean; release_date: number; show_vt: boolean; status: number; subtitle: string; title: string; watched: boolean; watchedHistory: 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[] } @@ -146,6 +156,7 @@ export type EpTag = { part_preview_tag: string; pay_tag: string; preview_tag: st export type Faq = { content: string; link: string; title: string } export type Faq1 = { items: Faq1Item[]; title: string } export type Faq1Item = { answer: string; question: string } +export type Fawkes = { config_version: number; ff_version: number } export type Flac = { display: boolean; audio: MediaInNormal | null } export type GetBangumiInfoParams = { EpId: number } | { SeasonId: number } export type GetCheeseInfoParams = { EpId: number } | { SeasonId: number } @@ -153,9 +164,13 @@ 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 IconFont = { name: string; text: string } +export type IconResource = Record export type Img = { aspect_ratio: number; url: string } +export type IpInfo = { ip: string; zone_ip: string; zone_id: number; country: string; province: string; city: string } export type JsonValue = null | boolean | number | string | JsonValue[] | { [key in string]: JsonValue } +export type LabelInPlayerInfo = { 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 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 LevelInfoInPlayerInfo = { current_level: number; current_min: number; current_exp: number; next_exp: number; level_up: number } 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" @@ -168,7 +183,9 @@ export type NormalInfo = { bvid: string; aid: number; videos: number; tid: numbe export type NormalMediaUrl = { from: string; result: string; message: string; quality: number; format: string; timelength: number; accept_format: string; accept_description: string[]; accept_quality: number[]; video_codecid: number; seek_param: string; seek_type: string; dash: DashInNormal; support_formats: SupportFormatInNormal[]; last_play_time: number; last_play_cid: number; play_conf: PlayConf } export type Official = { role: number; title: string; desc: string; type: number } export type OfficialVerify = { type: number; desc: string } +export type OnlineSwitch = { enable_gray_dash_playback: string; new_broadcast: string; realtime_dm: string; subtitle_submit_switch: string } export type Op = { end: number; start: number } +export type Options = { is_360: boolean; without_vip: boolean } 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 } @@ -181,6 +198,7 @@ export type PendantInUserInfo = { pid: number; name: string; image: string; expi export type PlayConf = { is_new_description: boolean } export type PlayStrategy = { strategies: string[] } export type PlayViewBusinessInfo = { user_status: UserStatusInCheeseUrl } +export type PlayerInfo = { aid: number; bvid: string; allow_bp: boolean; no_share: boolean; cid: number; max_limit: number; page_no: number; has_next: boolean; ip_info: IpInfo; login_mid: number; login_mid_hash: string; is_owner: boolean; name: string; permission: string; level_info: LevelInfoInPlayerInfo; vip: VipInPlayerInfo; answer_status: number; block_time: number; role: string; last_play_time: number; last_play_cid: number; now_time: number; online_count: number; need_login_subtitle: boolean; subtitle: SubtitleInPlayerInfo; view_points: ViewPoint[]; preview_toast: string; options: Options; online_switch: OnlineSwitch; fawkes: Fawkes; show_switch: ShowSwitch; toast_block: boolean; is_upower_exclusive: boolean; is_upower_play: boolean; is_ugc_pay_preview: boolean; elec_high_level: ElecHighLevel; disable_show_up_info: boolean; is_upower_exclusive_with_qa: boolean } export type Positive = { id: number; title: string } export type PreviewedPurchaseNote = { long_watch_text: string; pay_text: string; price_format: string; watch_text: string; watching_text: string } export type Publish = { is_finish: number; is_started: number; pub_time: string; pub_time_show: string; unknow_pub_date: number; weekday: number } @@ -204,6 +222,7 @@ export type SegmentBaseInCheese = { initialization: string; index_range: string export type SegmentBaseInNormal = { initialization: string; index_range: string } export type Series = { display_type: number; series_id: number; series_title: string } export type Show = { wide_screen: number } +export type ShowSwitch = { long_progress: boolean } export type Skip = { ed: Ed; op: Op } export type Staff = { mid: number; title: string; name: string; face: string; follower: number; label_style: number } export type StatInBangumi = { coins: number; danmakus: number; favorite: number; favorites: number; follow_text: string; likes: number; reply: number; share: number; views: number; vt: number } @@ -213,7 +232,9 @@ export type StatInNormalEp = { aid: number; view: number; danmaku: number; reply 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 StatInSeason = { favorites: number; series_follow: number; views: number; vt: 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 SubtitleDetailInPlayerInfo = { 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 SubtitleInPlayerInfo = { allow_submit: boolean; lan: string; lan_doc: string; subtitles: SubtitleDetailInPlayerInfo[] } export type SupportFormatInBangumi = { display_desc: string; has_preview: boolean; sub_description: string; superscript: string; need_login: boolean | null; codecs: string[]; format: string; description: string; need_vip: boolean | null; attribute: number; quality: number; new_description: string } export type SupportFormatInCheese = { display_desc: string; superscript: string; need_login: boolean; codecs: string[]; format: string; description: string; quality: number; new_description: string } export type SupportFormatInNormal = { quality: number; format: string; new_description: string; display_desc: string; superscript: string; codecs: string[] } @@ -225,6 +246,8 @@ export type UserInfo = { isLogin: boolean; email_verified: number; face: string; export type UserStatusInBangumi = { area_limit: number; ban_area_show: number; follow: number; follow_status: number; login: number; pay: number; pay_pack_paid: number; sponsor: number } export type UserStatusInCheese = { bp: number; expire_at: number; favored: number; favored_count: number; is_expired: boolean; is_first_paid: boolean; payed: number; user_expiry_content: string } export type UserStatusInCheeseUrl = { watch_progress: WatchProgress } +export type ViewPoint = { type: number; from: number; to: number; content: string; img_url: string | null; logo_url: string | null; team_type: string; team_name: string } +export type VipInPlayerInfo = { type: number; status: number; due_date: number; vip_pay_type: number; theme_type: number; label: LabelInPlayerInfo; 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; avatar_icon: AvatarIcon } 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 } export type Wallet = { mid: number; bcoin_balance: number; coupon_balance: number; coupon_due_time: number }