256 lines
7.7 KiB
Rust
256 lines
7.7 KiB
Rust
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;
|
|
use crate::unescape::unescape;
|
|
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<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),
|
|
}
|
|
)
|
|
}
|
|
}
|