From aafe001ec778efc0ed38b4ebdd3e6817cd4470df Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Mon, 9 Sep 2024 12:10:04 +0800 Subject: [PATCH] =?UTF-8?q?refactor(bilibili):=20bilibili=20=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E5=99=A8=E7=94=A8=20rust=20=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/error.rs | 6 + src-tauri/src/lib.rs | 3 +- .../src/parser/bilibili/bilibili_parser.rs | 146 ++++++++++++ .../src/parser/bilibili/cookie_verifier.rs | 69 ++++++ src-tauri/src/parser/bilibili/html_fetcher.rs | 61 +++++ src-tauri/src/parser/bilibili/link_parser.rs | 36 +++ src-tauri/src/parser/bilibili/mod.rs | 32 +++ .../src/parser/bilibili/room_info_fetcher.rs | 86 +++++++ .../parser/bilibili/room_play_info_fetcher.rs | 115 +++++++++ src-tauri/src/parser/http_client.rs | 12 +- src-tauri/src/parser/mod.rs | 2 + src/parser/bilibili/index.ts | 225 ++---------------- 12 files changed, 575 insertions(+), 218 deletions(-) create mode 100644 src-tauri/src/parser/bilibili/bilibili_parser.rs create mode 100644 src-tauri/src/parser/bilibili/cookie_verifier.rs create mode 100644 src-tauri/src/parser/bilibili/html_fetcher.rs create mode 100644 src-tauri/src/parser/bilibili/link_parser.rs create mode 100644 src-tauri/src/parser/bilibili/mod.rs create mode 100644 src-tauri/src/parser/bilibili/room_info_fetcher.rs create mode 100644 src-tauri/src/parser/bilibili/room_play_info_fetcher.rs diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs index 1ecfeba..4752a4f 100644 --- a/src-tauri/src/error.rs +++ b/src-tauri/src/error.rs @@ -93,6 +93,12 @@ impl From for HTTPError { } } +impl From for LsarError { + fn from(value: reqwest::Error) -> Self { + LsarError::Http(value.into()) + } +} + #[derive(Debug, Serialize, thiserror::Error)] pub(super) enum RoomStateError { Offline, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ab535c4..135e2f6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -26,7 +26,7 @@ use crate::error::LsarResult; use crate::eval::eval_result; use crate::http::{get, post}; use crate::log::{debug, error, info, trace, warn}; -use crate::parser::{parse_douyin, parse_douyu, parse_huya}; +use crate::parser::{parse_bilibili, parse_douyin, parse_douyu, parse_huya}; use crate::setup::{setup_app, setup_logging}; use crate::utils::md5; @@ -94,6 +94,7 @@ pub fn run() { parse_douyu, parse_huya, parse_douyin, + parse_bilibili, ]) .run(tauri::generate_context!()) .expect("Error while running tauri application"); diff --git a/src-tauri/src/parser/bilibili/bilibili_parser.rs b/src-tauri/src/parser/bilibili/bilibili_parser.rs new file mode 100644 index 0000000..ece9abf --- /dev/null +++ b/src-tauri/src/parser/bilibili/bilibili_parser.rs @@ -0,0 +1,146 @@ +use reqwest::Client; + +use crate::error::LsarResult; +use crate::parser::ParsedResult; +use crate::platform::Platform; + +use super::cookie_verifier::CookieVerifier; +use super::html_fetcher::HTMLFetcher; +use super::link_parser::LinkParser; +use super::room_info_fetcher::RoomInfoFetcher; +use super::room_play_info_fetcher::RoomPlayInfoFetcher; + +pub struct BilibiliParser { + room_id: u64, + page_url: String, + cookie: String, + client: Client, +} + +impl BilibiliParser { + pub fn new(cookie: String, room_id: u64, url: Option) -> Self { + let page_url = url.unwrap_or_else(|| format!("https://live.bilibili.com/{}", room_id)); + let client = reqwest::Client::new(); + + BilibiliParser { + room_id, + page_url, + cookie, + client, + } + } + + pub async fn parse(&mut self) -> LsarResult { + trace!("Starting parsing process for room ID: {}", self.room_id); + + let cookie_verifier = CookieVerifier::new(&self.client, &self.cookie); + match cookie_verifier.verify().await { + Ok(username) => { + info!( + "Cookie verification successful. Logged in user: {}", + username + ); + } + Err(e) => { + error!("Cookie verification failed. Error: {}. Details: {:?}", e, e); + return Err(e); + } + }; + + if self.room_id == 0 { + let html_fetcher = HTMLFetcher::new(&self.client, &self.page_url); + let html = match html_fetcher.fetch().await { + Ok(html) => { + debug!( + "Fetched page HTML successfully. Length: {} characters", + html.len() + ); + html + } + Err(e) => { + error!("Failed to fetch page HTML. Error: {}. Details: {:?}", e, e); + return Err(e); + } + }; + + self.room_id = self.parse_room_id(&html)?; + } + + let room_info_fetcher = RoomInfoFetcher::new(&self.client, self.room_id, &self.cookie); + let page_info = match room_info_fetcher.fetch().await { + Ok(info) => { + debug!( + "Fetched room info successfully. Title: {}, Anchor: {}, Category: {}", + info.0, info.1, info.2 + ); + info + } + Err(e) => { + error!("Failed to fetch room info. Error: {}. Details: {:?}", e, e); + return Err(e); + } + }; + + let room_play_info_fetcher = + RoomPlayInfoFetcher::new(&self.client, self.room_id, &self.cookie); + let room_play_info = match room_play_info_fetcher.fetch().await { + Ok(info) => { + debug!("Fetched room play info successfully"); + info + } + Err(e) => { + error!( + "Failed to fetch room play info. Error: {}. Details: {:?}", + e, e + ); + return Err(e); + } + }; + + let link_parser = LinkParser::new(); + let links = link_parser.parse(&room_play_info); + debug!("Parsed {} stream links", links.len()); + + let parsed_result = ParsedResult { + title: page_info.0, + anchor: page_info.1, + category: page_info.2, + platform: Platform::Bilibili, + links, + room_id: self.room_id, + }; + + info!( + "Parsing completed successfully for room ID: {}", + self.room_id + ); + Ok(parsed_result) + } + + fn parse_room_id(&self, html: &str) -> LsarResult { + trace!("Parsing room ID from HTML"); + let room_id = html + .split(r#""defaultRoomId":""#) + .nth(1) + .and_then(|s| s.split('"').next()) + .or_else(|| { + html.split(r#""roomid":"#) + .nth(1) + .and_then(|s| s.split(',').next()) + }) + .or_else(|| { + html.split(r#""roomId":"#) + .nth(1) + .and_then(|s| s.split(',').next()) + }) + .and_then(|s| s.parse::().ok()) + .ok_or_else(|| { + let err_msg = "Failed to parse room ID"; + error!("{}", err_msg); + err_msg + })?; + + debug!("Parsed room ID: {}", room_id); + Ok(room_id) + } +} diff --git a/src-tauri/src/parser/bilibili/cookie_verifier.rs b/src-tauri/src/parser/bilibili/cookie_verifier.rs new file mode 100644 index 0000000..d9007cc --- /dev/null +++ b/src-tauri/src/parser/bilibili/cookie_verifier.rs @@ -0,0 +1,69 @@ +use reqwest::Client; +use serde::Deserialize; +use serde_json::Value; + +use crate::error::LsarResult; + +const VERIFY_URL: &str = "https://api.bilibili.com/x/web-interface/nav"; + +#[derive(Debug, Deserialize)] +#[serde(rename_all(deserialize = "camelCase"))] +struct VerifyData { + uname: Option, +} + +#[derive(Debug, Deserialize)] +struct VerifyResponse { + code: i32, + message: String, + data: VerifyData, +} + +pub struct CookieVerifier<'a> { + client: &'a Client, + cookie: &'a str, +} + +impl<'a> CookieVerifier<'a> { + pub fn new(client: &'a Client, cookie: &'a str) -> Self { + CookieVerifier { client, cookie } + } + + pub async fn verify(&self) -> LsarResult { + debug!("Starting cookie verification process"); + + let response_value = self + .client + .get(VERIFY_URL) + .header("Cookie", self.cookie) + .send() + .await? + .json::() + .await?; + + debug!("Cookie verification result: {}", response_value); + + let response: VerifyResponse = serde_json::from_value(response_value)?; + + if response.code != 0 { + let err_msg = format!("Cookie verification failed: {}", response.message); + error!("{}. Response code: {}", err_msg, response.code); + + // -101 未登录 + if response.code == -101 && response.message == "账号未登录" { + return Err("账号未登录,cookie 未设置或已失效".into()); + } + + return Err(err_msg.into()); + } + + let username = response.data.uname.ok_or_else(|| { + let err_msg = "Username not found in verification response"; + error!("{}", err_msg); + err_msg + })?; + + debug!("Cookie verification successful for user: {}", username); + Ok(username) + } +} diff --git a/src-tauri/src/parser/bilibili/html_fetcher.rs b/src-tauri/src/parser/bilibili/html_fetcher.rs new file mode 100644 index 0000000..ca29ccd --- /dev/null +++ b/src-tauri/src/parser/bilibili/html_fetcher.rs @@ -0,0 +1,61 @@ +use reqwest::{header::USER_AGENT, Client}; +use tauri::http::{HeaderMap, HeaderValue}; + +use crate::error::LsarResult; + +pub struct HTMLFetcher<'a> { + client: &'a Client, + url: &'a str, +} + +impl<'a> HTMLFetcher<'a> { + pub fn new(client: &'a Client, url: &'a str) -> Self { + HTMLFetcher { client, url } + } + + pub async fn fetch(&self) -> LsarResult { + debug!("Fetching page HTML from: {}", self.url); + let mut headers = HeaderMap::new(); + headers.insert("Host", HeaderValue::from_static("live.bilibili.com")); + headers.insert( + USER_AGENT, + HeaderValue::from_static( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0", + ), + ); + headers.insert("Accept", HeaderValue::from_static("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")); + headers.insert("Accept-Language", HeaderValue::from_static("zh-CN")); + headers.insert("Connection", HeaderValue::from_static("keep-alive")); + headers.insert("Upgrade-Insecure-Requests", HeaderValue::from_static("1")); + headers.insert("Sec-Fetch-Dest", HeaderValue::from_static("document")); + headers.insert("Sec-Fetch-Mode", HeaderValue::from_static("navigate")); + headers.insert("Sec-Fetch-Site", HeaderValue::from_static("none")); + headers.insert("Sec-Fetch-User", HeaderValue::from_static("?1")); + headers.insert("DNT", HeaderValue::from_static("1")); + headers.insert("Sec-GPC", HeaderValue::from_static("1")); + + let response = self + .client + .get(self.url) + .headers(headers) + .send() + .await + .map_err(|e| { + let err_msg = format!("Failed to send request: {}", e); + error!("{}", err_msg); + err_msg + })?; + + let html = response.text().await.map_err(|e| { + let err_msg = format!("Failed to get response text: {}", e); + error!("{}", err_msg); + err_msg + })?; + + debug!( + "Successfully fetched HTML. Length: {} characters", + html.len() + ); + Ok(html) + } +} diff --git a/src-tauri/src/parser/bilibili/link_parser.rs b/src-tauri/src/parser/bilibili/link_parser.rs new file mode 100644 index 0000000..5b65e53 --- /dev/null +++ b/src-tauri/src/parser/bilibili/link_parser.rs @@ -0,0 +1,36 @@ +use super::room_play_info_fetcher::Response; + +pub struct LinkParser; + +impl LinkParser { + pub fn new() -> Self { + LinkParser + } + + pub fn parse(&self, info: &Response) -> Vec { + trace!("Starting to parse stream links"); + let mut links = Vec::new(); + + for (stream_index, stream) in info.data.playurl_info.playurl.stream.iter().enumerate() { + for (format_index, format) in stream.format.iter().enumerate() { + for (codec_index, codec) in format.codec.iter().enumerate() { + for (url_index, url_info) in codec.url_info.iter().enumerate() { + let link = format!("{}{}{}", url_info.host, codec.base_url, url_info.extra); + trace!( + "Parsed link: {} (Stream: {}, Format: {}, Codec: {}, URL: {})", + link, + stream_index, + format_index, + codec_index, + url_index + ); + links.push(link); + } + } + } + } + + debug!("Parsed {} stream links", links.len()); + links + } +} diff --git a/src-tauri/src/parser/bilibili/mod.rs b/src-tauri/src/parser/bilibili/mod.rs new file mode 100644 index 0000000..cf1d426 --- /dev/null +++ b/src-tauri/src/parser/bilibili/mod.rs @@ -0,0 +1,32 @@ +use bilibili_parser::BilibiliParser; + +use crate::error::LsarResult; + +use super::ParsedResult; + +mod bilibili_parser; +mod cookie_verifier; +mod html_fetcher; +mod link_parser; +mod room_info_fetcher; +mod room_play_info_fetcher; + +#[tauri::command] +pub async fn parse_bilibili( + room_id: u64, + cookie: String, + url: Option, +) -> LsarResult { + let mut parser = BilibiliParser::new(cookie, room_id, url); + + match parser.parse().await { + Ok(result) => { + info!(target: "main", "Parsing successful. Result: {:?}", result); + Ok(result) + } + Err(e) => { + error!(target: "main", "Parsing failed: {}. Error details: {:?}", e, e); + Err(e) + } + } +} diff --git a/src-tauri/src/parser/bilibili/room_info_fetcher.rs b/src-tauri/src/parser/bilibili/room_info_fetcher.rs new file mode 100644 index 0000000..3f72fff --- /dev/null +++ b/src-tauri/src/parser/bilibili/room_info_fetcher.rs @@ -0,0 +1,86 @@ +use reqwest::Client; +use serde::Deserialize; + +use crate::error::LsarResult; + +#[derive(Debug, Deserialize)] +struct RoomInfoData { + anchor_info: AnchorInfo, + room_info: RoomInfo, +} + +#[derive(Debug, Deserialize)] +struct AnchorInfo { + base_info: BaseInfo, +} + +#[derive(Debug, Deserialize)] +struct BaseInfo { + //face: String, // 头像,如果以后需要的话再使用 + uname: String, +} + +#[derive(Debug, Deserialize)] +struct RoomInfo { + area_name: String, + title: String, +} + +#[derive(Debug, Deserialize)] +struct RoomInfoResponse { + data: RoomInfoData, +} + +pub struct RoomInfoFetcher<'a> { + client: &'a Client, + room_id: u64, + cookie: &'a str, +} + +impl<'a> RoomInfoFetcher<'a> { + pub fn new(client: &'a Client, room_id: u64, cookie: &'a str) -> Self { + RoomInfoFetcher { + client, + room_id, + cookie, + } + } + + pub async fn fetch(&self) -> LsarResult<(String, String, String)> { + debug!("Fetching room info for room ID: {}", self.room_id); + let url = format!( + "https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom?room_id={}", + self.room_id + ); + let response = self + .client + .get(&url) + .header("Cookie", self.cookie) + .send() + .await + .map_err(|e| { + let err_msg = format!("Failed to send request: {}", e); + error!("{}", err_msg); + err_msg + })? + .json::() + .await + .map_err(|e| { + let err_msg = format!("Failed to parse response JSON: {}", e); + error!("{}", err_msg); + err_msg + })?; + + let data = response.data; + debug!( + "Successfully fetched room info. Title: {}, Anchor: {}, Category: {}", + data.room_info.title, data.anchor_info.base_info.uname, data.room_info.area_name + ); + + Ok(( + data.room_info.title, + data.anchor_info.base_info.uname, + data.room_info.area_name, + )) + } +} diff --git a/src-tauri/src/parser/bilibili/room_play_info_fetcher.rs b/src-tauri/src/parser/bilibili/room_play_info_fetcher.rs new file mode 100644 index 0000000..72ddf0e --- /dev/null +++ b/src-tauri/src/parser/bilibili/room_play_info_fetcher.rs @@ -0,0 +1,115 @@ +use reqwest::Client; +use serde::Deserialize; +use serde_json::Value; + +use crate::error::{LsarResult, RoomStateError}; + +const BASE_URL: &str = "https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo?protocol=0,1&format=0,1,2&codec=0,1&qn=10000&platform=web&ptype=8&dolby=5&panorama=1&room_id="; + +#[derive(Debug, Deserialize)] +pub struct CDNItem { + pub host: String, + pub extra: String, +} + +#[derive(Debug, Deserialize)] +pub struct CodecItem { + pub base_url: String, + pub url_info: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct FormatItem { + pub codec: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct StreamItem { + pub format: Vec, +} + +#[derive(Debug, Deserialize)] +pub(super) struct PlayUrlInfo { + pub playurl: PlayUrl, +} + +#[derive(Debug, Deserialize)] +pub struct PlayUrl { + pub stream: Vec, +} + +#[derive(Debug, Deserialize)] +pub(super) struct ResponseData { + live_status: i32, + pub playurl_info: PlayUrlInfo, +} + +#[derive(Debug, Deserialize)] +pub struct Response { + code: i32, + message: String, + pub data: ResponseData, +} + +pub struct RoomPlayInfoFetcher<'a> { + client: &'a Client, + room_id: u64, + cookie: &'a str, +} + +impl<'a> RoomPlayInfoFetcher<'a> { + pub fn new(client: &'a Client, room_id: u64, cookie: &'a str) -> Self { + RoomPlayInfoFetcher { + client, + room_id, + cookie, + } + } + + pub async fn fetch(&self) -> LsarResult { + debug!("Fetching room play info for room ID: {}", self.room_id); + let url = format!("{}{}", BASE_URL, self.room_id); + let response_value = self + .client + .get(&url) + .header("Cookie", self.cookie) + .send() + .await + .map_err(|e| { + let err_msg = format!("Failed to send request: {}", e); + error!("{}", err_msg); + err_msg + })? + .json::() + .await + .map_err(|e| { + let err_msg = format!("Failed to parse response JSON: {}", e); + error!("{}", err_msg); + err_msg + })?; + + debug!("Room play info response: {}", response_value); + + let live_status = response_value["data"]["live_status"].as_i64().unwrap_or(0); + if live_status == 0 { + return Err(RoomStateError::Offline.into()); + } + + let response: Response = serde_json::from_value(response_value)?; + + if response.code != 0 { + let err_msg = format!("Room play info request unsuccessful: {}", response.message); + warn!("{}", err_msg); + return Err(err_msg.into()); + } + + if response.data.live_status == 0 { + let err_msg = "Stream is not live"; + info!("{}", err_msg); + return Err(err_msg.into()); + } + + debug!("Successfully fetched room play info"); + Ok(response) + } +} diff --git a/src-tauri/src/parser/http_client.rs b/src-tauri/src/parser/http_client.rs index 2d517b5..b22515e 100644 --- a/src-tauri/src/parser/http_client.rs +++ b/src-tauri/src/parser/http_client.rs @@ -10,7 +10,7 @@ use crate::error::{LsarError, LsarResult}; #[derive(Clone)] pub struct HttpClient { - client: Client, + pub inner: Client, headers: HeaderMap, } @@ -27,7 +27,7 @@ impl HttpClient { ); let client = HttpClient { - client: Client::new(), + inner: Client::new(), headers, }; debug!("HttpClient instance created with default headers"); @@ -61,7 +61,7 @@ impl HttpClient { pub async fn get(&self, url: &str) -> LsarResult { info!("Sending GET request to: {}", url); - let response = self.send_request(self.client.get(url)).await?; + let response = self.send_request(self.inner.get(url)).await?; debug!("GET request successful, status: {}", response.status()); @@ -98,7 +98,7 @@ impl HttpClient { pub async fn get_json(&self, url: &str) -> LsarResult { info!("Sending GET request for JSON to: {}", url); - let response = self.send_request(self.client.get(url)).await?; + let response = self.send_request(self.inner.get(url)).await?; debug!( "GET request for JSON successful, status: {}", @@ -117,7 +117,7 @@ impl HttpClient { info!("Sending POST request with body to: {}", url); let response = self .send_request( - self.client + self.inner .post(url) .body(body.to_string()) .header(CONTENT_TYPE, "application/x-www-form-urlencoded"), @@ -140,7 +140,7 @@ impl HttpClient { body: &S, ) -> LsarResult { info!("Sending POST request with JSON body to: {}", url); - let response = self.send_request(self.client.post(url).json(body)).await?; + let response = self.send_request(self.inner.post(url).json(body)).await?; debug!("POST request successful, status: {}", response.status()); let json = response.json().await.map_err(|e| { diff --git a/src-tauri/src/parser/mod.rs b/src-tauri/src/parser/mod.rs index 1cc2fda..cdc15a0 100644 --- a/src-tauri/src/parser/mod.rs +++ b/src-tauri/src/parser/mod.rs @@ -1,3 +1,4 @@ +mod bilibili; mod douyin; mod douyu; mod http_client; @@ -6,6 +7,7 @@ mod time; use serde::Serialize; +pub use self::bilibili::parse_bilibili; pub use self::douyin::parse_douyin; pub use self::douyu::parse_douyu; pub use self::huya::parse_huya; diff --git a/src/parser/bilibili/index.ts b/src/parser/bilibili/index.ts index 7df24a0..73978b9 100644 --- a/src/parser/bilibili/index.ts +++ b/src/parser/bilibili/index.ts @@ -1,223 +1,26 @@ -import { debug, get, info } from "~/command"; -import { NOT_LIVE, platforms } from ".."; import LiveStreamParser from "../base"; - -interface CDNItem { - host: string; - extra: string; -} - -interface CodecItem { - accept_qn: number[]; - base_url: string; - current_qn: number; - url_info: CDNItem[]; -} - -interface FormatItem { - codec: CodecItem[]; - format_name: string; -} - -interface StreamItem { - format: FormatItem[]; -} - -interface Response { - code: number; - message: string; - data: { - live_status: 0 | 1; // 0 未播, 1 正在直播 - playurl_info: { - playurl: { - stream: StreamItem[]; - }; - }; - }; -} - -interface VerifyFailedResult { - code: -101; - message: string; - data: { - isLogin: false; - }; -} - -interface VerifySuccessResult { - code: 0; - message: string; - data: { - isLogin: true; - uname: string; - }; -} - -type VerifyResult = VerifyFailedResult | VerifySuccessResult; - -interface RoomInfo { - data: { - anchor_info: { - base_info: { - face: string; // 头像 - uname: string; - }; - }; - room_info: { - area_name: string; - title: string; - }; - }; -} - -const LOG_PREFIX = "bilibili"; -const BASE_URL = - "https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo?protocol=0,1&format=0,1,2&codec=0,1&qn=10000&platform=web&ptype=8&dolby=5&panorama=1&room_id="; -const VERIFY_URL = "https://api.bilibili.com/x/web-interface/nav"; +import { invoke } from "@tauri-apps/api/core"; class BilibiliParser extends LiveStreamParser { - private readonly pageURL: string = ""; - private readonly cookie: string; - + cookie: string; + url: string; constructor(cookie: string, roomID = 0, url = "") { - super(roomID, BASE_URL); - this.pageURL = url || platforms.bilibili.roomBaseURL + roomID; + super(roomID, ""); this.cookie = cookie; + this.url = url; } async parse(): Promise { - const username = await this.verifyCookie(); - if (username instanceof Error) { - return username; + try { + const result = await invoke("parse_bilibili", { + roomId: this.roomID, + cookie: this.cookie, + url: this.url || null, + }); + return result; + } catch (e) { + return Error(String(e)); } - - info(LOG_PREFIX, `验证成功,登录的用户:${username}`); - - const html = await this.fetchPageHTML(); - const pageInfo = await this.parsePageInfo(html); - - const roomInfo = await this.getRoomPlayInfo(); - if (roomInfo instanceof Error) { - return roomInfo; - } - - return this.parseRoomInfo(pageInfo, roomInfo); - } - - private async verifyCookie() { - debug(LOG_PREFIX, "验证 cookie"); - const { body: data } = await get(VERIFY_URL, { - cookie: this.cookie, - }); - if (data.code !== 0) { - return Error(data.message); - } - - return data.data.uname; - } - - private async getRoomInfo() { - const { body } = await get( - `https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom?room_id=${this.roomID}`, - { cookie: this.cookie }, - ); - const { - anchor_info: { - base_info: { uname }, - }, - room_info: { area_name, title }, - } = body.data; - - return { - title, - anchor: uname, - category: area_name, - }; - } - - private async getRoomPlayInfo() { - const { body } = await get(this.roomURL, { cookie: this.cookie }); - if (body.code !== 0) { - // code=0 仅代表请求成功, 不代表请求不合法, 也不代理直播状态 - return Error(body.message); - } - - return body.data.live_status === 0 ? NOT_LIVE : body; - } - - private async parsePageInfo(html: string) { - if (!this.roomID) { - this.roomID = this.parseRoomID(html); - } - - const roomInfo = await this.getRoomInfo(); - return roomInfo; - } - - private async fetchPageHTML(): Promise { - const { body: html } = await get(this.pageURL, { - Host: "live.bilibili.com", - "User-Agent": - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0", - Accept: - "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", - "Accept-Language": "zh-CN", - Connection: "keep-alive", - "Upgrade-Insecure-Requests": "1", - "Sec-Fetch-Dest": "document", - "Sec-Fetch-Mode": "navigate", - "Sec-Fetch-Site": "none", - "Sec-Fetch-User": "?1", - DNT: "1", - "Sec-GPC": "1", - }); - return html; - } - - private parseRoomInfo( - pageInfo: { - title: string; - anchor: string; - category: string; - }, - info: Response, - ): ParsedResult { - const links = this.parseLinks(info); - return { - ...pageInfo, - platform: "bilibili", - links, - roomID: this.roomID, - }; - } - - private parseLinks(info: Response): string[] { - const { - data: { - playurl_info: { - playurl: { stream }, - }, - }, - } = info; - - const links = stream.flatMap((s) => - s.format.flatMap((fmt) => - fmt.codec.flatMap((c) => - c.url_info.map((cdn) => cdn.host + c.base_url + cdn.extra), - ), - ), - ); - - return links; - } - - private parseRoomID(html: string) { - const findResult = - html.match(/"defaultRoomId":"(\d+)"/) || - html.match(/"roomid":(\d+)/) || - html.match(/"roomId":(\d+)/); - if (!findResult) throw Error("未找到房间 id"); - return Number(findResult[1]); } }