diff --git a/src-tauri/src/bili_client.rs b/src-tauri/src/bili_client.rs index 4d45d3f..afb0309 100644 --- a/src-tauri/src/bili_client.rs +++ b/src-tauri/src/bili_client.rs @@ -18,7 +18,8 @@ use crate::{ bangumi_info::BangumiInfo, cheese_info::CheeseInfo, get_bangumi_info_params::GetBangumiInfoParams, get_cheese_info_params::GetCheeseInfoParams, get_normal_info_params::GetNormalInfoParams, normal_info::NormalInfo, - qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, user_info::UserInfo, + normal_media_url::NormalMediaUrl, qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, + user_info::UserInfo, }, }; @@ -261,6 +262,46 @@ impl BiliClient { Ok(cheese_info) } + pub async fn get_normal_url(&self, bvid: &str, cid: i64) -> anyhow::Result { + let params = json!({ + "bvid": bvid, + "cid": cid, + "qn": 127, + "fnval": 4048, + }); + // 发送获取普通url的请求 + let request = self + .api_client + .read() + .get("https://api.bilibili.com/x/player/wbi/playurl") + .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解析为NormalMediaUrl + let data_str = data.to_string(); + let media_url: NormalMediaUrl = serde_json::from_str(&data_str) + .context(format!("将data解析为NormalMediaUrl失败: {data_str}"))?; + + Ok(media_url) + } + 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 1c69d88..da6a9a4 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -10,7 +10,8 @@ use crate::{ bangumi_info::BangumiInfo, cheese_info::CheeseInfo, get_bangumi_info_params::GetBangumiInfoParams, get_cheese_info_params::GetCheeseInfoParams, get_normal_info_params::GetNormalInfoParams, normal_info::NormalInfo, - qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, user_info::UserInfo, + normal_media_url::NormalMediaUrl, qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, + user_info::UserInfo, }, }; @@ -138,3 +139,18 @@ pub async fn get_cheese_info( .map_err(|err| CommandError::from("获取课程视频信息失败", err))?; Ok(cheese_info) } + +#[tauri::command(async)] +#[specta::specta] +pub async fn get_normal_url( + app: AppHandle, + bvid: String, + cid: i64, +) -> CommandResult { + let bili_client = app.get_bili_client(); + let media_url = bili_client + .get_normal_url(&bvid, cid) + .await + .map_err(|err| CommandError::from("获取普通视频url失败", err))?; + Ok(media_url) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 18cf368..9884fac 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -33,6 +33,7 @@ pub fn run() { get_normal_info, get_bangumi_info, get_cheese_info, + get_normal_url, ]) .events(tauri_specta::collect_events![LogEvent]); diff --git a/src-tauri/src/types/mod.rs b/src-tauri/src/types/mod.rs index 2b44dbc..fa7d392 100644 --- a/src-tauri/src/types/mod.rs +++ b/src-tauri/src/types/mod.rs @@ -5,6 +5,7 @@ pub mod get_cheese_info_params; pub mod get_normal_info_params; pub mod log_level; pub mod normal_info; +pub mod normal_media_url; pub mod qrcode_data; pub mod qrcode_status; pub mod user_info; diff --git a/src-tauri/src/types/normal_media_url.rs b/src-tauri/src/types/normal_media_url.rs new file mode 100644 index 0000000..8a80770 --- /dev/null +++ b/src-tauri/src/types/normal_media_url.rs @@ -0,0 +1,84 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct NormalMediaUrl { + pub from: String, + pub result: String, + pub message: String, + pub quality: i64, + pub format: String, + pub timelength: i64, + pub accept_format: String, + pub accept_description: Vec, + pub accept_quality: Vec, + pub video_codecid: i64, + pub seek_param: String, + pub seek_type: String, + pub dash: DashInNormal, + pub support_formats: Vec, + pub last_play_time: i64, + pub last_play_cid: i64, + pub play_conf: PlayConf, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct DashInNormal { + pub duration: u64, + pub min_buffer_time: f64, + pub video: Vec, + pub audio: Option>, + pub dolby: Dolby, + pub flac: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct Flac { + pub display: bool, + pub audio: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct MediaInNormal { + pub id: i64, + pub start_with_sap: i64, + pub bandwidth: i64, + pub sar: String, + pub codecs: String, + pub base_url: String, + pub backup_url: Vec, + pub segment_base: SegmentBaseInNormal, + pub mime_type: String, + pub frame_rate: String, + pub width: i64, + pub height: i64, + pub codecid: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct SegmentBaseInNormal { + pub initialization: String, + pub index_range: String, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct Dolby { + #[serde(rename = "type")] + pub type_field: i64, + pub audio: Option>, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct SupportFormatInNormal { + pub quality: i64, + pub format: String, + pub new_description: String, + pub display_desc: String, + pub superscript: String, + pub codecs: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct PlayConf { + pub is_new_description: bool, +} diff --git a/src/bindings.ts b/src/bindings.ts index e5b6f9a..930052f 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -66,6 +66,14 @@ async getCheeseInfo(params: GetCheeseInfoParams) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_normal_url", { bvid, cid }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} } } @@ -99,9 +107,11 @@ export type Config = { downloadDir: string; enableFileLogger: boolean; sessdata: export type Consulting = { consulting_flag: boolean; consulting_url: string } export type ContentList = { bold: boolean; content: string; number: string } export type Cooperation = { link: string } +export type DashInNormal = { duration: number; min_buffer_time: number; video: MediaInNormal[]; audio: MediaInNormal[] | null; dolby: Dolby; flac: Flac | null } export type DescV2 = { raw_text: string; type: number; biz_id: number } export type Dimension = { width: number; height: number; rotate: number } export type DimensionInBangumi = { height: number; rotate: number; width: number } +export type Dolby = { type: number; audio: MediaInNormal[] | null } export type Ed = { end: number; start: number } 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 } @@ -111,6 +121,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 Flac = { display: boolean; audio: MediaInNormal | null } export type GetBangumiInfoParams = { EpId: number } | { SeasonId: number } export type GetCheeseInfoParams = { EpId: number } | { SeasonId: number } export type GetNormalInfoParams = { Bvid: string } | { Aid: number } @@ -123,9 +134,11 @@ export type LabelInUserInfo = { path: string; text: string; label_theme: 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 MediaInNormal = { id: number; start_with_sap: number; bandwidth: number; sar: string; codecs: string; base_url: string; backup_url: string[]; segment_base: SegmentBaseInNormal; mime_type: string; frame_rate: string; width: number; height: number; codecid: number } export type NewEp = { desc: string; id: number; is_new: number; title: string } export type NewEpInSeason = { cover: string; id: number; index_show: string } 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 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 Op = { end: number; start: number } @@ -138,6 +151,7 @@ export type Payment = { bp_enough: number; desc: string; my_bp: number; pay_shad export type PaymentInBangumi = { discount: number; pay_type: PayType; price: string; promotion: string; tip: string; view_start_time: number; vip_discount: number; vip_first_promotion: string; vip_price: string; vip_promotion: string } export type PendantInCheese = { image: string; name: string; pid: number } export type PendantInUserInfo = { pid: number; name: string; image: string; expire: number; image_enhance: string; image_enhance_frame: string; n_pid: number } +export type PlayConf = { is_new_description: boolean } export type PlayStrategy = { strategies: string[] } 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 } @@ -156,6 +170,7 @@ export type RightsInNormalEp = { bp: number; elec: number; download: number; mov export type Season = { badge: string; badge_info: BadgeInfo; badge_type: number; cover: string; enable_vt: boolean; horizontal_cover_1610: string; horizontal_cover_169: string; icon_font: IconFont; media_id: number; new_ep: NewEpInSeason; season_id: number; season_title: string; season_type: number; stat: StatInSeason } export type SectionInBangumi = { attr: number; episodes: EpInBangumi[]; id: number; title: string; type: number; type2: number } export type SectionInNormal = { season_id: number; id: number; title: string; type: number; episodes: EpInNormal[] } +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 Skip = { ed: Ed; op: Op } @@ -168,6 +183,7 @@ export type StatInNormalSeason = { season_id: number; view: number; danmaku: num 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 SubtitleInNormal = { allow_submit: boolean; list: SubtitleDetailInNormal[] } +export type SupportFormatInNormal = { quality: number; format: string; new_description: string; display_desc: string; superscript: string; codecs: string[] } 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 UpInfoInBangumi = { avatar: string; mid: number; uname: string } export type UpInfoInCheese = { avatar: string; brief: string; follower: number; is_follow: number; is_living: boolean; link: string; mid: number; pendant: PendantInCheese; season_count: number; uname: string }