use std::fmt; use std::marker::PhantomData; use std::collections::HashMap; 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" } ); 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!("{}:", 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, 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 to_return = String::new(); 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)) => 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), } ) } }