hanimers/src/structs.rs

272 lines
8.1 KiB
Rust

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<HentaiTag>,
}
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::<i32>() {
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::<Vec<_>>()
.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::<Vec<_>>()
.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::<Vec<_>>()
.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<VideoServer>,
}
#[derive(Deserialize, Debug)]
pub struct VideoServer {
pub is_permanent: bool,
pub streams: Vec<VideoStream>,
}
#[derive(Deserialize, Debug)]
pub struct VideoStream {
pub height: String,
pub url: String,
pub filesize_mbs: i32,
}
fn remove_html<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
struct RemoveHTML<T>(PhantomData<fn() -> T>);
impl<'de> Visitor<'de> for RemoveHTML<String> {
type Value = String;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("HTML string")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
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<reqwest::Error> for Error {
#[inline]
fn from(error: reqwest::Error) -> Error {
Error::Reqwest(error)
}
}
impl From<quick_xml::Error> for Error {
#[inline]
fn from(error: quick_xml::Error) -> Error {
Error::QuickXML(error)
}
}
impl From<serde_json::Error> 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),
})
}
}