Initial commit
This commit is contained in:
commit
e1fcfd1b22
|
@ -0,0 +1 @@
|
|||
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "imgurx"
|
||||
version = "0.1.0"
|
||||
authors = ["blank X <theblankx@protonmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.5", features = ["rt-multi-thread"] }
|
||||
reqwest = "0.11"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
quick-xml = "0.22"
|
||||
warp = "0.3"
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 blank X
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,54 @@
|
|||
use std::convert::Infallible;
|
||||
use std::convert::TryInto;
|
||||
use std::env;
|
||||
use warp::Filter;
|
||||
mod structs;
|
||||
mod utils;
|
||||
use reqwest::Client;
|
||||
use tokio::runtime;
|
||||
extern crate warp;
|
||||
|
||||
async fn handle_album(id: String, client: Client) -> Result<impl warp::Reply, Infallible> {
|
||||
let (status_code, reply) = utils::generate_html(utils::get_album(client.clone(), &id).await);
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::with_header(reply, "Content-Type", "text/html"),
|
||||
status_code.try_into().unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn handle_media(id: String, client: Client) -> Result<impl warp::Reply, Infallible> {
|
||||
let (status_code, reply) = utils::generate_html(utils::get_media(client.clone(), &id).await);
|
||||
Ok(warp::reply::with_status(
|
||||
warp::reply::with_header(reply, "Content-Type", "text/html"),
|
||||
status_code.try_into().unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn async_main(port: u16) {
|
||||
let client = Client::new();
|
||||
let client = warp::any().map(move || client.clone());
|
||||
let album_path = warp::path!("a" / String)
|
||||
.and(client.clone())
|
||||
.and_then(handle_album);
|
||||
let media_path = warp::path!(String).and(client).and_then(handle_media);
|
||||
let not_found_handler = warp::any().map(|| warp::reply::with_status(
|
||||
warp::reply::html("<html><head><style>body { background-color: black; color: white; text-align: center; }\ndiv { border: 1px solid #fcc; background: #fee; padding: 0.5em 1em 0.5em 1em; color: black; }</style></head><body><div><b>404: Not Found</b></div></body></html>"),
|
||||
404.try_into().unwrap(),
|
||||
));
|
||||
let routes = warp::filters::method::get().and(album_path.or(media_path).or(not_found_handler));
|
||||
eprintln!("Serving on 0.0.0.0:{}", port);
|
||||
warp::serve(routes).run(([0u8, 0, 0, 0], port)).await;
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let port = match env::var("PORT") {
|
||||
Ok(port) => port.parse().unwrap(),
|
||||
Err(env::VarError::NotPresent) => 8080,
|
||||
Err(env::VarError::NotUnicode(_)) => panic!("Environment variable PORT isn't unicode"),
|
||||
};
|
||||
runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async_main(port));
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
//use std::fmt;
|
||||
use serde::Deserialize;
|
||||
extern crate reqwest;
|
||||
extern crate serde_json;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Album {
|
||||
pub title: String,
|
||||
pub is_mature: bool,
|
||||
pub media: Vec<Media>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Media {
|
||||
pub r#type: String,
|
||||
pub url: String,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub metadata: Metadata,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Metadata {
|
||||
pub description: String,
|
||||
pub duration: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Reqwest(reqwest::Error),
|
||||
SerdeJSON(serde_json::Error),
|
||||
APIErrors(APIErrors),
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
#[inline]
|
||||
fn from(error: reqwest::Error) -> Error {
|
||||
Error::Reqwest(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: {}", err),
|
||||
// Error::SerdeJSON(err) => format!("serde_json: {}", err),
|
||||
// Error::ApiErrors(err) =>
|
||||
// })
|
||||
// }
|
||||
//}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct APIErrors {
|
||||
pub errors: Vec<APIError>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct APIError {
|
||||
pub id: String,
|
||||
pub code: String,
|
||||
pub status: String,
|
||||
pub detail: String,
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
use crate::structs::{Album, Error};
|
||||
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
|
||||
use quick_xml::Writer;
|
||||
use reqwest::Client;
|
||||
use std::io::Cursor;
|
||||
extern crate serde_json;
|
||||
|
||||
pub async fn get_album(client: Client, id: &str) -> Result<Album, Error> {
|
||||
let resp = client
|
||||
.get(&format!(
|
||||
"https://api.imgur.com/post/v1/albums/{}?client_id=546c25a59c58ad7&include=media",
|
||||
id
|
||||
))
|
||||
.send()
|
||||
.await?;
|
||||
let status = resp.status();
|
||||
let text = resp.text().await?;
|
||||
if status == 200 {
|
||||
Ok(serde_json::from_str(&text)?)
|
||||
} else {
|
||||
Err(Error::APIErrors(serde_json::from_str(&text)?))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_media(client: Client, id: &str) -> Result<Album, Error> {
|
||||
let resp = client
|
||||
.get(&format!(
|
||||
"https://api.imgur.com/post/v1/media/{}?client_id=546c25a59c58ad7&include=media",
|
||||
id
|
||||
))
|
||||
.send()
|
||||
.await?;
|
||||
let status = resp.status();
|
||||
let text = resp.text().await?;
|
||||
if status == 200 {
|
||||
Ok(serde_json::from_str(&text)?)
|
||||
} else {
|
||||
Err(Error::APIErrors(serde_json::from_str(&text)?))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_html(album: Result<Album, Error>) -> (u16, Vec<u8>) {
|
||||
let mut writer = Writer::new(Cursor::new(vec![]));
|
||||
|
||||
let elem = BytesStart::owned(b"html".to_vec(), 4);
|
||||
writer.write_event(Event::Start(elem)).unwrap();
|
||||
|
||||
let elem = BytesStart::owned(b"head".to_vec(), 4);
|
||||
writer.write_event(Event::Start(elem)).unwrap();
|
||||
|
||||
let elem = BytesStart::owned(b"style".to_vec(), 5);
|
||||
writer.write_event(Event::Start(elem)).unwrap();
|
||||
|
||||
let elem = BytesText::from_plain_str("body { background-color: black; color: white; text-align: center; }\ndiv { border: 1px solid #fcc; background: #fee; padding: 0.5em 1em 0.5em 1em; color: black; }");
|
||||
writer.write_event(Event::Text(elem)).unwrap();
|
||||
|
||||
let elem = BytesEnd::owned(b"style".to_vec());
|
||||
writer.write_event(Event::End(elem)).unwrap();
|
||||
|
||||
let elem = BytesEnd::owned(b"head".to_vec());
|
||||
writer.write_event(Event::End(elem)).unwrap();
|
||||
|
||||
let elem = BytesStart::owned(b"body".to_vec(), 4);
|
||||
writer.write_event(Event::Start(elem)).unwrap();
|
||||
|
||||
let status_code = match album {
|
||||
Ok(album) => {
|
||||
if album.is_mature {
|
||||
let elem = BytesStart::owned(b"div".to_vec(), 3);
|
||||
writer.write_event(Event::Start(elem)).unwrap();
|
||||
|
||||
let elem = BytesStart::owned(b"b".to_vec(), 1);
|
||||
writer.write_event(Event::Start(elem)).unwrap();
|
||||
|
||||
let elem = BytesText::from_plain_str("Marked as NSFW");
|
||||
writer.write_event(Event::Text(elem)).unwrap();
|
||||
|
||||
let elem = BytesEnd::owned(b"b".to_vec());
|
||||
writer.write_event(Event::End(elem)).unwrap();
|
||||
|
||||
let elem = BytesEnd::owned(b"div".to_vec());
|
||||
writer.write_event(Event::End(elem)).unwrap();
|
||||
}
|
||||
|
||||
if !album.title.is_empty() {
|
||||
let elem = BytesStart::owned(b"h1".to_vec(), 2);
|
||||
writer.write_event(Event::Start(elem)).unwrap();
|
||||
|
||||
let elem = BytesText::from_plain_str(&album.title);
|
||||
writer.write_event(Event::Text(elem)).unwrap();
|
||||
|
||||
let elem = BytesEnd::owned(b"h1".to_vec());
|
||||
writer.write_event(Event::End(elem)).unwrap();
|
||||
}
|
||||
|
||||
for media in &album.media {
|
||||
if media.r#type == "image" {
|
||||
let mut elem = BytesStart::owned(b"img".to_vec(), 3);
|
||||
elem.push_attribute(("width", media.width.to_string().as_str()));
|
||||
elem.push_attribute(("height", media.height.to_string().as_str()));
|
||||
elem.push_attribute(("src", media.url.as_str()));
|
||||
writer.write_event(Event::Start(elem)).unwrap();
|
||||
|
||||
let elem = BytesEnd::owned(b"img".to_vec());
|
||||
writer.write_event(Event::End(elem)).unwrap();
|
||||
} else if media.r#type == "video" {
|
||||
let mut elem = BytesStart::owned(b"video".to_vec(), 5);
|
||||
elem.push_attribute(("controls", "1"));
|
||||
elem.push_attribute(("width", media.width.to_string().as_str()));
|
||||
elem.push_attribute(("height", media.height.to_string().as_str()));
|
||||
elem.push_attribute(("src", media.url.as_str()));
|
||||
writer.write_event(Event::Start(elem)).unwrap();
|
||||
|
||||
let elem = BytesEnd::owned(b"video".to_vec());
|
||||
writer.write_event(Event::End(elem)).unwrap();
|
||||
} else {
|
||||
let elem = BytesStart::owned(b"div".to_vec(), 3);
|
||||
writer.write_event(Event::Start(elem)).unwrap();
|
||||
|
||||
let elem = BytesStart::owned(b"b".to_vec(), 1);
|
||||
writer.write_event(Event::Start(elem)).unwrap();
|
||||
|
||||
let text = format!("Unknown media type {}, URL is ", &media.r#type);
|
||||
let elem = BytesText::from_plain_str(&text);
|
||||
writer.write_event(Event::Text(elem)).unwrap();
|
||||
|
||||
let mut elem = BytesStart::owned(b"a".to_vec(), 1);
|
||||
elem.push_attribute(("href", media.url.as_str()));
|
||||
writer.write_event(Event::Start(elem)).unwrap();
|
||||
|
||||
let elem = BytesText::from_plain_str(&media.url);
|
||||
writer.write_event(Event::Text(elem)).unwrap();
|
||||
|
||||
let elem = BytesEnd::owned(b"a".to_vec());
|
||||
writer.write_event(Event::End(elem)).unwrap();
|
||||
|
||||
let elem = BytesEnd::owned(b"b".to_vec());
|
||||
writer.write_event(Event::End(elem)).unwrap();
|
||||
|
||||
let elem = BytesEnd::owned(b"div".to_vec());
|
||||
writer.write_event(Event::End(elem)).unwrap();
|
||||
}
|
||||
|
||||
if !media.metadata.description.is_empty() {
|
||||
let elem = BytesStart::owned(b"p".to_vec(), 1);
|
||||
writer.write_event(Event::Start(elem)).unwrap();
|
||||
|
||||
let elem = BytesText::from_plain_str(&media.metadata.description);
|
||||
writer.write_event(Event::Text(elem)).unwrap();
|
||||
|
||||
let elem = BytesEnd::owned(b"a".to_vec());
|
||||
writer.write_event(Event::End(elem)).unwrap();
|
||||
}
|
||||
}
|
||||
200
|
||||
}
|
||||
Err(err) => {
|
||||
let show_details = match err {
|
||||
Error::APIErrors(ref err)
|
||||
if err.errors.len() == 1 && err.errors[0].code == "404" =>
|
||||
{
|
||||
false
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
let mut elem = BytesStart::owned(b"div".to_vec(), 3);
|
||||
if show_details {
|
||||
elem.push_attribute(("style", "text-align: left;"));
|
||||
}
|
||||
writer.write_event(Event::Start(elem)).unwrap();
|
||||
|
||||
let elem = BytesStart::owned(b"b".to_vec(), 1);
|
||||
writer.write_event(Event::Start(elem)).unwrap();
|
||||
|
||||
let to_return = match err {
|
||||
Error::APIErrors(err) if !show_details => {
|
||||
let text = format!("404: {}", err.errors[0].detail);
|
||||
let elem = BytesText::from_plain_str(&text);
|
||||
writer.write_event(Event::Text(elem)).unwrap();
|
||||
404
|
||||
}
|
||||
_ => {
|
||||
let elem = BytesStart::owned(b"pre".to_vec(), 3);
|
||||
writer.write_event(Event::Start(elem)).unwrap();
|
||||
|
||||
let text = format!("{:#?}", err);
|
||||
let elem = BytesText::from_plain_str(&text);
|
||||
writer.write_event(Event::Text(elem)).unwrap();
|
||||
|
||||
let elem = BytesEnd::owned(b"pre".to_vec());
|
||||
writer.write_event(Event::End(elem)).unwrap();
|
||||
500
|
||||
}
|
||||
};
|
||||
|
||||
let elem = BytesEnd::owned(b"b".to_vec());
|
||||
writer.write_event(Event::End(elem)).unwrap();
|
||||
|
||||
let elem = BytesEnd::owned(b"div".to_vec());
|
||||
writer.write_event(Event::End(elem)).unwrap();
|
||||
to_return
|
||||
}
|
||||
};
|
||||
|
||||
let elem = BytesEnd::owned(b"body".to_vec());
|
||||
writer.write_event(Event::End(elem)).unwrap();
|
||||
|
||||
let elem = BytesEnd::owned(b"html".to_vec());
|
||||
writer.write_event(Event::End(elem)).unwrap();
|
||||
|
||||
(status_code, writer.into_inner().into_inner())
|
||||
}
|
Loading…
Reference in New Issue