use crate::unescape::unescape; use quick_xml::events::Event; use quick_xml::Reader; use serde::de::{self, Visitor}; use serde::{Deserialize, Deserializer}; use std::collections::HashMap; use std::fmt; use std::marker::PhantomData; extern crate quick_xml; extern crate reqwest; extern crate serde_json; // contains more info that i won't add for search results, maybe non_exhaustive? not sure #[derive(Deserialize, Debug)] pub struct SearchResult { pub id: i32, pub name: String, } impl fmt::Display for SearchResult { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(&format!("{}: {}", self.id, &self.name)) } } #[derive(Deserialize, Debug)] pub struct SearchResultMetadata { pub hits: String, } #[derive(Deserialize, Debug)] pub struct HentaiInfo { pub state: HentaiState, } #[derive(Deserialize, Debug)] pub struct HentaiState { pub data: HentaiData, } #[derive(Deserialize, Debug)] pub struct HentaiData { pub video: HentaiDataVideo, } #[derive(Deserialize, Debug)] pub struct HentaiDataVideo { pub hentai_video: HentaiVideo, pub videos_manifest: VideosManifest, } #[derive(Deserialize, Debug)] pub struct HentaiVideo { pub id: i32, pub slug: String, pub name: String, #[serde(deserialize_with = "remove_html")] pub description: String, pub views: usize, pub is_hard_subtitled: bool, pub duration_in_ms: i32, pub is_censored: bool, pub likes: usize, pub dislikes: usize, pub hentai_tags: Vec, } impl fmt::Display for HentaiInfo { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { let video = &self.state.data.video.hentai_video; let mut text = format!("ID: {}\nSlug: {}\nName: {}\nViews: {}\nLikes: {}\nDislikes: {}\nCensored: {}\nHardsubbed: {}\n", video.id, &video.slug, &video.name, video.views, video.likes, video.dislikes, match video.is_censored { true => "Yes", false => "No" }, match video.is_hard_subtitled { true => "Yes", false => "No" } ); let servers = &self.state.data.video.videos_manifest.servers; if !servers.is_empty() { let mut string_servers = HashMap::new(); for server in servers { let mut tmp_string_servers = string_servers.clone(); for stream in &server.streams { if !stream.url.is_empty() && !tmp_string_servers.contains_key(&stream.height) { tmp_string_servers.insert(&stream.height, stream.filesize_mbs); } } string_servers.extend(tmp_string_servers); } if !string_servers.is_empty() { let mut int_servers = HashMap::with_capacity(string_servers.len()); for (i, j) in &string_servers { match i.parse::() { Ok(i) => int_servers.insert(i, j), Err(_) => { int_servers.clear(); break; } }; } text.push_str(&format!( "Resolution: {}\n", match int_servers.is_empty() { true => { let mut keys: Vec<&&String> = string_servers.keys().collect(); keys.sort(); keys.iter() .map(|&i| format!("{} ({}MB)", i, string_servers.get(i).unwrap())) .collect::>() .join(", ") } false => { let mut keys: Vec<&i32> = int_servers.keys().collect(); keys.sort(); keys.iter() .map(|i| format!("{} ({}MB)", i, int_servers.get(i).unwrap())) .collect::>() .join(", ") } } )); } } if video.duration_in_ms > 0 { let mut seconds = video.duration_in_ms / 1000; let mut minutes = seconds / 60; if minutes > 0 { seconds -= minutes * 60; } let hours = minutes / 60; if hours > 0 { minutes -= hours * 60; seconds -= hours * 60 * 60; } let mut duration = String::new(); if hours > 0 { duration.push_str(&format!("{:02}:", hours)); } if minutes > 0 { duration.push_str(&format!("{:02}:", minutes)); } text.push_str(&format!("Duration: {}{:02}\n", &duration, seconds)); } text.push_str(&format!( "Tags: {}", video .hentai_tags .iter() .map(|i| i.text.as_str()) .collect::>() .join(", ") )); if !video.description.is_empty() { text.push_str(&format!("\nDescription: {}", &video.description)); } formatter.write_str(&text) } } #[derive(Deserialize, Debug)] pub struct HentaiTag { pub id: i32, pub text: String, } #[derive(Deserialize, Debug)] pub struct VideosManifest { pub servers: Vec, } #[derive(Deserialize, Debug)] pub struct VideoServer { pub is_permanent: bool, pub streams: Vec, } #[derive(Deserialize, Debug)] pub struct VideoStream { pub height: String, pub url: String, pub filesize_mbs: i32, } fn remove_html<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { struct RemoveHTML(PhantomData T>); impl<'de> Visitor<'de> for RemoveHTML { type Value = String; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("HTML string") } fn visit_str(self, value: &str) -> Result where E: de::Error, { // https://brokenco.de/2020/08/03/serde-deserialize-with-string.html let mut text = String::with_capacity(value.len()); let mut reader = Reader::from_str(value); reader.check_end_names(false); let mut buf = Vec::new(); loop { match reader.read_event(&mut buf) { Ok(Event::Text(e)) => text.push_str(&unescape( reader .decode(e.escaped()) .map_err(serde::de::Error::custom)?, )), Ok(Event::Eof) => break, Err(err) => panic!("Error at position {}: {:?}", reader.buffer_position(), err), _ => (), }; buf.clear(); } Ok(text) } } deserializer.deserialize_any(RemoveHTML(PhantomData)) } #[derive(Debug)] pub enum Error { Reqwest(reqwest::Error), QuickXML(quick_xml::Error), SerdeJSON(serde_json::Error), } impl From for Error { #[inline] fn from(error: reqwest::Error) -> Error { Error::Reqwest(error) } } impl From for Error { #[inline] fn from(error: quick_xml::Error) -> Error { Error::QuickXML(error) } } impl From for Error { #[inline] fn from(error: serde_json::Error) -> Error { Error::SerdeJSON(error) } } impl fmt::Display for Error { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(&match self { Error::Reqwest(err) => format!("reqwest error: {}", err), Error::QuickXML(err) => format!("quick-xml error: {}", err), Error::SerdeJSON(err) => format!("serde_json error: {}", err), }) } }