From 3757c4f9170c54147273bce5733324a7c20f5ae1 Mon Sep 17 00:00:00 2001 From: amtoaer Date: Thu, 21 Mar 2024 00:21:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=A3=80=E6=9F=A5=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=94=99=E8=AF=AF=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=BF=85=E8=A6=81?= =?UTF-8?q?=E7=9A=84=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 151 ++++++++++++++++++++++++++++------ Cargo.toml | 6 +- src/bilibili/analyzer.rs | 36 +++++--- src/bilibili/client.rs | 7 +- src/bilibili/credential.rs | 111 +++++++++++++++++++------ src/bilibili/favorite_list.rs | 5 +- src/bilibili/video.rs | 2 +- src/downloader.rs | 3 + 8 files changed, 254 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 00730fd..c2043fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -112,6 +112,15 @@ version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.15.4" @@ -203,6 +212,15 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -239,6 +257,7 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ + "block-buffer", "const-oid", "crypto-common", ] @@ -394,9 +413,9 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "h2" -version = "0.3.25" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" +checksum = "51ee2dd2e4f378392eeff5d51618cd9a63166a2513846bbc55f21cfacd9199d4" dependencies = [ "bytes", "fnv", @@ -437,9 +456,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "http" -version = "0.2.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -448,12 +467,24 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes", "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", "pin-project-lite", ] @@ -463,47 +494,60 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - [[package]] name = "hyper" -version = "0.14.28" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" dependencies = [ "bytes", "futures-channel", - "futures-core", "futures-util", "h2", "http", "http-body", "httparse", - "httpdate", "itoa", "pin-project-lite", - "socket2", + "smallvec", "tokio", - "tower-service", - "tracing", "want", ] [[package]] name = "hyper-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", + "http-body-util", "hyper", + "hyper-util", "native-tls", "tokio", "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", ] [[package]] @@ -810,6 +854,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -965,9 +1029,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.26" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bf93c4af7a8bb7d879d51cebe797356ff10ae8516ace542b5182d9dcac10b2" +checksum = "58b48d98d932f4ee75e541614d32a7f44c889b72bd9c2e04d95edd135989df88" dependencies = [ "base64", "bytes", @@ -979,8 +1043,10 @@ dependencies = [ "h2", "http", "http-body", + "http-body-util", "hyper", "hyper-tls", + "hyper-util", "ipnet", "js-sys", "log", @@ -1021,6 +1087,7 @@ dependencies = [ "pkcs1", "pkcs8", "rand_core", + "sha2", "signature", "spki", "subtle", @@ -1148,6 +1215,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -1386,6 +1464,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -1398,6 +1498,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-core", ] diff --git a/Cargo.toml b/Cargo.toml index fec5635..7017f16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,14 +6,14 @@ edition = "2021" [dependencies] serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0" -reqwest = { version = "0.11", features = ["json", "stream", "cookies"] } -cookie = { version = "0.18.0", features = ["percent-encode"]} +reqwest = { version = "0.12.0", features = ["json", "stream", "cookies"] } +cookie = { version = "0.18.0", features = ["percent-encode"] } tokio = { version = "1", features = ["full"] } strum = { version = "0.26", features = ["derive"] } async-stream = "0.3.5" futures-core = "0.3" futures-util = "0.3.30" -rsa = "0.9.6" +rsa = { version = "0.9.6", features = ["sha2"] } hex = "0.4.3" rand = "0.8.5" regex = "1.10.3" diff --git a/src/bilibili/analyzer.rs b/src/bilibili/analyzer.rs index 47f227f..83cc2a2 100644 --- a/src/bilibili/analyzer.rs +++ b/src/bilibili/analyzer.rs @@ -38,6 +38,7 @@ pub enum VideoCodecs { AV1, } +// 视频流的筛选偏好 pub struct FilterOption { pub video_max_quality: VideoQuality, pub video_min_quality: VideoQuality, @@ -50,6 +51,7 @@ pub struct FilterOption { pub no_hires: bool, } +// 上游项目中的五种流类型,不过目测应该只有 Flv、DashVideo、DashAudio 三种会被用到 #[derive(Debug, PartialEq, PartialOrd)] pub enum Stream { Flv(String), @@ -66,6 +68,7 @@ pub enum Stream { }, } +// 通用的获取流链接的方法,交由 Downloader 使用 impl Stream { pub fn url(&self) -> &str { match self { @@ -78,6 +81,9 @@ impl Stream { } } +// 用于获取视频流的最佳筛选结果,有两种可能: +// 1. 单个混合流,作为 Mixed 返回 +// 2. 视频、音频分离,作为 VideoAudio 返回,其中音频流可能不存在(对于无声视频,如 BV1J7411H7KQ) #[derive(Debug)] pub enum BestStream { VideoAudio { @@ -139,18 +145,23 @@ impl PageAnalyzer { let video_stream_quality = VideoQuality::from_repr(video_data["id"].as_u64().unwrap() as usize) .ok_or("invalid video stream quality")?; - if (video_stream_quality == VideoQuality::QualityHdr && filter_option.no_hdr) // NO HDR - || (video_stream_quality == VideoQuality::QualityDolby && filter_option.no_dolby_video) // NO DOLBY + if (video_stream_quality == VideoQuality::QualityHdr && filter_option.no_hdr) + || (video_stream_quality == VideoQuality::QualityDolby + && filter_option.no_dolby_video) || (video_stream_quality != VideoQuality::QualityDolby && video_stream_quality != VideoQuality::QualityHdr && (video_stream_quality < filter_option.video_min_quality || video_stream_quality > filter_option.video_max_quality)) - // NOT IN RANGE + // 此处过滤包含三种情况: + // 1. HDR 视频,但指定不需要 HDR + // 2. 杜比视界视频,但指定不需要杜比视界 + // 3. 视频质量不在指定范围内 { continue; } - let video_codecs = video_data["codecs"].as_str().unwrap(); + let video_codecs = video_data["codecs"].as_str().unwrap(); + // 从视频流的 codecs 字段中获取编码格式,此处并非精确匹配而是判断包含,比如 codecs 是 av1.42c01e,需要匹配为 av1 let video_codecs = vec![VideoCodecs::HEV, VideoCodecs::AVC, VideoCodecs::AV1] .into_iter() .find(|c| video_codecs.contains(c.to_string().as_str())); @@ -158,7 +169,6 @@ impl PageAnalyzer { let Some(video_codecs) = video_codecs else { continue; }; - if !filter_option.codecs.contains(&video_codecs) { continue; } @@ -188,6 +198,7 @@ impl PageAnalyzer { } } if !(filter_option.no_hires || flac_data["audio"].is_null()) { + // 允许 hires 且存在 flac 音频流才会进来 let flac_stream_url = flac_data["audio"]["baseUrl"].as_str().unwrap().to_string(); let flac_stream_quality = AudioQuality::from_repr(flac_data["audio"]["id"].as_u64().unwrap() as usize) @@ -198,6 +209,7 @@ impl PageAnalyzer { }); } if !(filter_option.no_dolby_audio || dolby_data["audio"].is_null()) { + // 同理,允许杜比音频且存在杜比音频流才会进来 let dolby_stream_data = dolby_data["audio"].as_array().and_then(|v| v.first()); if dolby_stream_data.is_some() { let dolby_stream_data = dolby_stream_data.unwrap(); @@ -217,13 +229,14 @@ impl PageAnalyzer { pub fn best_stream(&mut self, filter_option: &FilterOption) -> Result { let streams = self.streams(filter_option)?; if self.is_flv_stream() || self.is_html5_mp4_stream() || self.is_episode_try_mp4_stream() { - return Ok(BestStream::Mixed( - streams.into_iter().next().ok_or("no stream found")?, - )); + // 按照 streams 中的假设,符合这三种情况的流只有一个,直接取 + return Ok(BestStream::Mixed(streams.into_iter().next().unwrap())); } + // 将视频流和音频流拆分,分别做排序 let (mut video_streams, mut audio_streams): (Vec<_>, Vec<_>) = streams .into_iter() .partition(|s| matches!(s, Stream::DashVideo { .. })); + // 因为该处的排序与筛选选项有关,因此不能在外面实现 PartialOrd trait,只能在这里写闭包 video_streams.sort_by(|a, b| match (a, b) { ( Stream::DashVideo { @@ -252,13 +265,14 @@ impl PageAnalyzer { if a_quality != b_quality { return a_quality.partial_cmp(b_quality).unwrap(); } + // 如果视频质量相同,按照偏好的编码优先级排序 filter_option .codecs .iter() .position(|c| c == b_codecs) .cmp(&filter_option.codecs.iter().position(|c| c == a_codecs)) } - _ => std::cmp::Ordering::Equal, + _ => unreachable!(), }); audio_streams.sort_by(|a, b| match (a, b) { ( @@ -277,14 +291,14 @@ impl PageAnalyzer { } a_quality.partial_cmp(b_quality).unwrap() } - _ => std::cmp::Ordering::Equal, + _ => unreachable!(), }); if video_streams.is_empty() { return Err("no stream found".into()); } Ok(BestStream::VideoAudio { video: video_streams.remove(video_streams.len() - 1), - // audio stream may be empty, representing no audio for the video + // 音频流可能为空,因此直接使用 pop 返回 Option audio: audio_streams.pop(), }) } diff --git a/src/bilibili/client.rs b/src/bilibili/client.rs index c1f41bc..3e199ea 100644 --- a/src/bilibili/client.rs +++ b/src/bilibili/client.rs @@ -3,10 +3,12 @@ use reqwest::{header, Method}; use crate::bilibili::Credential; use crate::Result; +// 一个对 reqwest::Client 的简单封装,用于 Bilibili 请求 pub struct Client(reqwest::Client); impl Client { pub fn new() -> Self { + // 正常访问 api 所必须的 header,作为默认 header 添加到每个请求中 let mut headers = header::HeaderMap::new(); headers.insert( header::USER_AGENT, @@ -23,6 +25,7 @@ impl Client { ) } + // a wrapper of reqwest::Client::request to add credential to the request pub fn request( &self, method: Method, @@ -30,6 +33,7 @@ impl Client { credential: Option<&Credential>, ) -> reqwest::RequestBuilder { let mut req = self.0.request(method, url); + // 如果有 credential,会将其转换成 cookie 添加到请求的 header 中 if let Some(credential) = credential { req = req .header(header::COOKIE, format!("SESSDATA={}", credential.sessdata)) @@ -48,6 +52,7 @@ impl Client { } } +// clippy 建议实现 Default trait impl Default for Client { fn default() -> Self { Self::new() @@ -71,11 +76,9 @@ impl BiliClient { pub async fn check_refresh(&mut self) -> Result<()> { let Some(credential) = self.credential.as_mut() else { - // no credential, just ignore it return Ok(()); }; if credential.check(&self.client).await? { - // is valid, no need to refresh return Ok(()); } credential.refresh(&self.client).await diff --git a/src/bilibili/credential.rs b/src/bilibili/credential.rs index c7d5cc1..fee8df6 100644 --- a/src/bilibili/credential.rs +++ b/src/bilibili/credential.rs @@ -5,7 +5,8 @@ use cookie::Cookie; use regex::Regex; use reqwest::{header, Method}; use rsa::pkcs8::DecodePublicKey; -use rsa::{Pkcs1v15Encrypt, RsaPublicKey}; +use rsa::sha2::Sha256; +use rsa::{Oaep, RsaPublicKey}; use crate::bilibili::Client; use crate::Result; @@ -36,6 +37,7 @@ impl Credential { } } + /// 检查凭据是否有效 pub async fn check(&self, client: &Client) -> Result { let res = client .request( @@ -56,14 +58,13 @@ impl Credential { let correspond_path = Self::get_correspond_path(); let csrf = self.get_refresh_csrf(client, correspond_path).await?; let new_credential = self.get_new_credential(client, &csrf).await?; - self.sessdata = new_credential.sessdata; - self.bili_jct = new_credential.bili_jct; - self.dedeuserid = new_credential.dedeuserid; + self.confirm_refresh(client, &new_credential).await?; + *self = new_credential; Ok(()) } fn get_correspond_path() -> String { - // maybe as a static value + // 调用频率很低,让 key 在函数内部构造影响不大 let key = RsaPublicKey::from_public_key_pem( "-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDLgd2OAkcGVtoE3ThUREbio0Eg @@ -79,7 +80,7 @@ impl Credential { .as_millis(); let data = format!("refresh_{}", ts).into_bytes(); let mut rng = rand::rngs::OsRng; - let encrypted = key.encrypt(&mut rng, Pkcs1v15Encrypt, &data).unwrap(); + let encrypted = key.encrypt(&mut rng, Oaep::new::(), &data).unwrap(); hex::encode(encrypted) } @@ -94,13 +95,15 @@ impl Credential { .send() .await?; if !res.status().is_success() { - return Err("error get csrf".into()); + return match res.status().as_u16() { + 404 => Err("correspond path is wrong or expired".into()), + _ => Err("get csrf failed".into()), + }; } - let re = Regex::new("
(.+?)
").unwrap(); - if let Some(res) = re.find(&res.text().await?) { - return Ok(res.as_str().to_string()); - } - Err("error get csrf".into()) + regex_find( + r#"
(.+?)
"#, + res.text().await?.as_str(), + ) } async fn get_new_credential(&self, client: &Client, csrf: &str) -> Result { @@ -111,21 +114,24 @@ impl Credential { Some(self), ) .header(header::COOKIE, "Domain=.bilibili.com") - .json(&serde_json::json!({ - "csrf": self.bili_jct, - "refresh_csrf": csrf, - "refresh_token": self.ac_time_value, - "source": "main_web", - })) + .form(&[ + // 这里不是 json,而是 form data + ("csrf", self.bili_jct.as_str()), + ("refresh_csrf", csrf), + ("refresh_token", self.ac_time_value.as_str()), + ("source", "main_web"), + ]) .send() .await?; let set_cookie = res .headers() .get(header::SET_COOKIE) - .ok_or("error refresh credential")? - .to_str() - .unwrap(); - let mut credential = Credential::default(); + .ok_or("no set_cookie header")? + .to_str()?; + let mut credential = Credential { + buvid3: self.buvid3.clone(), + ..Default::default() + }; let required_cookies = HashSet::from(["SESSDATA", "bili_jct", "DedeUserID"]); let cookies: Vec = Cookie::split_parse_encoded(set_cookie) .filter(|x| { @@ -134,19 +140,76 @@ impl Credential { }) .map(|x| x.unwrap()) .collect(); + if cookies.len() != required_cookies.len() { + return Err("not all required cookies found".into()); + } for cookie in cookies { match cookie.name() { "SESSDATA" => credential.sessdata = cookie.value().to_string(), "bili_jct" => credential.bili_jct = cookie.value().to_string(), "DedeUserID" => credential.dedeuserid = cookie.value().to_string(), - _ => continue, + _ => unreachable!(), } } let json = res.json::().await?; if !json["data"]["refresh_token"].is_string() { - return Err("error refresh credential".into()); + return Err("refresh_token not found".into()); } credential.ac_time_value = json["data"]["refresh_token"].as_str().unwrap().to_string(); Ok(credential) } + + async fn confirm_refresh(&self, client: &Client, new_credential: &Credential) -> Result<()> { + let res = client + .request( + Method::POST, + "https://passport.bilibili.com/x/passport-login/web/confirm/refresh", + // 此处用的是新的凭证 + Some(new_credential), + ) + .form(&[ + ("csrf", new_credential.bili_jct.as_str()), + ("refresh_token", self.ac_time_value.as_str()), + ]) + .send() + .await? + .json::() + .await?; + if res["code"] != 0 { + return Err(format!("confirm refresh failed: {}", res["message"]).into()); + } + Ok(()) + } +} + +// 用指定的 pattern 正则表达式在 doc 中查找,返回第一个匹配的捕获组 +fn regex_find(pattern: &str, doc: &str) -> Result { + let re = Regex::new(pattern)?; + Ok(re + .captures(doc) + .ok_or("pattern not match")? + .get(1) + .unwrap() + .as_str() + .to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_and_find() { + let doc = r#" + + +
b0cc8411ded2f9db2cff2edb3123acac
+ + + "#; + assert_eq!( + regex_find(r#"
(.+?)
"#, doc).unwrap(), + "b0cc8411ded2f9db2cff2edb3123acac", + ); + } } diff --git a/src/bilibili/favorite_list.rs b/src/bilibili/favorite_list.rs index 0aa2865..04c3af1 100644 --- a/src/bilibili/favorite_list.rs +++ b/src/bilibili/favorite_list.rs @@ -81,6 +81,7 @@ impl FavoriteList { Ok(res) } + // 拿到收藏夹的所有权,返回一个收藏夹下的视频流 pub fn into_video_stream(self) -> impl Stream { stream! { let mut page = 1; @@ -88,7 +89,9 @@ impl FavoriteList { let Ok(mut videos) = self.get_videos(page).await else{ break; }; - let videos_info: Vec = serde_json::from_value(videos["data"]["medias"].take()).unwrap(); + let Ok(videos_info) = serde_json::from_value::>(videos["data"]["medias"].take()) else{ + break; + }; for video_info in videos_info.into_iter(){ yield video_info; } diff --git a/src/bilibili/video.rs b/src/bilibili/video.rs index 0198795..294af54 100644 --- a/src/bilibili/video.rs +++ b/src/bilibili/video.rs @@ -35,7 +35,7 @@ pub struct Page { #[serde(rename = "part")] name: String, #[serde(default = "String::new")] - first_frame: String, // may not exist + first_frame: String, // 可能不存在,默认填充为空 } impl Video { diff --git a/src/downloader.rs b/src/downloader.rs index 9670073..c79fc0f 100644 --- a/src/downloader.rs +++ b/src/downloader.rs @@ -12,6 +12,9 @@ pub struct Downloader { } impl Downloader { + // Downloader 使用带有默认 Header 的 Client 构建 + // 拿到 url 后下载文件不需要任何 cookie 作为身份凭证 + // 但如果不设置默认 Header,下载时会遇到 403 Forbidden 错误 pub fn new(client: Client) -> Self { Self { client } }