feat: improve fetching images, show actual source from safebooru and add loading spinner
This commit is contained in:
parent
3e7076c748
commit
2bb23ea92a
29
Cargo.lock
generated
29
Cargo.lock
generated
@ -228,9 +228,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-channel"
|
name = "futures-channel"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
|
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
@ -238,9 +238,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
|
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-executor"
|
name = "futures-executor"
|
||||||
@ -255,15 +255,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -283,21 +283,21 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-sink"
|
name = "futures-sink"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
|
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
|
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.30"
|
version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@ -615,6 +615,7 @@ dependencies = [
|
|||||||
"fastrand",
|
"fastrand",
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"futures-retry",
|
"futures-retry",
|
||||||
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"maud",
|
"maud",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
@ -15,3 +15,4 @@ signal-hook = "0.3"
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
form_urlencoded = "1"
|
form_urlencoded = "1"
|
||||||
futures-retry = "0.6"
|
futures-retry = "0.6"
|
||||||
|
futures-util = "0.3.31"
|
||||||
|
@ -77,3 +77,9 @@ impl Data {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct FetchedLink {
|
||||||
|
pub(crate) image_url: String,
|
||||||
|
pub(crate) new_source: Option<Uri>,
|
||||||
|
}
|
||||||
|
@ -40,7 +40,7 @@ impl IntoResponse for AppError {
|
|||||||
(crate::get_page_head_common())
|
(crate::get_page_head_common())
|
||||||
}
|
}
|
||||||
body style=(crate::BODY_STYLE) {
|
body style=(crate::BODY_STYLE) {
|
||||||
p style=(format!("{} font-size: 1.3em;", crate::IMG_STYLE)) {
|
p style=("display: block; margin: auto; font-size: 1.3em;") {
|
||||||
"Something went wrong: "
|
"Something went wrong: "
|
||||||
br;
|
br;
|
||||||
(self.internal.to_string());
|
(self.internal.to_string());
|
||||||
|
159
src/main.rs
159
src/main.rs
@ -5,12 +5,14 @@ use axum::{
|
|||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use data::{Art, ArtKind, Data};
|
use data::{Art, ArtKind, Data, FetchedLink};
|
||||||
use error::AppResult;
|
use error::{AppError, AppResult};
|
||||||
|
use futures_util::{future::BoxFuture, FutureExt};
|
||||||
use http::Uri;
|
use http::Uri;
|
||||||
use maud::PreEscaped;
|
use maud::PreEscaped;
|
||||||
use std::{
|
use std::{
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
|
str::FromStr,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -19,10 +21,11 @@ mod error;
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let arts_file_path = get_conf("ARTS_PATH");
|
let arts_file_path = get_conf("ARTS_PATH").unwrap_or_else(|| "./utils/arts.txt".to_string());
|
||||||
let arts = std::fs::read_to_string(&arts_file_path).unwrap();
|
let arts = std::fs::read_to_string(&arts_file_path).unwrap();
|
||||||
let state = AppState::new(Data::parse(&arts).unwrap());
|
let state = AppState::new(Data::parse(&arts).unwrap());
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
std::thread::spawn({
|
std::thread::spawn({
|
||||||
use signal_hook::{consts::SIGUSR2, iterator::Signals};
|
use signal_hook::{consts::SIGUSR2, iterator::Signals};
|
||||||
|
|
||||||
@ -62,12 +65,13 @@ async fn show_art(
|
|||||||
|
|
||||||
let art = state.data.lock().unwrap().pick_random_art().clone();
|
let art = state.data.lock().unwrap().pick_random_art().clone();
|
||||||
let image_link = if let Some(image_link) = state.direct_links.get(&art.url) {
|
let image_link = if let Some(image_link) = state.direct_links.get(&art.url) {
|
||||||
image_link.to_string()
|
image_link.clone()
|
||||||
} else {
|
} else {
|
||||||
let image_link = match art.kind {
|
let image_link_fn = match art.kind {
|
||||||
ArtKind::Twitter => fetch_twitter_image_link(&state.http, &art.url).await?,
|
ArtKind::Twitter => fetch_twitter_image_link,
|
||||||
ArtKind::Safebooru => fetch_safebooru_image_link(&state.http, &art.url).await?,
|
ArtKind::Safebooru => fetch_safebooru_image_link,
|
||||||
};
|
};
|
||||||
|
let image_link = (image_link_fn)(&state.http, &art.url).await?;
|
||||||
state
|
state
|
||||||
.direct_links
|
.direct_links
|
||||||
.insert(art.url.clone(), image_link.clone());
|
.insert(art.url.clone(), image_link.clone());
|
||||||
@ -79,15 +83,16 @@ async fn show_art(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const BODY_STYLE: &str =
|
const BODY_STYLE: &str =
|
||||||
"margin: 0px; background: #0e0e0e; height: 100vh; width: 100vw; display: flex; font-family: \"PT Mono\", monospace; font-weight: 400; font-style: normal; font-optical-sizing: auto;";
|
"color: #ffffff; margin: 0px; background: #0e0e0e; height: 100vh; width: 100vw; display: flex; font-family: \"PT Mono\", monospace; font-weight: 400; font-style: normal; font-optical-sizing: auto;";
|
||||||
const IMG_STYLE: &str = "display: block; margin: auto; max-height: 100vh; max-width: 100vw;";
|
|
||||||
const ABOUT_STYLE: &str = "position: absolute; bottom: 0; font-size: 0.75em; color: #ffffff; background-color: #0e0e0eaa;";
|
const ABOUT_STYLE: &str = "position: absolute; bottom: 0; font-size: 0.75em; color: #ffffff; background-color: #0e0e0eaa;";
|
||||||
|
|
||||||
fn get_page_head_common() -> PreEscaped<String> {
|
fn get_page_head_common() -> PreEscaped<String> {
|
||||||
let title = get_conf("SITE_TITLE");
|
let title = get_conf("SITE_TITLE").unwrap_or_else(|| "random project moon art".to_string());
|
||||||
let embed_title = get_conf("EMBED_TITLE");
|
let embed_title =
|
||||||
let embed_content = get_conf("EMBED_DESC");
|
get_conf("EMBED_TITLE").unwrap_or_else(|| "random project moon art".to_string());
|
||||||
let embed_color = get_conf("EMBED_COLOR");
|
let embed_content =
|
||||||
|
get_conf("EMBED_DESC").unwrap_or_else(|| "random project moon art".to_string());
|
||||||
|
let embed_color = get_conf("EMBED_COLOR").unwrap_or_else(|| "#ffffff".to_string());
|
||||||
|
|
||||||
maud::html! {
|
maud::html! {
|
||||||
meta charset="utf8";
|
meta charset="utf8";
|
||||||
@ -97,6 +102,7 @@ fn get_page_head_common() -> PreEscaped<String> {
|
|||||||
link rel="preconnect" href="https://fonts.googleapis.com";
|
link rel="preconnect" href="https://fonts.googleapis.com";
|
||||||
link rel="preconnect" href="https://fonts.gstatic.com" crossorigin;
|
link rel="preconnect" href="https://fonts.gstatic.com" crossorigin;
|
||||||
link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=PT+Mono&display=swap";
|
link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=PT+Mono&display=swap";
|
||||||
|
link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@chgibb/css-spinners@2.2.1/css/spinners.min.css";
|
||||||
title { (title) }
|
title { (title) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -111,16 +117,20 @@ fn get_page_contact() -> PreEscaped<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_page(art: &Art, image_link: &str) -> Html<String> {
|
fn render_page(art: &Art, image_link: &FetchedLink) -> Html<String> {
|
||||||
|
let art_url = image_link.new_source.as_ref().unwrap_or(&art.url);
|
||||||
let content = maud::html! {
|
let content = maud::html! {
|
||||||
(maud::DOCTYPE)
|
(maud::DOCTYPE)
|
||||||
head {
|
head {
|
||||||
(get_page_head_common())
|
(get_page_head_common())
|
||||||
}
|
}
|
||||||
body style=(BODY_STYLE) {
|
body style=(BODY_STYLE) {
|
||||||
img style=(IMG_STYLE) src=(image_link);
|
div style="display: block; margin: auto; max-height: 98vh; max-width: 98vw;" {
|
||||||
a style=(format!("{ABOUT_STYLE} left: 0;")) href=(art.url) target="_blank" {
|
div class="throbber-loader" style="position: absolute; top: 50%; left: 50%; z-index: -1;" {}
|
||||||
"source: " (art.url)
|
img style="max-height: 98vh; max-width: 98vw;" src=(image_link.image_url);
|
||||||
|
}
|
||||||
|
a style=(format!("{ABOUT_STYLE} left: 0;")) href=(art_url) target="_blank" {
|
||||||
|
"source: " (art_url)
|
||||||
}
|
}
|
||||||
(get_page_contact())
|
(get_page_contact())
|
||||||
}
|
}
|
||||||
@ -128,7 +138,21 @@ fn render_page(art: &Art, image_link: &str) -> Html<String> {
|
|||||||
Html(content.into_string())
|
Html(content.into_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_safebooru_image_link(http: &reqwest::Client, url: &Uri) -> AppResult<String> {
|
fn fetch_safebooru_image_link<'a>(
|
||||||
|
http: &'a reqwest::Client,
|
||||||
|
url: &'a Uri,
|
||||||
|
) -> BoxFuture<'a, AppResult<FetchedLink>> {
|
||||||
|
_fetch_safebooru_image_link(http, url).boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fetch_twitter_image_link<'a>(
|
||||||
|
http: &'a reqwest::Client,
|
||||||
|
url: &'a Uri,
|
||||||
|
) -> BoxFuture<'a, AppResult<FetchedLink>> {
|
||||||
|
_fetch_twitter_image_link(http, url).boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn _fetch_safebooru_image_link(http: &reqwest::Client, url: &Uri) -> AppResult<FetchedLink> {
|
||||||
let mut id = String::new();
|
let mut id = String::new();
|
||||||
for (name, value) in form_urlencoded::parse(url.query().unwrap().as_bytes()) {
|
for (name, value) in form_urlencoded::parse(url.query().unwrap().as_bytes()) {
|
||||||
if name == "id" {
|
if name == "id" {
|
||||||
@ -166,15 +190,85 @@ async fn fetch_safebooru_image_link(http: &reqwest::Client, url: &Uri) -> AppRes
|
|||||||
.await
|
.await
|
||||||
.map_err(|(e, _)| e)?;
|
.map_err(|(e, _)| e)?;
|
||||||
|
|
||||||
let image_filename = data[0].get("image").unwrap().as_str().unwrap();
|
let source_url = data[0]
|
||||||
let image_directory = data[0].get("directory").unwrap().as_str().unwrap();
|
.get("source")
|
||||||
|
.and_then(|src| Uri::from_str(src.as_str()?).ok())
|
||||||
|
.map(|src| {
|
||||||
|
if src.host() == Some("i.pximg.net") {
|
||||||
|
let post_id = src
|
||||||
|
.path()
|
||||||
|
.split('/')
|
||||||
|
.last()
|
||||||
|
.unwrap()
|
||||||
|
.split("_")
|
||||||
|
.next()
|
||||||
|
.unwrap();
|
||||||
|
return Uri::builder()
|
||||||
|
.scheme("https")
|
||||||
|
.authority("pixiv.net")
|
||||||
|
.path_and_query(format!("/en/artworks/{post_id}"))
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
} else {
|
||||||
|
src
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Ok(format!(
|
if source_url.as_ref().map_or(false, |src| {
|
||||||
"http://safebooru.org/images/{image_directory}/{image_filename}"
|
src.host().unwrap().contains("twitter.com") || src.host().unwrap().contains("x.com")
|
||||||
))
|
}) {
|
||||||
|
let url = source_url.clone().unwrap();
|
||||||
|
println!("[safebooru] source was twitter, will try to fetch image from there instead");
|
||||||
|
if let Ok(mut fetched) = _fetch_twitter_image_link(http, &url).await {
|
||||||
|
println!("[safebooru] fetched image from twitter");
|
||||||
|
fetched.new_source = Some(url);
|
||||||
|
return Ok(fetched);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sample_url = data[0]
|
||||||
|
.get("sample_url")
|
||||||
|
.ok_or("safebooru did not return sample url")?
|
||||||
|
.as_str()
|
||||||
|
.ok_or("safebooru sample url wasnt a string")?;
|
||||||
|
let sample_url = Uri::from_str(sample_url)
|
||||||
|
.map_err(|err| AppError::from(format!("safebooru sample url was not valid: {err}")))?;
|
||||||
|
|
||||||
|
let fsample_url = format!(
|
||||||
|
"{}://{}{}",
|
||||||
|
sample_url.scheme_str().unwrap(),
|
||||||
|
sample_url.host().unwrap(),
|
||||||
|
sample_url.path()
|
||||||
|
);
|
||||||
|
let ssample_url = format!(
|
||||||
|
"{}://{}/{}",
|
||||||
|
sample_url.scheme_str().unwrap(),
|
||||||
|
sample_url.host().unwrap(),
|
||||||
|
sample_url.path()
|
||||||
|
);
|
||||||
|
|
||||||
|
let fsample_resp = http
|
||||||
|
.execute(http.get(&fsample_url).build()?)
|
||||||
|
.await
|
||||||
|
.and_then(|resp| resp.error_for_status());
|
||||||
|
let ssample_resp = http
|
||||||
|
.execute(http.get(&ssample_url).build()?)
|
||||||
|
.await
|
||||||
|
.and_then(|resp| resp.error_for_status());
|
||||||
|
|
||||||
|
let sample_url = fsample_resp
|
||||||
|
.is_ok()
|
||||||
|
.then(|| fsample_url)
|
||||||
|
.or_else(|| ssample_resp.is_ok().then(|| ssample_url))
|
||||||
|
.unwrap_or_else(|| sample_url.to_string());
|
||||||
|
|
||||||
|
Ok(FetchedLink {
|
||||||
|
image_url: sample_url,
|
||||||
|
new_source: source_url,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_twitter_image_link(http: &reqwest::Client, url: &Uri) -> AppResult<String> {
|
async fn _fetch_twitter_image_link(http: &reqwest::Client, url: &Uri) -> AppResult<FetchedLink> {
|
||||||
let fxurl = Uri::builder()
|
let fxurl = Uri::builder()
|
||||||
.scheme("https")
|
.scheme("https")
|
||||||
.authority("d.fxtwitter.com")
|
.authority("d.fxtwitter.com")
|
||||||
@ -190,16 +284,19 @@ async fn fetch_twitter_image_link(http: &reqwest::Client, url: &Uri) -> AppResul
|
|||||||
.ok_or_else(|| format!("twitter link {fxurl} did not return an image location"))?
|
.ok_or_else(|| format!("twitter link {fxurl} did not return an image location"))?
|
||||||
.to_str()?;
|
.to_str()?;
|
||||||
// use webp format for direct twitter links since webp is cheaper
|
// use webp format for direct twitter links since webp is cheaper
|
||||||
Ok(format!("{link}?format=webp"))
|
Ok(FetchedLink {
|
||||||
|
image_url: format!("{link}?format=webp"),
|
||||||
|
new_source: None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_conf(name: &str) -> String {
|
fn get_conf(name: &str) -> Option<String> {
|
||||||
std::env::var(name).unwrap()
|
std::env::var(name).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InternalAppState {
|
struct InternalAppState {
|
||||||
// cached direct links to images
|
// cached direct links to images
|
||||||
direct_links: DashMap<Uri, String>,
|
direct_links: DashMap<Uri, FetchedLink>,
|
||||||
data: Mutex<Data>,
|
data: Mutex<Data>,
|
||||||
http: reqwest::Client,
|
http: reqwest::Client,
|
||||||
}
|
}
|
||||||
@ -217,7 +314,11 @@ impl AppState {
|
|||||||
direct_links: Default::default(),
|
direct_links: Default::default(),
|
||||||
http: reqwest::ClientBuilder::new()
|
http: reqwest::ClientBuilder::new()
|
||||||
.redirect(reqwest::redirect::Policy::none())
|
.redirect(reqwest::redirect::Policy::none())
|
||||||
.user_agent(format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")))
|
.user_agent(format!(
|
||||||
|
"{}/{}",
|
||||||
|
env!("CARGO_PKG_NAME"),
|
||||||
|
env!("CARGO_PKG_VERSION")
|
||||||
|
))
|
||||||
.build()
|
.build()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
}),
|
}),
|
||||||
|
Loading…
Reference in New Issue
Block a user