Skip to content

Commit

Permalink
feat(Manganato): implement search page provider
Browse files Browse the repository at this point in the history
  • Loading branch information
josueBarretogit committed Feb 6, 2025
1 parent 76f10ba commit c7e1457
Show file tree
Hide file tree
Showing 10 changed files with 794 additions and 344 deletions.
37 changes: 28 additions & 9 deletions src/backend/manga_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ pub struct SearchManga {
pub title: String,
pub genres: Vec<Genres>,
pub description: Option<String>,
pub status: MangaStatus,
pub status: Option<MangaStatus>,
pub cover_img_url: Option<String>,
pub languages: Vec<Languages>,
/// Some manga providers provide the artist of the manga
Expand Down Expand Up @@ -352,6 +352,20 @@ impl Pagination {
}
}

pub fn from_first_page(items_per_page: u32) -> Self {
Self {
current_page: 1,
items_per_page,
// Total items should be set later after a search
total_items: 300,
}
}

pub fn reset(&mut self) {
self.current_page = 1;
self.total_items = 100;
}

pub fn go_next_page(&mut self) {
if self.current_page * self.items_per_page < self.total_items {
self.current_page += 1;
Expand Down Expand Up @@ -739,7 +753,7 @@ pub struct GetMangasResponse {
pub total_mangas: u32,
}

pub trait SearchPageProvider: DecodeBytesToImage + SearchMangaById + Clone + Send + Sync + 'static {
pub trait SearchPageProvider: DecodeBytesToImage + SearchMangaById + ProviderIdentity + Clone + Send + Sync + 'static {
/// The filter state that will be used in api calls, needs to be `Send` in order to do so
type InnerState: Send + Clone;
/// Struct which handles the key events of the user
Expand All @@ -755,14 +769,17 @@ pub trait SearchPageProvider: DecodeBytesToImage + SearchMangaById + Clone + Sen
) -> impl Future<Output = Result<GetMangasResponse, Box<dyn Error>>> + Send;
}

pub trait FeedPageProvider: SearchMangaById + Clone + Send + Sync + 'static {
pub trait FeedPageProvider: SearchMangaById + ProviderIdentity + Clone + Send + Sync + 'static {
fn get_latest_chapters(&self, manga_id: &str) -> impl Future<Output = Result<Vec<LatestChapter>, Box<dyn Error>>> + Send;
}

pub trait ProviderIdentity {
fn name(&self) -> MangaProviders;
}

pub trait MangaProvider:
HomePageMangaProvider + MangaPageProvider + SearchPageProvider + ReaderPageProvider + FeedPageProvider + Send + Sync
{
fn name() -> MangaProviders;
}

#[cfg(test)]
Expand Down Expand Up @@ -978,6 +995,12 @@ pub mod mock {

impl ReaderPageProvider for MockMangaPageProvider {}

impl ProviderIdentity for MockMangaPageProvider {
fn name(&self) -> MangaProviders {
MangaProviders::Mangadex
}
}

impl FeedPageProvider for MockMangaPageProvider {
async fn get_latest_chapters(&self, manga_id: &str) -> Result<Vec<LatestChapter>, Box<dyn Error>> {
if self.should_fail {
Expand All @@ -987,11 +1010,7 @@ pub mod mock {
}
}

impl MangaProvider for MockMangaPageProvider {
fn name() -> MangaProviders {
MangaProviders::Mangadex
}
}
impl MangaProvider for MockMangaPageProvider {}

#[derive(Clone)]
pub struct ReaderPageProvierMock {
Expand Down
11 changes: 7 additions & 4 deletions src/backend/manga_provider/mangadex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ use super::{
Artist, Author, Chapter, ChapterPage, ChapterPageUrl, ChapterToRead, DecodeBytesToImage, FeedPageProvider,
FetchChapterBookmarked, Genres, GetChapterPages, GetChaptersResponse, GetMangasResponse, GetRawImage, GoToReadChapter,
HomePageMangaProvider, Languages, ListOfChapters, MangaPageProvider, MangaProvider, MangaProviders, MangaStatus, PopularManga,
Rating, ReaderPageProvider, RecentlyAddedManga, SearchChapterById, SearchMangaById, SearchMangaPanel, SearchPageProvider,
ProviderIdentity, Rating, ReaderPageProvider, RecentlyAddedManga, SearchChapterById, SearchMangaById, SearchMangaPanel,
SearchPageProvider,
};
use crate::backend::database::ChapterBookmarked;
use crate::config::ImageQuality;
Expand Down Expand Up @@ -799,7 +800,7 @@ impl SearchPageProvider for MangadexClient {
author,
cover_img_url,
languages,
status,
status: Some(status),
}
})
.collect();
Expand Down Expand Up @@ -860,12 +861,14 @@ impl FeedPageProvider for MangadexClient {
}
}

impl MangaProvider for MangadexClient {
fn name() -> super::MangaProviders {
impl ProviderIdentity for MangadexClient {
fn name(&self) -> MangaProviders {
MangaProviders::Mangadex
}
}

impl MangaProvider for MangadexClient {}

#[cfg(test)]
mod test {
//use httpmock::Method::GET;
Expand Down
15 changes: 1 addition & 14 deletions src/backend/manga_provider/mangadex/filter_widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,22 +59,9 @@ impl StatefulWidgetFrame for MangadexFilterWidget {

fn render(&mut self, area: Rect, frame: &mut Frame<'_>, state: &mut Self::State) {
let buf = frame.buffer_mut();
let popup_area = centered_rect(area, 80, 70);

Clear.render(popup_area, buf);

let filter_instructions = Line::from(vec![
"Close ".into(),
Span::raw("<f>").style(*INSTRUCTIONS_STYLE),
" Reset filters ".into(),
Span::raw("<r>").style(*INSTRUCTIONS_STYLE),
]);

Block::bordered().title(filter_instructions).render(popup_area, buf);

let [tabs_area, current_filter_area] = Layout::vertical([Constraint::Percentage(20), Constraint::Percentage(80)])
.margin(2)
.areas(popup_area);
.areas(area);

let tabs: Vec<Line<'_>> = FILTERS
.iter()
Expand Down
147 changes: 107 additions & 40 deletions src/backend/manga_provider/manganato.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
use std::borrow::BorrowMut;
use std::error::Error;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use http::StatusCode;
use filter_state::{ManganatoFilterState, ManganatoFiltersProvider};
use filter_widget::ManganatoFilterWidget;
use http::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, CONTENT_TYPE, REFERER};
use http::{HeaderMap, HeaderValue, StatusCode};
use manga_tui::SearchTerm;
use reqwest::{Client, Url};
use response::{GetPopularMangasResponse, NewAddedMangas, NewMangaAddedToolTip, ToolTipItem};
use response::{GetPopularMangasResponse, NewAddedMangas, SearchMangaResponse, ToolTipItem};

use super::{DecodeBytesToImage, GetRawImage, HomePageMangaProvider, PopularManga, RecentlyAddedManga, SearchMangaById};
use super::{
DecodeBytesToImage, GetMangasResponse, GetRawImage, HomePageMangaProvider, PopularManga, ProviderIdentity, RecentlyAddedManga,
SearchMangaById, SearchPageProvider,
};
use crate::backend::html_parser::HtmlElement;

pub static MANGANATO_BASE_URL: &str = "https://manganato.com";

pub mod filter_state;
pub mod filter_widget;
pub mod response;

trait FromHtml: Sized {
Expand All @@ -22,21 +29,43 @@ trait FromHtml: Sized {
pub struct ManganatoProvider {
client: reqwest::Client,
base_url: Url,
home_page_doc: Option<HtmlElement>,
}

impl ManganatoProvider {
pub fn new(base_url: Url) -> Self {
let mut default_headers = HeaderMap::new();

default_headers.insert(
ACCEPT,
HeaderValue::from_static(
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8,application/json",
),
);

default_headers.insert(REFERER, HeaderValue::from_static("http://www.google.com"));
default_headers.insert(ACCEPT_ENCODING, HeaderValue::from_static("gzip, deflate, br"));
default_headers.insert(ACCEPT_LANGUAGE, HeaderValue::from_static("en-US,en;q=0.5"));

let client = Client::builder()
.timeout(Duration::from_secs(10))
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
.build()
.unwrap();
Self {
client,
base_url,
home_page_doc: None,
}
Self { client, base_url }
}

fn format_search_term(search_term: SearchTerm) -> String {
let mut search: String = search_term.get().split(" ").map(|word| format!("{word}_")).collect();

search.pop();

search
}
}

impl ProviderIdentity for ManganatoProvider {
fn name(&self) -> super::MangaProviders {
super::MangaProviders::Manganato
}
}

Expand All @@ -62,25 +91,17 @@ impl SearchMangaById for ManganatoProvider {

impl HomePageMangaProvider for ManganatoProvider {
async fn get_popular_mangas(&self) -> Result<Vec<super::PopularManga>, Box<dyn Error>> {
match &self.home_page_doc {
Some(doc) => {
let response = GetPopularMangasResponse::from_html(doc.clone())?;
let response = self.client.get(self.base_url.clone()).send().await?;

Ok(response.mangas.into_iter().map(PopularManga::from).collect())
},
None => {
let response = self.client.get(self.base_url.clone()).send().await?;
if response.status() != StatusCode::OK {
return Err("could not".into());
}

if response.status() != StatusCode::OK {
return Err("could not".into());
}
let doc = response.text().await?;
let doc = response.text().await?;

let response = GetPopularMangasResponse::from_html(HtmlElement::new(doc))?;
let response = GetPopularMangasResponse::from_html(HtmlElement::new(doc))?;

Ok(response.mangas.into_iter().map(PopularManga::from).collect())
},
}
Ok(response.mangas.into_iter().map(PopularManga::from).collect())
}

async fn get_recently_added_mangas(&self) -> Result<Vec<super::RecentlyAddedManga>, Box<dyn Error>> {
Expand All @@ -101,20 +122,58 @@ impl HomePageMangaProvider for ManganatoProvider {
}

let tool_tip_response: Vec<ToolTipItem> = tool_tip_response.json().await?;
let mut response: Vec<RecentlyAddedManga> = vec![];

for new_manga in new_mangas.mangas.into_iter() {
let from_tool_tip = tool_tip_response.iter().find(|data| data.id == new_manga.id).cloned().unwrap_or_default();
let manga = RecentlyAddedManga {
id: new_manga.manga_page_url,
title: from_tool_tip.name,
description: from_tool_tip.description,
cover_img_url: Some(from_tool_tip.image),
};

response.push(manga);
}

Ok(new_mangas
.mangas
.into_iter()
.map(move |manga| {
let from_tool_tip = tool_tip_response.clone().into_iter().find(|data| data.id == manga.id).unwrap_or_default();
RecentlyAddedManga {
id: manga.manga_page_url,
title: from_tool_tip.name,
description: from_tool_tip.description,
cover_img_url: Some(from_tool_tip.image),
}
})
.collect())
Ok(response)
}
}

impl SearchPageProvider for ManganatoProvider {
type FiltersHandler = ManganatoFiltersProvider;
type InnerState = ManganatoFilterState;
type Widget = ManganatoFilterWidget;

async fn search_mangas(
&self,
search_term: Option<SearchTerm>,
_filters: Self::InnerState,
pagination: super::Pagination,
) -> Result<super::GetMangasResponse, Box<dyn Error>> {
let search = match search_term {
Some(search) => ("keyw", Self::format_search_term(search)),
None => ("", "".to_string()),
};

let endpoint = format!("{}/advanced_search", self.base_url);

let response = self
.client
.get(endpoint)
.query(&[("page", pagination.current_page.to_string()), ("s", "all".to_string()), search])
.send()
.await?;

if response.status() != StatusCode::OK {
return Err("could not search mangas on manganato".into());
}

let doc = response.text().await?;

let result = SearchMangaResponse::from_html(HtmlElement::new(doc))?;

Ok(GetMangasResponse::from(result))
}
}

Expand All @@ -123,5 +182,13 @@ mod tests {
use super::*;

#[test]
fn test_name() {}
fn parses_search_term_correctly() {
let searchterm = SearchTerm::trimmed_lowercased("death note").unwrap();

assert_eq!("death_note", ManganatoProvider::format_search_term(searchterm));

let searchterm = SearchTerm::trimmed_lowercased("oshi no ko").unwrap();

assert_eq!("oshi_no_ko", ManganatoProvider::format_search_term(searchterm));
}
}
54 changes: 54 additions & 0 deletions src/backend/manga_provider/manganato/filter_state.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use crossterm::event::KeyCode;

use crate::backend::manga_provider::{EventHandler, FiltersHandler};
use crate::backend::tui::Events;

#[derive(Debug, Clone)]
pub struct ManganatoFilterState {}

#[derive(Debug, Clone)]
pub struct ManganatoFiltersProvider {
is_open: bool,
filter: ManganatoFilterState,
}

impl ManganatoFiltersProvider {
pub fn new(filter: ManganatoFilterState) -> Self {
Self {
is_open: false,
filter,
}
}
}

impl EventHandler for ManganatoFiltersProvider {
fn handle_events(&mut self, events: crate::backend::tui::Events) {
match events {
Events::Key(key) => match key.code {
KeyCode::Char('f') => self.toggle(),
_ => {},
},
_ => {},
}
}
}

impl FiltersHandler for ManganatoFiltersProvider {
type InnerState = ManganatoFilterState;

fn toggle(&mut self) {
self.is_open = !self.is_open;
}

fn is_open(&self) -> bool {
self.is_open
}

fn is_typing(&self) -> bool {
false
}

fn get_state(&self) -> &Self::InnerState {
&self.filter
}
}
Loading

0 comments on commit c7e1457

Please sign in to comment.