303 lines
10 KiB
Rust
303 lines
10 KiB
Rust
use crate::DbPool;
|
|
use crate::api::ClientVersion;
|
|
use crate::providers::Provider;
|
|
use crate::status::*;
|
|
use crate::util::cache::VideoCache;
|
|
use crate::util::discord::send_discord_error_report;
|
|
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::{decode, ICodedDataTrait};
|
|
use std::sync::{Arc, RwLock};
|
|
use std::vec;
|
|
use std::fmt::Write;
|
|
|
|
error_chain! {
|
|
foreign_links {
|
|
Io(std::io::Error);
|
|
HttpRequest(wreq::Error);
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct PmvhavenProvider {
|
|
url: String,
|
|
stars: Arc<RwLock<Vec<String>>>,
|
|
categories: Arc<RwLock<Vec<String>>>,
|
|
}
|
|
|
|
impl PmvhavenProvider {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
url: "https://pmvhaven.com".to_string(),
|
|
stars: Arc::new(RwLock::new(vec![])),
|
|
categories: Arc::new(RwLock::new(vec![])),
|
|
}
|
|
}
|
|
|
|
fn build_channel(&self, clientversion: ClientVersion) -> Channel {
|
|
let _ = clientversion;
|
|
|
|
let categories = self
|
|
.categories
|
|
.read()
|
|
.map(|g| g.clone())
|
|
.unwrap_or_default();
|
|
|
|
Channel {
|
|
id: "pmvhaven".to_string(),
|
|
name: "PMVHaven".to_string(),
|
|
description: "Best PMV Videos".to_string(),
|
|
premium: false,
|
|
favicon: "https://www.google.com/s2/favicons?sz=64&domain=pmvhaven.com".to_string(),
|
|
status: "active".to_string(),
|
|
categories,
|
|
options: vec![
|
|
ChannelOption {
|
|
id: "sort".into(),
|
|
title: "Sort".into(),
|
|
description: "Sort the Videos".into(),
|
|
systemImage: "list.number".into(),
|
|
colorName: "blue".into(),
|
|
options: vec![
|
|
FilterOption {
|
|
id: "relevance".into(),
|
|
title: "Relevance".into(),
|
|
},
|
|
FilterOption {
|
|
id: "newest".into(),
|
|
title: "Newest".into(),
|
|
},
|
|
FilterOption {
|
|
id: "oldest".into(),
|
|
title: "Oldest".into(),
|
|
},
|
|
FilterOption {
|
|
id: "most viewed".into(),
|
|
title: "Most Viewed".into(),
|
|
},
|
|
FilterOption {
|
|
id: "most liked".into(),
|
|
title: "Most Liked".into(),
|
|
},
|
|
FilterOption {
|
|
id: "most disliked".into(),
|
|
title: "Most Disliked".into(),
|
|
},
|
|
],
|
|
multiSelect: false,
|
|
},
|
|
ChannelOption {
|
|
id: "duration".into(),
|
|
title: "Duration".into(),
|
|
description: "Length of the Videos".into(),
|
|
systemImage: "timer".into(),
|
|
colorName: "green".into(),
|
|
options: vec![
|
|
FilterOption {
|
|
id: "any".into(),
|
|
title: "Any".into(),
|
|
},
|
|
FilterOption {
|
|
id: "<4 min".into(),
|
|
title: "<4 min".into(),
|
|
},
|
|
FilterOption {
|
|
id: "4-20 min".into(),
|
|
title: "4-20 min".into(),
|
|
},
|
|
FilterOption {
|
|
id: "20-60 min".into(),
|
|
title: "20-60 min".into(),
|
|
},
|
|
FilterOption {
|
|
id: ">1 hour".into(),
|
|
title: ">1 hour".into(),
|
|
},
|
|
],
|
|
multiSelect: false,
|
|
},
|
|
],
|
|
nsfw: true,
|
|
cacheDuration: None,
|
|
}
|
|
}
|
|
|
|
fn push_unique(target: &Arc<RwLock<Vec<String>>>, item: String) {
|
|
if let Ok(mut vec) = target.write() {
|
|
if !vec.iter().any(|x| x == &item) {
|
|
vec.push(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn query(
|
|
&self,
|
|
cache: VideoCache,
|
|
page: u8,
|
|
query: &str,
|
|
options: ServerOptions,
|
|
) -> Result<Vec<VideoItem>> {
|
|
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 = 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.is_empty() {
|
|
"api/videos"
|
|
} else {
|
|
"api/videos/search"
|
|
};
|
|
|
|
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}"));
|
|
}
|
|
}
|
|
|
|
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"));
|
|
}
|
|
}
|
|
|
|
if !search.is_empty() {
|
|
url.push_str(&format!("&q={search}"));
|
|
}
|
|
if let Some((time, items)) = cache.get(&url) {
|
|
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 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)
|
|
}
|
|
|
|
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) {
|
|
return items;
|
|
}
|
|
|
|
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());
|
|
}
|
|
}
|
|
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());
|
|
}
|
|
}
|
|
|
|
items.push(
|
|
VideoItem::new(id, title, video_url.replace(' ', "%20"), "pmvhaven".into(), thumb, duration as u32)
|
|
.views(views as u32)
|
|
.preview(preview)
|
|
);
|
|
}
|
|
|
|
items
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
impl Provider for PmvhavenProvider {
|
|
async fn get_videos(
|
|
&self,
|
|
cache: VideoCache,
|
|
_pool: DbPool,
|
|
_sort: String,
|
|
query: Option<String>,
|
|
page: String,
|
|
_per_page: String,
|
|
options: ServerOptions,
|
|
) -> Vec<VideoItem> {
|
|
let page = page.parse::<u8>().unwrap_or(1);
|
|
let query = query.unwrap_or_default();
|
|
|
|
match self.query(cache, page, &query, options).await {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
eprintln!("pmvhaven error: {e}");
|
|
let mut chain_str = String::new();
|
|
for (i, cause) in e.iter().enumerate() {
|
|
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;
|
|
vec![]
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_channel(&self, clientversion: ClientVersion) -> Option<Channel> {
|
|
Some(self.build_channel(clientversion))
|
|
}
|
|
}
|