use std::fmt; use std::marker::PhantomData; use serde::de::{self, Visitor}; use serde::{Deserialize, Deserializer}; use quick_xml::Reader; use quick_xml::events::Event; extern crate reqwest; extern crate quick_xml; 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" } ); 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!("{}:", hours)); } if minutes > 0 { duration.push_str(&format!("{}:", minutes)); } duration.push_str(&seconds.to_string()); text.push_str(&format!("Duration: {}\n", &duration)); } formatter.write_str(&format!("{}Tags: {}\nDescription: {}", &text, video.hentai_tags.iter().map(|i| i.text.as_str()).collect::>().join(", "), &video.description)) } } #[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 } 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 to_return = String::new(); let mut reader = Reader::from_str(&value); let mut buf = Vec::new(); loop { match reader.read_event(&mut buf) { Ok(Event::Text(e)) => to_return.push_str(&e.unescape_and_decode(&reader).map_err(de::Error::custom)?), Ok(Event::Eof) => break, Err(err) => panic!("Error at position {}: {:?}", reader.buffer_position(), err), _ => () }; buf.clear(); } Ok(to_return) } } 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), } ) } }