feat: 实现下载,给出例子

This commit is contained in:
amtoaer
2024-03-19 00:04:32 +08:00
parent 210a72c9cf
commit f46a837533
10 changed files with 259 additions and 150 deletions
Generated
+24
View File
@@ -208,6 +208,12 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]]
name = "futures-io"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.30" version = "0.3.30"
@@ -238,8 +244,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-io",
"futures-macro", "futures-macro",
"futures-sink",
"futures-task", "futures-task",
"memchr",
"pin-project-lite", "pin-project-lite",
"pin-utils", "pin-utils",
"slab", "slab",
@@ -659,10 +668,12 @@ dependencies = [
"system-configuration", "system-configuration",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
"tokio-util",
"tower-service", "tower-service",
"url", "url",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-streams",
"web-sys", "web-sys",
"winreg", "winreg",
] ]
@@ -1113,6 +1124,19 @@ version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]]
name = "wasm-streams"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129"
dependencies = [
"futures-util",
"js-sys",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.69" version = "0.3.69"
+4 -1
View File
@@ -6,9 +6,12 @@ edition = "2021"
[dependencies] [dependencies]
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
reqwest = { version = "0.11", features = ["json"] } reqwest = { version = "0.11", features = ["json", "stream"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
strum = { version = "0.26", features = ["derive"] } strum = { version = "0.26", features = ["derive"] }
async-stream = "0.3.5" async-stream = "0.3.5"
futures-core = "0.3" futures-core = "0.3"
futures-util = "0.3.30" futures-util = "0.3.30"
[profile.release]
strip = true
+69 -86
View File
@@ -1,6 +1,6 @@
use std::rc::Rc; use std::rc::Rc;
use crate::bilibili::Result; use crate::Result;
pub struct PageAnalyzer { pub struct PageAnalyzer {
info: serde_json::Value, info: serde_json::Value,
@@ -38,17 +38,29 @@ pub enum VideoCodecs {
AV1, AV1,
} }
pub struct FilterOption {
pub video_max_quality: VideoQuality,
pub video_min_quality: VideoQuality,
pub audio_max_quality: AudioQuality,
pub audio_min_quality: AudioQuality,
pub codecs: Rc<Vec<VideoCodecs>>,
pub no_dolby_video: bool,
pub no_dolby_audio: bool,
pub no_hdr: bool,
pub no_hires: bool,
}
#[derive(Debug, PartialEq, PartialOrd)] #[derive(Debug, PartialEq, PartialOrd)]
pub enum Stream { pub enum Stream {
FlvStream(String), Flv(String),
Html5Mp4Stream(String), Html5Mp4(String),
EpositeTryMp4Stream(String), EpositeTryMp4(String),
DashVideoStream { DashVideo {
url: String, url: String,
quality: VideoQuality, quality: VideoQuality,
codecs: VideoCodecs, codecs: VideoCodecs,
}, },
DashAudioStream { DashAudio {
url: String, url: String,
quality: AudioQuality, quality: AudioQuality,
}, },
@@ -57,19 +69,22 @@ pub enum Stream {
impl Stream { impl Stream {
pub fn url(&self) -> &str { pub fn url(&self) -> &str {
match self { match self {
Self::FlvStream(url) => url, Self::Flv(url) => url,
Self::Html5Mp4Stream(url) => url, Self::Html5Mp4(url) => url,
Self::EpositeTryMp4Stream(url) => url, Self::EpositeTryMp4(url) => url,
Self::DashVideoStream { url, .. } => url, Self::DashVideo { url, .. } => url,
Self::DashAudioStream { url, .. } => url, Self::DashAudio { url, .. } => url,
} }
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub enum BestStream { pub enum BestStream {
VideoAudioStream { video: Stream, audio: Stream }, VideoAudio {
MixedStream(Stream), video: Stream,
audio: Option<Stream>,
},
Mixed(Stream),
} }
impl PageAnalyzer { impl PageAnalyzer {
@@ -98,30 +113,19 @@ impl PageAnalyzer {
&& !(self.info["is_html5"].is_boolean() && self.info["is_html5"].as_bool().unwrap()) && !(self.info["is_html5"].is_boolean() && self.info["is_html5"].as_bool().unwrap())
} }
fn streams( fn streams(&mut self, filter_option: &FilterOption) -> Result<Vec<Stream>> {
&mut self,
video_max_quality: VideoQuality,
video_min_quality: VideoQuality,
audio_max_quality: AudioQuality,
audio_min_quality: AudioQuality,
codecs: Rc<Vec<VideoCodecs>>,
no_dolby_video: bool,
no_dolby_audio: bool,
no_hdr: bool,
no_hires: bool,
) -> Result<Vec<Stream>> {
if self.is_flv_stream() { if self.is_flv_stream() {
return Ok(vec![Stream::FlvStream( return Ok(vec![Stream::Flv(
self.info["durl"][0]["url"].as_str().unwrap().to_string(), self.info["durl"][0]["url"].as_str().unwrap().to_string(),
)]); )]);
} }
if self.is_html5_mp4_stream() { if self.is_html5_mp4_stream() {
return Ok(vec![Stream::Html5Mp4Stream( return Ok(vec![Stream::Html5Mp4(
self.info["durl"][0]["url"].as_str().unwrap().to_string(), self.info["durl"][0]["url"].as_str().unwrap().to_string(),
)]); )]);
} }
if self.is_episode_try_mp4_stream() { if self.is_episode_try_mp4_stream() {
return Ok(vec![Stream::EpositeTryMp4Stream( return Ok(vec![Stream::EpositeTryMp4(
self.info["durl"][0]["url"].as_str().unwrap().to_string(), self.info["durl"][0]["url"].as_str().unwrap().to_string(),
)]); )]);
} }
@@ -134,13 +138,13 @@ impl PageAnalyzer {
let video_stream_url = video_data["baseUrl"].as_str().unwrap().to_string(); let video_stream_url = video_data["baseUrl"].as_str().unwrap().to_string();
let video_stream_quality = let video_stream_quality =
VideoQuality::from_repr(video_data["id"].as_u64().unwrap() as usize) VideoQuality::from_repr(video_data["id"].as_u64().unwrap() as usize)
.ok_or_else(|| "invalid video stream quality")?; .ok_or("invalid video stream quality")?;
if (video_stream_quality == VideoQuality::QualityHdr && no_hdr) // NO HDR if (video_stream_quality == VideoQuality::QualityHdr && filter_option.no_hdr) // NO HDR
|| (video_stream_quality == VideoQuality::QualityDolby && no_dolby_video) // NO DOLBY || (video_stream_quality == VideoQuality::QualityDolby && filter_option.no_dolby_video) // NO DOLBY
|| (video_stream_quality != VideoQuality::QualityDolby || (video_stream_quality != VideoQuality::QualityDolby
&& video_stream_quality != VideoQuality::QualityHdr && video_stream_quality != VideoQuality::QualityHdr
&& (video_stream_quality < video_min_quality && (video_stream_quality < filter_option.video_min_quality
|| video_stream_quality > video_max_quality)) || video_stream_quality > filter_option.video_max_quality))
// NOT IN RANGE // NOT IN RANGE
{ {
continue; continue;
@@ -149,17 +153,16 @@ impl PageAnalyzer {
let video_codecs = vec![VideoCodecs::HEV, VideoCodecs::AVC, VideoCodecs::AV1] let video_codecs = vec![VideoCodecs::HEV, VideoCodecs::AVC, VideoCodecs::AV1]
.into_iter() .into_iter()
.filter(|c| video_codecs.contains(c.to_string().as_str())) .find(|c| video_codecs.contains(c.to_string().as_str()));
.next();
let Some(video_codecs) = video_codecs else { let Some(video_codecs) = video_codecs else {
continue; continue;
}; };
if !codecs.contains(&video_codecs) { if !filter_option.codecs.contains(&video_codecs) {
continue; continue;
} }
streams.push(Stream::DashVideoStream { streams.push(Stream::DashVideo {
url: video_stream_url, url: video_stream_url,
quality: video_stream_quality, quality: video_stream_quality,
codecs: video_codecs, codecs: video_codecs,
@@ -173,36 +176,36 @@ impl PageAnalyzer {
let Some(audio_stream_quality) = audio_stream_quality else { let Some(audio_stream_quality) = audio_stream_quality else {
continue; continue;
}; };
if audio_stream_quality > audio_max_quality if audio_stream_quality > filter_option.audio_max_quality
|| audio_stream_quality < audio_min_quality || audio_stream_quality < filter_option.audio_min_quality
{ {
continue; continue;
} }
streams.push(Stream::DashAudioStream { streams.push(Stream::DashAudio {
url: audio_stream_url, url: audio_stream_url,
quality: audio_stream_quality, quality: audio_stream_quality,
}); });
} }
} }
if !(no_hires || flac_data["audio"].is_null()) { if !(filter_option.no_hires || flac_data["audio"].is_null()) {
let flac_stream_url = flac_data["audio"]["baseUrl"].as_str().unwrap().to_string(); let flac_stream_url = flac_data["audio"]["baseUrl"].as_str().unwrap().to_string();
let flac_stream_quality = let flac_stream_quality =
AudioQuality::from_repr(flac_data["audio"]["id"].as_u64().unwrap() as usize) AudioQuality::from_repr(flac_data["audio"]["id"].as_u64().unwrap() as usize)
.unwrap(); .unwrap();
streams.push(Stream::DashAudioStream { streams.push(Stream::DashAudio {
url: flac_stream_url, url: flac_stream_url,
quality: flac_stream_quality, quality: flac_stream_quality,
}); });
} }
if !(no_dolby_audio || dolby_data["audio"].is_null()) { if !(filter_option.no_dolby_audio || dolby_data["audio"].is_null()) {
let dolby_stream_data = dolby_data["audio"].as_array().and_then(|v| v.get(0)); let dolby_stream_data = dolby_data["audio"].as_array().and_then(|v| v.first());
if dolby_stream_data.is_some() { if dolby_stream_data.is_some() {
let dolby_stream_data = dolby_stream_data.unwrap(); let dolby_stream_data = dolby_stream_data.unwrap();
let dolby_stream_url = dolby_stream_data["baseUrl"].as_str().unwrap().to_string(); let dolby_stream_url = dolby_stream_data["baseUrl"].as_str().unwrap().to_string();
let dolby_stream_quality = let dolby_stream_quality =
AudioQuality::from_repr(dolby_stream_data["id"].as_u64().unwrap() as usize) AudioQuality::from_repr(dolby_stream_data["id"].as_u64().unwrap() as usize)
.unwrap(); .unwrap();
streams.push(Stream::DashAudioStream { streams.push(Stream::DashAudio {
url: dolby_stream_url, url: dolby_stream_url,
quality: dolby_stream_quality, quality: dolby_stream_quality,
}); });
@@ -211,98 +214,78 @@ impl PageAnalyzer {
Ok(streams) Ok(streams)
} }
pub fn best_stream( pub fn best_stream(&mut self, filter_option: &FilterOption) -> Result<BestStream> {
&mut self, let streams = self.streams(filter_option)?;
video_max_quality: VideoQuality,
video_min_quality: VideoQuality,
audio_max_quality: AudioQuality,
audio_min_quality: AudioQuality,
codecs: Vec<VideoCodecs>,
no_dolby_video: bool,
no_dolby_audio: bool,
no_hdr: bool,
no_hires: bool,
) -> Result<BestStream> {
let codecs = Rc::new(codecs);
let streams = dbg!(self.streams(
video_max_quality,
video_min_quality,
audio_max_quality,
audio_min_quality,
codecs.clone(),
no_dolby_video,
no_dolby_audio,
no_hdr,
no_hires
))?;
if self.is_flv_stream() || self.is_html5_mp4_stream() || self.is_episode_try_mp4_stream() { if self.is_flv_stream() || self.is_html5_mp4_stream() || self.is_episode_try_mp4_stream() {
return Ok(BestStream::MixedStream( return Ok(BestStream::Mixed(
streams.into_iter().next().ok_or("no stream found")?, streams.into_iter().next().ok_or("no stream found")?,
)); ));
} }
let (mut video_streams, mut audio_streams): (Vec<_>, Vec<_>) = streams let (mut video_streams, mut audio_streams): (Vec<_>, Vec<_>) = streams
.into_iter() .into_iter()
.partition(|s| matches!(s, Stream::DashVideoStream { .. })); .partition(|s| matches!(s, Stream::DashVideo { .. }));
video_streams.sort_by(|a, b| match (a, b) { video_streams.sort_by(|a, b| match (a, b) {
( (
Stream::DashVideoStream { Stream::DashVideo {
quality: a_quality, quality: a_quality,
codecs: a_codecs, codecs: a_codecs,
.. ..
}, },
Stream::DashVideoStream { Stream::DashVideo {
quality: b_quality, quality: b_quality,
codecs: b_codecs, codecs: b_codecs,
.. ..
}, },
) => { ) => {
if a_quality == &VideoQuality::QualityDolby && !no_dolby_video { if a_quality == &VideoQuality::QualityDolby && !filter_option.no_dolby_video {
return std::cmp::Ordering::Greater; return std::cmp::Ordering::Greater;
} }
if b_quality == &VideoQuality::QualityDolby && !no_dolby_video { if b_quality == &VideoQuality::QualityDolby && !filter_option.no_dolby_video {
return std::cmp::Ordering::Less; return std::cmp::Ordering::Less;
} }
if a_quality == &VideoQuality::QualityHdr && !no_hdr { if a_quality == &VideoQuality::QualityHdr && !filter_option.no_hdr {
return std::cmp::Ordering::Greater; return std::cmp::Ordering::Greater;
} }
if b_quality == &VideoQuality::QualityHdr && !no_hdr { if b_quality == &VideoQuality::QualityHdr && !filter_option.no_hdr {
return std::cmp::Ordering::Less; return std::cmp::Ordering::Less;
} }
if a_quality != b_quality { if a_quality != b_quality {
return a_quality.partial_cmp(b_quality).unwrap(); return a_quality.partial_cmp(b_quality).unwrap();
} }
codecs filter_option
.codecs
.iter() .iter()
.position(|c| c == b_codecs) .position(|c| c == b_codecs)
.cmp(&codecs.iter().position(|c| c == a_codecs)) .cmp(&filter_option.codecs.iter().position(|c| c == a_codecs))
} }
_ => std::cmp::Ordering::Equal, _ => std::cmp::Ordering::Equal,
}); });
audio_streams.sort_by(|a, b| match (a, b) { audio_streams.sort_by(|a, b| match (a, b) {
( (
Stream::DashAudioStream { Stream::DashAudio {
quality: a_quality, .. quality: a_quality, ..
}, },
Stream::DashAudioStream { Stream::DashAudio {
quality: b_quality, .. quality: b_quality, ..
}, },
) => { ) => {
if a_quality == &AudioQuality::QualityDolby && !no_dolby_audio { if a_quality == &AudioQuality::QualityDolby && !filter_option.no_dolby_audio {
return std::cmp::Ordering::Greater; return std::cmp::Ordering::Greater;
} }
if b_quality == &AudioQuality::QualityDolby && !no_dolby_audio { if b_quality == &AudioQuality::QualityDolby && !filter_option.no_dolby_audio {
return std::cmp::Ordering::Less; return std::cmp::Ordering::Less;
} }
a_quality.partial_cmp(b_quality).unwrap() a_quality.partial_cmp(b_quality).unwrap()
} }
_ => std::cmp::Ordering::Equal, _ => std::cmp::Ordering::Equal,
}); });
if video_streams.is_empty() || audio_streams.is_empty() { if video_streams.is_empty() {
return Err("no stream found".into()); return Err("no stream found".into());
} }
Ok(BestStream::VideoAudioStream { Ok(BestStream::VideoAudio {
video: video_streams.remove(video_streams.len() - 1), video: video_streams.remove(video_streams.len() - 1),
audio: audio_streams.remove(audio_streams.len() - 1), // audio stream may be empty, representing no audio for the video
audio: audio_streams.pop(),
}) })
} }
} }
+24 -15
View File
@@ -1,4 +1,4 @@
use reqwest::Method; use reqwest::{header, Method};
pub struct Credential { pub struct Credential {
sessdata: String, sessdata: String,
@@ -26,6 +26,21 @@ impl Credential {
} }
} }
pub fn client_with_header() -> reqwest::Client {
let mut headers = header::HeaderMap::new();
headers.insert(
header::USER_AGENT,
header::HeaderValue::from_static("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.54"));
headers.insert(
header::REFERER,
header::HeaderValue::from_static("https://www.bilibili.com"),
);
reqwest::Client::builder()
.default_headers(headers)
.build()
.unwrap()
}
pub struct BiliClient { pub struct BiliClient {
credential: Option<Credential>, credential: Option<Credential>,
client: reqwest::Client, client: reqwest::Client,
@@ -34,34 +49,28 @@ pub struct BiliClient {
impl BiliClient { impl BiliClient {
pub fn anonymous() -> Self { pub fn anonymous() -> Self {
let credential = None; let credential = None;
let client = reqwest::Client::new(); let client = client_with_header();
Self { credential, client } Self { credential, client }
} }
pub fn authenticated(credential: Credential) -> Self { pub fn authenticated(credential: Credential) -> Self {
let credential = Some(credential); let credential = Some(credential);
let client = reqwest::Client::new(); let client = client_with_header();
Self { credential, client } Self { credential, client }
} }
fn set_header(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder { fn set_header(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
let req =req.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.54") let Some(credential) = &self.credential else {
.header("Referer", "https://www.bilibili.com"); return req;
if let Some(credential) = &self.credential { };
return req.header("cookie", format!("SESSDATA={}", credential.sessdata)) req.header("cookie", format!("SESSDATA={}", credential.sessdata))
.header("cookie", format!("bili_jct={}", credential.bili_jct)) .header("cookie", format!("bili_jct={}", credential.bili_jct))
.header("cookie", format!("buvid3={}", credential.buvid3)) .header("cookie", format!("buvid3={}", credential.buvid3))
.header( .header("cookie", format!("DedeUserID={}", credential.dedeuserid))
"cookie",
format!("DedeUserID={}", credential.dedeuserid),
)
.header( .header(
"cookie", "cookie",
format!("ac_time_value={}", credential.ac_time_value), format!("ac_time_value={}", credential.ac_time_value),
).header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.54") )
.header("Referer", "https://www.bilibili.com");
}
req
} }
pub fn request(&self, method: Method, url: &str) -> reqwest::RequestBuilder { pub fn request(&self, method: Method, url: &str) -> reqwest::RequestBuilder {
+9 -8
View File
@@ -4,7 +4,8 @@ use async_stream::stream;
use futures_core::stream::Stream; use futures_core::stream::Stream;
use serde_json::Value; use serde_json::Value;
use crate::bilibili::{BiliClient, Result}; use crate::bilibili::BiliClient;
use crate::Result;
pub struct FavoriteList { pub struct FavoriteList {
client: Rc<BiliClient>, client: Rc<BiliClient>,
fid: String, fid: String,
@@ -44,7 +45,7 @@ impl FavoriteList {
.client .client
.request( .request(
reqwest::Method::GET, reqwest::Method::GET,
&"https://api.bilibili.com/x/v3/fav/folder/info", "https://api.bilibili.com/x/v3/fav/folder/info",
) )
.query(&[("media_id", &self.fid)]) .query(&[("media_id", &self.fid)])
.send() .send()
@@ -59,15 +60,15 @@ impl FavoriteList {
.client .client
.request( .request(
reqwest::Method::GET, reqwest::Method::GET,
&"https://api.bilibili.com/x/v3/fav/resource/list", "https://api.bilibili.com/x/v3/fav/resource/list",
) )
.query(&[ .query(&[
("media_id", &self.fid), ("media_id", self.fid.as_str()),
("pn", &page.to_string()), ("pn", &page.to_string()),
("ps", &"20".to_string()), ("ps", "20"),
("order", &"mtime".to_string()), ("order", "mtime"),
("type", &"0".to_string()), ("type", "0"),
("tid", &"0".to_string()), ("tid", "0"),
]) ])
.send() .send()
.await? .await?
+4 -6
View File
@@ -3,11 +3,9 @@ mod client;
mod favorite_list; mod favorite_list;
mod video; mod video;
use std::error; pub use analyzer::{
AudioQuality, BestStream, FilterOption, PageAnalyzer, VideoCodecs, VideoQuality,
type Result<T> = std::result::Result<T, Box<dyn error::Error>>; };
pub use client::{client_with_header, BiliClient, Credential};
pub use analyzer::{AudioQuality, PageAnalyzer, VideoCodecs, VideoQuality};
pub use client::{BiliClient, Credential};
pub use favorite_list::FavoriteList; pub use favorite_list::FavoriteList;
pub use video::Video; pub use video::Video;
+7 -10
View File
@@ -4,7 +4,7 @@ use reqwest::Method;
use crate::bilibili::analyzer::PageAnalyzer; use crate::bilibili::analyzer::PageAnalyzer;
use crate::bilibili::client::BiliClient; use crate::bilibili::client::BiliClient;
use crate::bilibili::Result; use crate::Result;
static MASK_CODE: u64 = 2251799813685247; static MASK_CODE: u64 = 2251799813685247;
static XOR_CODE: u64 = 23442827791579; static XOR_CODE: u64 = 23442827791579;
@@ -46,7 +46,7 @@ impl Video {
pub async fn get_pages(&self) -> Result<Vec<Page>> { pub async fn get_pages(&self) -> Result<Vec<Page>> {
let mut res = self let mut res = self
.client .client
.request(Method::GET, &"https://api.bilibili.com/x/player/pagelist") .request(Method::GET, "https://api.bilibili.com/x/player/pagelist")
.query(&[("aid", &self.aid), ("bvid", &self.bvid)]) .query(&[("aid", &self.aid), ("bvid", &self.bvid)])
.send() .send()
.await? .await?
@@ -60,7 +60,7 @@ impl Video {
.client .client
.request( .request(
Method::GET, Method::GET,
&"https://api.bilibili.com/x/web-interface/view/detail/tag", "https://api.bilibili.com/x/web-interface/view/detail/tag",
) )
.query(&[("aid", &self.aid), ("bvid", &self.bvid)]) .query(&[("aid", &self.aid), ("bvid", &self.bvid)])
.send() .send()
@@ -73,10 +73,7 @@ impl Video {
pub async fn get_page_analyzer(&self, page: &Page) -> Result<PageAnalyzer> { pub async fn get_page_analyzer(&self, page: &Page) -> Result<PageAnalyzer> {
let mut res = self let mut res = self
.client .client
.request( .request(Method::GET, "https://api.bilibili.com/x/player/wbi/playurl")
Method::GET,
&"https://api.bilibili.com/x/player/wbi/playurl",
)
.query(&[ .query(&[
("avid", self.aid.as_str()), ("avid", self.aid.as_str()),
("cid", page.cid.to_string().as_str()), ("cid", page.cid.to_string().as_str()),
@@ -101,11 +98,11 @@ fn bvid_to_aid(bvid: &str) -> u64 {
(bvid[3], bvid[9]) = (bvid[9], bvid[3]); (bvid[3], bvid[9]) = (bvid[9], bvid[3]);
(bvid[4], bvid[7]) = (bvid[7], bvid[4]); (bvid[4], bvid[7]) = (bvid[7], bvid[4]);
let mut tmp = 0u64; let mut tmp = 0u64;
for i in 3..bvid.len() { for char in bvid.into_iter().skip(3) {
let idx = DATA.iter().position(|&x| x == bvid[i]).unwrap(); let idx = DATA.iter().position(|&x| x == char).unwrap();
tmp = tmp * BASE + idx as u64; tmp = tmp * BASE + idx as u64;
} }
return (tmp & MASK_CODE) ^ XOR_CODE; (tmp & MASK_CODE) ^ XOR_CODE
} }
#[cfg(test)] #[cfg(test)]
+65
View File
@@ -0,0 +1,65 @@
use std::path::Path;
use futures_util::StreamExt;
use tokio::fs::{self, File};
use tokio::io;
use crate::bilibili::client_with_header;
use crate::Result;
pub struct Downloader {
client: reqwest::Client,
}
impl Downloader {
pub fn new(client: reqwest::Client) -> Self {
Self { client }
}
pub async fn fetch(&self, url: &str, path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await?;
}
// must be a new file
let mut file = File::create(path).await?;
let mut res = self.client.get(url).send().await?.bytes_stream();
while let Some(item) = res.next().await {
io::copy(&mut item?.as_ref(), &mut file).await?;
}
Ok(())
}
pub async fn merge(
&self,
video_path: &Path,
audio_path: &Path,
output_path: &Path,
) -> Result<()> {
let output = tokio::process::Command::new("ffmpeg")
.args([
"-i",
video_path.to_str().unwrap(),
"-i",
audio_path.to_str().unwrap(),
"-c",
"copy",
output_path.to_str().unwrap(),
])
.output()
.await?;
if !output.status.success() {
return match String::from_utf8(output.stderr) {
Ok(err) => Err(err.into()),
_ => Err("ffmpeg error".into()),
};
}
let _ = fs::remove_file(video_path).await;
let _ = fs::remove_file(audio_path).await;
Ok(())
}
}
impl Default for Downloader {
fn default() -> Self {
Self::new(client_with_header())
}
}
+5
View File
@@ -1 +1,6 @@
use std::error;
pub mod bilibili; pub mod bilibili;
pub mod downloader;
type Result<T> = std::result::Result<T, Box<dyn error::Error>>;
+48 -24
View File
@@ -1,8 +1,11 @@
use std::path::Path;
use std::rc::Rc; use std::rc::Rc;
use bili_sync::bilibili::{ use bili_sync::bilibili::{
AudioQuality, BiliClient, FavoriteList, Video, VideoCodecs, VideoQuality, AudioQuality, BestStream, BiliClient, FavoriteList, FilterOption, Video, VideoCodecs,
VideoQuality,
}; };
use bili_sync::downloader::Downloader;
use futures_util::{pin_mut, StreamExt}; use futures_util::{pin_mut, StreamExt};
#[tokio::main] #[tokio::main]
@@ -10,36 +13,57 @@ async fn main() {
let bili_client = Rc::new(BiliClient::anonymous()); let bili_client = Rc::new(BiliClient::anonymous());
let favorite_list = FavoriteList::new(bili_client.clone(), "52642258".to_string()); let favorite_list = FavoriteList::new(bili_client.clone(), "52642258".to_string());
dbg!(favorite_list.get_info().await.unwrap()); dbg!(favorite_list.get_info().await.unwrap());
let video_stream = favorite_list.into_video_stream(); let video_stream = favorite_list.into_video_stream();
// from doc: https://docs.rs/async-stream/latest/async_stream/ // from doc: https://docs.rs/async-stream/latest/async_stream/
pin_mut!(video_stream); pin_mut!(video_stream);
let mut count = 3;
let mut third_video = None; let third_video_info = dbg!(video_stream.skip(2).next().await.unwrap());
while let Some(mut video) = video_stream.next().await { let third_video = Video::new(bili_client.clone(), third_video_info.bvid);
count -= 1;
video = dbg!(video);
if count <= 0 {
third_video = Some(video);
break;
}
}
let third_video = Video::new(bili_client.clone(), third_video.unwrap().bvid);
dbg!(third_video.get_tags().await.unwrap()); dbg!(third_video.get_tags().await.unwrap());
let pages = dbg!(third_video.get_pages().await.unwrap()); let pages = dbg!(third_video.get_pages().await.unwrap());
dbg!(third_video let best_stream = dbg!(third_video
.get_page_analyzer(&pages[0]) .get_page_analyzer(&pages[0])
.await .await
.unwrap() .unwrap()
.best_stream( .best_stream(&FilterOption {
VideoQuality::QualityDolby, video_max_quality: VideoQuality::QualityDolby,
VideoQuality::Quality360p, video_min_quality: VideoQuality::Quality360p,
AudioQuality::QualityDolby, audio_max_quality: AudioQuality::QualityDolby,
AudioQuality::Quality64k, audio_min_quality: AudioQuality::Quality64k,
vec![VideoCodecs::HEV, VideoCodecs::AVC], codecs: Rc::new(vec![VideoCodecs::HEV, VideoCodecs::AVC]),
false, no_dolby_video: false,
false, no_dolby_audio: false,
false, no_hdr: false,
false, no_hires: false,
)) }))
.unwrap();
let downloader = Downloader::default();
let base = Path::new("./");
let output_path = base.join(format!("{}.mp4", third_video_info.title));
match best_stream {
BestStream::Mixed(stream) => {
let url = dbg!(stream.url());
downloader.fetch(url, &output_path).await.unwrap();
}
BestStream::VideoAudio { video, audio } => {
let url = dbg!(video.url());
let Some(audio) = audio else {
downloader.fetch(url, &output_path).await.unwrap();
return;
};
let video_path = base.join(format!("{}_video_tmp", third_video_info.title));
downloader.fetch(url, &video_path).await.unwrap();
let url = dbg!(audio.url());
let audio_path = base.join(format!("{}_audio_tmp", third_video_info.title));
downloader.fetch(url, &audio_path).await.unwrap();
downloader
.merge(&video_path, &audio_path, &output_path)
.await
.unwrap(); .unwrap();
} }
}
}