first successful download
This commit is contained in:
parent
a6041a013b
commit
beb1177a11
16 changed files with 4076 additions and 121 deletions
|
|
@ -14,19 +14,8 @@ use reqwest::Method;
|
|||
use serde::Serialize;
|
||||
use url::Url;
|
||||
|
||||
use super::{
|
||||
response,
|
||||
ClientType, ContextYT, RustyTube, YTClient,
|
||||
};
|
||||
use crate::{
|
||||
client::response::player,
|
||||
deobfuscate::Deobfuscator,
|
||||
model::{
|
||||
AudioCodec, AudioStream, PlayerData, Subtitle, Thumbnail, VideoCodec, VideoInfo,
|
||||
VideoStream,
|
||||
},
|
||||
util,
|
||||
};
|
||||
use super::{response, ClientType, ContextYT, RustyTube, YTClient};
|
||||
use crate::{client::response::player, deobfuscate::Deobfuscator, model::*, util};
|
||||
|
||||
// REQUEST
|
||||
|
||||
|
|
@ -193,6 +182,8 @@ fn map_video_stream(
|
|||
deobf: &Deobfuscator,
|
||||
last_nsig: &mut [String; 2],
|
||||
) -> Option<VideoStream> {
|
||||
let (mtype, codecs) = some_or_bail!(parse_mime(&f.mime_type), None);
|
||||
|
||||
Some(VideoStream {
|
||||
url: some_or_bail!(map_url(f, deobf, last_nsig), None),
|
||||
itag: f.itag,
|
||||
|
|
@ -208,7 +199,8 @@ fn map_video_stream(
|
|||
hdr: f.color_info.clone().unwrap_or_default().primaries
|
||||
== player::Primaries::ColorPrimariesBt2020,
|
||||
mime: f.mime_type.to_owned(),
|
||||
codec: get_video_codec(&f.mime_type),
|
||||
format: some_or_bail!(get_video_format(mtype), None),
|
||||
codec: get_video_codec(codecs),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -217,6 +209,8 @@ fn map_audio_stream(
|
|||
deobf: &Deobfuscator,
|
||||
last_nsig: &mut [String; 2],
|
||||
) -> Option<AudioStream> {
|
||||
let (mtype, codecs) = some_or_bail!(parse_mime(&f.mime_type), None);
|
||||
|
||||
Some(AudioStream {
|
||||
url: some_or_bail!(map_url(f, deobf, last_nsig), None),
|
||||
itag: f.itag,
|
||||
|
|
@ -226,25 +220,38 @@ fn map_audio_stream(
|
|||
index_range: f.index_range.to_owned(),
|
||||
init_range: f.init_range.to_owned(),
|
||||
mime: f.mime_type.to_owned(),
|
||||
codec: get_audio_codec(&f.mime_type),
|
||||
format: some_or_bail!(get_audio_format(mtype), None),
|
||||
codec: get_audio_codec(codecs),
|
||||
})
|
||||
}
|
||||
|
||||
fn codecs_from_mime(mime: &str) -> Vec<&str> {
|
||||
fn parse_mime(mime: &str) -> Option<(&str, Vec<&str>)> {
|
||||
static PATTERN: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"(\w+/\w+);\scodecs="([a-zA-Z-0-9.,\s]*)""#).unwrap());
|
||||
|
||||
let captures = some_or_bail!(PATTERN.captures(&mime).ok().flatten(), vec![]);
|
||||
captures
|
||||
.get(2)
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.split(", ")
|
||||
.collect::<Vec<&str>>()
|
||||
let captures = some_or_bail!(PATTERN.captures(&mime).ok().flatten(), None);
|
||||
Some((
|
||||
captures.get(1).unwrap().as_str(),
|
||||
captures
|
||||
.get(2)
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.split(", ")
|
||||
.collect::<Vec<&str>>(),
|
||||
))
|
||||
}
|
||||
|
||||
fn get_video_codec(mime: &str) -> VideoCodec {
|
||||
for codec in codecs_from_mime(mime) {
|
||||
fn get_video_format(mtype: &str) -> Option<VideoFormat> {
|
||||
match mtype {
|
||||
"video/3gpp" => Some(VideoFormat::ThreeGp),
|
||||
"video/mp4" => Some(VideoFormat::Mp4),
|
||||
"video/webm" => Some(VideoFormat::Webm),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_video_codec(codecs: Vec<&str>) -> VideoCodec {
|
||||
for codec in codecs {
|
||||
if codec.starts_with("avc1") {
|
||||
return VideoCodec::Avc1;
|
||||
} else if codec.starts_with("vp9") || codec.starts_with("vp09") {
|
||||
|
|
@ -258,8 +265,16 @@ fn get_video_codec(mime: &str) -> VideoCodec {
|
|||
VideoCodec::Unknown
|
||||
}
|
||||
|
||||
fn get_audio_codec(mime: &str) -> AudioCodec {
|
||||
for codec in codecs_from_mime(mime) {
|
||||
fn get_audio_format(mtype: &str) -> Option<AudioFormat> {
|
||||
match mtype {
|
||||
"audio/mp4" => Some(AudioFormat::M4a),
|
||||
"audio/webm" => Some(AudioFormat::Webm),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_audio_codec(codecs: Vec<&str>) -> AudioCodec {
|
||||
for codec in codecs {
|
||||
if codec.starts_with("mp4a") {
|
||||
return AudioCodec::Mp4a;
|
||||
} else if codec.starts_with("opus") {
|
||||
|
|
@ -534,12 +549,14 @@ mod tests {
|
|||
assert_eq!(video.quality, "720p");
|
||||
assert_eq!(video.hdr, false);
|
||||
assert_eq!(video.mime, "video/webm; codecs=\"vp09.00.31.08\"");
|
||||
assert_eq!(video.format, VideoFormat::Webm);
|
||||
assert_eq!(video.codec, VideoCodec::Vp9);
|
||||
|
||||
assert_eq!(audio.bitrate, 130685);
|
||||
assert_eq!(audio.average_bitrate, 129496);
|
||||
assert_eq!(audio.size, 4193863);
|
||||
assert_eq!(audio.mime, "audio/mp4; codecs=\"mp4a.40.2\"");
|
||||
assert_eq!(audio.format, AudioFormat::M4a);
|
||||
assert_eq!(audio.codec, AudioCodec::Mp4a);
|
||||
} else {
|
||||
let video = player_data
|
||||
|
|
@ -562,12 +579,14 @@ mod tests {
|
|||
assert_eq!(video.quality, "720p");
|
||||
assert_eq!(video.hdr, false);
|
||||
assert_eq!(video.mime, "video/mp4; codecs=\"av01.0.05M.08\"");
|
||||
assert_eq!(video.format, VideoFormat::Mp4);
|
||||
assert_eq!(video.codec, VideoCodec::Av01);
|
||||
|
||||
assert_eq!(audio.bitrate, 142718);
|
||||
assert_eq!(audio.average_bitrate, 130708);
|
||||
assert_eq!(audio.size, 4232344);
|
||||
assert_eq!(audio.mime, "audio/webm; codecs=\"opus\"");
|
||||
assert_eq!(audio.format, AudioFormat::Webm);
|
||||
assert_eq!(audio.codec, AudioCodec::Opus);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,12 +13,18 @@ struct QPlaylist {
|
|||
browse_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TmpEntry {
|
||||
title: String,
|
||||
video_id: String,
|
||||
}
|
||||
|
||||
impl RustyTube {
|
||||
pub async fn get_playlist(
|
||||
&self,
|
||||
playlist_id: &str,
|
||||
client_type: ClientType,
|
||||
) -> Result<response::Playlist> {
|
||||
) -> Result<Vec<TmpEntry>> {
|
||||
// let client = self.desktop_client.clone();
|
||||
let client = self.get_ytclient(client_type);
|
||||
let context = client.get_context(true).await;
|
||||
|
|
@ -38,7 +44,31 @@ impl RustyTube {
|
|||
|
||||
let playlist_response = resp.json::<response::Playlist>().await?;
|
||||
|
||||
Ok(playlist_response)
|
||||
Ok(map_playlist_tmp(playlist_response))
|
||||
}
|
||||
}
|
||||
|
||||
fn map_playlist_tmp(response: response::Playlist) -> Vec<TmpEntry> {
|
||||
let content = &response
|
||||
.contents
|
||||
.two_column_browse_results_renderer
|
||||
.contents[0]
|
||||
.tab_renderer
|
||||
.content
|
||||
.section_list_renderer
|
||||
.contents[0];
|
||||
|
||||
match &content.item_section_renderer {
|
||||
Some(items) => items.contents[0]
|
||||
.playlist_video_list_renderer
|
||||
.contents
|
||||
.iter()
|
||||
.map(|it| TmpEntry {
|
||||
title: it.playlist_video_renderer.title.to_owned(),
|
||||
video_id: it.playlist_video_renderer.video_id.to_owned(),
|
||||
})
|
||||
.collect(),
|
||||
None => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,7 +81,7 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
#[allow(dead_code)]
|
||||
// #[test_log::test(tokio::test)]
|
||||
#[test_log::test(tokio::test)]
|
||||
async fn download_testfiles() {
|
||||
let tf_dir = Path::new("testfiles/playlist");
|
||||
let playlist_id = "RDCLAK5uy_mHW5bcduhjB-PkTePAe6EoRMj1xNT8gzY";
|
||||
|
|
@ -106,7 +136,7 @@ mod tests {
|
|||
let playlist = rt
|
||||
.get_playlist(
|
||||
"RDCLAK5uy_mHW5bcduhjB-PkTePAe6EoRMj1xNT8gzY",
|
||||
ClientType::DesktopMusic,
|
||||
ClientType::Desktop,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
|||
|
|
@ -29,32 +29,32 @@ pub struct Thumbnail {
|
|||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MusicItem {
|
||||
thumbnail: MusicThumbnailRenderer,
|
||||
playlist_item_data: PlaylistItemData,
|
||||
pub thumbnail: MusicThumbnailRenderer,
|
||||
pub playlist_item_data: PlaylistItemData,
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
flex_columns: Vec<MusicColumn>,
|
||||
pub flex_columns: Vec<MusicColumn>,
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "VecSkipError<_>")]
|
||||
fixed_columns: Vec<MusicColumn>,
|
||||
pub fixed_columns: Vec<MusicColumn>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MusicThumbnailRenderer {
|
||||
music_thumbnail_renderer: MusicThumbnailRenderer2,
|
||||
pub music_thumbnail_renderer: MusicThumbnailRenderer2,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MusicThumbnailRenderer2 {
|
||||
thumbnail: Thumbnails,
|
||||
pub thumbnail: Thumbnails,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlaylistItemData {
|
||||
video_id: String,
|
||||
pub video_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
|
|
@ -63,12 +63,12 @@ pub struct MusicColumn {
|
|||
rename = "musicResponsiveListItemFlexColumnRenderer",
|
||||
alias = "musicResponsiveListItemFixedColumnRenderer"
|
||||
)]
|
||||
renderer: MusicColumnRenderer,
|
||||
pub renderer: MusicColumnRenderer,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct MusicColumnRenderer {
|
||||
#[serde_as(as = "crate::serializer::text::TextLink")]
|
||||
text: TextLink,
|
||||
pub text: TextLink,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -235,5 +235,5 @@ pub struct PlayerMicroformatRenderer {
|
|||
pub category: String,
|
||||
pub publish_date: NaiveDate,
|
||||
// Only on YT Music
|
||||
pub tags: Option<Vec<String>>
|
||||
pub tags: Option<Vec<String>>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use serde_with::{json::JsonString, DefaultOnError, VecSkipError};
|
|||
|
||||
use crate::serializer::text::TextLink;
|
||||
|
||||
use super::{Thumbnails, MusicItem};
|
||||
use super::{MusicItem, Thumbnails};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
@ -42,19 +42,19 @@ pub struct ItemSection {
|
|||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlaylistVideoList {
|
||||
pub playlist_video_list_renderer: ContentsRenderer<PlaylistVideoItem>
|
||||
pub playlist_video_list_renderer: ContentsRenderer<PlaylistVideoItem>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlaylistVideoItem {
|
||||
playlist_video_renderer: PlaylistVideo,
|
||||
pub playlist_video_renderer: PlaylistVideo,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlaylistMusicItem {
|
||||
music_responsive_list_item_renderer: MusicItem,
|
||||
pub music_responsive_list_item_renderer: MusicItem,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
|
|
@ -94,12 +94,12 @@ pub struct HeaderRenderer {
|
|||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ContentRenderer<T> {
|
||||
pub content: T
|
||||
pub content: T,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ContentsRenderer<T> {
|
||||
#[serde(alias = "tabs")]
|
||||
pub contents: Vec<T>
|
||||
pub contents: Vec<T>,
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue