Skip to content

Commit 8596196

Browse files
Merge PR josueBarretogit#189 feat/save_filters_used_mangadex
Feat/save filters used mangadex
2 parents 7f169bf + ec0d9af commit 8596196

18 files changed

Lines changed: 792 additions & 154 deletions

File tree

src/backend.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use once_cell::sync::Lazy;
66
use strum::{Display, EnumIter, IntoEnumIterator};
77

88
use self::error_log::create_error_logs_files;
9-
use crate::config::{MangaTuiConfig, build_config_file};
9+
use crate::config::build_config_file;
1010
use crate::logger::ILogger;
1111

1212
pub mod cache;

src/backend/cache.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use std::error::Error;
2-
use std::fmt::{Debug, Display};
3-
use std::time::Duration;
2+
use std::fmt::Debug;
43

54
pub mod in_memory;
65

src/backend/cache/in_memory.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,12 @@ impl Cacher for InMemoryCache {
139139
#[cfg(test)]
140140
mod tests {
141141
use std::error::Error;
142-
use std::thread::sleep;
143142
use std::time::{Duration, Instant};
144143

145144
use pretty_assertions::assert_eq;
146145

147146
use super::*;
148-
use crate::backend::cache::{self, Entry, InsertEntry};
147+
use crate::backend::cache::{Entry, InsertEntry};
149148

150149
#[test]
151150
fn it_saves_and_retrieves_data() -> Result<(), Box<dyn Error>> {

src/backend/manga_downloader/pdf_downloader.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ use std::path::Path;
44

55
use flate2::Compression;
66
use flate2::write::ZlibEncoder;
7-
use image::{DynamicImage, GenericImageView, ImageFormat};
8-
use lopdf::{Document, Object, Stream, dictionary};
7+
use image::{GenericImageView, ImageFormat};
8+
use lopdf::{Document, Stream, dictionary};
99

1010
use super::MangaDownloader;
1111

@@ -28,14 +28,15 @@ impl MangaDownloader for PdfDownloader {
2828

2929
let pdf_path = base_directory.join(format!("{}.pdf", self.make_chapter_name(&chapter).display()));
3030

31+
let file = File::create(pdf_path)?;
3132
let mut doc = Document::with_version("1.7");
3233
let mut pages = Vec::new();
3334
let page_width = 595.0;
3435

35-
for (index, page) in chapter.pages.iter().enumerate() {
36+
for page in chapter.pages.iter() {
3637
let img = image::load_from_memory(&page.bytes)?;
3738
let (img_width, img_height) = img.dimensions();
38-
let mut img_data = Vec::new();
39+
let mut img_data = Vec::with_capacity(page.bytes.len());
3940
let filter;
4041
let color_space = if img.color().has_color() { "DeviceRGB" } else { "DeviceGray" };
4142

@@ -105,7 +106,6 @@ impl MangaDownloader for PdfDownloader {
105106

106107
doc.trailer.set("Root", catalog_id);
107108

108-
let mut file = File::create(pdf_path)?;
109109
doc.save_to(&mut BufWriter::new(file))?;
110110

111111
Ok(())
@@ -115,12 +115,9 @@ impl MangaDownloader for PdfDownloader {
115115
#[cfg(test)]
116116
mod tests {
117117
use std::error::Error;
118-
use std::fs;
119-
use std::path::PathBuf;
120118

121119
use fake::Fake;
122120
use fake::faker::name::en::Name;
123-
use lopdf::Document;
124121
use uuid::Uuid;
125122

126123
use super::*;

src/backend/manga_provider.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use crate::config::ImageQuality;
2121
use crate::global::PREFERRED_LANGUAGE;
2222
use crate::view::widgets::StatefulWidgetFrame;
2323

24+
pub mod filters;
2425
pub mod mangadex;
2526
pub mod weebcentral;
2627

@@ -203,6 +204,11 @@ impl Languages {
203204
}
204205
}
205206

207+
/// Returns an iterator which discards the 'Unknown' variant
208+
pub fn iterate() -> std::iter::Filter<LanguagesIter, impl FnMut(&Languages) -> bool> {
209+
Self::iter().filter(|lan| *lan != Self::Unkown)
210+
}
211+
206212
pub fn get_preferred_lang() -> &'static Languages {
207213
PREFERRED_LANGUAGE.get_or_init(Self::default)
208214
}
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
//! This module provides the `FiltersCache` struct, which is responsible for caching and retrieving filter data used in manga search
2+
//! operations.
3+
//!
4+
//! The `FiltersCache` allows you to serialize filter configurations (such as languages, publication status, sort order, tags,
5+
//! authors, and more) into TOML files for persistent storage, and deserialize them back when needed. This is useful for persisting
6+
//! user-selected filters or default filter sets between application runs.
7+
//!
8+
//! The cache is stored in a specified directory and file, and the module provides methods to write filter data to the cache and
9+
//! read it back. The filter data must implement `serde::Serialize` and `serde::de::DeserializeOwned`, making it flexible for
10+
//! various filter types.
11+
//!
12+
//! Example use cases include caching search filters for manga providers like MangaDex, where filters may include fields such as
13+
//! languages, publication status, sort order, tags, magazine demographics, authors, and artists.
14+
use std::error::Error;
15+
use std::fs::{File, create_dir_all};
16+
use std::io::{Read, Write};
17+
use std::path::PathBuf;
18+
19+
use serde::Serialize;
20+
use serde::de::DeserializeOwned;
21+
22+
/// A cache handler for serializing and deserializing filter data to and from TOML files.
23+
///
24+
/// `FiltersCache` is designed to persist filter configurations used in manga search operations, such as those for MangaDex.
25+
/// It stores filter data (implementing `serde::Serialize` and `serde::de::DeserializeOwned`) in a specified directory and file.
26+
///
27+
/// # Example Usage
28+
///
29+
/// The struct is typically used to cache filters like the following (see tests for more details):
30+
///
31+
/// ```rust
32+
/// # use crate::backend::manga_provider::mangadex::filters::api_parameter::{Filters, ContentRating, PublicationStatus, SortBy, Tags, TagData, TagSelection, MagazineDemographic, User, AuthorFilterState};
33+
/// # use crate::backend::manga_provider::Languages;
34+
/// let filters = Filters {
35+
/// content_rating: vec![ContentRating::Suggestive, ContentRating::Erotic],
36+
/// publication_status: vec![PublicationStatus::Completed, PublicationStatus::Ongoing],
37+
/// sort_by: SortBy::HighestRating,
38+
/// tags: Tags::new(vec![TagData::new("id_tag".to_string(), TagSelection::Included, "fantasy".to_string())]),
39+
/// magazine_demographic: vec![MagazineDemographic::Shoujo, MagazineDemographic::Seinen],
40+
/// authors: User::new(vec![AuthorFilterState::new("user_id".to_string(), "".to_string())]),
41+
/// artists: User::default(),
42+
/// languages: vec![Languages::English, Languages::Spanish],
43+
/// };
44+
/// ```
45+
///
46+
/// You can then write these filters to a cache file and retrieve them later:
47+
///
48+
/// ```rust
49+
/// # use std::path::Path;
50+
/// # let filters_cache = FiltersCache::new(Path::new("./cache_dir"), "filters.toml");
51+
/// filters_cache.write_to_cache(&filters).unwrap();
52+
/// let cached: Option<Filters> = filters_cache.get_cached_filters();
53+
/// ```
54+
///
55+
/// This enables persistent storage and retrieval of user or default filter sets between application runs.
56+
pub struct FiltersCache {
57+
base_directory: PathBuf,
58+
cache_filename: &'static str,
59+
}
60+
61+
impl FiltersCache {
62+
pub fn new<T: Into<PathBuf>>(base_directory: T, cache_filename: &'static str) -> Self {
63+
let path: PathBuf = base_directory.into();
64+
Self {
65+
base_directory: path,
66+
cache_filename,
67+
}
68+
}
69+
70+
fn save_filters<T: Write, I: Serialize>(&self, filters: &I, file: &mut T) -> Result<(), Box<dyn Error>> {
71+
let filters_as_toml = toml::to_string(filters)?;
72+
73+
file.write_all(filters_as_toml.as_bytes())?;
74+
75+
file.flush()?;
76+
77+
Ok(())
78+
}
79+
80+
#[inline]
81+
fn get_cache_file_path(&self) -> PathBuf {
82+
self.base_directory.join(self.cache_filename)
83+
}
84+
85+
fn parse_cache<T: Read, I: DeserializeOwned>(&self, file: &mut T) -> Result<I, Box<dyn Error>> {
86+
let mut contents = String::new();
87+
88+
file.read_to_string(&mut contents)?;
89+
90+
let filters: I = toml::from_str(&contents)?;
91+
92+
Ok(filters)
93+
}
94+
95+
fn ensure_cache_directory_exists(&self) -> Result<(), std::io::Error> {
96+
if !self.base_directory.exists() {
97+
create_dir_all(&self.base_directory)?
98+
}
99+
100+
Ok(())
101+
}
102+
103+
/// Reads the cache directory, and returns:
104+
/// Some(filters) if there is already a cache filters file,
105+
/// None if the file doesnt exist
106+
pub fn get_cached_filters<I: DeserializeOwned>(&self) -> Option<I> {
107+
let file_path = self.get_cache_file_path();
108+
109+
let maybe_filters = File::open(file_path)
110+
.inspect_err(|e| match e.kind() {
111+
std::io::ErrorKind::NotFound => {},
112+
_ => {
113+
#[cfg(not(test))]
114+
{
115+
use crate::backend::error_log::{ErrorType, write_to_error_log};
116+
117+
write_to_error_log(ErrorType::String(&e.to_string()))
118+
}
119+
},
120+
})
121+
.and_then(|mut file| self.parse_cache(&mut file).map_err(|e| std::io::Error::other(e.to_string())))
122+
.ok();
123+
124+
maybe_filters
125+
}
126+
127+
/// Writes the "Filters" to the cache file which is created if it
128+
/// doesnt exist in toml format
129+
pub fn write_to_cache<I: Serialize>(&self, filters: &I) -> Result<(), Box<dyn Error>> {
130+
let file_path = self.get_cache_file_path();
131+
132+
self.ensure_cache_directory_exists()?;
133+
134+
let mut file = File::create(file_path)?;
135+
136+
self.save_filters(filters, &mut file)?;
137+
138+
Ok(())
139+
}
140+
}
141+
142+
#[cfg(test)]
143+
mod tests {
144+
use std::error::Error;
145+
use std::fs::create_dir_all;
146+
use std::io::Cursor;
147+
use std::path::Path;
148+
149+
use pretty_assertions::assert_eq;
150+
151+
use super::*;
152+
use crate::backend::manga_provider::Languages;
153+
use crate::backend::manga_provider::mangadex::filters::api_parameter::{
154+
AuthorFilterState, ContentRating, Filters, MagazineDemographic, PublicationStatus, SortBy, TagData, TagSelection, Tags,
155+
User,
156+
};
157+
158+
const CACHE_TEST_DIRECTORY_PATH: &str = "./test_results/cache_test/";
159+
160+
#[test]
161+
fn it_writes_mangadex_filters_to_cache_file() -> Result<(), Box<dyn Error>> {
162+
let filters: Filters = Filters {
163+
content_rating: vec![ContentRating::Suggestive, ContentRating::Erotic],
164+
publication_status: vec![PublicationStatus::Completed, PublicationStatus::Ongoing],
165+
sort_by: SortBy::HighestRating,
166+
tags: Tags::new(vec![TagData::new("id_tag".to_string(), TagSelection::Included, "fantasy".to_string())]),
167+
magazine_demographic: vec![MagazineDemographic::Shoujo, MagazineDemographic::Seinen],
168+
authors: User::new(vec![AuthorFilterState::new("user_id".to_string(), "".to_string())]),
169+
artists: User::default(),
170+
languages: vec![Languages::English, Languages::Spanish],
171+
};
172+
173+
let mut test_file = Cursor::new(Vec::new());
174+
175+
let filters_cache = FiltersCache::new(Path::new(""), "");
176+
177+
filters_cache.save_filters(&filters, &mut test_file)?;
178+
179+
let contents = String::from_utf8(test_file.into_inner())?;
180+
181+
let result: Filters = toml::from_str(&contents)?;
182+
183+
assert_eq!(filters, result);
184+
185+
Ok(())
186+
}
187+
188+
#[test]
189+
fn it_parses_mangadex_filters_from_the_cache_from_file() -> Result<(), Box<dyn Error>> {
190+
let filters: Filters = Filters {
191+
content_rating: vec![ContentRating::Suggestive, ContentRating::Erotic],
192+
publication_status: vec![PublicationStatus::Completed, PublicationStatus::Ongoing],
193+
sort_by: SortBy::HighestRating,
194+
tags: Tags::new(vec![TagData::new("id_tag".to_string(), TagSelection::Included, "fantasy".to_string())]),
195+
magazine_demographic: vec![MagazineDemographic::Shoujo, MagazineDemographic::Seinen],
196+
authors: User::new(vec![AuthorFilterState::new("user_id".to_string(), "".to_string())]),
197+
artists: User::default(),
198+
languages: vec![Languages::English, Languages::Spanish],
199+
};
200+
201+
let mut test_file = Cursor::new(toml::to_string(&filters)?);
202+
203+
let filters_cache = FiltersCache::new(Path::new(""), "");
204+
205+
let cached = filters_cache.parse_cache(&mut test_file)?;
206+
207+
assert_eq!(filters, cached);
208+
209+
Ok(())
210+
}
211+
212+
fn delete_cached_file_if_already_exists(path: &Path) {
213+
if path.exists() {
214+
std::fs::remove_file(path).unwrap()
215+
}
216+
}
217+
218+
#[ignore]
219+
#[test]
220+
fn it_check_if_cache_file_exists_and_returns_none() -> Result<(), Box<dyn Error>> {
221+
let filters: Filters = Filters {
222+
content_rating: vec![ContentRating::Suggestive, ContentRating::Erotic],
223+
publication_status: vec![PublicationStatus::Completed, PublicationStatus::Ongoing],
224+
sort_by: SortBy::HighestRating,
225+
tags: Tags::new(vec![TagData::new("id_tag".to_string(), TagSelection::Included, "fantasy".to_string())]),
226+
magazine_demographic: vec![MagazineDemographic::Shoujo, MagazineDemographic::Seinen],
227+
authors: User::new(vec![AuthorFilterState::new("user_id".to_string(), "".to_string())]),
228+
artists: User::default(),
229+
languages: vec![Languages::English, Languages::Spanish],
230+
};
231+
232+
create_dir_all(CACHE_TEST_DIRECTORY_PATH)?;
233+
234+
let file_cache = FiltersCache::new(CACHE_TEST_DIRECTORY_PATH, "mangadex_filters.toml");
235+
236+
delete_cached_file_if_already_exists(&file_cache.get_cache_file_path());
237+
238+
let first_check: Option<Filters> = file_cache.get_cached_filters();
239+
240+
assert!(first_check.is_none());
241+
242+
file_cache.write_to_cache(&filters).expect("failed to create cache file");
243+
244+
let second_check: Option<Filters> = file_cache.get_cached_filters();
245+
246+
assert!(second_check.is_some());
247+
248+
dbg!(second_check);
249+
250+
Ok(())
251+
}
252+
}

0 commit comments

Comments
 (0)