use mangadex_api::{MangaDexClient, v5::upload::upload_session_id::post::UploadImage};
use mangadex_api_schema::v5::oauth::ClientInfo;
use mangadex_api_types::{Password, Username};
use uuid::Uuid;
use std::{
collections::HashMap,
env::{VarError, set_var, var},
path::{Path, PathBuf},
};
use mangadex_api_schema::v5::AuthTokens;
pub type VarResult<T, E = std::io::Error> = Result<T, E>;
const CLIENT_ID: &str = "CLIENT_ID";
const REFRESH_TOKEN: &str = "REFRESH_TOKEN";
const CLIENT_SECRET: &str = "CLIENT_SECRET";
const USERNAME: &str = "USERNAME_";
const PASSWORD: &str = "PASSWORD_";
#[derive(Debug)]
struct PreUserInfos {
username: String,
password: String,
}
impl PreUserInfos {
fn new() -> VarResult<Self> {
Ok(Self {
username: var(USERNAME).map_err(|e| match e {
VarError::NotPresent => std::io::Error::new(std::io::ErrorKind::NotFound, USERNAME),
VarError::NotUnicode(e) => std::io::Error::new(
std::io::ErrorKind::InvalidData,
e.to_str().unwrap_or_default().to_string(),
),
})?,
password: var(PASSWORD).map_err(|e| match e {
VarError::NotPresent => std::io::Error::new(std::io::ErrorKind::NotFound, PASSWORD),
VarError::NotUnicode(e) => std::io::Error::new(
std::io::ErrorKind::InvalidData,
e.to_str().unwrap_or_default().to_string(),
),
})?,
})
}
}
#[derive(Debug)]
struct UserInfos {
username: Username,
password: Password,
}
impl TryFrom<PreUserInfos> for UserInfos {
type Error = mangadex_api_types::error::Error;
fn try_from(value: PreUserInfos) -> Result<Self, Self::Error> {
Ok(Self {
username: Username::parse(value.username)?,
password: Password::parse(value.password)?,
})
}
}
fn get_client_info_from_var() -> VarResult<ClientInfo> {
Ok(non_exhaustive::non_exhaustive!(ClientInfo {
client_id: var(CLIENT_ID).map_err(|e| match e {
VarError::NotPresent => std::io::Error::new(std::io::ErrorKind::NotFound, CLIENT_ID),
VarError::NotUnicode(e) => std::io::Error::new(
std::io::ErrorKind::InvalidData,
e.to_str().unwrap_or_default().to_string(),
),
})?,
client_secret: var(CLIENT_SECRET).map_err(|e| match e {
VarError::NotPresent => {
std::io::Error::new(std::io::ErrorKind::NotFound, CLIENT_SECRET)
}
VarError::NotUnicode(e) => std::io::Error::new(
std::io::ErrorKind::InvalidData,
e.to_str().unwrap_or_default().to_string(),
),
})?,
}))
}
fn get_refresh_token_from_var() -> VarResult<String> {
var(REFRESH_TOKEN).map_err(|e| match e {
VarError::NotPresent => std::io::Error::new(std::io::ErrorKind::NotFound, REFRESH_TOKEN),
VarError::NotUnicode(e) => std::io::Error::new(
std::io::ErrorKind::InvalidData,
e.to_str().unwrap_or_default().to_string(),
),
})
}
async fn init_client() -> anyhow::Result<MangaDexClient> {
let client_info = get_client_info_from_var()?;
let client = MangaDexClient::default();
client.set_client_info(&client_info).await?;
Ok(client)
}
async fn login(client: &MangaDexClient) -> anyhow::Result<()> {
let user_info: UserInfos = TryFrom::try_from(PreUserInfos::new()?)?;
println!("Fetching your access token");
let oauth_res = client
.oauth()
.login()
.username(user_info.username)
.password(user_info.password)
.send()
.await?;
println!(
"Your token will expire in {} minutes",
(oauth_res.expires_in / 60)
);
println!("{}", oauth_res.refresh_token);
unsafe {
set_var(REFRESH_TOKEN, oauth_res.refresh_token);
}
println!("Your refresh token is now settled to the environment variable");
Ok(())
}
async fn refresh_token(client: &mut MangaDexClient, refresh_token: String) -> anyhow::Result<()> {
println!("Fetching your access token");
client
.set_auth_tokens(&non_exhaustive::non_exhaustive!(AuthTokens {
session: Default::default(),
refresh: refresh_token,
}))
.await?;
let oauth_res = client.oauth().refresh().send().await?;
println!(
"Your token will expire in {} minutes",
(oauth_res.expires_in / 60)
);
unsafe {
set_var(REFRESH_TOKEN, oauth_res.refresh_token);
}
println!("Your refresh token is now settled to the environment variable");
Ok(())
}
async fn show_user_name(client: &MangaDexClient) -> anyhow::Result<()> {
let user_info = client.user().me().get().send().await?;
println!(
"Welcome User {}/{}!",
user_info.data.attributes.username, user_info.data.id
);
Ok(())
}
async fn init() -> anyhow::Result<MangaDexClient> {
let mut client = init_client().await?;
if let Ok(refresh) = get_refresh_token_from_var() {
refresh_token(&mut client, refresh).await?;
} else {
println!("{REFRESH_TOKEN} Not found");
println!("using login");
login(&client).await?;
}
show_user_name(&client).await?;
Ok(client)
}
#[derive(Debug, thiserror::Error)]
enum CheckSessionError {
#[error("An upload session {0} already exists")]
AlreadyExists(Uuid),
#[error(transparent)]
MangadexApiError(#[from] mangadex_api::error::Error),
}
async fn check_session(client: &MangaDexClient) -> Result<(), CheckSessionError> {
match client.upload().get().send().await {
Ok(i) => Err(CheckSessionError::AlreadyExists(i.body.data.id)),
Err(e) => {
if let mangadex_api::error::Error::Api(error) = &e
&& error.errors.iter().any(|er| er.status == 404)
{
return Ok(());
}
Err(CheckSessionError::MangadexApiError(e))
}
}
}
async fn check_and_abandon_session_if_exists(
client: &MangaDexClient,
) -> Result<(), mangadex_api::error::Error> {
if let Err(e) = check_session(client).await {
match e {
CheckSessionError::AlreadyExists(id) => abandon(id, client).await?,
CheckSessionError::MangadexApiError(error) => return Err(error),
};
}
Ok(())
}
async fn abandon(session: Uuid, client: &MangaDexClient) -> Result<(), mangadex_api::error::Error> {
client
.upload()
.upload_session_id(session)
.delete()
.send()
.await?;
Ok(())
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = init().await?;
check_and_abandon_session_if_exists(&client).await?;
println!("Starting upload session...");
let _upload = client
.upload()
.begin()
.post()
.manga_id(Uuid::parse_str("f9c33607-9180-4ba6-b85c-e4b5faee7192")?)
.add_group_id(Uuid::parse_str("18dadd0b-cbce-41c4-a8a9-5e653780b9ff")?)
.send()
.await?;
let upload_session_id = _upload.data.id;
println!("Started!");
let files = [
Path::new("upload-test/1.png").to_path_buf(),
Path::new("upload-test/2.png").to_path_buf(),
];
println!("Uploading your files...");
let res_upload = client
.upload()
.upload_session_id(upload_session_id)
.post()
.files(
files
.iter()
.flat_map(<UploadImage as TryFrom<&PathBuf>>::try_from)
.collect::<Vec<UploadImage>>(),
)
.send()
.await?;
if res_upload.errors.is_empty() || !res_upload.data.is_empty() {
let files_id: HashMap<String, Uuid> = {
let mut d = res_upload.data.clone();
d.sort_by(|a, b| {
a.attributes
.original_file_name
.cmp(&b.attributes.original_file_name)
});
d
}
.iter()
.map(|e| (e.attributes.original_file_name.to_owned(), e.id))
.collect();
println!("Uploaded!");
println!("files_id: {files_id:?}");
println!("Commiting...");
let files_ids = files_id.values().map(Clone::clone).collect::<Vec<Uuid>>();
eprintln!("{:?}", &files_ids);
let res = client
.upload()
.upload_session_id(upload_session_id)
.commit()
.post()
.page_order(files_ids)
.chapter(Some(String::from("144")))
.translated_language(mangadex_api_types::Language::English)
.send()
.await?;
println!("Commited! Congratz! :3");
println!("Open https://mangadex.org/chapter/{}", res.data.id);
} else {
println!("some files upload files");
println!("number = {}", res_upload.errors.len());
println!("Canceling session...");
abandon(upload_session_id, &client).await?;
println!("Canceled!");
}
Ok(())
}