feat: 检查修复错误,添加必要的注释
This commit is contained in:
151
Cargo.lock
generated
151
Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<BestStream> {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<bool> {
|
||||
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::<Sha256>(), &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("<div id=\"1-name\">(.+?)</div>").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#"<div id="1-name">(.+?)</div>"#,
|
||||
res.text().await?.as_str(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn get_new_credential(&self, client: &Client, csrf: &str) -> Result<Credential> {
|
||||
@@ -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> = 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::<serde_json::Value>().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::<serde_json::Value>()
|
||||
.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<String> {
|
||||
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#"
|
||||
<html lang="zh-Hans">
|
||||
<body>
|
||||
<div id="1-name">b0cc8411ded2f9db2cff2edb3123acac</div>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
||||
assert_eq!(
|
||||
regex_find(r#"<div id="1-name">(.+?)</div>"#, doc).unwrap(),
|
||||
"b0cc8411ded2f9db2cff2edb3123acac",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ impl FavoriteList {
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
// 拿到收藏夹的所有权,返回一个收藏夹下的视频流
|
||||
pub fn into_video_stream(self) -> impl Stream<Item = VideoInfo> {
|
||||
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<VideoInfo> = serde_json::from_value(videos["data"]["medias"].take()).unwrap();
|
||||
let Ok(videos_info) = serde_json::from_value::<Vec<VideoInfo>>(videos["data"]["medias"].take()) else{
|
||||
break;
|
||||
};
|
||||
for video_info in videos_info.into_iter(){
|
||||
yield video_info;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user