From c1999aac8294d4f3a9a4b963153f2354374a2b39 Mon Sep 17 00:00:00 2001 From: lanyeeee Date: Wed, 30 Jul 2025 05:41:40 +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=8C=87=E5=AE=9A=E7=94=A8=E6=88=B7=E7=9A=84?= =?UTF-8?q?=E6=8A=95=E7=A8=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/Cargo.lock | 11 ++ src-tauri/Cargo.toml | 1 + src-tauri/src/bili_client.rs | 60 +++++++-- src-tauri/src/commands.rs | 20 ++- src-tauri/src/lib.rs | 2 + .../src/types/get_user_video_info_params.rs | 8 ++ src-tauri/src/types/mod.rs | 2 + src-tauri/src/types/user_video_info.rs | 92 ++++++++++++++ src-tauri/src/wbi.rs | 120 ++++++++++++++++++ src/bindings.ts | 15 +++ 10 files changed, 321 insertions(+), 10 deletions(-) create mode 100644 src-tauri/src/types/get_user_video_info_params.rs create mode 100644 src-tauri/src/types/user_video_info.rs create mode 100644 src-tauri/src/wbi.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 2965186..2f36307 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -273,6 +273,7 @@ dependencies = [ "chrono", "float-ord", "fs4", + "md-5", "memchr", "notify", "num_enum", @@ -2169,6 +2170,16 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.5" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2f9b8dc..d6aca7d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -49,6 +49,7 @@ prost = { version = "0.14.1" } yaserde = { version = "0.12.0", features = ["yaserde_derive"] } float-ord = { version = "0.3.2" } memchr = { version = "2.7.5" } +md-5 = { version = "0.10.6" } [profile.release] strip = true diff --git a/src-tauri/src/bili_client.rs b/src-tauri/src/bili_client.rs index 5a94534..4930eb7 100644 --- a/src-tauri/src/bili_client.rs +++ b/src-tauri/src/bili_client.rs @@ -23,9 +23,10 @@ use crate::{ cheese_media_url::CheeseMediaUrl, fav_folders::FavFolders, fav_info::FavInfo, get_bangumi_info_params::GetBangumiInfoParams, get_cheese_info_params::GetCheeseInfoParams, get_fav_info_params::GetFavInfoParams, get_normal_info_params::GetNormalInfoParams, - normal_info::NormalInfo, normal_media_url::NormalMediaUrl, player_info::PlayerInfo, - qrcode_data::QrcodeData, qrcode_status::QrcodeStatus, subtitle::Subtitle, tags::Tags, - user_info::UserInfo, watch_later_info::WatchLaterInfo, + get_user_video_info_params::GetUserVideoInfoParams, normal_info::NormalInfo, + normal_media_url::NormalMediaUrl, player_info::PlayerInfo, qrcode_data::QrcodeData, + qrcode_status::QrcodeStatus, subtitle::Subtitle, tags::Tags, user_info::UserInfo, + user_video_info::UserVideoInfo, watch_later_info::WatchLaterInfo, }, }; @@ -33,10 +34,10 @@ const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/ const REFERRER: &str = "https://www.bilibili.com/"; pub struct BiliClient { - app: AppHandle, - api_client: RwLock, - media_client: RwLock, - content_length_client: RwLock, + pub app: AppHandle, + pub api_client: RwLock, + pub media_client: RwLock, + pub content_length_client: RwLock, } impl BiliClient { @@ -281,6 +282,49 @@ impl BiliClient { Ok(cheese_info) } + pub async fn get_user_video_info( + &self, + params: GetUserVideoInfoParams, + ) -> anyhow::Result { + let mut params: Vec<(&str, String)> = vec![ + ("pn", params.pn.to_string()), + ("ps", "42".to_string()), + ("mid", params.mid.to_string()), + ]; + self.wbi(&mut params).await?; + + let request = self + .api_client + .read() + .get("https://api.bilibili.com/x/space/wbi/arc/search") + .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解析为UserVideoInfo + let data_str = data.to_string(); + let user_video_info: UserVideoInfo = serde_json::from_str(&data_str) + .context(format!("将data解析为UserVideoInfo失败: {data_str}"))?; + + Ok(user_video_info) + } + pub async fn get_normal_url(&self, bvid: &str, cid: i64) -> anyhow::Result { let params = json!({ "bvid": bvid, @@ -750,7 +794,7 @@ impl BiliClient { Ok(tags) } - fn get_cookie(&self) -> String { + pub 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 9eed2d1..c2e380e 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -11,9 +11,11 @@ use crate::{ cheese_media_url::CheeseMediaUrl, create_download_task_params::CreateDownloadTaskParams, fav_folders::FavFolders, fav_info::FavInfo, get_bangumi_info_params::GetBangumiInfoParams, get_cheese_info_params::GetCheeseInfoParams, get_fav_info_params::GetFavInfoParams, - get_normal_info_params::GetNormalInfoParams, normal_info::NormalInfo, + get_normal_info_params::GetNormalInfoParams, + get_user_video_info_params::GetUserVideoInfoParams, normal_info::NormalInfo, normal_media_url::NormalMediaUrl, player_info::PlayerInfo, qrcode_data::QrcodeData, - qrcode_status::QrcodeStatus, user_info::UserInfo, watch_later_info::WatchLaterInfo, + qrcode_status::QrcodeStatus, user_info::UserInfo, user_video_info::UserVideoInfo, + watch_later_info::WatchLaterInfo, }, }; @@ -142,6 +144,20 @@ pub async fn get_cheese_info( Ok(cheese_info) } +#[tauri::command(async)] +#[specta::specta] +pub async fn get_user_video_info( + app: AppHandle, + params: GetUserVideoInfoParams, +) -> CommandResult { + let bili_client = app.get_bili_client(); + let user_video_info = bili_client + .get_user_video_info(params) + .await + .map_err(|err| CommandError::from("获取用户视频信息失败", err))?; + Ok(user_video_info) +} + #[tauri::command(async)] #[specta::specta] pub async fn get_normal_url( diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 95434a2..e651143 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,6 +9,7 @@ mod extensions; mod logger; mod types; mod utils; +mod wbi; mod protobuf { include!("./bilibili.community.service.dm.v1.rs"); } @@ -42,6 +43,7 @@ pub fn run() { get_normal_info, get_bangumi_info, get_cheese_info, + get_user_video_info, get_normal_url, get_bangumi_url, get_cheese_url, diff --git a/src-tauri/src/types/get_user_video_info_params.rs b/src-tauri/src/types/get_user_video_info_params.rs new file mode 100644 index 0000000..e60c668 --- /dev/null +++ b/src-tauri/src/types/get_user_video_info_params.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct GetUserVideoInfoParams { + pub pn: i64, + pub mid: i64, +} diff --git a/src-tauri/src/types/mod.rs b/src-tauri/src/types/mod.rs index 732465a..f15db73 100644 --- a/src-tauri/src/types/mod.rs +++ b/src-tauri/src/types/mod.rs @@ -11,6 +11,7 @@ pub mod get_bangumi_info_params; pub mod get_cheese_info_params; pub mod get_fav_info_params; pub mod get_normal_info_params; +pub mod get_user_video_info_params; pub mod log_level; pub mod normal_info; pub mod normal_media_url; @@ -20,5 +21,6 @@ pub mod qrcode_status; pub mod subtitle; pub mod tags; pub mod user_info; +pub mod user_video_info; pub mod video_quality; pub mod watch_later_info; diff --git a/src-tauri/src/types/user_video_info.rs b/src-tauri/src/types/user_video_info.rs new file mode 100644 index 0000000..bea78c4 --- /dev/null +++ b/src-tauri/src/types/user_video_info.rs @@ -0,0 +1,92 @@ +use serde::{Deserialize, Serialize}; +use specta::Type; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct UserVideoInfo { + pub list: UserVideoList, + pub page: PageInUserVideo, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct UserVideoList { + pub vlist: Vec, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct EpInUserVideo { + pub comment: i64, + pub typeid: i64, + pub play: i64, + pub pic: String, + pub subtitle: String, + pub description: String, + pub copyright: String, + pub title: String, + pub review: i64, + pub author: String, + pub mid: i64, + pub created: i64, + pub length: String, + pub video_review: i64, + pub aid: i64, + pub bvid: String, + pub hide_click: bool, + pub is_pay: i64, + pub is_union_video: i64, + pub is_steins_gate: i64, + pub is_live_playback: i64, + pub is_lesson_video: i64, + pub is_lesson_finished: i64, + pub lesson_update_info: String, + pub jump_url: String, + pub meta: Option, + pub is_avoided: i64, + pub season_id: i64, + pub attribute: i64, + pub is_charging_arc: bool, + pub elec_arc_type: i64, + pub elec_arc_badge: String, + pub vt: i64, + pub enable_vt: i64, + pub vt_display: String, + pub playback_position: i64, + pub is_self_view: bool, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct MetaInUserVideo { + pub id: i64, + pub title: String, + pub cover: String, + pub mid: i64, + pub intro: String, + pub sign_state: i64, + pub attribute: i64, + pub stat: StatInUserVideo, + pub ep_count: i64, + pub first_aid: i64, + pub ptime: i64, + pub ep_num: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct StatInUserVideo { + pub season_id: i64, + pub view: i64, + pub danmaku: i64, + pub reply: i64, + pub favorite: i64, + pub coin: i64, + pub share: i64, + pub like: i64, + pub mtime: i64, + pub vt: i64, + pub vv: i64, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Type)] +pub struct PageInUserVideo { + pub pn: i64, + pub ps: i64, + pub count: i64, +} diff --git a/src-tauri/src/wbi.rs b/src-tauri/src/wbi.rs new file mode 100644 index 0000000..6841e9a --- /dev/null +++ b/src-tauri/src/wbi.rs @@ -0,0 +1,120 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::{anyhow, Context}; +use md5::{Digest, Md5}; +use serde::Deserialize; + +use crate::bili_client::{BiliClient, BiliResp}; + +const MIXIN_KEY_ENC_TAB: [usize; 64] = [ + 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, + 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, + 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52, +]; + +#[derive(Deserialize)] +struct WbiImgRespData { + img_url: String, + sub_url: String, +} + +#[derive(Deserialize)] +struct WeiRespData { + wbi_img: WbiImgRespData, +} + +impl BiliClient { + // 为请求参数进行 wbi 签名 + pub(crate) async fn wbi(&self, params: &mut Vec<(&str, String)>) -> anyhow::Result<()> { + let (img_key, sub_key) = self.get_wbi_keys().await.context("获取wbi keys失败")?; + let mixin_key = get_mixin_key((img_key + &sub_key).as_bytes()); + + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + // 添加当前时间戳 + params.push(("wts", timestamp.to_string())); + // 重新排序 + params.sort_by(|a, b| a.0.cmp(b.0)); + // 拼接参数 + let query = params + .iter() + .map(|(k, v)| format!("{}={}", get_url_encoded(k), get_url_encoded(v))) + .collect::>() + .join("&"); + // 计算签名 + let web_sign = format!("{:x}", Md5::digest(query.clone() + &mixin_key)); + params.push(("w_rid", web_sign)); + Ok(()) + } + + async fn get_wbi_keys(&self) -> anyhow::Result<(String, String)> { + let request = self + .api_client + .read() + .get("https://api.bilibili.com/x/web-interface/nav") + .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 != reqwest::StatusCode::OK { + return Err(anyhow!("预料之外的状态码({status}): {body}")); + } + // 尝试将body解析为BiliResp + let bili_resp: BiliResp = + serde_json::from_str(&body).context(format!("将body解析为BiliResp失败: {body}"))?; + // 检查BiliResp的data是否存在 + let Some(data) = bili_resp.data else { + return Err(anyhow!("BiliResp中不存在data字段: {bili_resp:?}")); + }; + // 尝试将data解析为Data + let data_str = data.to_string(); + let wei_resp_data: WeiRespData = + serde_json::from_str(&data_str).context(format!("将data解析为Data失败: {data_str}"))?; + + let img_url = wei_resp_data.wbi_img.img_url; + let sub_url = wei_resp_data.wbi_img.sub_url; + + let img_filename = + take_filename(&img_url).context(format!("从img_url中提取文件名失败: {img_url}"))?; + let sub_filename = + take_filename(&sub_url).context(format!("从sub_url中提取文件名失败: {sub_url}"))?; + + Ok((img_filename, sub_filename)) + } +} + +// 对 imgKey 和 subKey 进行字符顺序打乱编码 +fn get_mixin_key(orig: &[u8]) -> String { + MIXIN_KEY_ENC_TAB + .iter() + .take(32) + .map(|&i| orig[i] as char) + .collect::() +} + +fn get_url_encoded(s: &str) -> String { + s.chars() + .filter_map(|c| { + if c.is_ascii_alphanumeric() || "-_.~".contains(c) { + Some(c.to_string()) + } else { + // 过滤 value 中的 "!'()*" 字符 + if "!'()*".contains(c) { + return None; + } + let encoded = c + .encode_utf8(&mut [0; 4]) + .bytes() + .fold(String::new(), |acc, b| acc + &format!("%{b:02X}")); + Some(encoded) + } + }) + .collect::() +} + +fn take_filename(url: &str) -> Option { + url.rsplit_once('/') + .and_then(|(_, s)| s.rsplit_once('.')) + .map(|(s, _)| s.to_string()) +} diff --git a/src/bindings.ts b/src/bindings.ts index 737dc7d..684acdd 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -67,6 +67,14 @@ async getCheeseInfo(params: GetCheeseInfoParams) : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("get_user_video_info", { params }) }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +}, async getNormalUrl(bvid: string, cid: number) : Promise> { try { return { status: "ok", data: await TAURI_INVOKE("get_normal_url", { bvid, cid }) }; @@ -270,6 +278,7 @@ export type ElecHighLevel = { privilege_type: number; title: string; sub_title: 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[] } +export type EpInUserVideo = { comment: number; typeid: number; play: number; pic: string; subtitle: string; description: string; copyright: string; title: string; review: number; author: string; mid: number; created: number; length: string; video_review: number; aid: number; bvid: string; hide_click: boolean; is_pay: number; is_union_video: number; is_steins_gate: number; is_live_playback: number; is_lesson_video: number; is_lesson_finished: number; lesson_update_info: string; jump_url: string; meta: MetaInUserVideo | null; is_avoided: number; season_id: number; attribute: number; is_charging_arc: boolean; elec_arc_type: number; elec_arc_badge: string; vt: number; enable_vt: number; vt_display: string; playback_position: number; is_self_view: boolean } export type EpPage = { next: boolean; num: number; size: number; total: number } export type EpTag = { part_preview_tag: string; pay_tag: string; preview_tag: string } export type EpisodeType = "Normal" | "Bangumi" | "Cheese" @@ -285,6 +294,7 @@ export type GetBangumiInfoParams = { EpId: number } | { SeasonId: number } export type GetCheeseInfoParams = { EpId: number } | { SeasonId: number } export type GetFavInfoParams = { media_list_id: number; pn: number } export type GetNormalInfoParams = { Bvid: string } | { Aid: number } +export type GetUserVideoInfoParams = { pn: number; mid: 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 } @@ -307,6 +317,7 @@ export type MediaInFav = { id: number; type: number; title: string; cover: strin 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 MediaInWatchLater = { aid: number; videos: number; tid: number; tname: string; copyright: number; pic: string; title: string; pubdate: number; ctime: number; desc: string; state: number; duration: number; redirect_url: string | null; mission_id: number | null; rights: RightsInWatchLater; owner: OwnerInWatchLater; stat: StatInWatchLater; dynamic: string; dimension: DimensionInWatchLater; short_link_v2: string; up_from_v2: number | null; first_frame: string | null; pub_location: string | null; cover43: string; tidv2: number; tnamev2: string; pid_v2: number; pid_name_v2: string; page: PageInWatchLater; count: number; cid: number; progress: number; add_at: number; bvid: string; uri: string; enable_vt: number; view_text_1: string; card_type: number; left_icon_type: number; left_text: string; right_icon_type: number; right_text: string; arc_state: number; pgc_label: string; show_up: boolean; forbid_fav: boolean; forbid_sort: boolean; season_title: string; long_title: string; index_title: string; c_source: string; season_id: number | null } export type MergeTask = { selected: boolean; completed: boolean } +export type MetaInUserVideo = { id: number; title: string; cover: string; mid: number; intro: string; sign_state: number; attribute: number; stat: StatInUserVideo; ep_count: number; first_aid: number; ptime: number; ep_num: 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 NfoTask = { selected: boolean; completed: boolean } @@ -321,6 +332,7 @@ export type OwnerInNormal = { mid: number; name: string; face: string } export type OwnerInWatchLater = { 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 PageInUserVideo = { pn: number; ps: number; count: number } export type PageInWatchLater = { cid: number; page: number; from: string; part: string; duration: number; vid: string; weblink: string; dimension: DimensionInWatchLater; first_frame: string | null; ctime: number } export type PaidJump = { jump_url_for_app: string; url: string } export type PayType = { allow_discount: number; allow_pack: number; allow_ticket: number; allow_time_limit: number; allow_vip_discount: number; forbid_bb: number } @@ -368,6 +380,7 @@ export type StatInNormal = { aid: number; view: number; danmaku: number; reply: 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 StatInSeason = { favorites: number; series_follow: number; views: number; vt: number } +export type StatInUserVideo = { season_id: number; view: number; danmaku: number; reply: number; favorite: number; coin: number; share: number; like: number; mtime: number; vt: number; vv: number } export type StatInWatchLater = { aid: number; view: number; danmaku: number; reply: number; favorite: number; coin: number; share: number; now_rank: number; his_rank: number; like: number; dislike: 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 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 } @@ -388,6 +401,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 UserVideoInfo = { list: UserVideoList; page: PageInUserVideo } +export type UserVideoList = { vlist: EpInUserVideo[] } export type VideoQuality = "Unknown" | "240P" | "360P" | "480P" | "720P" | "720P60" | "1080P" | "AiRepair" | "1080P+" | "1080P60" | "4K" | "HDR" | "Dolby" | "8K" export type VideoTask = { selected: boolean; url: string; video_quality: VideoQuality; codec_type: CodecType; content_length: number; chunks: MediaChunk[]; completed: boolean } 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 }