diff --git a/src/info.rs b/src/info.rs index 2fc9e6b..7dac2c0 100644 --- a/src/info.rs +++ b/src/info.rs @@ -148,10 +148,11 @@ impl Video { ) .as_str(), ) - .unwrap(); + .unwrap_or_default(); let initial_response = - serde_json::from_str::(initial_response_string.trim()).unwrap(); + serde_json::from_str::(initial_response_string.trim()) + .unwrap_or_default(); (player_response, initial_response) }; @@ -170,7 +171,7 @@ impl Video { let embed_ytconfig = self.get_embeded_ytconfig(&response).await?; let player_response_new = - serde_json::from_str::(&embed_ytconfig).unwrap(); + serde_json::from_str::(&embed_ytconfig).unwrap_or_default(); player_response.streaming_data = player_response_new.streaming_data; player_response.storyboards = player_response_new.storyboards; @@ -183,7 +184,7 @@ impl Video { let video_details = clean_video_details( &initial_response, &player_response, - get_media(&initial_response).unwrap(), + get_media(&initial_response).unwrap_or_default(), self.video_id.clone(), ); @@ -203,7 +204,11 @@ impl Video { formats: { parse_video_formats( &player_response, - get_functions(get_html5player(response.as_str()).unwrap(), client).await?, + get_functions( + get_html5player(response.as_str()).unwrap_or_default(), + client, + ) + .await?, ) .unwrap_or_default() }, @@ -551,16 +556,17 @@ async fn get_m3u8( let body = get_html(client, &url, None).await?; - let http_regex = regex::Regex::new(r"^https?://").unwrap(); - let itag_regex = regex::Regex::new(r"/itag/(\d+)/").unwrap(); + static HTTP_REGEX: Lazy = Lazy::new(|| regex::Regex::new(r"^https?://").unwrap()); + static ITAG_REGEX: Lazy = + Lazy::new(|| regex::Regex::new(r"/itag/(\d+)/").unwrap()); let itag_and_url = body .split('\n') - .filter(|x| http_regex.is_match(x) && itag_regex.is_match(x)); + .filter(|x| HTTP_REGEX.is_match(x) && ITAG_REGEX.is_match(x)); let itag_and_url: Vec<(String, String)> = itag_and_url .map(|line| { - let itag = itag_regex + let itag = ITAG_REGEX .captures(line) .expect("IMPOSSIBLE") .get(1) diff --git a/src/info_extras.rs b/src/info_extras.rs index 1c473a8..530d0d8 100644 --- a/src/info_extras.rs +++ b/src/info_extras.rs @@ -1,3 +1,4 @@ +use once_cell::sync::Lazy; use regex::Regex; use serde::{Deserialize, Serialize}; use serde_json::{from_str, json, map::Map, Value}; @@ -13,11 +14,9 @@ pub fn get_related_videos(info: &Value) -> Option> { let mut secondary_results: Vec = vec![]; let mut rvs_params_closure = || -> Result<(), &str> { - rvs_params = info - .as_object() - .and_then(|x| x.get("webWatchNextResponseExtensionData")) - .and_then(|x| x.get("relatedVideoArgs")) - .and_then(|x| x.as_str().map(|c| c.split(',').collect::>())) + rvs_params = info["webWatchNextResponseExtensionData"]["relatedVideoArgs"] + .as_str() + .map(|c| c.split(',').collect::>()) .unwrap_or_default(); Ok(()) }; @@ -27,15 +26,12 @@ pub fn get_related_videos(info: &Value) -> Option> { } let mut secondary_results_closure = || -> Result<(), &str> { - secondary_results = info - .as_object() - .and_then(|x| x.get("contents")) - .and_then(|x| x.get("twoColumnWatchNextResults")) - .and_then(|x| x.get("secondaryResults")) - .and_then(|x| x.get("secondaryResults")) - .and_then(|x| x.get("results")) - .and_then(|x| Some(x.as_array()?.to_vec())) - .unwrap_or_default(); + secondary_results = info["contents"]["twoColumnWatchNextResults"]["secondaryResults"] + ["secondaryResults"]["results"] + .as_array() + .cloned() + .unwrap_or_default() + .to_vec(); Ok(()) }; @@ -43,52 +39,37 @@ pub fn get_related_videos(info: &Value) -> Option> { secondary_results = vec![]; } - let contents_fallback: Vec = vec![]; - let fallback_value = Map::new(); - let mut videos: Vec = vec![]; for result in secondary_results { - let details = result - .as_object() - .and_then(|x| { - x.get("compactVideoRenderer") - .map(|c| c.as_object().unwrap()) - }) - .unwrap_or(&fallback_value); - - if !details.is_empty() { - let video = parse_related_video(details, &rvs_params); + let details = result.as_object().and_then(|x| { + x.get("compactVideoRenderer") + .map(|c| c.as_object().cloned().unwrap_or_default()) + }); - if let Some(video_some) = video { + if let Some(details) = details { + if let Some(video_some) = parse_related_video(&details, &rvs_params) { videos.push(video_some) } - } else { - let autoplay = result - .as_object() - .and_then(|x| x.get("compactAutoplayRenderer").and_then(|c| c.as_object())) - .unwrap_or(&fallback_value); - - if !autoplay.contains_key("contents") { - continue; - }; - + } else if let Some(autoplay) = result.as_object().and_then(|x| { + x.get("compactAutoplayRenderer") + .map(|c| c.as_object().cloned().unwrap_or_default()) + }) { let contents = autoplay .get("contents") - .and_then(|x| x.as_array()) - .unwrap_or(&contents_fallback); - - for content in contents { - let content_details = content - .get("compactVideoRenderer") - .and_then(|x| x.as_object()) - .unwrap_or(&fallback_value); - if content_details.is_empty() { - continue; - } - - let video = parse_related_video(content_details, &rvs_params); - if let Some(video_some) = video { - videos.push(video_some) + .map(|x| x.as_array().cloned().unwrap_or_default()); + + if let Some(contents) = contents { + for content in contents { + let content_details = content + .get("compactVideoRenderer") + .map(|x| x.as_object().cloned().unwrap_or_default()); + + if let Some(content_details) = content_details { + let video = parse_related_video(&content_details, &rvs_params); + if let Some(video_some) = video { + videos.push(video_some) + } + } } } } @@ -107,21 +88,22 @@ pub fn parse_related_video( short_view_count_text: String, length_seconds: String, } - let mut view_count = if details.contains_key("viewCountText") { - get_text(&details["viewCountText"]).as_str().unwrap_or("") - } else { - "0" - }; - - let mut short_view_count = if details.contains_key("shortViewCountText") { - get_text(&details["shortViewCountText"]) - .as_str() - .unwrap_or("") - .to_string() + let mut view_count = if let Some(view_count_text) = details.get("viewCountText") { + get_text(view_count_text).as_str().unwrap_or("").to_string() } else { "0".to_string() }; + let mut short_view_count = + if let Some(short_view_count_text) = details.get("shortViewCountText") { + get_text(short_view_count_text) + .as_str() + .unwrap_or("") + .to_string() + } else { + "0".to_string() + }; + let first = |string: &str| { string .chars() @@ -134,7 +116,12 @@ pub fn parse_related_video( let rvs_details_index = rvs_params .iter() .map(|x| serde_qs::from_str::(x).unwrap()) - .position(|r| r.id == *details["videoId"].as_str().unwrap_or("0")); + .position(|r| { + r.id == *details + .get("videoId") + .and_then(|x| x.as_str()) + .unwrap_or("0") + }); if let Some(rvs_details_index_some) = rvs_details_index { let rvs_params_to_short_view_count = rvs_params @@ -144,17 +131,18 @@ pub fn parse_related_video( short_view_count = serde_qs::from_str::(rvs_params_to_short_view_count) .map(|x| x.short_view_count_text) - .unwrap_or("0".to_string()) + .unwrap_or("0".to_string()); } } - view_count = if first(view_count) { + view_count = if first(&view_count) { view_count .split(' ') .collect::>() .first() .cloned() .unwrap_or("") + .to_string() } else { short_view_count .split(' ') @@ -162,6 +150,7 @@ pub fn parse_related_video( .first() .cloned() .unwrap_or("") + .to_string() }; let is_live = details @@ -170,10 +159,7 @@ pub fn parse_related_video( c.as_array() .map(|x| { x.iter() - .filter(|x| { - let json = json!(x); - json["metadataBadgeRenderer"]["label"] == "LIVE NOW" - }) + .filter(|x| x["metadataBadgeRenderer"]["label"] == "LIVE NOW") .count() > 0 }) @@ -181,25 +167,23 @@ pub fn parse_related_video( }) .unwrap_or(false); - let browse_end_point = - &details["shortBylineText"]["runs"][0]["navigationEndpoint"]["browseEndpoint"]; + let browse_end_point = &details + .get("shortBylineText") + .map(|x| x["runs"][0]["navigationEndpoint"]["browseEndpoint"].clone()) + .unwrap_or_default(); let channel_id = &browse_end_point["browseId"]; - let author_user = browse_end_point - .get("canonicalBaseUrl") - .map(|x| { - x.as_str() - .map(|c| { - c.split('/') - .collect::>() - .last() - .cloned() - .unwrap_or("") - }) + let author_user = browse_end_point["canonicalBaseUrl"] + .as_str() + .map(|c| { + c.split('/') + .collect::>() + .last() + .cloned() .unwrap_or("") }) .unwrap_or(""); - let view_count_regex = Regex::new(r",").unwrap(); + static VIEW_COUNT_REGEX: Lazy = Lazy::new(|| Regex::new(r",").unwrap()); let video = RelatedVideo { id: details @@ -207,20 +191,13 @@ pub fn parse_related_video( .and_then(|x| x.as_str()) .unwrap_or("") .to_string(), - title: if details.contains_key("title") { - get_text(&details["title"]) - .as_str() - .unwrap_or("") - .to_string() + title: if let Some(title) = details.get("title") { + get_text(title).as_str().unwrap_or("").to_string() } else { String::from("") }, - url: if details.contains_key("videoId") { - let id = details - .get("videoId") - .and_then(|x| x.as_str()) - .unwrap_or("") - .to_string(); + url: if let Some(video_id) = details.get("videoId") { + let id = video_id.as_str().unwrap_or("").to_string(); if !id.is_empty() { format!("{}{}", BASE_URL, id) } else { @@ -229,8 +206,8 @@ pub fn parse_related_video( } else { String::from("") }, - published: if details.contains_key("publishedTimeText") { - get_text(&details["publishedTimeText"]) + published: if let Some(published_time_text) = details.get("publishedTimeText") { + get_text(published_time_text) .as_str() .unwrap_or("") .to_string() @@ -240,11 +217,8 @@ pub fn parse_related_video( author: if !browse_end_point.is_null() { Some(Author { id: channel_id.as_str().unwrap_or("").to_string(), - name: if details.contains_key("shortBylineText") { - get_text(&details["shortBylineText"]) - .as_str() - .unwrap_or("") - .to_string() + name: if let Some(text) = details.get("shortBylineText") { + get_text(text).as_str().unwrap_or("").to_string() } else { String::from("") }, @@ -274,8 +248,11 @@ pub fn parse_related_video( } else { String::from("") }, - thumbnails: if !details["channelThumbnail"]["thumbnails"].is_null() { - details["channelThumbnail"]["thumbnails"] + thumbnails: if let Some(thumbnails) = details + .get("channelThumbnail") + .and_then(|x| x.get("thumbnails")) + { + thumbnails .as_array() .map(|f| { f.iter() @@ -316,8 +293,8 @@ pub fn parse_related_video( } else { vec![] }, - verified: if details.contains_key("ownerBadges") { - is_verified(&details["ownerBadges"]) + verified: if let Some(badges) = details.get("ownerBadges") { + is_verified(badges) } else { false }, @@ -332,16 +309,17 @@ pub fn parse_related_video( .first() .unwrap_or(&"") .to_string(), - view_count: view_count_regex.replace_all(view_count, "").to_string(), - length_seconds: if details.contains_key("lengthText") { - let time = (time_to_ms(get_text(&details["lengthText"]).as_str().unwrap_or("0")) / 1000) - as f32; + view_count: VIEW_COUNT_REGEX.replace_all(&view_count, "").to_string(), + length_seconds: if let Some(length_text) = details.get("lengthText") { + let time = (time_to_ms(get_text(length_text).as_str().unwrap_or("0")) / 1000) as f32; time.floor().to_string() } else { "0".to_string() }, - thumbnails: if !details["thumbnail"]["thumbnails"].is_null() { - details["thumbnail"]["thumbnails"] + thumbnails: if let Some(thumbnails) = + details.get("thumbnail").and_then(|x| x.get("thumbnails")) + { + thumbnails .as_array() .map(|f| { f.iter() @@ -385,94 +363,41 @@ pub fn parse_related_video( } pub fn get_media(info: &Value) -> Option { - let empty_serde_array = json!([]); - let empty_serde_object_array = vec![json!({})]; - let empty_serde_object = json!({}); - - let results = info - .as_object() - .and_then(|x| x.get("contents")) - .and_then(|x| x.get("twoColumnWatchNextResults")) - .and_then(|x| x.get("results")) - .and_then(|x| x.get("results")) - .and_then(|x| x.get("contents")) - .unwrap_or(&empty_serde_array) + let results = info["contents"]["twoColumnWatchNextResults"]["results"]["results"]["contents"] .as_array() - .unwrap_or(&empty_serde_object_array); + .cloned() + .unwrap_or_default(); let result_option = results .iter() - .find(|x| x.get("videoSecondaryInfoRenderer").is_some()); + .find(|x| !x["videoSecondaryInfoRenderer"].is_null()); let json_result = if let Some(result) = result_option { - let metadata_rows = if result.get("metadataRowContainer").is_some() { - result - .get("metadataRowContainer") - .and_then(|x| x.get("metadataRowContainerRenderer")) - .and_then(|x| x.get("rows")) - .unwrap_or(&empty_serde_object) - } else if result.get("videoSecondaryInfoRenderer").is_some() - && result - .get("videoSecondaryInfoRenderer") - .and_then(|x| x.get("metadataRowContainer")) - .is_some() - { - result - .get("videoSecondaryInfoRenderer") - .and_then(|x| x.get("metadataRowContainer")) - .and_then(|x| x.get("metadataRowContainerRenderer")) - .and_then(|x| x.get("rows")) - .unwrap_or(&empty_serde_object) + let metadata_rows = if let Some(result) = result.get("metadataRowContainer") { + result["metadataRowContainerRenderer"]["rows"].clone() + } else if let Some(result) = result.get("videoSecondaryInfoRenderer") { + result["metadataRowContainer"]["metadataRowContainerRenderer"]["rows"].clone() } else { - &empty_serde_object - } - .as_array() - .unwrap_or(&empty_serde_object_array); - - let mut return_object = json!({}); + serde_json::Value::Null + }; - for row in metadata_rows { - if row.get("metadataRowRenderer").is_some() { - let title = get_text( - row.get("metadataRowRenderer") - .and_then(|x| x.get("title")) - .unwrap_or(&empty_serde_object), - ) - .as_str() - .unwrap_or("title"); - let contents = row - .get("metadataRowRenderer") - .and_then(|x| x.get("contents")) - .and_then(|x| x.as_array()) - .unwrap_or(&empty_serde_object_array) - .first() - .unwrap_or(&empty_serde_object); + let mut return_object: Option = None; - let runs = contents.get("runs"); + for row in metadata_rows.as_array().cloned().unwrap_or_default() { + if let Some(row) = row.get("metadataRowRenderer") { + let title = get_text(&row["title"]).as_str().unwrap_or("title"); + let contents_value = row["contents"].as_array().cloned().unwrap_or_default(); - let mut title_url = ""; + let contents = contents_value.first().cloned().unwrap_or_default(); - if runs.is_some() - && runs.unwrap_or(&empty_serde_object).is_array() - && runs - .unwrap_or(&empty_serde_object) - .as_array() - .and_then(|x| x.first()) - .and_then(|x| x.get("navigationEndpoint")) - .is_some() - { - title_url = runs - .unwrap_or(&empty_serde_array) - .as_array() - .unwrap_or(&empty_serde_object_array) - .first() - .and_then(|x| x.get("navigationEndpoint")) - .and_then(|x| x.get("commandMetadata")) - .and_then(|x| x.get("webCommandMetadata")) - .and_then(|x| x.get("url")) - .and_then(|x| x.as_str()) - .unwrap_or(""); - } + let runs = contents["runs"].as_array().cloned().unwrap_or_default(); + let title_url = runs + .first() + .map(|x| { + x["navigationEndpoint"]["commandMetadata"]["webCommandMetadata"]["url"] + .clone() + }) + .unwrap_or_default(); let mut category = ""; let mut category_url = ""; @@ -489,26 +414,15 @@ pub fn get_media(info: &Value) -> Option { "category: {category}, "category_url": {category_url}, "#, - title = title, - title_content = get_text(contents).as_str().unwrap_or(""), - title_url = title_url, - category = category, - category_url = category_url, + title_content = get_text(&contents).as_str().unwrap_or(""), ); - return_object = from_str(data.as_str()).unwrap_or(json!({})); - } else if row.get("richMetadataRowRenderer").is_some() { - let contents = row - .get("richMetadataRowRenderer") - .and_then(|x| x.get("contents")) - .and_then(|x| x.as_array()) - .unwrap_or(&empty_serde_object_array); + return_object = from_str(data.as_str()).unwrap_or_default(); + } else if let Some(row) = row.get("richMetadataRowRenderer") { + let contents = row["contents"].as_array().cloned().unwrap_or_default(); let box_art = contents.iter().filter(|x| { - x.get("richMetadataRenderer") - .and_then(|c| c.get("style")) - .and_then(|c| c.as_str()) - .unwrap_or("") + x["richMetadataRenderer"]["style"].as_str().unwrap_or("") == "RICH_METADATA_RENDERER_STYLE_BOX_ART" }); @@ -516,18 +430,14 @@ pub fn get_media(info: &Value) -> Option { let mut media_type = "type"; let mut media_type_title = ""; let mut media_type_url = ""; - let mut media_thumbnails = &empty_serde_array; + let mut media_thumbnails = json!([]); for box_art_value in box_art { - let meta = box_art_value - .get("richMetadataRenderer") - .unwrap_or(&empty_serde_object); - - media_year = get_text(meta.get("subtitle").unwrap_or(&empty_serde_object)) + media_year = get_text(&box_art_value["richMetadataRenderer"]["subtitle"]) .as_str() .unwrap_or(""); - media_type = get_text(meta.get("callToAction").unwrap_or(&empty_serde_object)) + media_type = get_text(&box_art_value["richMetadataRenderer"]["callToAction"]) .as_str() .unwrap_or("type") .split(' ') @@ -535,28 +445,21 @@ pub fn get_media(info: &Value) -> Option { .get(1) .unwrap_or(&"type"); - media_type_title = get_text(meta.get("title").unwrap_or(&empty_serde_object)) + media_type_title = get_text(&box_art_value["richMetadataRenderer"]["title"]) .as_str() .unwrap_or(""); - media_type_url = meta - .get("endpoint") - .and_then(|x| x.get("commandMetadata")) - .and_then(|x| x.get("webCommandMetadata")) - .and_then(|x| x.get("url")) - .and_then(|x| x.as_str()) + media_type_url = box_art_value["richMetadataRenderer"]["endpoint"] + ["commandMetadata"]["webCommandMetadata"]["url"] + .as_str() .unwrap_or(""); - media_thumbnails = meta - .get("thumbnail") - .and_then(|x| x.get("thumbnails")) - .unwrap_or(&empty_serde_array); + + media_thumbnails = + box_art_value["richMetadataRenderer"]["thumbnail"]["thumbnails"].clone() } let topic = contents.iter().filter(|x| { - x.get("richMetadataRenderer") - .and_then(|x| x.get("style")) - .and_then(|x| x.as_str()) - .unwrap_or("") + x["richMetadataRenderer"]["style"].as_str().unwrap_or("") == "RICH_METADATA_RENDERER_STYLE_TOPIC" }); @@ -564,20 +467,13 @@ pub fn get_media(info: &Value) -> Option { let mut category_url = ""; for topic_value in topic { - let meta = topic_value - .get("richMetadataRenderer") - .unwrap_or(&empty_serde_object); - - category = get_text(meta.get("title").unwrap_or(&empty_serde_object)) + category = get_text(&topic_value["richMetadataRenderer"]["title"]) .as_str() .unwrap_or(""); - category_url = meta - .get("endpoint") - .and_then(|x| x.get("commandMetadata")) - .and_then(|x| x.get("webCommandMetadata")) - .and_then(|x| x.get("url")) - .and_then(|x| x.as_str()) + category_url = topic_value["richMetadataRenderer"]["endpoint"] + ["commandMetadata"]["webCommandMetadata"]["url"] + .as_str() .unwrap_or(""); } @@ -590,43 +486,31 @@ pub fn get_media(info: &Value) -> Option { "category: {category}, "category_url": {category_url}, "#, - media_year = media_year, - media_type = media_type, - media_type_title = media_type_title, - media_type_url = media_type_url, - media_thumbnails = media_thumbnails, - category = category, - category_url = category_url, ); - return_object = from_str(data.as_str()).unwrap_or(json!({})); + return_object = from_str(data.as_str()).unwrap_or_default(); } } - Some(return_object) + return_object } else { - Some(json!({})) + None }; json_result } pub fn get_author(initial_response: &Value, player_response: &PlayerResponse) -> Option { - let serde_empty_object = json!({}); - let empty_serde_object_array: Vec = vec![]; - let mut results: Vec = vec![]; let mut results_closure = || -> Result<(), &str> { - results = initial_response - .as_object() - .and_then(|x| x.get("contents")) - .and_then(|x| x.get("twoColumnWatchNextResults")) - .and_then(|x| x.get("results")) - .and_then(|x| x.get("results")) - .and_then(|x| x.get("contents")) - .and_then(|x| Some(x.as_array()?.to_vec())) - .unwrap_or_default(); + results = initial_response["contents"]["twoColumnWatchNextResults"]["results"]["results"] + ["contents"] + .as_array() + .cloned() + .unwrap_or_default() + .to_vec(); + Ok(()) }; @@ -636,37 +520,22 @@ pub fn get_author(initial_response: &Value, player_response: &PlayerResponse) -> let v_position = results .iter() - .position(|x| { - let video_owner_renderer_index = x - .as_object() - .and_then(|x| x.get("videoSecondaryInfoRenderer")) - .and_then(|x| x.get("owner")) - .and_then(|x| x.get("videoOwnerRenderer")); - video_owner_renderer_index.unwrap_or(&Value::Null) != &Value::Null - }) + .position(|x| !x["videoSecondaryInfoRenderer"]["owner"]["videoOwnerRenderer"].is_null()) .unwrap_or(usize::MAX); // match v_position - let v = results.get(v_position).unwrap_or(&serde_empty_object); - - let video_ownder_renderer = v - .get("videoSecondaryInfoRenderer") - .and_then(|x| x.get("owner")) - .and_then(|x| x.get("videoOwnerRenderer")) - .unwrap_or(&serde_empty_object); - - let channel_id = video_ownder_renderer - .get("navigationEndpoint") - .and_then(|x| x.get("browseEndpoint")) - .and_then(|x| x.get("browseId")) - .and_then(|x| x.as_str()) + let v = results.get(v_position).cloned().unwrap_or_default(); + + let video_ownder_renderer = + v["videoSecondaryInfoRenderer"]["owner"]["videoOwnerRenderer"].clone(); + + let channel_id = video_ownder_renderer["navigationEndpoint"]["browseEndpoint"]["browseId"] + .as_str() .unwrap_or(""); - let thumbnails = video_ownder_renderer - .get("thumbnail") - .and_then(|x| x.get("thumbnails")) - .and_then(|x| x.as_array()) - .unwrap_or(&empty_serde_object_array) - .clone() + let thumbnails = video_ownder_renderer["thumbnail"]["thumbnails"] + .as_array() + .cloned() + .unwrap_or_default() .iter() .map(|x| Thumbnail { width: x @@ -689,28 +558,15 @@ pub fn get_author(initial_response: &Value, player_response: &PlayerResponse) -> } }) .unwrap_or(0i64) as u64, - url: x - .get("url") - .and_then(|x| x.as_str()) - .unwrap_or("") - .to_string(), + url: x["url"].as_str().unwrap_or("").to_string(), }) .collect::>(); - let zero_viewer = json!("0"); let subscriber_count = parse_abbreviated_number( - get_text( - video_ownder_renderer - .get("subscriberCountText") - .unwrap_or(&zero_viewer), - ) - .as_str() - .unwrap_or("0"), - ); - let verified = is_verified( - video_ownder_renderer - .get("badges") - .unwrap_or(&serde_empty_object), + get_text(&video_ownder_renderer["subscriberCountText"]) + .as_str() + .unwrap_or("0"), ); + let verified = is_verified(&video_ownder_renderer["badges"]); let video_details = player_response .micro_format .as_ref() @@ -776,25 +632,17 @@ pub fn get_author(initial_response: &Value, player_response: &PlayerResponse) -> }, thumbnails, verified, - subscriber_count: subscriber_count as i32, + subscriber_count: subscriber_count as u64, }) } pub fn get_likes(info: &Value) -> u64 { - let serde_empty_object = json!({}); - let empty_serde_object_array = vec![json!({})]; - - let contents = info - .get("contents") - .and_then(|x| x.get("twoColumnWatchNextResults")) - .and_then(|x| x.get("results")) - .and_then(|x| x.get("results")) - .and_then(|x| x.get("contents")) - .unwrap_or(&serde_empty_object); + let contents = + info["contents"]["twoColumnWatchNextResults"]["results"]["results"]["contents"].clone(); let video = contents .as_array() - .and_then(|x| { + .map(|x| { let info_renderer_position = x .iter() .position(|c| c.get("videoPrimaryInfoRenderer").is_some()) @@ -802,56 +650,40 @@ pub fn get_likes(info: &Value) -> u64 { contents .as_array() - .map(|c| c.get(info_renderer_position)) - .unwrap_or(Some(&serde_empty_object)) + .and_then(|c| c.get(info_renderer_position).cloned()) + .unwrap_or_default() }) - .unwrap_or(&serde_empty_object); + .unwrap_or_default(); - let buttons = video - .get("videoPrimaryInfoRenderer") - .and_then(|x| x.get("videoActions")) - .and_then(|x| x.get("menuRenderer")) - .and_then(|x| x.get("topLevelButtons")) - .and_then(|x| x.as_array()) - .unwrap_or(&empty_serde_object_array); + let buttons = video["videoPrimaryInfoRenderer"]["videoActions"]["menuRenderer"] + ["topLevelButtons"] + .as_array() + .cloned() + .unwrap_or_default(); let like_index = buttons .iter() .position(|x| x.get("segmentedLikeDislikeButtonViewModel").is_some()) .unwrap_or(usize::MAX); - let like = buttons.get(like_index).unwrap_or(&serde_empty_object); - - let count = like - .get("segmentedLikeDislikeButtonViewModel") - .and_then(|x| x.get("likeButtonViewModel")) - .and_then(|x| x.get("likeButtonViewModel")) - .and_then(|x| x.get("toggleButtonViewModel")) - .and_then(|x| x.get("toggleButtonViewModel")) - .and_then(|x| x.get("defaultButtonViewModel")) - .and_then(|x| x.get("buttonViewModel")) - .and_then(|x| x.get("title")) - .and_then(|x| x.as_str()) + let like = buttons.get(like_index).cloned().unwrap_or_default(); + + let count = like["segmentedLikeDislikeButtonViewModel"]["likeButtonViewModel"] + ["likeButtonViewModel"]["toggleButtonViewModel"]["toggleButtonViewModel"] + ["defaultButtonViewModel"]["buttonViewModel"]["title"] + .as_str() .unwrap_or("0"); parse_abbreviated_number(count) as u64 } pub fn get_dislikes(info: &Value) -> u64 { - let serde_empty_object = json!({}); - let empty_serde_object_array = vec![json!({})]; - - let contents = info - .get("contents") - .and_then(|x| x.get("twoColumnWatchNextResults")) - .and_then(|x| x.get("results")) - .and_then(|x| x.get("results")) - .and_then(|x| x.get("contents")) - .unwrap_or(&serde_empty_object); + let contents = + info["contents"]["twoColumnWatchNextResults"]["results"]["results"]["contents"].clone(); let video = contents .as_array() - .and_then(|x| { + .map(|x| { let info_renderer_position = x .iter() .position(|c| c.get("videoPrimaryInfoRenderer").is_some()) @@ -859,36 +691,28 @@ pub fn get_dislikes(info: &Value) -> u64 { contents .as_array() - .map(|c| c.get(info_renderer_position)) - .unwrap_or(Some(&serde_empty_object)) + .and_then(|c| c.get(info_renderer_position).cloned()) + .unwrap_or_default() }) - .unwrap_or(&serde_empty_object); + .unwrap_or_default(); - let buttons = video - .get("videoPrimaryInfoRenderer") - .and_then(|x| x.get("videoActions")) - .and_then(|x| x.get("menuRenderer")) - .and_then(|x| x.get("topLevelButtons")) - .and_then(|x| x.as_array()) - .unwrap_or(&empty_serde_object_array); + let buttons = video["videoPrimaryInfoRenderer"]["videoActions"]["menuRenderer"] + ["topLevelButtons"] + .as_array() + .cloned() + .unwrap_or_default(); let dislike_index = buttons .iter() .position(|x| x.get("segmentedLikeDislikeButtonViewModel").is_some()) .unwrap_or(usize::MAX); - let like = buttons.get(dislike_index).unwrap_or(&serde_empty_object); - - let count = like - .get("segmentedLikeDislikeButtonViewModel") - .and_then(|x| x.get("dislikeButtonViewModel")) - .and_then(|x| x.get("dislikeButtonViewModel")) - .and_then(|x| x.get("toggleButtonViewModel")) - .and_then(|x| x.get("toggleButtonViewModel")) - .and_then(|x| x.get("defaultButtonViewModel")) - .and_then(|x| x.get("buttonViewModel")) - .and_then(|x| x.get("title")) - .and_then(|x| x.as_str()) + let dislike = buttons.get(dislike_index).cloned().unwrap_or_default(); + + let count = dislike["segmentedLikeDislikeButtonViewModel"]["dislikeButtonViewModel"] + ["dislikeButtonViewModel"]["toggleButtonViewModel"]["toggleButtonViewModel"] + ["defaultButtonViewModel"]["buttonViewModel"]["title"] + .as_str() .unwrap_or("0"); parse_abbreviated_number(count) as u64 @@ -976,40 +800,25 @@ pub fn get_storyboards(info: &PlayerResponse) -> Option> { } pub fn get_chapters(info: &Value) -> Option> { - let serde_empty_object = json!({}); - let empty_serde_object_array = vec![json!({})]; - - let player_overlay_renderer = info - .get("playerOverlays") - .and_then(|x| x.get("playerOverlayRenderer")) - .unwrap_or(&serde_empty_object); - - let player_bar = player_overlay_renderer - .get("decoratedPlayerBarRenderer") - .and_then(|x| x.get("decoratedPlayerBarRenderer")) - .and_then(|x| x.get("playerBar")) - .unwrap_or(&serde_empty_object); - - let markers_map = player_bar - .get("multiMarkersPlayerBarRenderer") - .and_then(|x| x.get("markersMap")) - .and_then(|x| x.as_array()) - .unwrap_or(&empty_serde_object_array); + let markers_map = info["playerOverlays"]["playerOverlayRenderer"]["decoratedPlayerBarRenderer"] + ["decoratedPlayerBarRenderer"]["playerBar"]["multiMarkersPlayerBarRenderer"]["markersMap"] + .as_array() + .cloned() + .unwrap_or_default(); let marker_index = markers_map .iter() .position(|x| { - x.get("value").is_some() - && x.get("value") - .map(|c| c.get("chapters").map(|d| d.is_array()).unwrap_or(false)) - .unwrap_or(false) + x.get("value") + .map(|c| c.get("chapters").map(|d| d.is_array()).unwrap_or(false)) + .unwrap_or(false) }) .unwrap_or(usize::MAX); let marker = markers_map .get(marker_index) - .and_then(|x| x.as_object()) - .unwrap_or(serde_empty_object.as_object().unwrap()); + .map(|x| x.as_object().cloned().unwrap_or_default()) + .unwrap_or_default(); if marker.is_empty() { return Some(vec![]); @@ -1018,26 +827,20 @@ pub fn get_chapters(info: &Value) -> Option> { let chapters = marker .get("value") .and_then(|x| x.get("chapters")) - .and_then(|x| x.as_array()) - .unwrap_or(&empty_serde_object_array); + .and_then(|x| x.as_array().cloned()) + .unwrap_or_default(); Some( chapters .iter() .map(|x| Chapter { - title: get_text( - x.get("chapterRenderer") - .and_then(|x| x.get("title")) - .unwrap_or(&serde_empty_object), - ) - .as_str() - .unwrap_or("") - .to_string(), - start_time: (x - .get("chapterRenderer") - .and_then(|x| x.get("timeRangeStartMillis")) - .and_then(|x| x.as_f64()) - .unwrap_or(0f64) + title: get_text(&x["chapterRenderer"]["title"]) + .as_str() + .unwrap_or("") + .to_string(), + start_time: (x["chapterRenderer"]["timeRangeStartMillis"] + .as_f64() + .unwrap_or_default() / 1000f64) as i32, }) .collect::>(), diff --git a/src/stream/streams/mod.rs b/src/stream/streams/mod.rs index 66295aa..7485b02 100644 --- a/src/stream/streams/mod.rs +++ b/src/stream/streams/mod.rs @@ -153,7 +153,9 @@ impl FFmpegStream { .headers(headers) .send() .await - .map_err(VideoError::ReqwestMiddleware)?; + .map_err(VideoError::ReqwestMiddleware)? + .error_for_status() + .map_err(VideoError::Reqwest)?; let mut buf: BytesMut = BytesMut::new(); diff --git a/src/stream/streams/non_live.rs b/src/stream/streams/non_live.rs index f35065d..49df89b 100644 --- a/src/stream/streams/non_live.rs +++ b/src/stream/streams/non_live.rs @@ -208,7 +208,9 @@ impl Stream for NonLiveStream { .headers(headers) .send() .await - .map_err(VideoError::ReqwestMiddleware)?; + .map_err(VideoError::ReqwestMiddleware)? + .error_for_status() + .map_err(VideoError::Reqwest)?; let mut buf: BytesMut = BytesMut::new(); diff --git a/src/structs.rs b/src/structs.rs index e16f26d..9d65a28 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -494,7 +494,7 @@ pub struct Author { pub thumbnails: Vec, pub verified: bool, #[serde(rename = "subscriberCount")] - pub subscriber_count: i32, + pub subscriber_count: u64, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -776,7 +776,7 @@ impl FFmpegArgs { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, Default)] pub struct PlayerResponse { #[serde(rename = "streamingData")] pub streaming_data: Option, diff --git a/src/utils.rs b/src/utils.rs index faabac4..3c5bdfc 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -31,14 +31,10 @@ pub fn get_html5player(body: &str) -> Option { Regex::new(r#"|"jsUrl":"([^"]+)""#).unwrap() }); - let caps = HTML5PLAYER_RES.captures(body).unwrap(); - match caps.get(2) { - Some(caps) => Some(caps.as_str().to_string()), - None => match caps.get(3) { - Some(caps) => Some(caps.as_str().to_string()), - None => Some(String::from("")), - }, - } + let caps = HTML5PLAYER_RES.captures(body)?; + caps.get(2) + .or_else(|| caps.get(3)) + .map(|cap| cap.as_str().to_string()) } #[cfg_attr(feature = "performance_analysis", flamer::flame)] @@ -459,16 +455,14 @@ fn decipher( Err(_) => return get_url_string(), }; - let return_url = url::Url::parse( - args.get("url") - .and_then(serde_json::Value::as_str) - .unwrap_or(""), - ); - if return_url.is_err() { - return get_url_string(); - } - - let mut return_url = return_url.unwrap(); + let mut return_url = match args + .get("url") + .and_then(serde_json::Value::as_str) + .map_or_else(|| Err(url::ParseError::EmptyHost), url::Url::parse) + { + Ok(url) => url, + Err(_) => return get_url_string(), + }; let query_name = args .get("sp") .and_then(serde_json::Value::as_str) @@ -513,10 +507,10 @@ fn ncode( } #[cfg_attr(feature = "performance_analysis", flamer::flame)] - fn create_transform_script(script: &str) -> Context<'_> { + fn create_transform_script(script: &str) -> Option> { let mut context = Context::default(); - context.eval(Source::from_bytes(script)).unwrap(); - context + context.eval(Source::from_bytes(script)).ok()?; + Some(context) } #[cfg_attr(feature = "performance_analysis", flamer::flame)] @@ -539,11 +533,16 @@ fn ncode( }) } - let mut context = create_transform_script(n_transform_script_string.1); + let mut context = match create_transform_script(n_transform_script_string.1) { + Some(res) => res, + None => return url.to_string(), + }; - let result = - execute_transform_script(&mut context, n_transform_script_string.0, n_transform_value); - let result = match result { + let result = match execute_transform_script( + &mut context, + n_transform_script_string.0, + n_transform_value, + ) { Some(res) => res, None => return url.to_string(), }; @@ -578,11 +577,11 @@ fn ncode( /// Excavate video id from URLs or id with Regex #[cfg_attr(feature = "performance_analysis", flamer::flame)] pub fn get_video_id(url: &str) -> Option { - let url_regex = Regex::new(r"^https?://").unwrap(); + static URL_REGEX: Lazy = Lazy::new(|| Regex::new(r"^https?://").unwrap()); if validate_id(url.to_string()) { Some(url.to_string()) - } else if url_regex.is_match(url.trim()) { + } else if URL_REGEX.is_match(url.trim()) { get_url_video_id(url) } else { None @@ -591,82 +590,62 @@ pub fn get_video_id(url: &str) -> Option { #[cfg_attr(feature = "performance_analysis", flamer::flame)] pub fn validate_id(id: String) -> bool { - let id_regex = Regex::new(r"^[a-zA-Z0-9-_]{11}$").unwrap(); - - id_regex.is_match(id.trim()) + static ID_REGEX: Lazy = Lazy::new(|| Regex::new(r"^[a-zA-Z0-9-_]{11}$").unwrap()); + ID_REGEX.is_match(id.trim()) } #[cfg_attr(feature = "performance_analysis", flamer::flame)] fn get_url_video_id(url: &str) -> Option { - // 1ms gain/req on Ryzen 9 5950XT (probably a lot more on slower CPUs) static VALID_PATH_DOMAINS: Lazy = Lazy::new(|| { Regex::new(r"(?m)(?:^|\W)(?:youtube(?:-nocookie)?\.com/(?:.*[?&]v=|v/|shorts/|e(?:mbed)?/|[^/]+/.+/)|youtu\.be/)([\w-]+)") .unwrap() }); - let parsed_result = url::Url::parse(url.trim()); - - if parsed_result.is_err() { - return None; - } - - let parsed = url::Url::parse(url.trim()).unwrap(); + let parsed = url::Url::parse(url.trim()).ok()?; - let mut id: Option = None; - - for value in parsed.query_pairs() { - if value.0.to_string().as_str() == "v" { - id = Some(value.1.to_string()); + if let Some(id) = parsed.query_pairs().find_map(|(key, value)| { + if key == "v" { + Some(value.to_string()) + } else { + None } + }) { + return Some(id).filter(|id| { + let return_id = id.substring(0, 11); + validate_id(return_id.into()) + }); } - if VALID_PATH_DOMAINS.is_match(url.trim()) && id.is_none() { - let captures = VALID_PATH_DOMAINS.captures(url.trim()); - // println!("{:#?}", captures); - if let Some(captures_some) = captures { - let id_group = captures_some.get(1); - if let Some(id_group_some) = id_group { - id = Some(id_group_some.as_str().to_string()); + if VALID_PATH_DOMAINS.is_match(url.trim()) { + if let Some(captures) = VALID_PATH_DOMAINS.captures(url.trim()) { + if let Some(id) = captures.get(1).map(|m| m.as_str().to_string()) { + return Some(id).filter(|id| { + let return_id = id.substring(0, 11); + validate_id(return_id.into()) + }); } } - } else if url::Url::parse(url.trim()).unwrap().host_str().is_some() - && !VALID_QUERY_DOMAINS + } + + // Check if the host is valid + if parsed.host_str().is_some() + && VALID_QUERY_DOMAINS .iter() .any(|domain| domain == &parsed.host_str().unwrap_or("")) { return None; } - if let Some(id_some) = id { - id = Some(id_some.substring(0, 11).to_string()); - - if !validate_id(id.clone().unwrap()) { - return None; - } - - id - } else { - None - } + None } #[cfg_attr(feature = "performance_analysis", flamer::flame)] pub fn get_text(obj: &serde_json::Value) -> &serde_json::Value { - let null_referance = &serde_json::Value::Null; - obj.as_object() - .and_then(|x| { - if x.contains_key("runs") { - x.get("runs").and_then(|c| { - c.as_array() - .unwrap() - .first() - .and_then(|d| d.as_object().and_then(|f| f.get("text"))) - }) - } else { - x.get("simpleText") - } - }) - .unwrap_or(null_referance) + if !obj["runs"].is_null() { + &obj["runs"][0]["text"] + } else { + &obj["simpleText"] + } } #[cfg_attr(feature = "performance_analysis", flamer::flame)] @@ -864,29 +843,15 @@ pub fn is_verified(badges: &serde_json::Value) -> bool { #[cfg_attr(feature = "performance_analysis", flamer::flame)] pub fn is_age_restricted(media: &serde_json::Value) -> bool { - let mut age_restricted = false; - if media.is_object() && media.as_object().is_some() { - age_restricted = AGE_RESTRICTED_URLS.iter().any(|url| { - media - .as_object() - .map(|x| { - let mut bool_vec: Vec = vec![]; - - for (_key, value) in x { - if let Some(value_some) = value.as_str() { - bool_vec.push(value_some.contains(url)) - } else { - bool_vec.push(false); - } - } - - bool_vec.iter().any(|v| v == &true) - }) - .unwrap_or(false) + if let Some(media_object) = media.as_object() { + AGE_RESTRICTED_URLS.iter().any(|url| { + media_object + .values() + .any(|value| value.as_str().map_or(false, |v| v.contains(url))) }) + } else { + false } - - age_restricted } #[cfg_attr(feature = "performance_analysis", flamer::flame)] @@ -1056,13 +1021,12 @@ pub fn extract_functions(body: String) -> Vec<(String, String)> { } let function_start = format!(r#"var {function_name}={{"#); - let ndx = body.find(function_start.as_str()); - - if ndx.is_none() { - return String::new(); - } + let ndx = match body.find(function_start.as_str()) { + Some(i) => i, + None => return String::new(), + }; - let sub_body = body.slice((ndx.unwrap() + function_start.len() - 1)..); + let sub_body = body.slice((ndx + function_start.len() - 1)..); let cut_after_sub_body = cut_after_js(sub_body).unwrap_or("null"); @@ -1159,19 +1123,15 @@ pub async fn get_html( client.get(url) } .send() - .await; + .await + .map_err(VideoError::ReqwestMiddleware)?; - if request.is_err() { - return Err(VideoError::ReqwestMiddleware(request.err().unwrap())); - } - - let response_first = request.unwrap().text().await; - - if response_first.is_err() { - return Err(VideoError::BodyCannotParsed); - } + let response_first = request + .text() + .await + .map_err(|_x| VideoError::BodyCannotParsed)?; - Ok(response_first.unwrap()) + Ok(response_first) } /// Try to generate IPv6 with custom valid block @@ -1188,22 +1148,12 @@ pub fn get_random_v6_ip(ip: impl Into) -> Result { } let format_attr = ipv6_format.split('/').collect::>(); - let raw_addr = format_attr.first(); - let raw_mask = format_attr.get(1); - - if raw_addr.is_none() || raw_mask.is_none() { - return Err(VideoError::InvalidIPv6Format); - } - - let raw_addr = raw_addr.unwrap(); - let raw_mask = raw_mask.unwrap(); - - let base_10_mask = raw_mask.parse::(); - if base_10_mask.is_err() { - return Err(VideoError::InvalidIPv6Subnet); - } + let raw_addr = format_attr.first().ok_or(VideoError::InvalidIPv6Format)?; + let raw_mask = format_attr.get(1).ok_or(VideoError::InvalidIPv6Format)?; - let mut base_10_mask = base_10_mask.unwrap(); + let mut base_10_mask = raw_mask + .parse::() + .map_err(|_x| VideoError::InvalidIPv6Subnet)?; if !(24..=128).contains(&base_10_mask) { return Err(VideoError::InvalidIPv6Subnet); @@ -1282,9 +1232,9 @@ pub fn time_to_ms(duration: &str) -> usize { #[cfg_attr(feature = "performance_analysis", flamer::flame)] pub fn parse_abbreviated_number(time_str: &str) -> usize { let replaced_string = time_str.replace(',', ".").replace(' ', ""); - let string_match_regex: Lazy = Lazy::new(|| Regex::new(r"([\d,.]+)([MK]?)").unwrap()); + static STRING_MATCH_REGEX: Lazy = Lazy::new(|| Regex::new(r"([\d,.]+)([MK]?)").unwrap()); - if let Some(caps) = string_match_regex.captures(replaced_string.as_str()) { + if let Some(caps) = STRING_MATCH_REGEX.captures(replaced_string.as_str()) { let return_value = if caps.len() > 0 { let mut num;