From 4a7528c516e420e095c2390b553a5cd035b61836 Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 3 Jan 2026 10:17:39 +0000 Subject: [PATCH] bugfixes --- src/providers/pmvhaven.rs | 244 ++++++++++++++++++++------------------ 1 file changed, 129 insertions(+), 115 deletions(-) diff --git a/src/providers/pmvhaven.rs b/src/providers/pmvhaven.rs index 8e2e44c..1817e39 100644 --- a/src/providers/pmvhaven.rs +++ b/src/providers/pmvhaven.rs @@ -7,9 +7,9 @@ use crate::util::time::parse_time_to_seconds; use crate::videos::{ServerOptions, VideoItem}; use async_trait::async_trait; use error_chain::error_chain; -use htmlentity::entity::{ICodedDataTrait, decode}; +use htmlentity::entity::{decode, ICodedDataTrait}; use std::sync::{Arc, RwLock}; -use std::{vec}; +use std::vec; error_chain! { foreign_links { @@ -24,19 +24,25 @@ pub struct PmvhavenProvider { stars: Arc>>, categories: Arc>>, } + impl PmvhavenProvider { pub fn new() -> Self { - let provider = PmvhavenProvider { + Self { url: "https://pmvhaven.com".to_string(), stars: Arc::new(RwLock::new(vec![])), categories: Arc::new(RwLock::new(vec![])), - }; - provider + } } fn build_channel(&self, clientversion: ClientVersion) -> Channel { - // if clientversion >= ClientVersion::new(22, 101, "22e".to_string()) { let _ = clientversion; + + let categories = self + .categories + .read() + .map(|g| g.clone()) + .unwrap_or_default(); + Channel { id: "pmvhaven".to_string(), name: "PMVHaven".to_string(), @@ -44,14 +50,14 @@ impl PmvhavenProvider { premium: false, favicon: "https://www.google.com/s2/favicons?sz=64&domain=pmvhaven.com".to_string(), status: "active".to_string(), - categories: self.categories.read().unwrap().iter().map(|c| c.clone()).collect(), + categories, options: vec![ ChannelOption { - id: "sort".to_string(), - title: "Sort".to_string(), - description: "Sort the Videos".to_string(), - systemImage: "list.number".to_string(), - colorName: "blue".to_string(), + id: "sort".into(), + title: "Sort".into(), + description: "Sort the Videos".into(), + systemImage: "list.number".into(), + colorName: "blue".into(), options: vec![ FilterOption { id: "relevance".into(), @@ -81,11 +87,11 @@ impl PmvhavenProvider { multiSelect: false, }, ChannelOption { - id: "duration".to_string(), - title: "Duration".to_string(), - description: "Length of the Videos".to_string(), - systemImage: "timer".to_string(), - colorName: "green".to_string(), + id: "duration".into(), + title: "Duration".into(), + description: "Length of the Videos".into(), + systemImage: "timer".into(), + colorName: "green".into(), options: vec![ FilterOption { id: "any".into(), @@ -116,7 +122,6 @@ impl PmvhavenProvider { } } - // Push one item with minimal lock time and dedup by id fn push_unique(target: &Arc>>, item: String) { if let Ok(mut vec) = target.write() { if !vec.iter().any(|x| x == &item) { @@ -132,119 +137,128 @@ impl PmvhavenProvider { query: &str, options: ServerOptions, ) -> Result> { - let search_string = query.trim().to_string(); - let sort_string = match options.sort.unwrap_or("".to_string()).as_str() { - "newest" => "sort=-uploadDate", - "oldest" => "sort=uploadDate", - "most viewed" => "sort=-views", - "most liked" => "sort=-likes", - "most disliked" => "sort=-dislikes", + let search = query.trim().to_string(); + + let sort = match options.sort.as_deref() { + Some("newest") => "&sort=-uploadDate", + Some("oldest") => "&sort=uploadDate", + Some("most viewed") => "&sort=-views", + Some("most liked") => "&sort=-likes", + Some("most disliked") => "&sort=-dislikes", _ => "", }; - let duration_string = match options.duration.unwrap_or("".to_string()).as_str(){ - "<4 min" => "durationMax=240", - "4-20 min" => "durationMin=240&durationMax=1200", - "20-60 min" => "durationMin=1200&durationMax=3600", - ">1 hour" => "durationMin=3600", + + let duration = match options.duration.as_deref() { + Some("<4 min") => "&durationMax=240", + Some("4-20 min") => "&durationMin=240&durationMax=1200", + Some("20-60 min") => "&durationMin=1200&durationMax=3600", + Some(">1 hour") => "&durationMin=3600", _ => "", }; - let endpoint = if search_string.is_empty() { + + let endpoint = if search.is_empty() { "api/videos" } else { "api/videos/search" }; - let mut video_url = format!("{}/{}?limit=100&page={}&{}&{}", self.url, endpoint, page, duration_string, sort_string); - if let Some(star) = self.stars.read().unwrap().iter().find(|s| s.to_ascii_lowercase() == search_string.to_ascii_lowercase()) { - video_url = format!("{}&stars={}", video_url, star); - } else if let Some(category) = self.categories.read().unwrap().iter().find(|s| s.to_ascii_lowercase() == search_string.to_ascii_lowercase()) { - video_url = format!("{}&tagMode=AND&tags={}", video_url, category); - } else { - video_url = format!("{}&q={}", video_url, search_string); + + let mut url = format!( + "{}/{endpoint}?limit=100&page={page}{duration}{sort}", + self.url + ); + + if let Ok(stars) = self.stars.read() { + if let Some(star) = stars.iter().find(|s| s.eq_ignore_ascii_case(&search)) { + url.push_str(&format!("&stars={star}")); + } } - 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()); - } + + if let Ok(cats) = self.categories.read() { + if let Some(cat) = cats.iter().find(|c| c.eq_ignore_ascii_case(&search)) { + url.push_str(&format!("&tagMode=OR&tags={cat}&expandTags=false")); } - None => { - vec![] + } + + if !search.is_empty() { + url.push_str(&format!("&q={search}")); + } + println!("pmvhaven query url: {}", url); + if let Some((time, items)) = cache.get(&url) { + println!("pmvhaven cache hit for url: {}", url); + println!("cache age: {} secs", time.elapsed().unwrap_or_default().as_secs()); + println!("cached items: {}", items.len()); + if time.elapsed().unwrap_or_default().as_secs() < 300 { + return Ok(items.clone()); } + } + + let mut requester = match options.requester { + Some(r) => r, + None => return Ok(vec![]), }; - let mut requester = options.requester.clone().unwrap(); + let text = requester.get(&url, None).await.unwrap_or_default(); + let json = serde_json::from_str(&text).unwrap_or(serde_json::Value::Null); + let items = self.get_video_items_from_json(json).await; - let text = requester.get(&video_url, None).await.unwrap(); - let json = serde_json::from_str::(&text).unwrap_or(serde_json::Value::Null); - let video_items: Vec = self - .get_video_items_from_json(json) - .await; - if !video_items.is_empty() { - cache.remove(&video_url); - cache.insert(video_url.clone(), video_items.clone()); - } else { - return Ok(old_items); + if !items.is_empty() { + cache.remove(&url); + cache.insert(url, items.clone()); } - Ok(video_items) + Ok(items) } - async fn get_video_items_from_json( - &self, - json: serde_json::Value, - ) -> Vec { - if json.is_null() { - return vec![]; - } + async fn get_video_items_from_json(&self, json: serde_json::Value) -> Vec { let mut items = vec![]; - let success = json["success"].as_bool().unwrap_or(false); - if !success { + + if !json.get("success").and_then(|v| v.as_bool()).unwrap_or(false) { return items; } - let videos = json["data"].as_array().cloned().unwrap_or_default(); - if videos.is_empty() { - return items; - } - for video in videos.clone() { - let title = decode(video["title"].as_str().unwrap_or("").as_bytes()).to_string().unwrap_or("".to_string()); - let id = video["_id"].as_str().unwrap_or(title.clone().as_str()).to_string(); - let video_url = video["videoUrl"].as_str().unwrap_or("").to_string(); - let views = video["views"].as_u64().unwrap_or(0); - let thumb = video["thumbnailUrl"].as_str().unwrap_or("").to_string(); - let duration_str = video["duration"].as_str().unwrap_or("0"); - let duration = parse_time_to_seconds(duration_str).unwrap_or(0); - let preview = video["previewUrl"].as_str().unwrap_or("").to_string(); - let tags_array = video["tags"].as_array().cloned().unwrap_or_default(); - for tag in tags_array.clone() { - let tag_str = decode(tag.as_str().unwrap_or("").as_bytes()).to_string().unwrap_or("".to_string()); - Self::push_unique(&self.categories, tag_str.clone()); + + let videos = json.get("data").and_then(|v| v.as_array()).cloned().unwrap_or_default(); + + for video in videos { + let title = decode(video.get("title").and_then(|v| v.as_str()).unwrap_or("").as_bytes()) + .to_string() + .unwrap_or_default(); + + let id = video + .get("_id") + .and_then(|v| v.as_str()) + .unwrap_or(&title) + .to_string(); + + let video_url = video.get("videoUrl").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let thumb = video.get("thumbnailUrl").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let preview = video.get("previewUrl").and_then(|v| v.as_str()).unwrap_or("").to_string(); + + let views = video.get("views").and_then(|v| v.as_u64()).unwrap_or(0); + let duration = parse_time_to_seconds(video.get("duration").and_then(|v| v.as_str()).unwrap_or("0")).unwrap_or(0); + + let tags = video.get("tags").and_then(|v| v.as_array()).cloned().unwrap_or_default(); + let stars = video.get("starsTags").and_then(|v| v.as_array()).cloned().unwrap_or_default(); + for t in tags.iter() { + if let Some(s) = t.as_str() { + let decoded = decode(s.as_bytes()).to_string().unwrap_or_default(); + Self::push_unique(&self.categories, decoded.clone()); + } } - let stars_array = video["starsTags"].as_array().cloned().unwrap_or_default(); - for tag in stars_array.clone() { - let tag_str = decode(tag.as_str().unwrap_or("").as_bytes()).to_string().unwrap_or("".to_string()); - Self::push_unique(&self.stars, tag_str.clone()); + for t in stars.iter() { + if let Some(s) = t.as_str() { + let decoded = decode(s.as_bytes()).to_string().unwrap_or_default(); + Self::push_unique(&self.stars, decoded.clone()); + } } - let tags = stars_array.iter().chain(tags_array.iter()).cloned().collect::>(); - let video_item = VideoItem::new( - id, - title, - video_url.replace(" ", "%20").to_string(), - "pmvhaven".to_string(), - thumb, - duration as u32, - ) - .views(views as u32) - .preview(preview) - .tags(tags.iter().map(|t| decode(t.as_str().unwrap_or("").as_bytes()).to_string().unwrap_or("".to_string())).collect()); - items.push(video_item); + items.push( + VideoItem::new(id, title, video_url.replace(' ', "%20"), "pmvhaven".into(), thumb, duration as u32) + .views(views as u32) + .preview(preview) + ); } - - return items; - } + items + } } #[async_trait] @@ -252,26 +266,26 @@ impl Provider for PmvhavenProvider { async fn get_videos( &self, cache: VideoCache, - pool: DbPool, + _pool: DbPool, _sort: String, query: Option, page: String, - per_page: String, + _per_page: String, options: ServerOptions, ) -> Vec { - let _ = per_page; - let _ = pool; - let videos: std::result::Result, Error> = self.query(cache, page.parse::().unwrap_or(1), query.unwrap_or("".to_string()).as_str(), options) - .await; - match videos { + let page = page.parse::().unwrap_or(1); + let query = query.unwrap_or_default(); + + match self.query(cache, page, &query, options).await { Ok(v) => v, Err(e) => { - println!("Error fetching videos: {}", e); + eprintln!("pmvhaven error: {e}"); vec![] } } } - fn get_channel(&self, clientversion: ClientVersion) -> crate::status::Channel { + + fn get_channel(&self, clientversion: ClientVersion) -> Channel { self.build_channel(clientversion) } }