From a2995a4946548ea8ce81d160b4743076f43431f4 Mon Sep 17 00:00:00 2001 From: Xientra Date: Sat, 15 Nov 2025 02:19:05 +0000 Subject: [PATCH 1/5] feat: add support for SpeedApp --- backend/migrations/0018_speedapp_support.sql | 2 + backend/src/models/indexer.rs | 8 +- backend/src/services/user_stats/mod.rs | 1 + backend/src/services/user_stats/speedapp.rs | 99 ++++++++++++++++++++ 4 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 backend/migrations/0018_speedapp_support.sql create mode 100644 backend/src/services/user_stats/speedapp.rs diff --git a/backend/migrations/0018_speedapp_support.sql b/backend/migrations/0018_speedapp_support.sql new file mode 100644 index 0000000..12132a4 --- /dev/null +++ b/backend/migrations/0018_speedapp_support.sql @@ -0,0 +1,2 @@ +INSERT INTO indexers (name, auth_data) VALUES +('SpeedApp', '{"api_key": {"value": "", "explanation": "Generate one in My Profile > API Tokens, with all permissions"}}'); diff --git a/backend/src/models/indexer.rs b/backend/src/models/indexer.rs index 1fdae01..b006d5b 100644 --- a/backend/src/models/indexer.rs +++ b/backend/src/models/indexer.rs @@ -14,8 +14,8 @@ use crate::{ ita_torrents::ItaTorrentsScraper, lst::LSTScraper, myanonamouse::MyAnonamouseScraper, oldtoons::OldToonsScraper, only_encodes::OnlyEncodesScraper, orpheus::OrpheusScraper, phoenix_project::PhoenixProjectScraper, redacted::RedactedScraper, - reel_flix::ReelFlixScraper, seed_pool::SeedPoolScraper, upload_cx::UploadCXScraper, - yu_scene::YuSceneScraper, + reel_flix::ReelFlixScraper, seed_pool::SeedPoolScraper, speedapp::SpeedappScraper, + upload_cx::UploadCXScraper, yu_scene::YuSceneScraper, }, }; @@ -140,6 +140,10 @@ impl Indexer { static MY_ANONAMOUSE_SCRAPER: MyAnonamouseScraper = MyAnonamouseScraper; &MY_ANONAMOUSE_SCRAPER } + "SpeedApp" => { + static SPEED_APP_SCRAPER: SpeedappScraper = SpeedappScraper; + &SPEED_APP_SCRAPER + } _ => { return Err(Error::CouldNotScrapeIndexer( "indexer has no scraper".into(), diff --git a/backend/src/services/user_stats/mod.rs b/backend/src/services/user_stats/mod.rs index 2a544f8..6b9f186 100644 --- a/backend/src/services/user_stats/mod.rs +++ b/backend/src/services/user_stats/mod.rs @@ -17,5 +17,6 @@ pub mod redacted; pub mod reel_flix; pub mod scrape_indexers; pub mod seed_pool; +pub mod speedapp; pub mod upload_cx; pub mod yu_scene; diff --git a/backend/src/services/user_stats/speedapp.rs b/backend/src/services/user_stats/speedapp.rs new file mode 100644 index 0000000..87cf680 --- /dev/null +++ b/backend/src/services/user_stats/speedapp.rs @@ -0,0 +1,99 @@ +use serde::Deserialize; + +use async_trait::async_trait; + +use crate::{ + error::{Error, Result}, + models::{ + indexer::{Indexer, Scraper}, + user_stats::UserProfileScraped, + }, +}; + +#[derive(Debug, Deserialize)] +struct SpeedappResponse { + error: Option, + message: Option, + // id: Option, + // username: Option, + // email: Option, + // created_at: Option, + // class: Option, + // avatar: Option, + uploaded: Option, + downloaded: Option, + // title: Option, + is_donor: Option, + warned: Option, + // passkey: Option, + // invites: Option, + // timezone: Option, + // hit_and_run_count: Option, + // snatch_count: Option, + // need_seed: Option, + average_seed_time: Option, + // locale: Option, + // free_leech_tokens: Option, + // double_upload_tokens: Option, +} + +impl From for UserProfileScraped { + fn from(wrapper: SpeedappResponse) -> Self { + let uploaded = wrapper.uploaded.unwrap_or(0); + let downloaded = wrapper.downloaded.unwrap_or(0); + + UserProfileScraped { + uploaded: uploaded, + downloaded: downloaded, + ratio: (uploaded as f32 / downloaded as f32), + // class: wrapper.class.unwrap_or(0).to_string(), + donor: wrapper.is_donor, + warned: wrapper.warned, + average_seed_time: wrapper.average_seed_time, + ..Default::default() + } + } +} + +pub struct SpeedappScraper; + +#[async_trait] +impl Scraper for SpeedappScraper { + async fn scrape( + &self, + indexer: Indexer, + client: &reqwest::Client, + ) -> Result { + let res = client + .get("https://speedapp.io/api/me") + .header( + "Authorization", + format!( + "Bearer {}", + indexer + .auth_data + .get("api_key") + .ok_or("Speedapp api key not found.") + .map_err(|e| Error::CouldNotScrapeIndexer(e.into()))? + .get("value") + .ok_or("Speedapp api key value not found") + .map_err(|e| Error::CouldNotScrapeIndexer(e.into()))? + .as_str() + .unwrap() + ), + ) + .send() + .await + .map_err(|e| Error::CouldNotScrapeIndexer(e.to_string()))?; + + let body = res.text().await.unwrap(); + let response = serde_json::from_str::(&body) + .map_err(|e| Error::CouldNotScrapeIndexer(e.to_string()))?; + + if response.error.unwrap_or(false) == true { + return Err(Error::CouldNotScrapeIndexer(response.message.unwrap())); + } + + Ok(response.into()) + } +} From 8794bad3d3be6b0eb25e25c38a75085b0598d746 Mon Sep 17 00:00:00 2001 From: Xientra Date: Sat, 15 Nov 2025 04:12:35 +0000 Subject: [PATCH 2/5] docs: add SpeedApp to supported indexers --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 50fe0a1..81939d0 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ You can then visit the frontend at `http://localhost:3000` and the swagger at `h * RED * ReelFlix * SP +* SpeedApp * ULCX * YUS From c47d8341aac8cee8e82241ffe64b8b76aa654980 Mon Sep 17 00:00:00 2001 From: Xientra Date: Sat, 15 Nov 2025 04:25:09 +0000 Subject: [PATCH 3/5] feat: handle possible divide by zero errors --- backend/src/services/user_stats/speedapp.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/src/services/user_stats/speedapp.rs b/backend/src/services/user_stats/speedapp.rs index 87cf680..388a40b 100644 --- a/backend/src/services/user_stats/speedapp.rs +++ b/backend/src/services/user_stats/speedapp.rs @@ -41,11 +41,16 @@ impl From for UserProfileScraped { fn from(wrapper: SpeedappResponse) -> Self { let uploaded = wrapper.uploaded.unwrap_or(0); let downloaded = wrapper.downloaded.unwrap_or(0); + let ratio = if downloaded == 0 && uploaded > 0 { + f32::MAX + } else { + uploaded as f32 / downloaded as f32 + }; UserProfileScraped { uploaded: uploaded, downloaded: downloaded, - ratio: (uploaded as f32 / downloaded as f32), + ratio: ratio, // class: wrapper.class.unwrap_or(0).to_string(), donor: wrapper.is_donor, warned: wrapper.warned, From 96aa0622c17947fdb8deaba5a368d233b3019fef Mon Sep 17 00:00:00 2001 From: Xientra Date: Sun, 16 Nov 2025 11:19:11 +0000 Subject: [PATCH 4/5] feat: add snatch count --- backend/src/services/user_stats/speedapp.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/services/user_stats/speedapp.rs b/backend/src/services/user_stats/speedapp.rs index 388a40b..2408ae3 100644 --- a/backend/src/services/user_stats/speedapp.rs +++ b/backend/src/services/user_stats/speedapp.rs @@ -29,7 +29,7 @@ struct SpeedappResponse { // invites: Option, // timezone: Option, // hit_and_run_count: Option, - // snatch_count: Option, + snatch_count: Option, // need_seed: Option, average_seed_time: Option, // locale: Option, @@ -55,6 +55,7 @@ impl From for UserProfileScraped { donor: wrapper.is_donor, warned: wrapper.warned, average_seed_time: wrapper.average_seed_time, + snatched: wrapper.snatch_count, ..Default::default() } } From 4966dd44e3b37a8de76448f81cdeef36a9843aee Mon Sep 17 00:00:00 2001 From: Xientra Date: Wed, 19 Nov 2025 01:30:33 +0000 Subject: [PATCH 5/5] fix: clippy causing failed lint --- backend/src/services/user_stats/speedapp.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/services/user_stats/speedapp.rs b/backend/src/services/user_stats/speedapp.rs index 2408ae3..5d9819c 100644 --- a/backend/src/services/user_stats/speedapp.rs +++ b/backend/src/services/user_stats/speedapp.rs @@ -48,9 +48,9 @@ impl From for UserProfileScraped { }; UserProfileScraped { - uploaded: uploaded, - downloaded: downloaded, - ratio: ratio, + uploaded, + downloaded, + ratio, // class: wrapper.class.unwrap_or(0).to_string(), donor: wrapper.is_donor, warned: wrapper.warned, @@ -96,7 +96,7 @@ impl Scraper for SpeedappScraper { let response = serde_json::from_str::(&body) .map_err(|e| Error::CouldNotScrapeIndexer(e.to_string()))?; - if response.error.unwrap_or(false) == true { + if response.error.unwrap_or(false) { return Err(Error::CouldNotScrapeIndexer(response.message.unwrap())); }