use crate::DbPool; use crate::api::ClientVersion; use crate::providers::Provider; use crate::status::*; use crate::util::cache::VideoCache; 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::{decode, ICodedDataTrait}; use wreq::Version; 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 { Self { url: "https://noodlemagazine.com".to_string(), } } fn build_channel(&self, _clientversion: ClientVersion) -> Channel { Channel { 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".into(), status: "active".into(), categories: vec![], options: vec![], nsfw: true, cacheDuration: Some(1800), } } async fn get( &self, cache: VideoCache, page: u8, _sort: &str, options: ServerOptions, ) -> Result> { let video_url = format!( "{}/popular/recent?sort_by=views&sort_order=desc&p={}", self.url, page.saturating_sub(1) ); let old_items = match cache.get(&video_url) { 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 = 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 { cache.remove(&video_url); cache.insert(video_url, items.clone()); Ok(items) } } async fn query( &self, cache: VideoCache, page: u8, query: &str, options: ServerOptions, ) -> Result> { let q = query.trim().replace(' ', "%20"); let video_url = format!("{}/video/{}?p={}", self.url, q, page.saturating_sub(1)); let old_items = match cache.get(&video_url) { 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 = 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 { cache.remove(&video_url); cache.insert(video_url, items.clone()); Ok(items) } } async fn get_video_items_from_html( &self, html: String, requester: Requester, ) -> Vec { if html.is_empty() || html.contains("404 Not Found") { return vec![]; } 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( &self, video_segment: String, requester: Requester, ) -> Result { let href = video_segment .split("") .nth(1) .and_then(|s| s.split('<').next()) .unwrap_or("") .trim() .to_string(); title = decode(title.as_bytes()) .to_string() .unwrap_or(title) .titlecase(); 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("data-src=\"") .nth(1) .and_then(|s| s.split('"').next()) .unwrap_or("") .to_string(); let raw_duration = video_segment .split("#clock-o\">") .nth(1) .and_then(|s| s.split('<').next()) .unwrap_or("0:00"); 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), ) } 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![]; 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()), ); } Some(formats.into_iter().rev().collect()) } } #[async_trait] impl Provider for NoodlemagazineProvider { async fn get_videos( &self, cache: VideoCache, pool: DbPool, sort: String, query: Option, page: String, per_page: String, options: ServerOptions, ) -> Vec { let _ = pool; 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, }; res.unwrap_or_else(|e| { eprintln!("Noodlemagazine error: {e}"); vec![] }) } fn get_channel(&self, clientversion: ClientVersion) -> Option { Some(self.build_channel(clientversion)) } }