add pixiv API stub

This commit is contained in:
theBreadCompany 2024-09-25 05:12:58 +02:00
parent 3a35b2100c
commit 129d60f07f
7 changed files with 109 additions and 204 deletions

122
Cargo.lock generated
View file

@ -78,9 +78,9 @@ dependencies = [
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.82" version = "0.1.83"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -355,18 +355,6 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.1.1" version = "2.1.1"
@ -506,10 +494,9 @@ name = "fxpixiv"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"json", "json",
"libpixiv",
"maud", "maud",
"reqwest",
"rocket", "rocket",
"rusqlite",
"scraper", "scraper",
"tl", "tl",
] ]
@ -602,18 +589,6 @@ name = "hashbrown"
version = "0.14.5" version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
]
[[package]]
name = "hashlink"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
dependencies = [
"hashbrown",
]
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
@ -788,9 +763,9 @@ dependencies = [
[[package]] [[package]]
name = "hyper-util" name = "hyper-util"
version = "0.1.8" version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-channel", "futures-channel",
@ -801,7 +776,6 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"tokio", "tokio",
"tower",
"tower-service", "tower-service",
"tracing", "tracing",
] ]
@ -902,9 +876,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.158" version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]] [[package]]
name = "libpixiv" name = "libpixiv"
@ -918,16 +892,6 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "libsqlite3-sys"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.14" version = "0.4.14"
@ -1339,26 +1303,6 @@ dependencies = [
"siphasher", "siphasher",
] ]
[[package]]
name = "pin-project"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3"
dependencies = [
"pin-project-internal",
]
[[package]]
name = "pin-project-internal"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.14" version = "0.2.14"
@ -1373,9 +1317,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
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 = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]] [[package]]
name = "powerfmt" name = "powerfmt"
@ -1484,9 +1428,9 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.4" version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" checksum = "62871f2d65009c0256aed1b9cfeeb8ac272833c404e13d53d400cd0dad7a2ac0"
dependencies = [ dependencies = [
"bitflags", "bitflags",
] ]
@ -1639,6 +1583,7 @@ dependencies = [
"rocket_codegen", "rocket_codegen",
"rocket_http", "rocket_http",
"serde", "serde",
"serde_json",
"state", "state",
"tempfile", "tempfile",
"time", "time",
@ -1694,20 +1639,6 @@ dependencies = [
"uncased", "uncased",
] ]
[[package]]
name = "rusqlite"
version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
dependencies = [
"bitflags",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.24" version = "0.1.24"
@ -2261,9 +2192,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.21" version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
@ -2272,27 +2203,6 @@ dependencies = [
"winnow", "winnow",
] ]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"pin-project",
"pin-project-lite",
"tokio",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.3" version = "0.3.3"
@ -2762,9 +2672,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.18" version = "0.6.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" checksum = "c52ac009d615e79296318c1bcce2d422aaca15ad08515e344feeda07df67a587"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]

View file

@ -5,3 +5,13 @@ The pixiv embed fixer
## DISCLAIMER ## DISCLAIMER
This project is still in development, please be patient until this project is actually usable. This project is still in development, please be patient until this project is actually usable.
## nix build
As this mainly runs on my NixOS server for now, so build and service files are included.
Run `nix-shell -E 'with import <nixpkgs> {}; callPackage ./default.nix {}'` to enter the development shell.
## nix options
There is some hope that this service will make its way to the NixOS options, but I need to disappoint you for now.
You can still just clone this project and manually import the service file of course, make sure to keep the `default.nix` reachable tho.

View file

@ -5,9 +5,8 @@ edition = "2021"
[dependencies] [dependencies]
tl = "0.7.8" tl = "0.7.8"
reqwest = "0.12.7"
json = "0.12.4" json = "0.12.4"
rocket = "0.5.1"
scraper = "0.20.0" scraper = "0.20.0"
maud = "0.26.0" maud = "0.26.0"
rusqlite = "0.32.1" libpixiv = { path = "../libpixiv" }
rocket = { version = "0.5.1", features = ["json"] }

View file

@ -1,99 +1,65 @@
#[macro_use] #![deny(elided_lifetimes_in_paths)]
extern crate rocket; #[macro_use] extern crate rocket;
use libpixiv::PixivAppClient;
use maud::{html, DOCTYPE}; use maud::{html, DOCTYPE};
use reqwest::Client; use rocket::{http::Status, response::content::RawHtml, State};
use rocket::http::Status;
use rocket::response::content::RawHtml;
use scraper::{Html, Selector}; struct Metadata {
pub image: String,
pub title: String,
pub desc: String,
}
#[get("/<path..>")] #[get("/<path..>")]
async fn handle_route(path: std::path::PathBuf) -> Result<RawHtml<String>, Status> { async fn handle_route(state: &State<Option<PixivAppClient>>, path: std::path::PathBuf) -> Result<RawHtml<String>, Status> {
let target = format!("https://pixiv.net/{}", path.display()); let target = format!("https://pixiv.net/{}", path.display());
let html = match fetch_content(&target).await { if let Some(id) = path.file_name() {
Ok(html) => html, let meta = fetch_illust(state, id.to_str().unwrap().parse::<u32>().unwrap());
Err(err) => return Err(err), Ok(RawHtml(create_page(&target, &meta.await.unwrap()).await.unwrap()))
}; } else {
Err(Status::InternalServerError)
}
let modified = create_page(&target, &html).await.unwrap();
Ok(RawHtml(modified))
} }
async fn fetch_content(url: &String) -> Result<String, Status> { async fn fetch_illust(client: &Option<PixivAppClient>, illust_id: u32) -> Option<Metadata> {
println!("{}", url); if let Some(client) = client {
let client = Client::new(); if let Ok(illust) = client.illust_details(illust_id).await {
let response = match client.get(url).send().await { let image = if illust.page_count == 1 { illust.meta_single_page.unwrap().original_image_url.unwrap() } else { illust.meta_pages[0].image_urls.large.clone() };
Ok(resp) => resp, return Some(Metadata {
Err(_) => return Err(Status::BadGateway), image: image,
}; title: illust.title,
desc: illust.caption,
})
}
}
let html = match response.text().await { None
Ok(text) => text,
Err(_) => return Err(Status::InternalServerError),
};
Ok(html)
} }
async fn create_page(source: &String, html: &String) -> Result<String, Status> { async fn create_page(source: &str, meta: &Metadata) -> Result<String, Status> {
let dom = Html::parse_document(html);
let data_selector = match Selector::parse(r#"meta[name="preload-data"]"#) {
Ok(sel) => sel,
Err(_) => return Err(Status::InternalServerError),
};
let data_meta = match dom.select(&data_selector).next() {
Some(meta) => meta,
None => return Err(Status::InternalServerError),
};
let illust = json::parse(data_meta.value().attr("content").unwrap()).unwrap();
let image = match illust["illust"].entries().next() {
Some(j) => j.1["urls"]["regular"]
.as_str()
.unwrap()
.replace("pximg.net", "fixiv.net"),
None => "https://http.cat/images/501.jpg".to_string(),
};
let title_selector = match Selector::parse(r#"meta[property="og:title"]"#) {
Ok(sel) => sel,
Err(_) => return Err(Status::InternalServerError),
};
let title_meta = match dom.select(&title_selector).next() {
Some(meta) => meta.attr("content").unwrap(),
None => "unknown title",
};
let desc_selector = match Selector::parse(r#"meta[property="og:description"]"#) {
Ok(sel) => sel,
Err(_) => return Err(Status::InternalServerError),
};
let desc_meta = match dom.select(&desc_selector).next() {
Some(meta) => meta.attr("content").unwrap(),
None => "unknown title",
};
Ok(html! { Ok(html! {
(DOCTYPE) (DOCTYPE)
html lang="en" { html lang="en" {
head { head {
meta charset="utf-8"; meta charset="utf-8";
meta property="og:title" content=(title_meta); meta property="og:title" content=(meta.title);
meta property="og:image" content=(image); meta property="og:image" content=(meta.image);
meta property="og:url" content=(source); meta property="og:url" content=(source);
meta property="og:type" content="article"; meta property="og:type" content="article";
meta property="og:site_name" content="pixiv"; meta property="og:site_name" content="pixiv";
meta property="og:description" content=(desc_meta); meta property="og:description" content=(meta.desc);
meta property="twitter:card" content="summary_large_image"; meta property="twitter:card" content="summary_large_image";
meta property="twitter:site" content="@pixiv"; meta property="twitter:site" content="@pixiv";
meta property="twitter:url" content=(source); meta property="twitter:url" content=(source);
meta property="twitter:title" content=(title_meta); meta property="twitter:title" content=(meta.title);
meta property="twitter:description" content=(desc_meta); meta property="twitter:description" content=(meta.desc);
meta property="twitter:image" content=(image); meta property="twitter:image" content=(meta.image);
meta http-equiv="Refresh" content=(format!("0; url='{}'", source)); meta http-equiv="Refresh" content=(format!("0; url='{}'", source));
@ -110,6 +76,17 @@ async fn create_page(source: &String, html: &String) -> Result<String, Status> {
} }
#[launch] #[launch]
fn launch() -> _ { async fn launch() -> _ {
rocket::build().mount("/", routes![handle_route])
let mut pixiv_client: Option<PixivAppClient> = None;
if let Ok(token) = std::env::var("PIXIV_REFRESH_TOKEN") {
let mut client = PixivAppClient::new(token);
client.refresh_token().await; // TODO: refresh token on a regular basis
pixiv_client = Some(client);
}
rocket::build()
.manage(pixiv_client)
.mount("/", routes![handle_route])
} }

View file

@ -12,3 +12,7 @@ You can run the following command to test existing features:
```bash ```bash
PIXIV_REFRESH_TOKEN= cargo test --lib PIXIV_REFRESH_TOKEN= cargo test --lib
``` ```
## Notes
This library may be available as a standalone crate in the future, depending on how many of the available endpoints get actually implemented.

View file

@ -5,11 +5,12 @@ extern crate serde_json;
use chrono::Local; use chrono::Local;
use reqwest::header::{HeaderName, AUTHORIZATION, CONTENT_TYPE, USER_AGENT}; use reqwest::header::{HeaderName, AUTHORIZATION, CONTENT_TYPE, USER_AGENT};
use serde_json::Value; use serde_json::Value;
use std::sync::{Arc, Mutex}; use tokio::sync::Mutex;
use std::sync::Arc;
mod models; mod models;
struct PixivAppClient { pub struct PixivAppClient {
/// bearer token /// bearer token
access_token: Arc<Mutex<String>>, access_token: Arc<Mutex<String>>,
refresh_token: Arc<Mutex<String>>, refresh_token: Arc<Mutex<String>>,
@ -18,10 +19,10 @@ struct PixivAppClient {
} }
impl PixivAppClient { impl PixivAppClient {
fn new(token: &str) -> Self { pub fn new(token: String) -> Self {
let client = Self { let client = Self {
access_token: Arc::new(Mutex::new(String::new())), access_token: Arc::new(Mutex::new(String::new())),
refresh_token: Arc::new(Mutex::new(String::from(token))), refresh_token: Arc::new(Mutex::new(token)),
http_client: reqwest::Client::new(), http_client: reqwest::Client::new(),
host: String::from("https://app-api.pixiv.net"), host: String::from("https://app-api.pixiv.net"),
}; };
@ -37,7 +38,7 @@ impl PixivAppClient {
let time = Local::now().format("%y-%m-%dT%H:%m:%s+00:00"); let time = Local::now().format("%y-%m-%dT%H:%m:%s+00:00");
let time_str = format!("{}", time); let time_str = format!("{}", time);
let cloned_refresh_token = Arc::clone(&self.refresh_token); let cloned_refresh_token = Arc::clone(&self.refresh_token);
let cloned_refresh_token_str = &cloned_refresh_token.lock().unwrap(); let cloned_refresh_token_str = &cloned_refresh_token.lock().await;
let client_id = "MOBrBDS8blbauoSck0ZfDbtuzpyT"; let client_id = "MOBrBDS8blbauoSck0ZfDbtuzpyT";
let client_secret = "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj"; let client_secret = "lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj";
@ -86,7 +87,7 @@ impl PixivAppClient {
.get(url) .get(url)
.header( .header(
AUTHORIZATION, AUTHORIZATION,
format!("Bearer {}", self.access_token.lock().unwrap()), format!("Bearer {}", self.access_token.lock().await),
) )
.header(HeaderName::from_lowercase(b"app-os").unwrap(), "ios") .header(HeaderName::from_lowercase(b"app-os").unwrap(), "ios")
.header( .header(
@ -125,20 +126,18 @@ impl PixivAppClient {
#[cfg(test)] #[cfg(test)]
mod client_tests { mod client_tests {
use super::*; use super::*;
use std::{assert, assert_eq, env, panic}; use std::{assert, assert_eq, env};
#[tokio::test] #[tokio::test]
async fn login() { async fn login() {
let token = env::var("PIXIV_REFRESH_TOKEN"); let token = env::var("PIXIV_REFRESH_TOKEN");
let mut client = PixivAppClient::new( let mut client = PixivAppClient::new(
&token.expect("expecting PIXIV_REFRESH_TOKEN variable for testing!"), token.expect("expecting PIXIV_REFRESH_TOKEN variable for testing!"),
); );
client.refresh_token().await; client.refresh_token().await;
let cloned_access_token = Arc::clone(&client.access_token); let cloned_access_token = Arc::clone(&client.access_token);
match cloned_access_token.lock() { let t = cloned_access_token.lock().await;
Ok(t) => assert!(!t.is_empty(), "Expected to receive token!"), assert!(!t.is_empty(), "Expected to receive token!");
Err(_) => panic!("No token received!"),
};
} }
#[tokio::test] #[tokio::test]
@ -146,7 +145,7 @@ mod client_tests {
let illust_id = 122388293; let illust_id = 122388293;
let token = env::var("PIXIV_REFRESH_TOKEN"); let token = env::var("PIXIV_REFRESH_TOKEN");
let mut client = PixivAppClient::new( let mut client = PixivAppClient::new(
&token.expect("expecting PIXIV_REFRESH_TOKEN variable for testing!"), token.expect("expecting PIXIV_REFRESH_TOKEN variable for testing!"),
); );
client.refresh_token().await; client.refresh_token().await;
let illust = client.illust_details(illust_id).await; let illust = client.illust_details(illust_id).await;

View file

@ -17,6 +17,7 @@ pub struct Illustration {
pub restrict: i32, pub restrict: i32,
pub x_restrict: i32, pub x_restrict: i32,
pub image_urls: IllustrationURLs, pub image_urls: IllustrationURLs,
pub meta_single_page: Option<IllustrationMetaURL>,
pub meta_pages: Vec<IllustrationURLsWrapper>, pub meta_pages: Vec<IllustrationURLsWrapper>,
pub total_view: u32, pub total_view: u32,
pub total_bookmarks: u32, pub total_bookmarks: u32,
@ -40,26 +41,31 @@ pub struct User {
#[derive(Serialize, Deserialize, std::fmt::Debug)] #[derive(Serialize, Deserialize, std::fmt::Debug)]
pub struct UserProfileImages { pub struct UserProfileImages {
medium: String, pub medium: String,
} }
#[derive(Serialize, Deserialize, std::fmt::Debug)] #[derive(Serialize, Deserialize, std::fmt::Debug)]
pub struct IllustrationURLsWrapper { pub struct IllustrationURLsWrapper {
image_urls: IllustrationURLs, pub image_urls: IllustrationURLs,
}
#[derive(Serialize, Deserialize, std::fmt::Debug)]
pub struct IllustrationMetaURL {
pub original_image_url: Option<String>,
} }
#[derive(Serialize, Deserialize, std::fmt::Debug)] #[derive(Serialize, Deserialize, std::fmt::Debug)]
pub struct IllustrationURLs { pub struct IllustrationURLs {
square_medium: String, pub square_medium: String,
medium: String, pub medium: String,
large: String, pub large: String,
original: Option<String>, pub original: Option<String>,
} }
#[derive(Serialize, Deserialize, std::fmt::Debug)] #[derive(Serialize, Deserialize, std::fmt::Debug)]
pub struct Tag { pub struct Tag {
name: String, pub name: String,
translated_name: Option<String>, pub translated_name: Option<String>,
} }
#[derive(Serialize, Deserialize, std::fmt::Debug)] #[derive(Serialize, Deserialize, std::fmt::Debug)]