From 98e89382610a59249ca45eb52120a747f7e621f2 Mon Sep 17 00:00:00 2001 From: Nathan Argenti Date: Sun, 5 Jul 2026 10:15:48 -0700 Subject: [PATCH 1/4] added username and password for suwayomi --- sources/multi.suwayomi/Cargo.lock | 7 ++ sources/multi.suwayomi/Cargo.toml | 1 + sources/multi.suwayomi/res/settings.json | 25 ++++- sources/multi.suwayomi/res/source.json | 2 +- sources/multi.suwayomi/src/lib.rs | 123 +++++++++++++++++++++-- sources/multi.suwayomi/src/settings.rs | 29 ++++-- 6 files changed, 172 insertions(+), 15 deletions(-) diff --git a/sources/multi.suwayomi/Cargo.lock b/sources/multi.suwayomi/Cargo.lock index 9ad12e87b..03038d158 100644 --- a/sources/multi.suwayomi/Cargo.lock +++ b/sources/multi.suwayomi/Cargo.lock @@ -40,6 +40,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "byteorder" version = "1.5.0" @@ -294,6 +300,7 @@ name = "suwayomi" version = "0.1.0" dependencies = [ "aidoku", + "base64", "serde", "serde_json", ] diff --git a/sources/multi.suwayomi/Cargo.toml b/sources/multi.suwayomi/Cargo.toml index 4dfd114d8..edadc8df7 100644 --- a/sources/multi.suwayomi/Cargo.toml +++ b/sources/multi.suwayomi/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" aidoku = { git = "https://github.com/Aidoku/aidoku-rs.git", version = "0.3.0", features = ["json"] } serde = { version = "1.0", features = ["derive"], default-features = false } serde_json = { version = "1.0.143", default-features = false, features = ["alloc"] } +base64 = { version = "0.22", default-features = false, features = ["alloc"] } [dev-dependencies] aidoku = { git = "https://github.com/Aidoku/aidoku-rs.git", features = ["test"] } diff --git a/sources/multi.suwayomi/res/settings.json b/sources/multi.suwayomi/res/settings.json index b8708292d..44d64bc4f 100644 --- a/sources/multi.suwayomi/res/settings.json +++ b/sources/multi.suwayomi/res/settings.json @@ -11,7 +11,30 @@ "returnKeyType": 9, "autocorrectionDisabled": true, "refreshes": ["content"] + }, + { + "type": "select", + "key": "authMode", + "title": "Auth Mode", + "values": ["auto", "none", "basic_auth", "simple_login"], + "titles": [ + "Auto", + "None", + "Basic Auth", + "Simple Login" + ], + "default": "auto", + "refreshes": ["content"] + }, + { + "type": "login", + "method": "basic", + "key": "credentials", + "title": "Login", + "requires": "baseUrl", + "refreshes": ["content"] } - ] + ], + "footer": "Enter your Suwayomi server URL and optionally login with your credentials." } ] diff --git a/sources/multi.suwayomi/res/source.json b/sources/multi.suwayomi/res/source.json index 53aa0ad40..0e4092141 100644 --- a/sources/multi.suwayomi/res/source.json +++ b/sources/multi.suwayomi/res/source.json @@ -2,7 +2,7 @@ "info": { "id": "multi.suwayomi", "name": "Suwayomi", - "version": 3, + "version": 4, "url": "https://github.com/Suwayomi", "contentRating": 0, "languages": [ diff --git a/sources/multi.suwayomi/src/lib.rs b/sources/multi.suwayomi/src/lib.rs index 336614d04..fb1b06cea 100644 --- a/sources/multi.suwayomi/src/lib.rs +++ b/sources/multi.suwayomi/src/lib.rs @@ -13,14 +13,15 @@ use crate::models::{ }; use aidoku::imports::std::send_partial_result; use aidoku::{ - AidokuError, BaseUrlProvider, Chapter, DynamicListings, FilterValue, Listing, ListingProvider, - Manga, MangaPageResult, Page, PageContent, Result, Source, + AidokuError, BaseUrlProvider, BasicLoginHandler, Chapter, DynamicListings, FilterValue, + Listing, ListingProvider, Manga, MangaPageResult, Page, PageContent, Result, Source, alloc::{String, Vec}, imports::net::Request, prelude::*, }; use alloc::string::ToString; use alloc::vec; +use base64::{Engine, engine::general_purpose::STANDARD}; struct Suwayomi; @@ -30,10 +31,53 @@ impl Suwayomi { T: serde::de::DeserializeOwned, { let base_url = settings::get_base_url()?; - Request::post(format!("{base_url}/api/graphql"))? - .header("Content-Type", "application/json") - .body(body.to_string()) - .json_owned::>() + let auth_mode = settings::get_auth_mode(); + let body_str = body.to_string(); + + let send_req = |with_basic: bool| -> Result> { + let mut request = Request::post(format!("{base_url}/api/graphql"))? + .header("Content-Type", "application/json") + .body(body_str.clone()); + + if with_basic && let Some((user, pass)) = settings::get_credentials() { + let auth = STANDARD.encode(format!("{user}:{pass}")); + request = request.header("Authorization", &format!("Basic {auth}")); + } + request.json_owned::>() + }; + + let do_login_html = || -> Result<()> { + if let Some((user, pass)) = settings::get_credentials() { + let form = format!( + "user={}&pass={}", + aidoku::helpers::uri::encode_uri_component(user), + aidoku::helpers::uri::encode_uri_component(pass) + ); + let _ = Request::post(format!("{base_url}/login.html"))? + .header("Content-Type", "application/x-www-form-urlencoded") + .body(form) + .send() + .ok(); + } + Ok(()) + }; + + match auth_mode.as_str() { + "none" => send_req(false), + "basic_auth" => send_req(true), + "simple_login" => { + do_login_html()?; + send_req(false) + } + _ => { + let resp = send_req(true); + if resp.is_err() { + do_login_html()?; + return send_req(true); + } + resp + } + } } fn execute_query( @@ -271,4 +315,69 @@ impl BaseUrlProvider for Suwayomi { } } -register_source!(Suwayomi, ListingProvider, BaseUrlProvider, DynamicListings); +impl BasicLoginHandler for Suwayomi { + fn handle_basic_login(&self, _key: String, username: String, password: String) -> Result { + let base_url = settings::get_base_url()?; + let auth_mode = settings::get_auth_mode(); + + let send_basic_req = || { + let auth = STANDARD.encode(format!("{username}:{password}")); + let body = serde_json::json!({ + "operationName": graphql::GraphQLQuery::CATEGORIES.operation_name, + "query": graphql::GraphQLQuery::CATEGORIES.query, + }); + Request::post(format!("{base_url}/api/graphql"))? + .header("Content-Type", "application/json") + .header("Authorization", &format!("Basic {auth}")) + .body(body.to_string()) + .send() + }; + + let send_form_req = || { + let form = format!( + "user={}&pass={}", + aidoku::helpers::uri::encode_uri_component(username.clone()), + aidoku::helpers::uri::encode_uri_component(password.clone()) + ); + Request::post(format!("{base_url}/login.html"))? + .header("Content-Type", "application/x-www-form-urlencoded") + .body(form) + .send() + }; + + match auth_mode.as_str() { + "none" => Ok(true), + "basic_auth" => { + let resp = send_basic_req()?; + Ok(resp.status_code() == 200) + } + "simple_login" => { + let resp = send_form_req()?; + Ok(resp.status_code() == 200) + } + _ => { + // auto: try basic auth first + if let Ok(resp) = send_basic_req() + && resp.status_code() == 200 + { + return Ok(true); + } + // try form login next + if let Ok(resp) = send_form_req() + && resp.status_code() == 200 + { + return Ok(true); + } + Ok(false) + } + } + } +} + +register_source!( + Suwayomi, + ListingProvider, + BaseUrlProvider, + DynamicListings, + BasicLoginHandler +); diff --git a/sources/multi.suwayomi/src/settings.rs b/sources/multi.suwayomi/src/settings.rs index 141efe8c1..a1a018e09 100644 --- a/sources/multi.suwayomi/src/settings.rs +++ b/sources/multi.suwayomi/src/settings.rs @@ -1,11 +1,28 @@ -use aidoku::{AidokuError, alloc::string::String, imports::defaults::defaults_get, prelude::bail}; +use aidoku::{Result, alloc::String, imports::defaults::defaults_get, prelude::*}; const BASE_URL_KEY: &str = "baseUrl"; +const AUTH_MODE_KEY: &str = "authMode"; +const USERNAME_KEY: &str = "credentials.username"; +const PASSWORD_KEY: &str = "credentials.password"; -pub fn get_base_url() -> Result { - let base_url = defaults_get::(BASE_URL_KEY); - match base_url { - Some(url) if !url.is_empty() => Ok(url), - _ => bail!("Base Url not configured"), +pub fn get_base_url() -> Result { + let url: String = defaults_get::(BASE_URL_KEY).ok_or(error!("Missing baseUrl"))?; + Ok(url.trim_end_matches('/').into()) +} + +pub fn get_auth_mode() -> String { + match defaults_get::(AUTH_MODE_KEY) { + Some(v) if !v.is_empty() => v, + _ => "auto".into(), + } +} + +pub fn get_credentials() -> Option<(String, String)> { + let user: String = defaults_get::(USERNAME_KEY)?; + let pass: String = defaults_get::(PASSWORD_KEY)?; + + if user.is_empty() { + return None; } + Some((user, pass)) } From 1b488a549d4eba8454867eb716ba63e366ee538d Mon Sep 17 00:00:00 2001 From: Nathan Argenti Date: Thu, 9 Jul 2026 21:42:06 -0600 Subject: [PATCH 2/4] Update sources/multi.suwayomi/res/settings.json Co-authored-by: kantan <31490942+kkantan@users.noreply.github.com> --- sources/multi.suwayomi/res/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/multi.suwayomi/res/settings.json b/sources/multi.suwayomi/res/settings.json index 44d64bc4f..5f814dc84 100644 --- a/sources/multi.suwayomi/res/settings.json +++ b/sources/multi.suwayomi/res/settings.json @@ -32,7 +32,7 @@ "key": "credentials", "title": "Login", "requires": "baseUrl", - "refreshes": ["content"] + "refreshes": ["content", "listings"] } ], "footer": "Enter your Suwayomi server URL and optionally login with your credentials." From fcdb67688bd48f73167e2add6d2909277ea31587 Mon Sep 17 00:00:00 2001 From: Nathan Argenti Date: Thu, 9 Jul 2026 21:42:16 -0600 Subject: [PATCH 3/4] Update sources/multi.suwayomi/res/settings.json Co-authored-by: kantan <31490942+kkantan@users.noreply.github.com> --- sources/multi.suwayomi/res/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/multi.suwayomi/res/settings.json b/sources/multi.suwayomi/res/settings.json index 5f814dc84..b48578df6 100644 --- a/sources/multi.suwayomi/res/settings.json +++ b/sources/multi.suwayomi/res/settings.json @@ -30,7 +30,7 @@ "type": "login", "method": "basic", "key": "credentials", - "title": "Login", + "title": "LOGIN", "requires": "baseUrl", "refreshes": ["content", "listings"] } From 62f60ce66d8b04dd962cb356e1986f4dacebd996 Mon Sep 17 00:00:00 2001 From: nathanjace Date: Thu, 9 Jul 2026 22:01:23 -0600 Subject: [PATCH 4/4] edited login logic --- sources/multi.suwayomi/src/lib.rs | 80 ++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/sources/multi.suwayomi/src/lib.rs b/sources/multi.suwayomi/src/lib.rs index fb1b06cea..965e1cbf0 100644 --- a/sources/multi.suwayomi/src/lib.rs +++ b/sources/multi.suwayomi/src/lib.rs @@ -26,6 +26,48 @@ use base64::{Engine, engine::general_purpose::STANDARD}; struct Suwayomi; impl Suwayomi { + fn send_graphql_post_request( + &self, + base_url: &str, + body: String, + ) -> core::result::Result { + let req = Request::post(format!("{base_url}/api/graphql"))? + .header("Content-Type", "application/json") + .body(body); + Ok(req) + } + + fn send_basic_auth_request( + &self, + base_url: &str, + user: &str, + pass: &str, + body: String, + ) -> core::result::Result { + let auth = STANDARD.encode(format!("{user}:{pass}")); + let req = self + .send_graphql_post_request(base_url, body)? + .header("Authorization", &format!("Basic {auth}")); + Ok(req) + } + + fn send_form_login_request( + &self, + base_url: &str, + user: &str, + pass: &str, + ) -> core::result::Result { + let form = format!( + "user={}&pass={}", + aidoku::helpers::uri::encode_uri_component(user), + aidoku::helpers::uri::encode_uri_component(pass) + ); + let req = Request::post(format!("{base_url}/login.html"))? + .header("Content-Type", "application/x-www-form-urlencoded") + .body(form); + Ok(req) + } + fn graphql_request(&self, body: serde_json::Value) -> Result> where T: serde::de::DeserializeOwned, @@ -35,27 +77,18 @@ impl Suwayomi { let body_str = body.to_string(); let send_req = |with_basic: bool| -> Result> { - let mut request = Request::post(format!("{base_url}/api/graphql"))? - .header("Content-Type", "application/json") - .body(body_str.clone()); - - if with_basic && let Some((user, pass)) = settings::get_credentials() { - let auth = STANDARD.encode(format!("{user}:{pass}")); - request = request.header("Authorization", &format!("Basic {auth}")); - } + let request = if with_basic && let Some((user, pass)) = settings::get_credentials() { + self.send_basic_auth_request(&base_url, &user, &pass, body_str.clone())? + } else { + self.send_graphql_post_request(&base_url, body_str.clone())? + }; request.json_owned::>() }; let do_login_html = || -> Result<()> { if let Some((user, pass)) = settings::get_credentials() { - let form = format!( - "user={}&pass={}", - aidoku::helpers::uri::encode_uri_component(user), - aidoku::helpers::uri::encode_uri_component(pass) - ); - let _ = Request::post(format!("{base_url}/login.html"))? - .header("Content-Type", "application/x-www-form-urlencoded") - .body(form) + let _ = self + .send_form_login_request(&base_url, &user, &pass)? .send() .ok(); } @@ -321,27 +354,16 @@ impl BasicLoginHandler for Suwayomi { let auth_mode = settings::get_auth_mode(); let send_basic_req = || { - let auth = STANDARD.encode(format!("{username}:{password}")); let body = serde_json::json!({ "operationName": graphql::GraphQLQuery::CATEGORIES.operation_name, "query": graphql::GraphQLQuery::CATEGORIES.query, }); - Request::post(format!("{base_url}/api/graphql"))? - .header("Content-Type", "application/json") - .header("Authorization", &format!("Basic {auth}")) - .body(body.to_string()) + self.send_basic_auth_request(&base_url, &username, &password, body.to_string())? .send() }; let send_form_req = || { - let form = format!( - "user={}&pass={}", - aidoku::helpers::uri::encode_uri_component(username.clone()), - aidoku::helpers::uri::encode_uri_component(password.clone()) - ); - Request::post(format!("{base_url}/login.html"))? - .header("Content-Type", "application/x-www-form-urlencoded") - .body(form) + self.send_form_login_request(&base_url, &username, &password)? .send() };