Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions sources/en.asurascans/res/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,19 @@
"default": true
}
]
},
{
"type": "group",
"items": [
{
"type": "login",
"key": "login",
"notification": "login",
"method": "web",
"title": "LOGIN",
"url": "https://asurascans.com/login",
"refreshes": ["listings"]
}
]
}
]
11 changes: 9 additions & 2 deletions sources/en.asurascans/res/source.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@
"info": {
"id": "en.asurascans",
"name": "Asura Scans",
"version": 13,
"version": 14,
"url": "https://asurascans.com",
"contentRating": 0,
"languages": ["en"]
"languages": ["en"],
"minAppVersion": "0.7.1"
},
"listings": [
{
"id": "Ranking",
"kind": 1
}
],
"config": {
"breakingChangeVersion": 12
}
Expand Down
48 changes: 48 additions & 0 deletions sources/en.asurascans/src/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use crate::{API_URL, models::*};
use aidoku::{
HashMap, Result,
alloc::string::String,
imports::{
defaults::{DefaultValue, defaults_get, defaults_set, defaults_set_data},
net::Request,
},
prelude::*,
};

const AUTH_KEY: &str = "auth";

pub fn is_logged_in() -> bool {
defaults_get::<LoginStatus>(AUTH_KEY).is_some()
}

pub fn is_subscribed() -> bool {
defaults_get::<LoginStatus>(AUTH_KEY).is_some_and(|s| s.is_subscribed)
}

pub fn handle_login(cookies: HashMap<String, String>) -> Result<bool> {
let Some(refresh_token) = cookies.get("refresh_token") else {
return Ok(false);
};
let Ok(status) = refresh(refresh_token) else {
bail!("Failed to authenticate");
};
defaults_set_data(AUTH_KEY, status);
Ok(true)
}

pub fn logout() {
defaults_set(AUTH_KEY, DefaultValue::Null);
}

pub fn get_access_token() -> Result<String> {
let old_status = defaults_get::<LoginStatus>(AUTH_KEY).ok_or(error!("Not logged in"))?;
let status = refresh(&old_status.refresh_token)?;
Ok(status.access_token)
}

fn refresh(refresh_token: &str) -> Result<LoginStatus> {
let res: RefreshResponse = Request::post(format!("{API_URL}/auth/refresh"))?
.body(format!("{{\"refresh_token\":\"{refresh_token}\"}}"))
.json_owned()?;
Ok(LoginStatus::from(res))
}
117 changes: 110 additions & 7 deletions sources/en.asurascans/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#![no_std]
use aidoku::{
AidokuError, Chapter, ContentRating, DeepLinkHandler, DeepLinkResult, FilterValue, Home,
HomeComponent, HomeComponentValue, HomeLayout, Link, Manga, MangaPageResult, MangaStatus,
MangaWithChapter, MigrationHandler, Page, PageContent, Result, Source, Viewer,
alloc::{String, Vec, string::ToString},
AidokuError, Chapter, ContentRating, DeepLinkHandler, DeepLinkResult, DynamicListings,
FilterValue, HashMap, Home, HomeComponent, HomeComponentValue, HomeLayout, Link, Listing,
ListingProvider, Manga, MangaPageResult, MangaStatus, MangaWithChapter, MigrationHandler,
NotificationHandler, Page, PageContent, Result, Source, Viewer, WebLoginHandler,
alloc::{String, Vec, string::ToString, vec},
helpers::uri::QueryParameters,
imports::{
defaults::defaults_get,
Expand All @@ -13,9 +14,14 @@ use aidoku::{
prelude::*,
};

mod auth;
mod helpers;
mod models;

use models::*;

const BASE_URL: &str = "https://asurascans.com";
const API_URL: &str = "https://api.asurascans.com/api";

struct AsuraScans;

Expand Down Expand Up @@ -186,14 +192,16 @@ impl Source for AsuraScans {
.ok_or_else(|| error!("Missing chapters"))?;

let skip_locked = !defaults_get::<bool>("showLocked").unwrap_or(true);
let is_subscribed = auth::is_subscribed();

manga.chapters = Some(
chapters_arr
.iter()
.filter_map(|obj| {
let obj = obj[1].as_object()?;

let locked = obj["is_locked"][1].as_bool().unwrap_or_default();
let locked =
!is_subscribed && obj["is_locked"][1].as_bool().unwrap_or_default();
if skip_locked && locked {
return None;
}
Expand Down Expand Up @@ -228,7 +236,15 @@ impl Source for AsuraScans {

fn get_page_list(&self, manga: Manga, chapter: Chapter) -> Result<Vec<Page>> {
let url = helpers::get_chapter_url(&chapter.key, &manga.key);
let html = Request::get(url)?.html()?;
let mut req = Request::get(url)?;
if auth::is_subscribed() {
// untested
if let Ok(token) = auth::get_access_token() {
req.set_header("Authorization", &format!("Bearer {token}"));
req.set_header("Cookie", &format!("access_token={token}"));
}
}
let html = req.html()?;

let island_props = html
.select_first(
Expand Down Expand Up @@ -385,4 +401,91 @@ impl MigrationHandler for AsuraScans {
}
}

register_source!(AsuraScans, Home, DeepLinkHandler, MigrationHandler);
impl ListingProvider for AsuraScans {
fn get_manga_list(&self, listing: Listing, page: i32) -> Result<MangaPageResult> {
match listing.id.as_str() {
"Ranking" => {
let html = Request::get(format!("{BASE_URL}/series-ranking"))?.html()?;
let entries = html
.select(".comics-ranking-list > a")
.map(|els| {
els.filter_map(|el| {
Some(Manga {
key: el
.attr("abs:href")
.and_then(|url| helpers::get_manga_key(&url))?,
title: el.select_first(".flex-1 > .text-sm")?.own_text()?,
cover: el.select_first("img").and_then(|el| el.attr("abs:src")),
..Default::default()
})
})
.collect()
})
.unwrap_or_default();
Ok(MangaPageResult {
entries,
has_next_page: false,
})
}
"Bookmarks" => {
let offset = 20 * (page - 1);
let token = auth::get_access_token()?;
let url = format!(
"{API_URL}/me/bookmarks?sort=updated&order=desc&limit=20&offset={offset}",
);
let json: BookmarkResponse = Request::get(url)?
.header("Authorization", &format!("Bearer {token}"))
.json_owned()?;
let entries = json.data.into_iter().map(Into::into).collect();
let has_next_page = page < json.meta.total;
Ok(MangaPageResult {
entries,
has_next_page,
})
}
_ => bail!("Invalid listing"),
}
}
}

impl DynamicListings for AsuraScans {
fn get_dynamic_listings(&self) -> Result<Vec<Listing>> {
if !auth::is_logged_in() {
return Ok(Vec::new());
}
Ok(vec![Listing {
id: "Bookmarks".into(),
name: "Bookmarks".into(),
..Default::default()
}])
}
}

impl WebLoginHandler for AsuraScans {
fn handle_web_login(&self, _key: String, cookies: HashMap<String, String>) -> Result<bool> {
auth::handle_login(cookies)
}
}

impl NotificationHandler for AsuraScans {
fn handle_notification(&self, notification: String) {
if notification != "login" {
return;
}
let is_logged_in = defaults_get::<String>("login").is_some();
if !is_logged_in {
auth::logout();
}
}
}

register_source!(
AsuraScans,
Home,
DeepLinkHandler,
MigrationHandler,
ListingProvider,
DynamicListings,
WebLoginHandler,
NotificationHandler
);
74 changes: 74 additions & 0 deletions sources/en.asurascans/src/models.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use aidoku::{
Manga,
alloc::{string::String, vec::Vec},
};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct LoginStatus {
pub access_token: String,
pub refresh_token: String,
pub is_subscribed: bool,
}

impl From<RefreshResponse> for LoginStatus {
fn from(value: RefreshResponse) -> Self {
Self {
access_token: value.data.access_token,
refresh_token: value.data.refresh_token,
is_subscribed: value.data.subscription_status.has_subscription,
}
}
}

#[derive(Deserialize)]
pub struct RefreshResponse {
pub data: RefreshResponseData,
}

#[derive(Deserialize)]
pub struct RefreshResponseData {
pub access_token: String,
pub refresh_token: String,
pub subscription_status: RefreshSubscriptionStatus,
}

#[derive(Deserialize)]
pub struct RefreshSubscriptionStatus {
pub has_subscription: bool,
}

#[derive(Deserialize)]
pub struct BookmarkResponse {
pub data: Vec<BookmarkItem>,
pub meta: BookmarkResponseMeta,
}

#[derive(Deserialize)]
pub struct BookmarkResponseMeta {
pub total: i32,
}

#[derive(Deserialize)]
pub struct BookmarkItem {
// pub id: i32,
series: BookmarkSeries,
}

impl From<BookmarkItem> for Manga {
fn from(value: BookmarkItem) -> Self {
Manga {
key: value.series.slug,
title: value.series.title,
cover: Some(value.series.cover_url),
..Default::default()
}
}
}

#[derive(Deserialize)]
pub struct BookmarkSeries {
cover_url: String,
slug: String,
title: String,
}
Loading