refactor: convert _or_bail macros to ok_or functions

This commit is contained in:
ThetaDev 2022-10-18 19:09:16 +02:00
parent 9aafb84e0f
commit b5f6b7a174
13 changed files with 274 additions and 282 deletions

View file

@ -1,3 +1,5 @@
use std::borrow::Cow;
use serde::Serialize; use serde::Serialize;
use url::Url; use url::Url;
@ -15,7 +17,7 @@ use super::{response, ClientType, MapResponse, RustyPipeQuery, YTContext};
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct QChannel<'a> { struct QChannel<'a> {
context: YTContext, context: YTContext<'a>,
browse_id: &'a str, browse_id: &'a str,
params: Params, params: Params,
} }
@ -257,11 +259,11 @@ fn map_vanity_url(url: &str, id: &str) -> Option<String> {
return None; return None;
} }
let mut parsed_url = ok_or_bail!(Url::parse(url), None); Url::parse(url).ok().map(|mut parsed_url| {
// The vanity URL from YouTube is http for some reason
// The vanity URL from YouTube is http for some reason let _ = parsed_url.set_scheme("https");
let _ = parsed_url.set_scheme("https"); parsed_url.to_string()
Some(parsed_url.to_string()) })
} }
fn map_channel<T>( fn map_channel<T>(
@ -272,13 +274,17 @@ fn map_channel<T>(
id: &str, id: &str,
lang: Language, lang: Language,
) -> Result<Channel<T>, ExtractionError> { ) -> Result<Channel<T>, ExtractionError> {
let header = let header = header.ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
header.ok_or_else(|| ExtractionError::ContentUnavailable("channel not found".into()))?; "channel not found",
)))?;
let metadata = metadata let metadata = metadata
.ok_or_else(|| ExtractionError::ContentUnavailable("channel not found".into()))? .ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
"channel not found",
)))?
.channel_metadata_renderer; .channel_metadata_renderer;
let microformat = microformat let microformat = microformat.ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
.ok_or_else(|| ExtractionError::ContentUnavailable("channel not found".into()))?; "channel not found",
)))?;
if metadata.external_id != id { if metadata.external_id != id {
return Err(ExtractionError::WrongResult(format!( return Err(ExtractionError::WrongResult(format!(
@ -384,7 +390,9 @@ fn map_channel_content(
} }
}) })
.next() .next()
.ok_or_else(|| ExtractionError::InvalidData("could not extract content".into()))?; .ok_or(ExtractionError::InvalidData(Cow::Borrowed(
"could not extract content",
)))?;
if let Some(target_id) = target_id { if let Some(target_id) = target_id {
// YouTube falls back to the featured page if the channel does not have a "videos" tab. // YouTube falls back to the featured page if the channel does not have a "videos" tab.

View file

@ -15,8 +15,8 @@ mod video_details;
#[cfg_attr(docsrs, doc(cfg(feature = "rss")))] #[cfg_attr(docsrs, doc(cfg(feature = "rss")))]
mod channel_rss; mod channel_rss;
use std::fmt::Debug;
use std::sync::Arc; use std::sync::Arc;
use std::{borrow::Cow, fmt::Debug};
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use fancy_regex::Regex; use fancy_regex::Regex;
@ -69,29 +69,29 @@ impl ClientType {
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct YTContext { struct YTContext<'a> {
client: ClientInfo, client: ClientInfo<'a>,
/// only used on desktop /// only used on desktop
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
request: Option<RequestYT>, request: Option<RequestYT>,
user: User, user: User,
/// only used for the embedded player /// only used for the embedded player
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
third_party: Option<ThirdParty>, third_party: Option<ThirdParty<'a>>,
} }
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct ClientInfo { struct ClientInfo<'a> {
client_name: String, client_name: &'a str,
client_version: String, client_version: Cow<'a, str>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
client_screen: Option<String>, client_screen: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
device_model: Option<String>, device_model: Option<&'a str>,
platform: String, platform: &'a str,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
original_url: Option<String>, original_url: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
visitor_data: Option<String>, visitor_data: Option<String>,
hl: Language, hl: Language,
@ -124,21 +124,21 @@ struct User {
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct ThirdParty { struct ThirdParty<'a> {
embed_url: String, embed_url: &'a str,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct QBrowse { struct QBrowse<'a> {
context: YTContext, context: YTContext<'a>,
browse_id: String, browse_id: String,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct QContinuation<'a> { struct QContinuation<'a> {
context: YTContext, context: YTContext<'a>,
continuation: &'a str, continuation: &'a str,
} }
@ -531,11 +531,11 @@ impl RustyPipe {
) )
.await?; .await?;
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &swjs, 1).ok_or_else(|| { util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &swjs, 1).ok_or(
Error::from(ExtractionError::InvalidData( Error::Extraction(ExtractionError::InvalidData(Cow::Borrowed(
"Could not find desktop client version in sw.js".into(), "Could not find desktop client version in sw.js",
)) ))),
}) )
}; };
let from_html = async { let from_html = async {
@ -549,11 +549,11 @@ impl RustyPipe {
) )
.await?; .await?;
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &html, 1).ok_or_else(|| { util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &html, 1).ok_or(
Error::from(ExtractionError::InvalidData( Error::Extraction(ExtractionError::InvalidData(Cow::Borrowed(
"Could not find desktop client version in sw.js".into(), "Could not find desktop client version on html page",
)) ))),
}) )
}; };
match from_swjs.await { match from_swjs.await {
@ -578,11 +578,11 @@ impl RustyPipe {
) )
.await?; .await?;
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &swjs, 1).ok_or_else(|| { util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &swjs, 1).ok_or(
Error::from(ExtractionError::InvalidData( Error::Extraction(ExtractionError::InvalidData(Cow::Borrowed(
"Could not find music client version in sw.js".into(), "Could not find music client version in sw.js",
)) ))),
}) )
}; };
let from_html = async { let from_html = async {
@ -596,11 +596,11 @@ impl RustyPipe {
) )
.await?; .await?;
util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &html, 1).ok_or_else(|| { util::get_cg_from_regexes(CLIENT_VERSION_REGEXES.iter(), &html, 1).ok_or(
Error::from(ExtractionError::InvalidData( Error::Extraction(ExtractionError::InvalidData(Cow::Borrowed(
"Could not find music client version on html page".into(), "Could not find music client version on html page",
)) ))),
}) )
}; };
match from_swjs.await { match from_swjs.await {
@ -757,12 +757,12 @@ impl RustyPipeQuery {
match ctype { match ctype {
ClientType::Desktop => YTContext { ClientType::Desktop => YTContext {
client: ClientInfo { client: ClientInfo {
client_name: "WEB".to_owned(), client_name: "WEB",
client_version: self.client.get_desktop_client_version().await, client_version: Cow::Owned(self.client.get_desktop_client_version().await),
client_screen: None, client_screen: None,
device_model: None, device_model: None,
platform: "DESKTOP".to_owned(), platform: "DESKTOP",
original_url: Some("https://www.youtube.com/".to_owned()), original_url: Some("https://www.youtube.com/"),
visitor_data: None, visitor_data: None,
hl, hl,
gl, gl,
@ -773,12 +773,12 @@ impl RustyPipeQuery {
}, },
ClientType::DesktopMusic => YTContext { ClientType::DesktopMusic => YTContext {
client: ClientInfo { client: ClientInfo {
client_name: "WEB_REMIX".to_owned(), client_name: "WEB_REMIX",
client_version: self.client.get_music_client_version().await, client_version: Cow::Owned(self.client.get_music_client_version().await),
client_screen: None, client_screen: None,
device_model: None, device_model: None,
platform: "DESKTOP".to_owned(), platform: "DESKTOP",
original_url: Some("https://music.youtube.com/".to_owned()), original_url: Some("https://music.youtube.com/"),
visitor_data: None, visitor_data: None,
hl, hl,
gl, gl,
@ -789,11 +789,11 @@ impl RustyPipeQuery {
}, },
ClientType::TvHtml5Embed => YTContext { ClientType::TvHtml5Embed => YTContext {
client: ClientInfo { client: ClientInfo {
client_name: "TVHTML5_SIMPLY_EMBEDDED_PLAYER".to_owned(), client_name: "TVHTML5_SIMPLY_EMBEDDED_PLAYER",
client_version: TVHTML5_CLIENT_VERSION.to_owned(), client_version: Cow::Borrowed(TVHTML5_CLIENT_VERSION),
client_screen: Some("EMBED".to_owned()), client_screen: Some("EMBED"),
device_model: None, device_model: None,
platform: "TV".to_owned(), platform: "TV",
original_url: None, original_url: None,
visitor_data: None, visitor_data: None,
hl, hl,
@ -802,16 +802,16 @@ impl RustyPipeQuery {
request: Some(RequestYT::default()), request: Some(RequestYT::default()),
user: User::default(), user: User::default(),
third_party: Some(ThirdParty { third_party: Some(ThirdParty {
embed_url: "https://www.youtube.com/".to_owned(), embed_url: "https://www.youtube.com/",
}), }),
}, },
ClientType::Android => YTContext { ClientType::Android => YTContext {
client: ClientInfo { client: ClientInfo {
client_name: "ANDROID".to_owned(), client_name: "ANDROID",
client_version: MOBILE_CLIENT_VERSION.to_owned(), client_version: Cow::Borrowed(MOBILE_CLIENT_VERSION),
client_screen: None, client_screen: None,
device_model: None, device_model: None,
platform: "MOBILE".to_owned(), platform: "MOBILE",
original_url: None, original_url: None,
visitor_data: None, visitor_data: None,
hl, hl,
@ -823,11 +823,11 @@ impl RustyPipeQuery {
}, },
ClientType::Ios => YTContext { ClientType::Ios => YTContext {
client: ClientInfo { client: ClientInfo {
client_name: "IOS".to_owned(), client_name: "IOS",
client_version: MOBILE_CLIENT_VERSION.to_owned(), client_version: Cow::Borrowed(MOBILE_CLIENT_VERSION),
client_screen: None, client_screen: None,
device_model: Some(IOS_DEVICE_MODEL.to_owned()), device_model: Some(IOS_DEVICE_MODEL),
platform: "MOBILE".to_owned(), platform: "MOBILE",
original_url: None, original_url: None,
visitor_data: None, visitor_data: None,
hl, hl,

View file

@ -1,3 +1,5 @@
use std::borrow::Cow;
use crate::error::{Error, ExtractionError}; use crate::error::{Error, ExtractionError};
use crate::model::{Comment, Paginator, PlaylistVideo, YouTubeItem}; use crate::model::{Comment, Paginator, PlaylistVideo, YouTubeItem};
use crate::param::ContinuationEndpoint; use crate::param::ContinuationEndpoint;
@ -52,14 +54,13 @@ impl<T: TryFrom<YouTubeItem>> MapResponse<Paginator<T>> for response::Continuati
_deobf: Option<&crate::deobfuscate::Deobfuscator>, _deobf: Option<&crate::deobfuscate::Deobfuscator>,
) -> Result<MapResult<Paginator<T>>, ExtractionError> { ) -> Result<MapResult<Paginator<T>>, ExtractionError> {
let mut actions = self.on_response_received_actions; let mut actions = self.on_response_received_actions;
let items = some_or_bail!( let items = actions
actions.try_swap_remove(0), .try_swap_remove(0)
Err(ExtractionError::InvalidData( .ok_or(ExtractionError::InvalidData(Cow::Borrowed(
"no item section renderer".into() "no item section renderer",
)) )))?
) .append_continuation_items_action
.append_continuation_items_action .continuation_items;
.continuation_items;
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(lang); let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(lang);
mapper.map_response(items); mapper.map_response(items);

View file

@ -27,7 +27,7 @@ use super::{
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct QPlayer<'a> { struct QPlayer<'a> {
context: YTContext, context: YTContext<'a>,
/// Website playback context /// Website playback context
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
playback_context: Option<QPlaybackContext>, playback_context: Option<QPlaybackContext>,
@ -79,14 +79,11 @@ impl RustyPipeQuery {
video_id: &str, video_id: &str,
client_type: ClientType, client_type: ClientType,
) -> Result<VideoPlayer, Error> { ) -> Result<VideoPlayer, Error> {
let q1 = self.clone(); let (context, deobf) = tokio::join!(
let t_context = tokio::spawn(async move { q1.get_context(client_type, false).await }); self.get_context(client_type, false),
let q2 = self.client.clone(); self.client.get_deobf()
let t_deobf = tokio::spawn(async move { q2.get_deobf().await }); );
let deobf = deobf?;
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 {
@ -168,14 +165,16 @@ impl MapResponse<VideoPlayer> for response::Player {
} }
}; };
let mut streaming_data = some_or_bail!( let mut streaming_data =
self.streaming_data, self.streaming_data
Err(ExtractionError::InvalidData("no streaming data".into())) .ok_or(ExtractionError::InvalidData(Cow::Borrowed(
); "no streaming data",
let video_details = some_or_bail!( )))?;
self.video_details, let video_details =
Err(ExtractionError::InvalidData("no video details".into())) self.video_details
); .ok_or(ExtractionError::InvalidData(Cow::Borrowed(
"no video details",
)))?;
if video_details.video_id != id { if video_details.video_id != id {
return Err(ExtractionError::WrongResult(format!( return Err(ExtractionError::WrongResult(format!(
@ -294,12 +293,11 @@ fn cipher_to_url_params(
// `sp`: Signature parameter // `sp`: Signature parameter
// `url`: URL that is missing the signature parameter // `url`: URL that is missing the signature parameter
let sig = some_or_bail!(params.get("s"), Err(DeobfError::Extraction("s param"))); let sig = params.get("s").ok_or(DeobfError::Extraction("s param"))?;
let sp = some_or_bail!(params.get("sp"), Err(DeobfError::Extraction("sp param"))); let sp = params.get("sp").ok_or(DeobfError::Extraction("sp param"))?;
let raw_url = some_or_bail!( let raw_url = params
params.get("url"), .get("url")
Err(DeobfError::Extraction("no url param")) .ok_or(DeobfError::Extraction("no url param"))?;
);
let (url_base, mut url_params) = let (url_base, mut url_params) =
util::url_to_params(raw_url).or(Err(DeobfError::Extraction("url params")))?; util::url_to_params(raw_url).or(Err(DeobfError::Extraction("url params")))?;

View file

@ -1,4 +1,4 @@
use std::convert::TryFrom; use std::{borrow::Cow, convert::TryFrom};
use crate::{ use crate::{
deobfuscate::Deobfuscator, deobfuscate::Deobfuscator,
@ -63,42 +63,40 @@ impl MapResponse<Playlist> for response::Playlist {
}; };
let mut tcbr_contents = contents.two_column_browse_results_renderer.contents; let mut tcbr_contents = contents.two_column_browse_results_renderer.contents;
let video_items = some_or_bail!(
some_or_bail!( let video_items = tcbr_contents
some_or_bail!( .try_swap_remove(0)
tcbr_contents.try_swap_remove(0), .ok_or(ExtractionError::InvalidData(Cow::Borrowed(
Err(ExtractionError::InvalidData( "twoColumnBrowseResultsRenderer empty",
"twoColumnBrowseResultsRenderer empty".into() )))?
)) .tab_renderer
) .content
.tab_renderer .section_list_renderer
.content .contents
.section_list_renderer .try_swap_remove(0)
.contents .ok_or(ExtractionError::InvalidData(Cow::Borrowed(
.try_swap_remove(0), "sectionListRenderer empty",
Err(ExtractionError::InvalidData( )))?
"sectionListRenderer empty".into()
))
)
.item_section_renderer .item_section_renderer
.contents .contents
.try_swap_remove(0), .try_swap_remove(0)
Err(ExtractionError::InvalidData( .ok_or(ExtractionError::InvalidData(Cow::Borrowed(
"itemSectionRenderer empty".into() "itemSectionRenderer empty",
)) )))?
) .playlist_video_list_renderer
.playlist_video_list_renderer .contents;
.contents;
let (videos, ctoken) = map_playlist_items(video_items.c); let (videos, ctoken) = map_playlist_items(video_items.c);
let (thumbnails, last_update_txt) = match self.sidebar { let (thumbnails, last_update_txt) = match self.sidebar {
Some(sidebar) => { Some(sidebar) => {
let mut sidebar_items = sidebar.playlist_sidebar_renderer.items; let mut sidebar_items = sidebar.playlist_sidebar_renderer.items;
let mut primary = some_or_bail!( let mut primary =
sidebar_items.try_swap_remove(0), sidebar_items
Err(ExtractionError::InvalidData("no primary sidebar".into())) .try_swap_remove(0)
); .ok_or(ExtractionError::InvalidData(Cow::Borrowed(
"no primary sidebar",
)))?;
( (
primary primary
@ -113,10 +111,12 @@ impl MapResponse<Playlist> for response::Playlist {
) )
} }
None => { None => {
let header_banner = some_or_bail!( let header_banner = header
header.playlist_header_renderer.playlist_header_banner, .playlist_header_renderer
Err(ExtractionError::InvalidData("no thumbnail found".into())) .playlist_header_banner
); .ok_or(ExtractionError::InvalidData(Cow::Borrowed(
"no thumbnail found",
)))?;
let mut byline = header.playlist_header_renderer.byline; let mut byline = header.playlist_header_renderer.byline;
let last_update_txt = byline let last_update_txt = byline
@ -131,12 +131,8 @@ impl MapResponse<Playlist> for response::Playlist {
}; };
let n_videos = match ctoken { let n_videos = match ctoken {
Some(_) => { Some(_) => util::parse_numeric(&header.playlist_header_renderer.num_videos_text)
ok_or_bail!( .map_err(|_| ExtractionError::InvalidData(Cow::Borrowed("no video count")))?,
util::parse_numeric(&header.playlist_header_renderer.num_videos_text),
Err(ExtractionError::InvalidData("no video count".into()))
)
}
None => videos.len() as u64, None => videos.len() as u64,
}; };
@ -187,7 +183,9 @@ impl MapResponse<Paginator<PlaylistVideo>> for response::PlaylistCont {
let mut actions = self.on_response_received_actions; let mut actions = self.on_response_received_actions;
let action = actions let action = actions
.try_swap_remove(0) .try_swap_remove(0)
.ok_or_else(|| ExtractionError::InvalidData("no onResponseReceivedAction".into()))?; .ok_or(ExtractionError::InvalidData(Cow::Borrowed(
"no onResponseReceivedAction",
)))?;
let (items, ctoken) = let (items, ctoken) =
map_playlist_items(action.append_continuation_items_action.continuation_items.c); map_playlist_items(action.append_continuation_items_action.continuation_items.c);

View file

@ -1,3 +1,5 @@
use std::borrow::Cow;
use serde::{de::IgnoredAny, Serialize}; use serde::{de::IgnoredAny, Serialize};
use crate::{ use crate::{
@ -12,7 +14,7 @@ use super::{response, ClientType, MapResponse, MapResult, RustyPipeQuery, YTCont
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct QSearch<'a> { struct QSearch<'a> {
context: YTContext, context: YTContext<'a>,
query: &'a str, query: &'a str,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
params: Option<String>, params: Option<String>,
@ -69,11 +71,11 @@ impl RustyPipeQuery {
.http_request_txt(self.client.inner.http.get(url).build()?) .http_request_txt(self.client.inner.http.get(url).build()?)
.await?; .await?;
let trimmed = response.get(5..).ok_or_else(|| { let trimmed = response
Error::Extraction(ExtractionError::InvalidData( .get(5..)
"could not get string slice".into(), .ok_or(Error::Extraction(ExtractionError::InvalidData(
)) Cow::Borrowed("could not get string slice"),
})?; )))?;
let parsed = serde_json::from_str::<( let parsed = serde_json::from_str::<(
IgnoredAny, IgnoredAny,

View file

@ -1,3 +1,5 @@
use std::borrow::Cow;
use crate::{ use crate::{
error::{Error, ExtractionError}, error::{Error, ExtractionError},
model::{Paginator, VideoItem}, model::{Paginator, VideoItem},
@ -54,7 +56,7 @@ impl MapResponse<Paginator<VideoItem>> for response::Startpage {
let mut contents = self.contents.two_column_browse_results_renderer.tabs; let mut contents = self.contents.two_column_browse_results_renderer.tabs;
let grid = contents let grid = contents
.try_swap_remove(0) .try_swap_remove(0)
.ok_or_else(|| ExtractionError::InvalidData("no contents".into()))? .ok_or(ExtractionError::InvalidData(Cow::Borrowed("no contents")))?
.tab_renderer .tab_renderer
.content .content
.section_list_renderer .section_list_renderer
@ -78,7 +80,7 @@ impl MapResponse<Vec<VideoItem>> for response::Trending {
let mut contents = self.contents.two_column_browse_results_renderer.tabs; let mut contents = self.contents.two_column_browse_results_renderer.tabs;
let items = contents let items = contents
.try_swap_remove(0) .try_swap_remove(0)
.ok_or_else(|| ExtractionError::InvalidData("no contents".into()))? .ok_or(ExtractionError::InvalidData(Cow::Borrowed("no contents")))?
.tab_renderer .tab_renderer
.content .content
.section_list_renderer .section_list_renderer

View file

@ -1,3 +1,5 @@
use std::borrow::Cow;
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
@ -12,8 +14,8 @@ use super::{response, ClientType, MapResponse, RustyPipeQuery, YTContext};
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct QResolveUrl { struct QResolveUrl<'a> {
context: YTContext, context: YTContext<'a>,
url: String, url: String,
} }
@ -28,7 +30,7 @@ impl RustyPipeQuery {
}); });
let mut path_split = url let mut path_split = url
.path_segments() .path_segments()
.ok_or_else(|| Error::Other("invalid url: empty path".into()))?; .ok_or(Error::Other(Cow::Borrowed("invalid url: empty path")))?;
let get_start_time = || { let get_start_time = || {
params params
@ -41,7 +43,7 @@ impl RustyPipeQuery {
Some("watch") => { Some("watch") => {
let id = params let id = params
.get("v") .get("v")
.ok_or_else(|| Error::Other("invalid url: no video id".into()))? .ok_or(Error::Other(Cow::Borrowed("invalid url: no video id")))?
.to_string(); .to_string();
Ok(UrlTarget::Video { Ok(UrlTarget::Video {
@ -56,7 +58,7 @@ impl RustyPipeQuery {
Some("playlist") => { Some("playlist") => {
let id = params let id = params
.get("list") .get("list")
.ok_or_else(|| Error::Other("invalid url: no playlist id".into()))? .ok_or(Error::Other(Cow::Borrowed("invalid url: no playlist id")))?
.to_string(); .to_string();
Ok(UrlTarget::Playlist { id }) Ok(UrlTarget::Playlist { id })
@ -190,14 +192,16 @@ impl MapResponse<UrlTarget> for response::ResolvedUrl {
let page_type = self let page_type = self
.endpoint .endpoint
.command_metadata .command_metadata
.ok_or_else(|| ExtractionError::InvalidData("No command metadata".into()))? .ok_or(ExtractionError::InvalidData(Cow::Borrowed(
"No command metadata",
)))?
.web_command_metadata .web_command_metadata
.web_page_type; .web_page_type;
let id = self let id = self
.endpoint .endpoint
.browse_endpoint .browse_endpoint
.ok_or_else(|| ExtractionError::InvalidData("No browse ID".into()))? .ok_or(ExtractionError::InvalidData(Cow::Borrowed("No browse ID")))?
.browse_id; .browse_id;
Ok(MapResult { Ok(MapResult {

View file

@ -1,3 +1,5 @@
use std::borrow::Cow;
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
@ -16,7 +18,7 @@ use super::{
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct QVideo<'a> { struct QVideo<'a> {
context: YTContext, context: YTContext<'a>,
/// YouTube video ID /// YouTube video ID
video_id: &'a str, video_id: &'a str,
/// Set to true to allow extraction of streams with sensitive content /// Set to true to allow extraction of streams with sensitive content
@ -79,10 +81,14 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
let contents = self let contents = self
.contents .contents
.ok_or_else(|| ExtractionError::ContentUnavailable("Video not found".into()))?; .ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
let current_video_endpoint = self "Video not found",
.current_video_endpoint )))?;
.ok_or_else(|| ExtractionError::ContentUnavailable("Video not found".into()))?; let current_video_endpoint =
self.current_video_endpoint
.ok_or(ExtractionError::ContentUnavailable(Cow::Borrowed(
"Video not found",
)))?;
let video_id = current_video_endpoint.watch_endpoint.video_id; let video_id = current_video_endpoint.watch_endpoint.video_id;
if id != video_id { if id != video_id {

View file

@ -92,48 +92,44 @@ fn get_sig_fn(player_js: &str) -> Result<String> {
let function_pattern_str = let function_pattern_str =
"(".to_owned() + &dfunc_name.replace('$', "\\$") + "=function\\([a-zA-Z0-9_]+\\)\\{.+?\\})"; "(".to_owned() + &dfunc_name.replace('$', "\\$") + "=function\\([a-zA-Z0-9_]+\\)\\{.+?\\})";
let function_pattern = ok_or_bail!( let function_pattern = Regex::new(&function_pattern_str)
Regex::new(&function_pattern_str), .map_err(|_| DeobfError::Other("could not parse function pattern regex"))?;
Err(DeobfError::Other("could not parse function pattern regex"))
);
let deobfuscate_function = "var ".to_owned() let deobfuscate_function = "var ".to_owned()
+ some_or_bail!( + function_pattern
function_pattern.captures(player_js).ok().flatten(), .captures(player_js)
Err(DeobfError::Extraction("deobf function")) .ok()
) .flatten()
.get(1) .ok_or(DeobfError::Extraction("deobf function"))?
.unwrap() .get(1)
.as_str() .unwrap()
.as_str()
+ ";"; + ";";
static HELPER_OBJECT_NAME_REGEX: Lazy<Regex> = static HELPER_OBJECT_NAME_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(";([A-Za-z0-9_\\$]{2})\\...\\(").unwrap()); Lazy::new(|| Regex::new(";([A-Za-z0-9_\\$]{2})\\...\\(").unwrap());
let helper_object_name = some_or_bail!( let helper_object_name = HELPER_OBJECT_NAME_REGEX
HELPER_OBJECT_NAME_REGEX .captures(&deobfuscate_function)
.captures(&deobfuscate_function) .ok()
.ok() .flatten()
.flatten(), .ok_or(DeobfError::Extraction("helper object name"))?
Err(DeobfError::Extraction("helper object name")) .get(1)
) .unwrap()
.get(1) .as_str();
.unwrap()
.as_str();
let helper_pattern_str = let helper_pattern_str =
"(var ".to_owned() + &helper_object_name.replace('$', "\\$") + "=\\{.+?\\}\\};)"; "(var ".to_owned() + &helper_object_name.replace('$', "\\$") + "=\\{.+?\\}\\};)";
let helper_pattern = ok_or_bail!( let helper_pattern = Regex::new(&helper_pattern_str)
Regex::new(&helper_pattern_str), .map_err(|_| DeobfError::Other("could not parse helper pattern regex"))?;
Err(DeobfError::Other("could not parse helper pattern regex"))
);
let player_js_nonl = player_js.replace('\n', ""); let player_js_nonl = player_js.replace('\n', "");
let helper_object = some_or_bail!( let helper_object = helper_pattern
helper_pattern.captures(&player_js_nonl).ok().flatten(), .captures(&player_js_nonl)
Err(DeobfError::Extraction("helper object")) .ok()
) .flatten()
.get(1) .ok_or(DeobfError::Extraction("helper object"))?
.unwrap() .get(1)
.as_str(); .unwrap()
.as_str();
Ok(helper_object.to_owned() + &deobfuscate_function + &caller_function(&dfunc_name)) Ok(helper_object.to_owned() + &deobfuscate_function + &caller_function(&dfunc_name))
} }
@ -156,10 +152,11 @@ fn get_nsig_fn_name(player_js: &str) -> Result<String> {
.unwrap() .unwrap()
}); });
let fname_match = some_or_bail!( let fname_match = FUNCTION_NAME_REGEX
FUNCTION_NAME_REGEX.captures(player_js).ok().flatten(), .captures(player_js)
Err(DeobfError::Extraction("n_deobf function")) .ok()
); .flatten()
.ok_or(DeobfError::Extraction("n_deobf function"))?;
let function_name = fname_match.get(1).unwrap().as_str(); let function_name = fname_match.get(1).unwrap().as_str();
@ -179,18 +176,19 @@ fn get_nsig_fn_name(player_js: &str) -> Result<String> {
"could not parse helper pattern regex", "could not parse helper pattern regex",
)))?; )))?;
let array_str = some_or_bail!( let array_str = array_pattern
array_pattern.captures(player_js).ok().flatten(), .captures(player_js)
Err(DeobfError::Extraction("n_deobf array_str")) .ok()
) .flatten()
.get(1) .ok_or(DeobfError::Extraction("n_deobf array_str"))?
.unwrap() .get(1)
.as_str(); .unwrap()
.as_str();
let mut names = array_str.split(','); let mut names = array_str.split(',');
let name = some_or_bail!( let name = names
names.nth(array_num), .nth(array_num)
Err(DeobfError::Extraction("n_deobf function name")) .ok_or(DeobfError::Extraction("n_deobf function name"))?;
);
Ok(name.to_owned()) Ok(name.to_owned())
} }
@ -279,13 +277,14 @@ async fn get_player_js_url(http: &Client) -> Result<String> {
Regex::new(r#"https:\\\/\\\/www\.youtube\.com\\\/s\\\/player\\\/([a-z0-9]{8})\\\/"#) Regex::new(r#"https:\\\/\\\/www\.youtube\.com\\\/s\\\/player\\\/([a-z0-9]{8})\\\/"#)
.unwrap() .unwrap()
}); });
let player_hash = some_or_bail!( let player_hash = PLAYER_HASH_PATTERN
PLAYER_HASH_PATTERN.captures(&text).ok().flatten(), .captures(&text)
Err(DeobfError::Extraction("player hash")) .ok()
) .flatten()
.get(1) .ok_or(DeobfError::Extraction("player hash"))?
.unwrap() .get(1)
.as_str(); .unwrap()
.as_str();
Ok(format!( Ok(format!(
"https://www.youtube.com/s/player/{}/player_ias.vflset/en_US/base.js", "https://www.youtube.com/s/player/{}/player_ias.vflset/en_US/base.js",
@ -302,14 +301,15 @@ fn get_sts(player_js: &str) -> Result<String> {
static STS_PATTERN: Lazy<Regex> = static STS_PATTERN: Lazy<Regex> =
Lazy::new(|| Regex::new("signatureTimestamp[=:](\\d+)").unwrap()); Lazy::new(|| Regex::new("signatureTimestamp[=:](\\d+)").unwrap());
Ok(some_or_bail!( Ok(STS_PATTERN
STS_PATTERN.captures(player_js).ok().flatten(), .captures(player_js)
Err(DeobfError::Extraction("sts")) .ok()
) .flatten()
.get(1) .ok_or(DeobfError::Extraction("sts"))?
.unwrap() .get(1)
.as_str() .unwrap()
.to_owned()) .as_str()
.to_owned())
} }
#[cfg(test)] #[cfg(test)]

View file

@ -1,6 +1,6 @@
//! YouTube audio/video downloader //! YouTube audio/video downloader
use std::{cmp::Ordering, ffi::OsString, ops::Range, path::PathBuf}; use std::{borrow::Cow, cmp::Ordering, ffi::OsString, ops::Range, path::PathBuf};
use fancy_regex::Regex; use fancy_regex::Regex;
use futures::stream::{self, StreamExt}; use futures::stream::{self, StreamExt};
@ -45,16 +45,15 @@ fn get_download_range(offset: u64, size: Option<u64>) -> Range<u64> {
fn parse_cr_header(cr_header: &str) -> Result<(u64, u64)> { fn parse_cr_header(cr_header: &str) -> Result<(u64, u64)> {
static PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(r#"bytes (\d+)-(\d+)/(\d+)"#).unwrap()); static PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(r#"bytes (\d+)-(\d+)/(\d+)"#).unwrap());
let captures = some_or_bail!( let captures = PATTERN.captures(cr_header).ok().flatten().ok_or_else(|| {
PATTERN.captures(cr_header).ok().flatten(), DownloadError::Progressive(
Err(DownloadError::Progressive(
format!( format!(
"Content-Range header '{}' does not match pattern", "Content-Range header '{}' does not match pattern",
cr_header cr_header
) )
.into() .into(),
)) )
); })?;
Ok(( Ok((
captures.get(2).unwrap().as_str().parse().map_err(|_| { captures.get(2).unwrap().as_str().parse().map_err(|_| {
@ -106,16 +105,18 @@ async fn download_single_file<P: Into<PathBuf>>(
.await? .await?
.error_for_status()?; .error_for_status()?;
let cr_header = some_or_bail!( let cr_header = res
res.headers().get(header::CONTENT_RANGE), .headers()
Err(DownloadError::Progressive( .get(header::CONTENT_RANGE)
"Did not get Content-Range header".into() .ok_or(DownloadError::Progressive(Cow::Borrowed(
)) "Did not get Content-Range header",
) )))?
.to_str() .to_str()
.map_err(|_| { .map_err(|_| {
DownloadError::Progressive("could not convert Content-Range header to string".into()) DownloadError::Progressive(
})?; "could not convert Content-Range header to string".into(),
)
})?;
let (_, original_size) = parse_cr_header(cr_header)?; let (_, original_size) = parse_cr_header(cr_header)?;
@ -193,16 +194,18 @@ async fn download_chunks_by_header(
.error_for_status()?; .error_for_status()?;
// Content-Range: bytes 0-100/451368980 // Content-Range: bytes 0-100/451368980
let cr_header = some_or_bail!( let cr_header = res
res.headers().get(header::CONTENT_RANGE), .headers()
Err(DownloadError::Progressive( .get(header::CONTENT_RANGE)
"Did not get Content-Range header".into() .ok_or(DownloadError::Progressive(Cow::Borrowed(
)) "Did not get Content-Range header",
) )))?
.to_str() .to_str()
.map_err(|_| { .map_err(|_| {
DownloadError::Progressive("could not convert Content-Range header to string".into()) DownloadError::Progressive(
})?; "could not convert Content-Range header to string".into(),
)
})?;
let (parsed_offset, parsed_size) = parse_cr_header(cr_header)?; let (parsed_offset, parsed_size) = parse_cr_header(cr_header)?;

View file

@ -1,5 +1,4 @@
/// Returns an unwrapped Option if Some() otherwise returns the passed expression /// Returns an unwrapped Option if Some() otherwise returns the passed expression
#[allow(unused_macros)]
macro_rules! some_or_bail { macro_rules! some_or_bail {
($opt:expr, $ret:expr $(,)?) => {{ ($opt:expr, $ret:expr $(,)?) => {{
match $opt { match $opt {
@ -11,21 +10,7 @@ macro_rules! some_or_bail {
}}; }};
} }
/// Returns an unwrapped Option if Some() otherwise continues a loop.
#[allow(unused_macros)]
macro_rules! some_or_continue {
($opt:expr $(,)?) => {{
match $opt {
Some(stuff) => stuff,
None => {
continue;
}
}
}};
}
/// Returns an unwrapped Result if Ok() otherwise returns the passed expression /// Returns an unwrapped Result if Ok() otherwise returns the passed expression
#[allow(unused_macros)]
macro_rules! ok_or_bail { macro_rules! ok_or_bail {
($result:expr, $ret:expr $(,)?) => {{ ($result:expr, $ret:expr $(,)?) => {{
match $result { match $result {
@ -36,15 +21,3 @@ macro_rules! ok_or_bail {
} }
}}; }};
} }
#[allow(unused_macros)]
macro_rules! ok_or_continue {
($opt:expr $(,)?) => {{
match $opt {
Ok(stuff) => stuff,
Err(_) => {
continue;
}
}
}};
}

View file

@ -304,13 +304,10 @@ impl VideoPlayer {
(Some(video_stream), None) => (Some(video_stream), None), (Some(video_stream), None) => (Some(video_stream), None),
(Some(video_stream), Some(video_only_stream)) => match video_only_stream > video_stream (Some(video_stream), Some(video_only_stream)) => match video_only_stream > video_stream
{ {
true => ( true => match self.select_audio_stream(filter) {
Some(video_only_stream), Some(audio_stream) => (Some(video_only_stream), Some(audio_stream)),
Some(some_or_bail!( None => (Some(video_stream), None),
self.select_audio_stream(filter), },
(Some(video_stream), None)
)),
),
false => (Some(video_stream), None), false => (Some(video_stream), None),
}, },
} }