mirror of
https://github.com/amtoaer/bili-sync.git
synced 2026-09-06 07:57:12 +08:00
feat: 实现下载,给出例子
This commit is contained in:
Generated
+24
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)]
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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();
|
.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user