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]
rustypipe = {path = "../", default_features = false, features = ["rustls-tls-native-roots"]}
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"
futures = "0.3.21"
anyhow = "1.0"

View file

@ -154,13 +154,12 @@ static CLIENT_VERSION_REGEXES: Lazy<[Regex; 1]> =
#[derive(Clone)]
pub struct RustyPipe {
inner: Arc<RustyPipeRef>,
opts: RustyPipeOpts,
}
struct RustyPipeRef {
http: Client,
storage: Option<Box<dyn CacheStorage>>,
reporter: Option<Box<dyn Reporter>>,
storage: Option<Box<dyn CacheStorage + Sync + Send>>,
reporter: Option<Box<dyn Reporter + Sync + Send>>,
user_agent: String,
consent_cookie: String,
cache: Mutex<CacheData>,
@ -174,6 +173,12 @@ struct RustyPipeOpts {
strict: bool,
}
#[derive(Clone)]
pub struct RustyPipeQuery {
client: RustyPipe,
opts: RustyPipeOpts,
}
impl Default for RustyPipe {
fn default() -> Self {
Self::new(
@ -244,8 +249,8 @@ impl<T> From<T> for CacheEntry<T> {
impl RustyPipe {
/// Create a new RustyPipe instance
pub fn new(
storage: Option<Box<dyn CacheStorage>>,
reporter: Option<Box<dyn Reporter>>,
storage: Option<Box<dyn CacheStorage + Sync + Send>>,
reporter: Option<Box<dyn Reporter + Sync + Send>>,
user_agent: Option<String>,
) -> Self {
let user_agent = user_agent.unwrap_or(DEFAULT_UA.to_owned());
@ -287,7 +292,6 @@ impl RustyPipe {
),
cache: Mutex::new(cache),
}),
opts: RustyPipeOpts::default(),
}
}
@ -300,9 +304,35 @@ impl RustyPipe {
Some(Box::new(crate::report::YamlFileReporter::default())),
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
/// This will change multilanguage video titles, descriptions and textual dates
pub fn lang(mut self, lang: Language) -> Self {
@ -431,6 +461,7 @@ impl RustyPipe {
) -> RequestBuilder {
match ctype {
ClientType::Desktop => self
.client
.inner
.http
.request(
@ -442,10 +473,14 @@ impl RustyPipe {
)
.header(header::ORIGIN, "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-Version", self.get_desktop_client_version().await),
.header(
"X-YouTube-Client-Version",
self.get_desktop_client_version().await,
),
ClientType::DesktopMusic => self
.client
.inner
.http
.request(
@ -460,10 +495,14 @@ impl RustyPipe {
)
.header(header::ORIGIN, "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-Version", self.get_music_client_version().await),
.header(
"X-YouTube-Client-Version",
self.get_music_client_version().await,
),
ClientType::TvHtml5Embed => self
.client
.inner
.http
.request(
@ -478,6 +517,7 @@ impl RustyPipe {
.header("X-YouTube-Client-Name", "1")
.header("X-YouTube-Client-Version", TVHTML5_CLIENT_VERSION),
ClientType::Android => self
.client
.inner
.http
.request(
@ -499,6 +539,7 @@ impl RustyPipe {
)
.header("X-Goog-Api-Format-Version", "2"),
ClientType::Ios => self
.client
.inner
.http
.request(
@ -545,13 +586,13 @@ impl RustyPipe {
let request_url = request.url().to_string();
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 resp_str = response.text().await?;
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 {
package: "rustypipe".to_owned(),
version: "0.1.0".to_owned(),
@ -636,11 +677,16 @@ impl RustyPipe {
}
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() {
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) => {
cache.desktop_client = CacheEntry::from(ClientData {
version: version.to_owned(),
@ -657,11 +703,16 @@ impl RustyPipe {
}
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() {
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) => {
cache.music_client = CacheEntry::from(ClientData {
version: version.to_owned(),
@ -678,109 +729,21 @@ impl RustyPipe {
}
async fn get_deobf(&self) -> Result<Deobfuscator> {
let mut cache = self.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)
}
let mut cache = self.client.inner.cache.lock().await;
async fn extract_desktop_client_version(&self) -> Result<String> {
let from_swjs = async {
let swjs = self
.exec_request_text(
self.inner
.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, 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,
match cache.deobf.get() {
Some(deobf) => Ok(Deobfuscator::from(deobf.to_owned())),
None => {
let deobf = Deobfuscator::new(self.client.inner.http.clone()).await?;
cache.deobf = CacheEntry::from(deobf.get_data());
self.write_cache(&cache);
Ok(deobf)
}
}
}
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) {
if let Some(storage) = &self.inner.storage {
if let Some(storage) = &self.client.inner.storage {
match serde_json::to_string(cache) {
Ok(data) => storage.write(&data),
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> {
fn map_response(
self,
@ -812,3 +859,21 @@ where
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::{
response::{self, player},
ClientType, ContextYT, MapResponse, MapResult, RustyPipe,
ClientType, ContextYT, MapResponse, MapResult, RustyPipeQuery,
};
#[derive(Clone, Debug, Serialize)]
@ -57,13 +57,21 @@ struct QContentPlaybackContext {
referer: String,
}
impl RustyPipe {
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 = self.get_context(client_type, false).await;
// let deobf = self.get_deobf().await;
impl RustyPipeQuery {
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 deobf = deobf?;
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() {
QPlayer {
@ -187,8 +195,11 @@ impl MapResponse<VideoPlayer> for response::Player {
is_family_safe,
};
let mut formats = streaming_data.formats;
formats.append(&mut streaming_data.adaptive_formats);
let mut formats = streaming_data.formats.c;
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()];
@ -579,7 +590,11 @@ fn get_audio_codec(codecs: Vec<&str>) -> AudioCodec {
mod tests {
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 rstest::rstest;
@ -606,8 +621,12 @@ mod tests {
}
let reporter = TestFileReporter::new(json_path);
let rp = RustyPipe::new(None, Some(Box::new(reporter)), None).report(true);
rp.get_player(video_id, client_type).await.unwrap();
let rp = RustyPipe::new(None, Some(Box::new(reporter)), None);
rp.test_query()
.report(true)
.get_player(video_id, client_type)
.await
.unwrap();
}
}
@ -623,7 +642,11 @@ mod tests {
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();
serde_json::to_writer_pretty(file, &player_data).unwrap();
}
@ -685,7 +708,11 @@ mod tests {
#[test_log::test(tokio::test)]
async fn t_get_player(#[case] client_type: ClientType) {
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);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,7 +3,7 @@ use fancy_regex::Regex;
use log::debug;
use once_cell::sync::Lazy;
use reqwest::Client;
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
use std::result::Result::Ok;
use crate::util;
@ -43,7 +43,7 @@ impl Deobfuscator {
sig_fn,
sts,
},
})
})
}
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)]
async fn t_update() {
let client = Client::new();
let deobf = Deobfuscator::new(client)
.await
.unwrap();
let deobf = Deobfuscator::new(client).await.unwrap();
let deobf_sig = deobf.deobfuscate_sig("GOqGOqGOq0QJ8wRAIgaryQHfplJ9xJSKFywyaSMHuuwZYsoMTAvRvfm51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5fb5i").unwrap();
println!("{}", deobf_sig);

View file

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

View file

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