diff --git a/src/providers/noodlemagazine.rs b/src/providers/noodlemagazine.rs index c10f733..ef7a7c7 100644 --- a/src/providers/noodlemagazine.rs +++ b/src/providers/noodlemagazine.rs @@ -7,43 +7,45 @@ use crate::util::requester::Requester; use crate::util::parse_abbreviated_number; use crate::util::time::parse_time_to_seconds; use crate::videos::{ServerOptions, VideoFormat, VideoItem}; + use async_trait::async_trait; use error_chain::error_chain; use futures::future::join_all; -use htmlentity::entity::{ICodedDataTrait, decode}; +use htmlentity::entity::{decode, ICodedDataTrait}; use wreq::Version; -use std::{vec}; use titlecase::Titlecase; +use std::vec; error_chain! { foreign_links { Io(std::io::Error); HttpRequest(wreq::Error); } + errors { + Parse(msg: String) + } } #[derive(Debug, Clone)] pub struct NoodlemagazineProvider { url: String, } + impl NoodlemagazineProvider { pub fn new() -> Self { - let provider = NoodlemagazineProvider { + Self { url: "https://noodlemagazine.com".to_string(), - }; - provider + } } - fn build_channel(&self, clientversion: ClientVersion) -> Channel { - let _ = clientversion; + fn build_channel(&self, _clientversion: ClientVersion) -> Channel { Channel { - id: "noodlemagazine".to_string(), - name: "Noodlemagazine".to_string(), - description: "The Best Search Engine of HD Videos".to_string(), + id: "noodlemagazine".into(), + name: "Noodlemagazine".into(), + description: "The Best Search Engine of HD Videos".into(), premium: false, - favicon: "https://www.google.com/s2/favicons?sz=64&domain=noodlemagazine.com" - .to_string(), - status: "active".to_string(), + favicon: "https://www.google.com/s2/favicons?sz=64&domain=noodlemagazine.com".into(), + status: "active".into(), categories: vec![], options: vec![], nsfw: true, @@ -55,40 +57,40 @@ impl NoodlemagazineProvider { &self, cache: VideoCache, page: u8, - sort: &str, + _sort: &str, options: ServerOptions, ) -> Result> { - let _ = sort; let video_url = format!( "{}/popular/recent?sort_by=views&sort_order=desc&p={}", self.url, - page - 1 + page.saturating_sub(1) ); + let old_items = match cache.get(&video_url) { - Some((time, items)) => { - if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 { - return Ok(items.clone()); - } else { - items.clone() - } - } - None => { - vec![] - } + Some((t, i)) if t.elapsed().unwrap_or_default().as_secs() < 300 => return Ok(i.clone()), + Some((_, i)) => i.clone(), + None => vec![], }; - let mut requester = options.requester.clone().unwrap(); - let text = requester.get(&video_url, Some(Version::HTTP_2)).await.unwrap(); - let video_items: Vec = self - .get_video_items_from_html(text.clone(), requester.clone()) - .await; - if !video_items.is_empty() { - cache.remove(&video_url); - cache.insert(video_url.clone(), video_items.clone()); + let mut requester = match options.requester.clone() { + Some(r) => r, + None => return Ok(old_items), + }; + + let text = requester + .get(&video_url, Some(Version::HTTP_2)) + .await + .unwrap_or_default(); + + let items = self.get_video_items_from_html(text, requester).await; + + if items.is_empty() { + Ok(old_items) } else { - return Ok(old_items); + cache.remove(&video_url); + cache.insert(video_url, items.clone()); + Ok(items) } - Ok(video_items) } async fn query( @@ -98,37 +100,34 @@ impl NoodlemagazineProvider { query: &str, options: ServerOptions, ) -> Result> { - let search_string = query.trim().replace(" ", "%20").to_string(); + let q = query.trim().replace(' ', "%20"); + let video_url = format!("{}/video/{}?p={}", self.url, q, page.saturating_sub(1)); - let video_url = format!("{}/video/{}?p={}", self.url, search_string, page - 1); - // Check our Video Cache. If the result is younger than 1 hour, we return it. let old_items = match cache.get(&video_url) { - Some((time, items)) => { - if time.elapsed().unwrap_or_default().as_secs() < 60 * 5 { - return Ok(items.clone()); - } else { - let _ = cache.check().await; - return Ok(items.clone()); - } - } - None => { - vec![] - } + Some((t, i)) if t.elapsed().unwrap_or_default().as_secs() < 300 => return Ok(i.clone()), + Some((_, i)) => i.clone(), + None => vec![], }; - let mut requester = options.requester.clone().unwrap(); + let mut requester = match options.requester.clone() { + Some(r) => r, + None => return Ok(old_items), + }; - let text = requester.get(&video_url, Some(Version::HTTP_2)).await.unwrap(); - let video_items: Vec = self - .get_video_items_from_html(text.clone(), requester.clone()) - .await; - if !video_items.is_empty() { - cache.remove(&video_url); - cache.insert(video_url.clone(), video_items.clone()); + let text = requester + .get(&video_url, Some(Version::HTTP_2)) + .await + .unwrap_or_default(); + + let items = self.get_video_items_from_html(text, requester).await; + + if items.is_empty() { + Ok(old_items) } else { - return Ok(old_items); + cache.remove(&video_url); + cache.insert(video_url, items.clone()); + Ok(items) } - Ok(video_items) } async fn get_video_items_from_html( @@ -139,19 +138,29 @@ impl NoodlemagazineProvider { if html.is_empty() || html.contains("404 Not Found") { return vec![]; } - let raw_videos = html.split(">Show more").collect::>()[0] - .split("
") - .collect::>()[1] - .split("
") - .collect::>()[1..] - .to_vec(); - let futures = raw_videos - .into_iter() - .map(|el| self.get_video_item(el.to_string(), requester.clone())); - let results: Vec> = join_all(futures).await; - let video_items: Vec = results.into_iter().filter_map(Result::ok).collect(); - return video_items; + let section = match html.split(">Show more
").next() { + Some(s) => s, + None => return vec![], + }; + + let list = match section + .split("
") + .nth(1) + { + Some(l) => l, + None => return vec![], + }; + + let raw_videos = list + .split("
") + .skip(1) + .map(|s| s.to_string()); + + let futures = raw_videos.map(|v| self.get_video_item(v, requester.clone())); + let results = join_all(futures).await; + + results.into_iter().filter_map(Result::ok).collect() } async fn get_video_item( @@ -159,97 +168,109 @@ impl NoodlemagazineProvider { video_segment: String, requester: Requester, ) -> Result { - let video_url: String = format!( - "{}{}", - self.url, - video_segment.split(">()[1] - .split("\"") - .collect::>()[0] - .to_string() - ); + let href = video_segment + .split("") - .collect::>()[1] - .split("<") - .collect::>()[0] + .nth(1) + .and_then(|s| s.split('<').next()) + .unwrap_or("") .trim() .to_string(); - // html decode + title = decode(title.as_bytes()) .to_string() .unwrap_or(title) .titlecase(); - let id = video_url.split("/").collect::>()[4] - .split(".") - .collect::>()[0] + + let id = video_url + .split('/') + .nth(4) + .and_then(|s| s.split('.').next()) + .ok_or_else(|| Error::from("missing id"))? .to_string(); - let thumb = video_segment.split(">()[1] + let thumb = video_segment .split("data-src=\"") - .collect::>()[1] - .split("\"") - .collect::>()[0] + .nth(1) + .and_then(|s| s.split('"').next()) + .unwrap_or("") .to_string(); + let raw_duration = video_segment .split("#clock-o\">") - .collect::>()[1] - .split("<") - .collect::>()[0] - .trim() - .to_string(); - let duration = parse_time_to_seconds(raw_duration.as_str()).unwrap_or(0) as u32; - let views = parse_abbreviated_number( video_segment - .split("") - .collect::>()[1] - .split("<") - .collect::>()[0] - .trim()).unwrap_or(0); + .nth(1) + .and_then(|s| s.split('<').next()) + .unwrap_or("0:00"); - let formats = match self.extract_media(&video_url, requester.clone()).await{ - Some(f) => f, - None => return Err(Error::from("Failed to extract media formats")), - }; - let video_item = VideoItem::new( - id, - title, - video_url, - "noodlemagazine".to_string(), - thumb, - duration, + let duration = parse_time_to_seconds(raw_duration).unwrap_or(0) as u32; + + let views = video_segment + .split("#eye\">") + .nth(1) + .and_then(|s| s.split('<').next()) + .and_then(|v| parse_abbreviated_number(v.trim())) + .unwrap_or(0); + + let formats = self + .extract_media(&video_url, requester) + .await + .ok_or_else(|| Error::from("media extraction failed"))?; + + Ok( + VideoItem::new( + id, + title, + video_url, + "noodlemagazine".into(), + thumb, + duration, + ) + .views(views) + .formats(formats), ) - .views(views) - .formats(formats) - ; - return Ok(video_item); } - async fn extract_media(&self, video_url: &String, mut requester: Requester) -> Option> { - let text = requester.get(video_url, Some(Version::HTTP_2)).await.unwrap_or("".to_string()); - if text.is_empty() { - return None; - } + async fn extract_media( + &self, + video_url: &String, + mut requester: Requester, + ) -> Option> { + let text = requester + .get(video_url, Some(Version::HTTP_2)) + .await + .unwrap_or_default(); + + let json_str = text + .split("window.playlist = ") + .nth(1)? + .split(';') + .next()?; + + let json: serde_json::Value = serde_json::from_str(json_str).ok()?; + let sources = json["sources"].as_array()?; + let mut formats = vec![]; - let json_str = text.split("window.playlist = ") - .collect::>()[1] - .split(";") - .collect::>()[0]; - let json: serde_json::Value = serde_json::from_str(json_str).unwrap(); - let sources = json["sources"].as_array().unwrap(); - for source in sources { - let file = source["file"].as_str().unwrap().to_string(); - let label = source["label"].as_str().unwrap_or("unknown").to_string(); - let format = VideoFormat::new( - file, - label.clone(), - "video/mp4".to_string(), - ) - .format_id(label.clone()) - .format_note(label.clone()) - .http_header("Referer".to_string(), video_url.clone()) - ; - formats.push(format); + + for s in sources { + let file = s["file"].as_str()?.to_string(); + let label = s["label"].as_str().unwrap_or("unknown").to_string(); + + formats.push( + VideoFormat::new(file, label.clone(), "video/mp4".into()) + .format_id(label.clone()) + .format_note(label.clone()) + .http_header("Referer".into(), video_url.clone()), + ); } - return Some(formats.into_iter().rev().collect()); + + Some(formats.into_iter().rev().collect()) } } @@ -265,27 +286,23 @@ impl Provider for NoodlemagazineProvider { per_page: String, options: ServerOptions, ) -> Vec { - let _ = per_page; let _ = pool; - let videos: std::result::Result, Error> = match query { - Some(q) => { - self.query(cache, page.parse::().unwrap_or(1), &q, options) - .await - } - None => { - self.get(cache, page.parse::().unwrap_or(1), &sort, options) - .await - } + let _ = per_page; + + let page = page.parse::().unwrap_or(1); + + let res = match query { + Some(q) => self.query(cache, page, &q, options).await, + None => self.get(cache, page, &sort, options).await, }; - match videos { - Ok(v) => v, - Err(e) => { - println!("Error fetching videos: {}", e); - vec![] - } - } + + res.unwrap_or_else(|e| { + eprintln!("Noodlemagazine error: {e}"); + vec![] + }) } - fn get_channel(&self, clientversion: ClientVersion) -> crate::status::Channel { + + fn get_channel(&self, clientversion: ClientVersion) -> Channel { self.build_channel(clientversion) } }