fixes and cleanup

This commit is contained in:
Simon
2026-03-05 18:18:48 +00:00
parent 76fd5a4f4f
commit 2627505ade
49 changed files with 3245 additions and 1376 deletions

View File

@@ -1,6 +1,6 @@
use crate::DbPool;
use crate::api::ClientVersion;
use crate::providers::Provider;
use crate::providers::{Provider, report_provider_error_background, requester_or_default};
use crate::status::*;
use crate::util::cache::VideoCache;
use crate::util::discord::send_discord_error_report;
@@ -8,10 +8,11 @@ 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 htmlentity::entity::{decode, ICodedDataTrait};
use htmlentity::entity::{ICodedDataTrait, decode};
use std::fmt::Write;
use std::sync::{Arc, RwLock};
use std::vec;
use std::fmt::Write;
use url::form_urlencoded::byte_serialize;
error_chain! {
foreign_links {
@@ -135,9 +136,7 @@ impl PmvhavenProvider {
fn is_direct_media_url(url: &str) -> bool {
let lower = url.to_ascii_lowercase();
(lower.starts_with("http://") || lower.starts_with("https://"))
&& (lower.contains("/videos/")
|| lower.contains(".mp4")
|| lower.contains(".m3u8"))
&& (lower.contains("/videos/") || lower.contains(".mp4") || lower.contains(".m3u8"))
}
fn pick_downloadable_media_url(&self, video: &serde_json::Value) -> Option<String> {
@@ -192,67 +191,119 @@ impl PmvhavenProvider {
_ => "",
};
let endpoint = if search.is_empty() {
"api/videos"
} else {
"api/videos/search"
};
let mut url = format!(
"{}/{endpoint}?limit=100&page={page}{duration}{sort}",
self.url
);
let encoded_search: String = byte_serialize(search.as_bytes()).collect();
let mut extra_filters = String::new();
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 encoded_star: String = byte_serialize(star.as_bytes()).collect();
extra_filters.push_str(&format!("&stars={encoded_star}"));
}
}
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"));
let encoded_cat: String = byte_serialize(cat.as_bytes()).collect();
extra_filters.push_str(&format!("&tagMode=OR&tags={encoded_cat}&expandTags=false"));
}
}
if !search.is_empty() {
url.push_str(&format!("&q={search}"));
let mut urls = vec![];
if search.is_empty() {
urls.push(format!(
"{}/api/videos?limit=100&page={page}{duration}{sort}{extra_filters}",
self.url
));
} else {
urls.push(format!(
"{}/api/videos/search?limit=100&page={page}{duration}{sort}{extra_filters}&q={encoded_search}",
self.url
));
urls.push(format!(
"{}/api/videos/search?limit=100&page={page}{duration}{sort}{extra_filters}&query={encoded_search}",
self.url
));
urls.push(format!(
"{}/api/videos?limit=100&page={page}{duration}{sort}{extra_filters}&q={encoded_search}",
self.url
));
urls.push(format!(
"{}/api/videos?limit=100&page={page}{duration}{sort}{extra_filters}&search={encoded_search}",
self.url
));
}
if let Some((time, items)) = cache.get(&url) {
if time.elapsed().unwrap_or_default().as_secs() < 300 {
return Ok(items.clone());
let mut requester = requester_or_default(&options, "pmvhaven", "query");
for url in urls {
if let Some((time, items)) = cache.get(&url) {
if time.elapsed().unwrap_or_default().as_secs() < 300 {
return Ok(items.clone());
}
}
let text = match requester.get(&url, None).await {
Ok(text) => text,
Err(err) => {
report_provider_error_background(
"pmvhaven",
"get.request",
&format!("url={url}; error={err}"),
);
continue;
}
};
let json: serde_json::Value = match serde_json::from_str(&text) {
Ok(json) => json,
Err(err) => {
report_provider_error_background(
"pmvhaven",
"parse.json",
&format!("url={url}; error={err}"),
);
continue;
}
};
let items = self.get_video_items_from_json(json).await;
if !items.is_empty() {
cache.remove(&url);
cache.insert(url, items.clone());
return Ok(items);
}
}
let mut requester = match options.requester {
Some(r) => r,
None => return Ok(vec![]),
};
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;
if !items.is_empty() {
cache.remove(&url);
cache.insert(url, items.clone());
}
Ok(items)
Ok(vec![])
}
async fn get_video_items_from_json(&self, json: serde_json::Value) -> Vec<VideoItem> {
let mut items = vec![];
if !json.get("success").and_then(|v| v.as_bool()).unwrap_or(false) {
if !json
.get("success")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
return items;
}
let videos = json.get("data").and_then(|v| v.as_array()).cloned().unwrap_or_default();
let videos = json
.get("data")
.and_then(|v| v.as_array())
.or_else(|| json.get("videos").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 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")
@@ -266,14 +317,36 @@ impl PmvhavenProvider {
continue;
}
};
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 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 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();
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();
@@ -293,14 +366,21 @@ impl PmvhavenProvider {
"mp4".to_string()
};
items.push(
VideoItem::new(id, title, video_url.clone(), "pmvhaven".into(), thumb, duration as u32)
.views(views as u32)
.formats(vec![VideoFormat::new(
video_url,
"1080".to_string(),
format_type,
)])
.preview(preview)
VideoItem::new(
id,
title,
video_url.clone(),
"pmvhaven".into(),
thumb,
duration as u32,
)
.views(views as u32)
.formats(vec![VideoFormat::new(
video_url,
"1080".to_string(),
format_type,
)])
.preview(preview),
);
}
@@ -308,6 +388,56 @@ impl PmvhavenProvider {
}
}
#[cfg(test)]
mod tests {
use super::PmvhavenProvider;
use serde_json::json;
#[tokio::test]
async fn parses_videos_from_videos_key() {
let provider = PmvhavenProvider::new();
let payload = json!({
"success": true,
"videos": [{
"_id": "abc123",
"title": "Sample Title",
"videoUrl": "https://video.pmvhaven.com/videos/sample.mp4",
"thumbnailUrl": "https://video.pmvhaven.com/thumbnails/sample.webp",
"previewUrl": "https://video.pmvhaven.com/previews/sample.mp4",
"views": 42,
"duration": "2:11",
"tags": [],
"starsTags": []
}]
});
let items = provider.get_video_items_from_json(payload).await;
assert_eq!(items.len(), 1);
}
#[tokio::test]
async fn parses_videos_from_data_key() {
let provider = PmvhavenProvider::new();
let payload = json!({
"success": true,
"data": [{
"_id": "abc123",
"title": "Sample Title",
"videoUrl": "https://video.pmvhaven.com/videos/sample.mp4",
"thumbnailUrl": "https://video.pmvhaven.com/thumbnails/sample.webp",
"previewUrl": "https://video.pmvhaven.com/previews/sample.mp4",
"views": 42,
"duration": "2:11",
"tags": [],
"starsTags": []
}]
});
let items = provider.get_video_items_from_json(payload).await;
assert_eq!(items.len(), 1);
}
}
#[async_trait]
impl Provider for PmvhavenProvider {
async fn get_videos(
@@ -332,14 +462,15 @@ impl Provider for PmvhavenProvider {
let _ = writeln!(chain_str, "{}. {}", i + 1, cause);
}
send_discord_error_report(
e.to_string(),
Some(chain_str),
Some("PMVHaven Provider"),
Some("Failed to load videos from PMVHaven"),
file!(),
line!(),
module_path!(),
).await;
e.to_string(),
Some(chain_str),
Some("PMVHaven Provider"),
Some("Failed to load videos from PMVHaven"),
file!(),
line!(),
module_path!(),
)
.await;
vec![]
}
}