refactored client API (query builder)

use VecLogError for player streams
This commit is contained in:
ThetaDev 2022-09-14 23:55:44 +02:00
parent dda2211e04
commit b52fd7349b
11 changed files with 277 additions and 177 deletions

View file

@ -6,7 +6,7 @@ edition = "2021"
[dependencies] [dependencies]
rustypipe = {path = "../", default_features = false, features = ["rustls-tls-native-roots"]} rustypipe = {path = "../", default_features = false, features = ["rustls-tls-native-roots"]}
reqwest = {version = "0.11.11", default_features = false} reqwest = {version = "0.11.11", default_features = false}
tokio = {version = "1.20.0", features = ["rt-multi-thread"]} tokio = {version = "1.20.0", features = ["macros", "rt-multi-thread"]}
indicatif = "0.17.0" indicatif = "0.17.0"
futures = "0.3.21" futures = "0.3.21"
anyhow = "1.0" anyhow = "1.0"

View file

@ -154,13 +154,12 @@ static CLIENT_VERSION_REGEXES: Lazy<[Regex; 1]> =
#[derive(Clone)] #[derive(Clone)]
pub struct RustyPipe { pub struct RustyPipe {
inner: Arc<RustyPipeRef>, inner: Arc<RustyPipeRef>,
opts: RustyPipeOpts,
} }
struct RustyPipeRef { struct RustyPipeRef {
http: Client, http: Client,
storage: Option<Box<dyn CacheStorage>>, storage: Option<Box<dyn CacheStorage + Sync + Send>>,
reporter: Option<Box<dyn Reporter>>, reporter: Option<Box<dyn Reporter + Sync + Send>>,
user_agent: String, user_agent: String,
consent_cookie: String, consent_cookie: String,
cache: Mutex<CacheData>, cache: Mutex<CacheData>,
@ -174,6 +173,12 @@ struct RustyPipeOpts {
strict: bool, strict: bool,
} }
#[derive(Clone)]
pub struct RustyPipeQuery {
client: RustyPipe,
opts: RustyPipeOpts,
}
impl Default for RustyPipe { impl Default for RustyPipe {
fn default() -> Self { fn default() -> Self {
Self::new( Self::new(
@ -244,8 +249,8 @@ impl<T> From<T> for CacheEntry<T> {
impl RustyPipe { impl RustyPipe {
/// Create a new RustyPipe instance /// Create a new RustyPipe instance
pub fn new( pub fn new(
storage: Option<Box<dyn CacheStorage>>, storage: Option<Box<dyn CacheStorage + Sync + Send>>,
reporter: Option<Box<dyn Reporter>>, reporter: Option<Box<dyn Reporter + Sync + Send>>,
user_agent: Option<String>, user_agent: Option<String>,
) -> Self { ) -> Self {
let user_agent = user_agent.unwrap_or(DEFAULT_UA.to_owned()); let user_agent = user_agent.unwrap_or(DEFAULT_UA.to_owned());
@ -287,7 +292,6 @@ impl RustyPipe {
), ),
cache: Mutex::new(cache), cache: Mutex::new(cache),
}), }),
opts: RustyPipeOpts::default(),
} }
} }
@ -300,9 +304,35 @@ impl RustyPipe {
Some(Box::new(crate::report::YamlFileReporter::default())), Some(Box::new(crate::report::YamlFileReporter::default())),
None, None,
) )
.strict(true)
} }
pub fn query(&self) -> RustyPipeQuery {
RustyPipeQuery {
client: self.clone(),
opts: RustyPipeOpts {
lang: Language::En,
country: Country::Us,
report: false,
strict: false,
},
}
}
#[cfg(test)]
pub fn test_query(&self) -> RustyPipeQuery {
RustyPipeQuery {
client: self.clone(),
opts: RustyPipeOpts {
lang: Language::En,
country: Country::Us,
report: false,
strict: true,
},
}
}
}
impl RustyPipeQuery {
/// Set the language parameter used when accessing the YouTube API /// Set the language parameter used when accessing the YouTube API
/// This will change multilanguage video titles, descriptions and textual dates /// This will change multilanguage video titles, descriptions and textual dates
pub fn lang(mut self, lang: Language) -> Self { pub fn lang(mut self, lang: Language) -> Self {
@ -431,6 +461,7 @@ impl RustyPipe {
) -> RequestBuilder { ) -> RequestBuilder {
match ctype { match ctype {
ClientType::Desktop => self ClientType::Desktop => self
.client
.inner .inner
.http .http
.request( .request(
@ -442,10 +473,14 @@ impl RustyPipe {
) )
.header(header::ORIGIN, "https://www.youtube.com") .header(header::ORIGIN, "https://www.youtube.com")
.header(header::REFERER, "https://www.youtube.com") .header(header::REFERER, "https://www.youtube.com")
.header(header::COOKIE, self.inner.consent_cookie.to_owned()) .header(header::COOKIE, self.client.inner.consent_cookie.to_owned())
.header("X-YouTube-Client-Name", "1") .header("X-YouTube-Client-Name", "1")
.header("X-YouTube-Client-Version", self.get_desktop_client_version().await), .header(
"X-YouTube-Client-Version",
self.get_desktop_client_version().await,
),
ClientType::DesktopMusic => self ClientType::DesktopMusic => self
.client
.inner .inner
.http .http
.request( .request(
@ -460,10 +495,14 @@ impl RustyPipe {
) )
.header(header::ORIGIN, "https://music.youtube.com") .header(header::ORIGIN, "https://music.youtube.com")
.header(header::REFERER, "https://music.youtube.com") .header(header::REFERER, "https://music.youtube.com")
.header(header::COOKIE, self.inner.consent_cookie.to_owned()) .header(header::COOKIE, self.client.inner.consent_cookie.to_owned())
.header("X-YouTube-Client-Name", "67") .header("X-YouTube-Client-Name", "67")
.header("X-YouTube-Client-Version", self.get_music_client_version().await), .header(
"X-YouTube-Client-Version",
self.get_music_client_version().await,
),
ClientType::TvHtml5Embed => self ClientType::TvHtml5Embed => self
.client
.inner .inner
.http .http
.request( .request(
@ -478,6 +517,7 @@ impl RustyPipe {
.header("X-YouTube-Client-Name", "1") .header("X-YouTube-Client-Name", "1")
.header("X-YouTube-Client-Version", TVHTML5_CLIENT_VERSION), .header("X-YouTube-Client-Version", TVHTML5_CLIENT_VERSION),
ClientType::Android => self ClientType::Android => self
.client
.inner .inner
.http .http
.request( .request(
@ -499,6 +539,7 @@ impl RustyPipe {
) )
.header("X-Goog-Api-Format-Version", "2"), .header("X-Goog-Api-Format-Version", "2"),
ClientType::Ios => self ClientType::Ios => self
.client
.inner .inner
.http .http
.request( .request(
@ -545,13 +586,13 @@ impl RustyPipe {
let request_url = request.url().to_string(); let request_url = request.url().to_string();
let request_headers = request.headers().to_owned(); let request_headers = request.headers().to_owned();
let response = self.inner.http.execute(request).await?; let response = self.client.inner.http.execute(request).await?;
let status = response.status(); let status = response.status();
let resp_str = response.text().await?; let resp_str = response.text().await?;
let create_report = |level: Level, error: Option<String>, msgs: Vec<String>| { let create_report = |level: Level, error: Option<String>, msgs: Vec<String>| {
if let Some(reporter) = &self.inner.reporter { if let Some(reporter) = &self.client.inner.reporter {
let report = Report { let report = Report {
package: "rustypipe".to_owned(), package: "rustypipe".to_owned(),
version: "0.1.0".to_owned(), version: "0.1.0".to_owned(),
@ -636,11 +677,16 @@ impl RustyPipe {
} }
async fn get_desktop_client_version(&self) -> String { async fn get_desktop_client_version(&self) -> String {
let mut cache = self.inner.cache.lock().await; let mut cache = self.client.inner.cache.lock().await;
match cache.desktop_client.get() { match cache.desktop_client.get() {
Some(cdata) => cdata.version.to_owned(), Some(cdata) => cdata.version.to_owned(),
None => match self.extract_desktop_client_version().await { None => match extract_desktop_client_version(
self.client.inner.http.clone(),
self.client.inner.consent_cookie.to_owned(),
)
.await
{
Ok(version) => { Ok(version) => {
cache.desktop_client = CacheEntry::from(ClientData { cache.desktop_client = CacheEntry::from(ClientData {
version: version.to_owned(), version: version.to_owned(),
@ -657,11 +703,16 @@ impl RustyPipe {
} }
async fn get_music_client_version(&self) -> String { async fn get_music_client_version(&self) -> String {
let mut cache = self.inner.cache.lock().await; let mut cache = self.client.inner.cache.lock().await;
match cache.music_client.get() { match cache.music_client.get() {
Some(cdata) => cdata.version.to_owned(), Some(cdata) => cdata.version.to_owned(),
None => match self.extract_music_client_version().await { None => match extract_music_client_version(
self.client.inner.http.clone(),
self.client.inner.consent_cookie.to_owned(),
)
.await
{
Ok(version) => { Ok(version) => {
cache.music_client = CacheEntry::from(ClientData { cache.music_client = CacheEntry::from(ClientData {
version: version.to_owned(), version: version.to_owned(),
@ -678,109 +729,21 @@ impl RustyPipe {
} }
async fn get_deobf(&self) -> Result<Deobfuscator> { async fn get_deobf(&self) -> Result<Deobfuscator> {
let mut cache = self.inner.cache.lock().await; let mut cache = self.client.inner.cache.lock().await;
let deobf = Deobfuscator::new(self.inner.http.clone()).await?;
cache.deobf = CacheEntry::from(deobf.get_data());
self.write_cache(&cache);
Ok(deobf)
}
async fn extract_desktop_client_version(&self) -> Result<String> { match cache.deobf.get() {
let from_swjs = async { Some(deobf) => Ok(Deobfuscator::from(deobf.to_owned())),
let swjs = self None => {
.exec_request_text( let deobf = Deobfuscator::new(self.client.inner.http.clone()).await?;
self.inner cache.deobf = CacheEntry::from(deobf.get_data());
.http self.write_cache(&cache);
.get("https://www.youtube.com/sw.js") Ok(deobf)
.header(header::ORIGIN, "https://www.youtube.com") }
.header(header::REFERER, "https://www.youtube.com")
.header(header::COOKIE, self.inner.consent_cookie.to_owned())
.build()
.unwrap(),
)
.await
.context("Failed to download sw.js")?;
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &swjs, 1)
.ok_or(anyhow!("Could not find desktop client version in sw.js"))
};
let from_html = async {
let html = self
.exec_request_text(
self.inner
.http
.get("https://www.youtube.com/results?search_query=")
.build()
.unwrap(),
)
.await
.context("Failed to get YT Desktop page")?;
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &html, 1).ok_or(anyhow!(
"Could not find desktop client version on html page"
))
};
match from_swjs.await {
Ok(client_version) => Ok(client_version),
Err(_) => from_html.await,
} }
} }
async fn extract_music_client_version(&self) -> Result<String> {
let from_swjs = async {
let swjs = self
.exec_request_text(
self.inner
.http
.get("https://music.youtube.com/sw.js")
.header(header::ORIGIN, "https://music.youtube.com")
.header(header::REFERER, "https://music.youtube.com")
.header(header::COOKIE, self.inner.consent_cookie.to_owned())
.build()
.unwrap(),
)
.await
.context("Failed to download sw.js")?;
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &swjs, 1)
.ok_or(anyhow!("Could not find desktop client version in sw.js"))
};
let from_html = async {
let html = self
.exec_request_text(
self.inner
.http
.get("https://music.youtube.com")
.build()
.unwrap(),
)
.await
.context("Failed to get YT Desktop page")?;
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &html, 1).ok_or(anyhow!(
"Could not find desktop client version on html page"
))
};
match from_swjs.await {
Ok(client_version) => Ok(client_version),
Err(_) => from_html.await,
}
}
async fn exec_request(&self, request: Request) -> Result<Response> {
Ok(self.inner.http.execute(request).await?.error_for_status()?)
}
async fn exec_request_text(&self, request: Request) -> Result<String> {
Ok(self.exec_request(request).await?.text().await?)
}
fn write_cache(&self, cache: &CacheData) { fn write_cache(&self, cache: &CacheData) {
if let Some(storage) = &self.inner.storage { if let Some(storage) = &self.client.inner.storage {
match serde_json::to_string(cache) { match serde_json::to_string(cache) {
Ok(data) => storage.write(&data), Ok(data) => storage.write(&data),
Err(e) => error!("Could not serialize cache. Error: {}", e), Err(e) => error!("Could not serialize cache. Error: {}", e),
@ -789,6 +752,90 @@ impl RustyPipe {
} }
} }
async fn extract_desktop_client_version(http: Client, consent_cookie: String) -> Result<String> {
let from_swjs = async {
let swjs = exec_request_text(
http.clone(),
http.get("https://www.youtube.com/sw.js")
.header(header::ORIGIN, "https://www.youtube.com")
.header(header::REFERER, "https://www.youtube.com")
.header(header::COOKIE, consent_cookie)
.build()
.unwrap(),
)
.await
.context("Failed to download sw.js")?;
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &swjs, 1)
.ok_or(anyhow!("Could not find desktop client version in sw.js"))
};
let from_html = async {
let html = exec_request_text(
http.clone(),
http.get("https://www.youtube.com/results?search_query=")
.build()
.unwrap(),
)
.await
.context("Failed to get YT Desktop page")?;
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &html, 1).ok_or(anyhow!(
"Could not find desktop client version on html page"
))
};
match from_swjs.await {
Ok(client_version) => Ok(client_version),
Err(_) => from_html.await,
}
}
async fn extract_music_client_version(http: Client, consent_cookie: String) -> Result<String> {
let from_swjs = async {
let swjs = exec_request_text(
http.clone(),
http.get("https://music.youtube.com/sw.js")
.header(header::ORIGIN, "https://music.youtube.com")
.header(header::REFERER, "https://music.youtube.com")
.header(header::COOKIE, consent_cookie)
.build()
.unwrap(),
)
.await
.context("Failed to download sw.js")?;
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &swjs, 1)
.ok_or(anyhow!("Could not find desktop client version in sw.js"))
};
let from_html = async {
let html = exec_request_text(
http.clone(),
http.get("https://music.youtube.com").build().unwrap(),
)
.await
.context("Failed to get YT Desktop page")?;
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &html, 1).ok_or(anyhow!(
"Could not find desktop client version on html page"
))
};
match from_swjs.await {
Ok(client_version) => Ok(client_version),
Err(_) => from_html.await,
}
}
async fn exec_request(http: Client, request: Request) -> Result<Response> {
Ok(http.execute(request).await?.error_for_status()?)
}
async fn exec_request_text(http: Client, request: Request) -> Result<String> {
Ok(exec_request(http, request).await?.text().await?)
}
trait MapResponse<T> { trait MapResponse<T> {
fn map_response( fn map_response(
self, self,
@ -812,3 +859,21 @@ where
self.c.fmt(f) self.c.fmt(f)
} }
} }
impl<T> Default for MapResult<T>
where
T: Default,
{
fn default() -> Self {
Self {
c: Default::default(),
warnings: Vec::new(),
}
}
}
#[cfg(test)]
#[cfg(feature = "yaml")]
mod tests {
// use super::*;
}

View file

@ -21,7 +21,7 @@ use crate::{
use super::{ use super::{
response::{self, player}, response::{self, player},
ClientType, ContextYT, MapResponse, MapResult, RustyPipe, ClientType, ContextYT, MapResponse, MapResult, RustyPipeQuery,
}; };
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
@ -57,13 +57,21 @@ struct QContentPlaybackContext {
referer: String, referer: String,
} }
impl RustyPipe { impl RustyPipeQuery {
pub async fn get_player(&self, video_id: &str, client_type: ClientType) -> Result<VideoPlayer> { pub async fn get_player(self, video_id: &str, client_type: ClientType) -> Result<VideoPlayer> {
let (context, deobf) = tokio::join!(self.get_context(client_type, false), self.get_deobf()); // let (context, deobf) = tokio::join!(self.get_context(client_type, false), self.get_deobf());
// let context = self.get_context(client_type, false).await; // let deobf = deobf?;
// let deobf = self.get_deobf().await;
let deobf = deobf?; let q1 = self.clone();
let t_context = tokio::spawn(async move { q1.get_context(client_type, false).await });
let q2 = self.clone();
let t_deobf = tokio::spawn(async move { q2.get_deobf().await });
// let context = t_context.await.unwrap();
// let deobf = t_deobf.await.unwrap()?;
let (context, deobf) = tokio::join!(t_context, t_deobf);
let context = context.unwrap();
let deobf = deobf.unwrap()?;
let request_body = if client_type.is_web() { let request_body = if client_type.is_web() {
QPlayer { QPlayer {
@ -187,8 +195,11 @@ impl MapResponse<VideoPlayer> for response::Player {
is_family_safe, is_family_safe,
}; };
let mut formats = streaming_data.formats; let mut formats = streaming_data.formats.c;
formats.append(&mut streaming_data.adaptive_formats); formats.append(&mut streaming_data.adaptive_formats.c);
warnings.append(&mut streaming_data.formats.warnings);
warnings.append(&mut streaming_data.adaptive_formats.warnings);
let mut last_nsig: [String; 2] = ["".to_owned(), "".to_owned()]; let mut last_nsig: [String; 2] = ["".to_owned(), "".to_owned()];
@ -579,7 +590,11 @@ fn get_audio_codec(codecs: Vec<&str>) -> AudioCodec {
mod tests { mod tests {
use std::{fs::File, io::BufReader, path::Path}; use std::{fs::File, io::BufReader, path::Path};
use crate::{deobfuscate::DeobfData, client2::CLIENT_TYPES, report::TestFileReporter}; use crate::{
client2::{RustyPipe, CLIENT_TYPES},
deobfuscate::DeobfData,
report::TestFileReporter,
};
use super::*; use super::*;
use rstest::rstest; use rstest::rstest;
@ -606,8 +621,12 @@ mod tests {
} }
let reporter = TestFileReporter::new(json_path); let reporter = TestFileReporter::new(json_path);
let rp = RustyPipe::new(None, Some(Box::new(reporter)), None).report(true); let rp = RustyPipe::new(None, Some(Box::new(reporter)), None);
rp.get_player(video_id, client_type).await.unwrap(); rp.test_query()
.report(true)
.get_player(video_id, client_type)
.await
.unwrap();
} }
} }
@ -623,7 +642,11 @@ mod tests {
continue; continue;
} }
let player_data = rp.get_player(id, ClientType::Desktop).await.unwrap(); let player_data = rp
.test_query()
.get_player(id, ClientType::Desktop)
.await
.unwrap();
let file = File::create(json_path).unwrap(); let file = File::create(json_path).unwrap();
serde_json::to_writer_pretty(file, &player_data).unwrap(); serde_json::to_writer_pretty(file, &player_data).unwrap();
} }
@ -685,7 +708,11 @@ mod tests {
#[test_log::test(tokio::test)] #[test_log::test(tokio::test)]
async fn t_get_player(#[case] client_type: ClientType) { async fn t_get_player(#[case] client_type: ClientType) {
let rp = RustyPipe::new_test(); let rp = RustyPipe::new_test();
let player_data = rp.get_player("n4tK7LYFxI0", client_type).await.unwrap(); let player_data = rp
.test_query()
.get_player("n4tK7LYFxI0", client_type)
.await
.unwrap();
// dbg!(&player_data); // dbg!(&player_data);

View file

@ -9,7 +9,7 @@ use crate::{
timeago, util, timeago, util,
}; };
use super::{response, ClientType, ContextYT, MapResponse, MapResult, RustyPipe}; use super::{response, ClientType, ContextYT, MapResponse, MapResult, RustyPipeQuery};
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -25,8 +25,8 @@ struct QPlaylistCont {
continuation: String, continuation: String,
} }
impl RustyPipe { impl RustyPipeQuery {
pub async fn get_playlist(&self, playlist_id: &str) -> Result<Playlist> { pub async fn get_playlist(self, playlist_id: &str) -> Result<Playlist> {
let context = self.get_context(ClientType::Desktop, true).await; let context = self.get_context(ClientType::Desktop, true).await;
let request_body = QPlaylist { let request_body = QPlaylist {
context, context,
@ -44,7 +44,7 @@ impl RustyPipe {
.await .await
} }
pub async fn get_playlist_cont(&self, playlist: &mut Playlist) -> Result<()> { pub async fn get_playlist_cont(self, playlist: &mut Playlist) -> Result<()> {
match &playlist.ctoken { match &playlist.ctoken {
Some(ctoken) => { Some(ctoken) => {
let context = self.get_context(ClientType::Desktop, true).await; let context = self.get_context(ClientType::Desktop, true).await;
@ -308,7 +308,7 @@ mod tests {
use rstest::rstest; use rstest::rstest;
use crate::report::TestFileReporter; use crate::{client2::RustyPipe, report::TestFileReporter};
use super::*; use super::*;
@ -349,7 +349,7 @@ mod tests {
#[case] channel: Option<Channel>, #[case] channel: Option<Channel>,
) { ) {
let rp = RustyPipe::new_test(); let rp = RustyPipe::new_test();
let playlist = rp.get_playlist(id).await.unwrap(); let playlist = rp.test_query().get_playlist(id).await.unwrap();
assert_eq!(playlist.id, id); assert_eq!(playlist.id, id);
assert_eq!(playlist.name, name); assert_eq!(playlist.name, name);
@ -380,8 +380,8 @@ mod tests {
} }
let reporter = TestFileReporter::new(json_path); let reporter = TestFileReporter::new(json_path);
let rp = RustyPipe::new(None, Some(Box::new(reporter)), None).report(true); let rp = RustyPipe::new(None, Some(Box::new(reporter)), None);
rp.get_playlist(id).await.unwrap(); rp.test_query().report(true).get_playlist(id).await.unwrap();
} }
} }
@ -412,12 +412,16 @@ mod tests {
async fn t_playlist_cont() { async fn t_playlist_cont() {
let rp = RustyPipe::new_test(); let rp = RustyPipe::new_test();
let mut playlist = rp let mut playlist = rp
.test_query()
.get_playlist("PLbZIPy20-1pN7mqjckepWF78ndb6ci_qi") .get_playlist("PLbZIPy20-1pN7mqjckepWF78ndb6ci_qi")
.await .await
.unwrap(); .unwrap();
while playlist.ctoken.is_some() { while playlist.ctoken.is_some() {
rp.get_playlist_cont(&mut playlist).await.unwrap(); rp.test_query()
.get_playlist_cont(&mut playlist)
.await
.unwrap();
} }
assert!(playlist.videos.len() > 100); assert!(playlist.videos.len() > 100);

View file

@ -4,6 +4,7 @@ use serde_with::VecSkipError;
use super::TimeOverlay; use super::TimeOverlay;
use super::{ContentRenderer, ContentsRenderer, Thumbnails, VideoListItem}; use super::{ContentRenderer, ContentsRenderer, Thumbnails, VideoListItem};
use crate::serializer::text::Text;
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -63,11 +64,11 @@ pub struct GridRenderer {
pub struct ChannelVideo { pub struct ChannelVideo {
pub video_id: String, pub video_id: String,
pub thumbnail: Thumbnails, pub thumbnail: Thumbnails,
#[serde_as(as = "crate::serializer::text::Text")] #[serde_as(as = "Text")]
pub title: String, pub title: String,
#[serde_as(as = "Option<crate::serializer::text::Text>")] #[serde_as(as = "Option<Text>")]
pub published_time_text: Option<String>, pub published_time_text: Option<String>,
#[serde_as(as = "crate::serializer::text::Text")] #[serde_as(as = "Text")]
pub view_count_text: String, pub view_count_text: String,
#[serde_as(as = "VecSkipError<_>")] #[serde_as(as = "VecSkipError<_>")]
pub thumbnail_overlays: Vec<TimeOverlay>, pub thumbnail_overlays: Vec<TimeOverlay>,

View file

@ -16,7 +16,7 @@ pub use video::VideoRecommendations;
use serde::Deserialize; use serde::Deserialize;
use serde_with::{serde_as, DefaultOnError, VecSkipError}; use serde_with::{serde_as, DefaultOnError, VecSkipError};
use crate::serializer::text::TextLink; use crate::serializer::text::{Text, TextLink, TextLinks};
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -94,10 +94,10 @@ pub struct VideoOwner {
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct VideoOwnerRenderer { pub struct VideoOwnerRenderer {
#[serde_as(as = "crate::serializer::text::TextLink")] #[serde_as(as = "TextLink")]
pub title: TextLink, pub title: TextLink,
pub thumbnail: Thumbnails, pub thumbnail: Thumbnails,
#[serde_as(as = "Option<crate::serializer::text::Text>")] #[serde_as(as = "Option<Text>")]
pub subscriber_count_text: Option<String>, pub subscriber_count_text: Option<String>,
#[serde(default)] #[serde(default)]
#[serde_as(as = "VecSkipError<_>")] #[serde_as(as = "VecSkipError<_>")]
@ -133,7 +133,7 @@ pub struct TimeOverlay {
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct TimeOverlayRenderer { pub struct TimeOverlayRenderer {
#[serde_as(as = "crate::serializer::text::Text")] #[serde_as(as = "Text")]
pub text: String, pub text: String,
#[serde(default)] #[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")] #[serde_as(deserialize_as = "DefaultOnError")]
@ -199,7 +199,7 @@ pub struct MusicColumn {
#[serde_as] #[serde_as]
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
pub struct MusicColumnRenderer { pub struct MusicColumnRenderer {
#[serde_as(as = "crate::serializer::text::TextLinks")] #[serde_as(as = "TextLinks")]
pub text: Vec<TextLink>, pub text: Vec<TextLink>,
} }

View file

@ -3,9 +3,11 @@ use std::ops::Range;
use chrono::NaiveDate; use chrono::NaiveDate;
use serde::Deserialize; use serde::Deserialize;
use serde_with::serde_as; use serde_with::serde_as;
use serde_with::{json::JsonString, DefaultOnError, VecSkipError}; use serde_with::{json::JsonString, DefaultOnError};
use super::Thumbnails; use super::Thumbnails;
use crate::client2::MapResult;
use crate::serializer::{text::Text, VecLogError};
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -45,11 +47,11 @@ pub struct StreamingData {
#[serde_as(as = "JsonString")] #[serde_as(as = "JsonString")]
pub expires_in_seconds: u32, pub expires_in_seconds: u32,
#[serde(default)] #[serde(default)]
#[serde_as(as = "VecSkipError<_>")] #[serde_as(as = "VecLogError<_>")]
pub formats: Vec<Format>, pub formats: MapResult<Vec<Format>>,
#[serde(default)] #[serde(default)]
#[serde_as(as = "VecSkipError<_>")] #[serde_as(as = "VecLogError<_>")]
pub adaptive_formats: Vec<Format>, pub adaptive_formats: MapResult<Vec<Format>>,
/// Only on livestreams /// Only on livestreams
pub dash_manifest_url: Option<String>, pub dash_manifest_url: Option<String>,
/// Only on livestreams /// Only on livestreams
@ -73,9 +75,9 @@ pub struct Format {
pub width: Option<u32>, pub width: Option<u32>,
pub height: Option<u32>, pub height: Option<u32>,
#[serde_as(as = "Option<crate::serializer::range::Range>")] #[serde_as(as = "Option<crate::serializer::Range>")]
pub index_range: Option<Range<u32>>, pub index_range: Option<Range<u32>>,
#[serde_as(as = "Option<crate::serializer::range::Range>")] #[serde_as(as = "Option<crate::serializer::Range>")]
pub init_range: Option<Range<u32>>, pub init_range: Option<Range<u32>>,
#[serde_as(as = "JsonString")] #[serde_as(as = "JsonString")]
@ -188,7 +190,7 @@ pub struct PlayerCaptionsTracklistRenderer {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CaptionTrack { pub struct CaptionTrack {
pub base_url: String, pub base_url: String,
#[serde_as(as = "crate::serializer::text::Text")] #[serde_as(as = "Text")]
pub name: String, pub name: String,
pub language_code: String, pub language_code: String,
} }

View file

@ -3,7 +3,8 @@ use serde_with::serde_as;
use serde_with::{json::JsonString, DefaultOnError, VecSkipError}; use serde_with::{json::JsonString, DefaultOnError, VecSkipError};
use crate::client2::MapResult; use crate::client2::MapResult;
use crate::serializer::text::TextLink; use crate::serializer::text::{Text, TextLink};
use crate::serializer::VecLogError;
use super::{ContentRenderer, ContentsRenderer, Thumbnails, ThumbnailsWrap, VideoListItem}; use super::{ContentRenderer, ContentsRenderer, Thumbnails, ThumbnailsWrap, VideoListItem};
@ -57,7 +58,7 @@ pub struct PlaylistVideoListRenderer {
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct PlaylistVideoList { pub struct PlaylistVideoList {
#[serde_as(as = "crate::serializer::VecLogError<_>")] #[serde_as(as = "VecLogError<_>")]
pub contents: MapResult<Vec<VideoListItem<PlaylistVideo>>>, pub contents: MapResult<Vec<VideoListItem<PlaylistVideo>>>,
} }
@ -67,10 +68,10 @@ pub struct PlaylistVideoList {
pub struct PlaylistVideo { pub struct PlaylistVideo {
pub video_id: String, pub video_id: String,
pub thumbnail: Thumbnails, pub thumbnail: Thumbnails,
#[serde_as(as = "crate::serializer::text::Text")] #[serde_as(as = "Text")]
pub title: String, pub title: String,
#[serde(rename = "shortBylineText")] #[serde(rename = "shortBylineText")]
#[serde_as(as = "crate::serializer::text::TextLink")] #[serde_as(as = "TextLink")]
pub channel: TextLink, pub channel: TextLink,
#[serde_as(as = "JsonString")] #[serde_as(as = "JsonString")]
pub length_seconds: u32, pub length_seconds: u32,
@ -87,14 +88,14 @@ pub struct Header {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct HeaderRenderer { pub struct HeaderRenderer {
pub playlist_id: String, pub playlist_id: String,
#[serde_as(as = "crate::serializer::text::Text")] #[serde_as(as = "Text")]
pub title: String, pub title: String,
#[serde(default)] #[serde(default)]
#[serde_as(as = "DefaultOnError<Option<crate::serializer::text::Text>>")] #[serde_as(as = "DefaultOnError<Option<Text>>")]
pub description_text: Option<String>, pub description_text: Option<String>,
#[serde_as(as = "crate::serializer::text::Text")] #[serde_as(as = "Text")]
pub num_videos_text: String, pub num_videos_text: String,
#[serde_as(as = "Option<crate::serializer::text::TextLink>")] #[serde_as(as = "Option<TextLink>")]
pub owner_text: Option<TextLink>, pub owner_text: Option<TextLink>,
// Alternative layout // Alternative layout
@ -119,7 +120,7 @@ pub struct Byline {
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct BylineRenderer { pub struct BylineRenderer {
#[serde_as(as = "crate::serializer::text::Text")] #[serde_as(as = "Text")]
pub text: String, pub text: String,
} }
@ -151,7 +152,7 @@ pub struct SidebarPrimaryInfoRenderer {
// - `"495", " videos"` // - `"495", " videos"`
// - `"3,310,996 views"` // - `"3,310,996 views"`
// - `"Last updated on ", "Aug 7, 2022"` // - `"Last updated on ", "Aug 7, 2022"`
#[serde_as(as = "Vec<crate::serializer::text::Text>")] #[serde_as(as = "Vec<Text>")]
pub stats: Vec<String>, pub stats: Vec<String>,
} }
@ -173,7 +174,7 @@ pub struct OnResponseReceivedAction {
#[derive(Clone, Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct AppendAction { pub struct AppendAction {
#[serde_as(as = "crate::serializer::VecLogError<_>")] #[serde_as(as = "VecLogError<_>")]
pub continuation_items: MapResult<Vec<VideoListItem<PlaylistVideo>>>, pub continuation_items: MapResult<Vec<VideoListItem<PlaylistVideo>>>,
pub target_id: String, pub target_id: String,
} }

View file

@ -3,7 +3,7 @@ use fancy_regex::Regex;
use log::debug; use log::debug;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use reqwest::Client; use reqwest::Client;
use serde::{Serialize, Deserialize}; use serde::{Deserialize, Serialize};
use std::result::Result::Ok; use std::result::Result::Ok;
use crate::util; use crate::util;
@ -43,7 +43,7 @@ impl Deobfuscator {
sig_fn, sig_fn,
sts, sts,
}, },
}) })
} }
pub fn deobfuscate_sig(&self, sig: &str) -> Result<String> { pub fn deobfuscate_sig(&self, sig: &str) -> Result<String> {
@ -480,9 +480,7 @@ c[36](c[8],c[32]),c[20](c[25],c[10]),c[2](c[22],c[8]),c[32](c[20],c[16]),c[32](c
#[test(tokio::test)] #[test(tokio::test)]
async fn t_update() { async fn t_update() {
let client = Client::new(); let client = Client::new();
let deobf = Deobfuscator::new(client) let deobf = Deobfuscator::new(client).await.unwrap();
.await
.unwrap();
let deobf_sig = deobf.deobfuscate_sig("GOqGOqGOq0QJ8wRAIgaryQHfplJ9xJSKFywyaSMHuuwZYsoMTAvRvfm51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5fb5i").unwrap(); let deobf_sig = deobf.deobfuscate_sig("GOqGOqGOq0QJ8wRAIgaryQHfplJ9xJSKFywyaSMHuuwZYsoMTAvRvfm51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5fb5i").unwrap();
println!("{}", deobf_sig); println!("{}", deobf_sig);

View file

@ -86,7 +86,7 @@ impl JsonFileReporter {
impl Default for JsonFileReporter { impl Default for JsonFileReporter {
fn default() -> Self { fn default() -> Self {
Self { Self {
path: Path::new("RustyPipeReports").to_path_buf(), path: Path::new("rustypipe_reports").to_path_buf(),
} }
} }
} }
@ -98,12 +98,12 @@ impl Reporter for JsonFileReporter {
} }
} }
#[cfg(feature="yaml")] #[cfg(feature = "yaml")]
pub struct YamlFileReporter { pub struct YamlFileReporter {
path: PathBuf, path: PathBuf,
} }
#[cfg(feature="yaml")] #[cfg(feature = "yaml")]
impl YamlFileReporter { impl YamlFileReporter {
pub fn new<P: AsRef<Path>>(path: P) -> Self { pub fn new<P: AsRef<Path>>(path: P) -> Self {
Self { Self {
@ -118,7 +118,7 @@ impl YamlFileReporter {
} }
} }
#[cfg(feature="yaml")] #[cfg(feature = "yaml")]
impl Default for YamlFileReporter { impl Default for YamlFileReporter {
fn default() -> Self { fn default() -> Self {
Self { Self {
@ -127,7 +127,7 @@ impl Default for YamlFileReporter {
} }
} }
#[cfg(feature="yaml")] #[cfg(feature = "yaml")]
impl Reporter for YamlFileReporter { impl Reporter for YamlFileReporter {
fn report(&self, report: &Report) { fn report(&self, report: &Report) {
self._report(report) self._report(report)

View file

@ -1,5 +1,7 @@
pub mod range;
pub mod text; pub mod text;
mod range;
mod vec_log_err; mod vec_log_err;
pub use range::Range;
pub use vec_log_err::VecLogError; pub use vec_log_err::VecLogError;