3 Commits

Author SHA1 Message Date
Domipoke cdaf0c4a05 0.1.0 2026-06-05 18:36:02 +02:00
Domipoke 758dc3fad2 update 2026-04-01 02:16:25 +02:00
Domipoke 2f90d71911 01/04/26_00.35 2026-04-01 00:35:07 +02:00
33 changed files with 11106 additions and 126 deletions
+5
View File
@@ -0,0 +1,5 @@
[target.aarch64-linux-android]
linker = "/home/domenico/Android/Sdk/ndk/30.0.14904198/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android36-clang"
[env]
CC_aarch64_linux_android = "/home/domenico/Android/Sdk/ndk/30.0.14904198/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android36-clang"
Executable
+1
View File
@@ -0,0 +1 @@
/target
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Generated
+5254
View File
File diff suppressed because it is too large Load Diff
Executable
+38
View File
@@ -0,0 +1,38 @@
[package]
name = "Aniyomi"
version = "0.1.0"
edition = "2024"
[dependencies]
async-trait = "0.1.89"
color-eyre = "0.6.5"
curl = "0.4.49"
ratatui = "0.30.0"
reqwest = { version = "0.13.2", features = ["rustls","blocking", "cookies"] }
scraper = "0.25.0"
serde = { version = "1.0.228", features = ["derive"] }
strum = { version = "0.28", default-features = false, features = ["derive"] }
tokio = { version = "1.50.0", features = ["full"] }
toml = "1.0.4"
openssl = { version = "0.10", features = ["vendored"] }
zip = "8.5.0"
crc32fast = "1.5.0"
hex = "0.4.3"
regex = "1.12.3"
base64 = "0.22.1"
rquickjs = "0.11.0"
aes = "0.8"
cbc = "0.1"
block-padding = "0.3"
anyhow = "1.0.102"
cipher = { version = "0.5.1", features = ["block-padding"]}
once_cell = "1.21.4"
image = "0.25.10"
boa_engine = "0.21.1"
url = "2.5.8"
[features]
default = ["debug"]
debug = []
-100
View File
@@ -1,100 +0,0 @@
---
marp: true
---
# Inizializzazione
1. Aggiungere il percorso di vlc alla variabile d'ambiente path.
- Installare vlc se non già fatto.
- Verificare se l'installazione è come da default in "C:\Program Files\VideoLAN\VLC", altrimenti:
- Cercare vlc sulla ricerca di windows (forse è chiamato in modi leggermente diversi, quali ad esempio VLC media player)
- Apri percorso file
- Tasto destro -> Apri percorso file sul file selezionato da windows
- Ci troveremo nella cartella nella quale c'è un file chiamato vlc (questo così com'è scritto, non in modi diversi)
---
- Cerchiamo "modifica le variabili di ambiente relative al sistema", apriamo
- clicchiamo su variabile d'ambiente in basso a destra
- Doppio click su Path per aprire la variabile corrispondente
- Aggiungiamo il percorso cliccando su nuovo e incollando il percorso, nel mio caso con un'installazione base è "C:\Program Files\VideoLAN\VLC"
---
# Spiegazione sui file
## Formato toml
Toml è un file di configurazione simile a yml che si pone come alternativa più semplice alla lettura del classico file json usato principalmente per il web.
---
### Json
Il formato di json è il seguente:
```json
{
"key1": "value",
"key2": {
"key2.1": "value"
},
"key3": ["value", 5140]
}
```
---
### Yaml
Il formato yml o yaml è il seguente:
```yaml
key1: value
key2:
key2.1: value
key3:
- value
- 5140
```
---
### Toml
Il formato toml è il seguente:
```toml
key1 = value
[key2]
key2.1 = value
[[key3]]
value
[[key3]]
5140
```
---
### Perchè toml?
Pur preferendo json perchè mi sembra il più facilmente intuibile anche se molto pieno di caratteri speciali che possono inizialmente creare confusione, useremo toml perchè nativamente supportato da rust, il linguaggio che ho usato per la creazione di questo programma.
Yaml è molto più pulito di json ma ha spazi significativi, il che non lo rende così facile da scrivere su blocco note.
---
## Toml nei file qui presenti
### File Library
I file animelibrary.toml e mangalibrary.toml sono i due file designati alla gestione dei preferiti. Hanno formato:
```toml
[[AnimeSource]] # Scelta tra: Animeworld
name = "Nome dell'opera"
episodeList = ""
```
```toml
[[MangaSource]] # Scelta tra: Mangaworld, Mangago
name = "Nome dell'opera"
mangapage = ""
```
---
#### Formati accettati
Saranno scritti in RegEx (ovvero regular expression), un linguaggio per scrivere pattern. Può essere testato su tool online come [regexr.com](https://regexr.com).
##### Animeworld
```re
https?\:\/\/www\.animeworld\.\w{2,3}\/play\/[a-zA-Z0-9\-]+\.\w+(\/\w+)?
```
Es.
- ```https://www.animeworld.ac/play/frieren-beyond-journeys-end-2.JvOQx```
- ```https://www.animeworld.ac/play/frieren-beyond-journeys-end-2.JvOQx/Xduvm5```
-4
View File
@@ -1,4 +0,0 @@
[[Animeworld]]
name="Frieren: Beyond Journey's End 2"
episodeList="https://www.animeworld.ac/play/frieren-beyond-journeys-end-2.JvOQx"
MalUrl="https://myanimelist.net/anime/59978"
+3
View File
@@ -0,0 +1,3 @@
# cargo build --target aarch64-linux-android --release
cargo build --release
cargo build --target x86_64-pc-windows-gnu --release
+41 -9
View File
@@ -1,10 +1,42 @@
[Settings]
[Settings.Folders]
basedir = "." # !! Should be an Absolute Path
extensions = "./extensions"
anime = "./anime"
manga = "./manga"
[Anime]
source = "animeworld"
[Extension]
defaultanime = "animeworld" # USELESS
defaultmanga = "mangaworld" # USELESS
[Manga]
source = "mangaworld"
# Le opzioni sono in base al sistema operativo in uso:
# Windows:
# Sumatra - Un lettore di pdf e file cbz gratuito e opensource. è estremamente leggero, soprattutto a confronto con google e altri browser. (11 MB di peso)
#// SumatraCBZ
# Linux:
# Okular - Lettore Pdf incluso con l'ambiente desktop KDE-Plasma
# Windows e Linux:
# Calibre - Miglior lettore epub che abbia mai provato, l'ho usato per tanto tempo su windows. Supporta anche Cbz e pdf e tanti altri tipi. Meno preferito rispetto a Sumatra nel caso non lo utiliziate per altro in quanto Calibre, possedendo tante funzionalità, non è così leggero. (300 MB circa)
# Encoding:
# pdf - scarica in formato pdf e lascia all'utente la possibilità di aprirlo come preferisce
# cbz - scarica in formato cbz e lascia all'utente la possibilità di aprirlo come preferisce
#
# Specifica sul formato cbz: Il formato cbz è un sostanzialmente una zip costituita da un file di metadata e un insieme di immagini numerate.
# A differenza del pdf ogni pagina del cbz è gestita come un'immagine.
reader="Okular"
# Don't care if pdf or cbz selected
programCmd="okular"
programArg=""
[General]
askToQuit = false
## Cmd
[Theme]
base_border = "#fef3c7"
anime_border = "#fef3c7"
manga_border = "#fef3c7"
menu_text = "#ffffff"
menu_selection = "#fbbf24"
res_border = "#fef3c7"
res_text = "#ffffff"
res_selection = "#fbbf24"
cmd_text = "#ffffff"
cmd_keys = "#7dcefe"
status_loading = "#fef3c7"
list_border = "#818cf8"
View File
Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
Type = "Manga"
[Info]
Name = "Mangago"
Description = ""
Language = "EN/en"
[Sitemanager]
Hostname = "mangago.me"
Url = "https://www.mangag.me"
+7
View File
@@ -0,0 +1,7 @@
[[Animeworld]]
name="Frieren: Beyond Journey's End 2"
episodes="https://www.animeworld.ac/play/frieren-beyond-journeys-end-2.JvOQx"
[[Animeworld]]
name="One Piece"
episodes="https://www.animeworld.ac/play/one-piece-subita.qzG-LE/HPKmX1"
+17
View File
@@ -0,0 +1,17 @@
[[Mangaworld]]
name="Kingdom"
episodes="https://www.mangaworld.mx/manga/1683/kingdom"
[[Mangaworld]]
name="Chainsaw Man"
episodes="https://www.mangaworld.mx/manga/1637/chainsaw-man"
[[Mangago]]
name="Medalist"
episodes="https://www.mangago.me/read-manga/medalist/"
[[Mangago]]
name="Atelier of the Witch Hat"
episodes="https://www.mangago.me/read-manga/tongari_boushi_no_atelier/"
View File
-13
View File
@@ -1,13 +0,0 @@
[[Mangaworld]]
name="Kingdom"
mangapage="https://www.mangaworld.mx/manga/1683/kingdom"
[[Mangaworld]]
name="Chainsaw Man"
mangapage="https://www.mangaworld.mx/manga/1637/chainsaw-man"
[[Mangago]]
name="Medalist"
mangapage="https://www.mangago.me/read-manga/medalist/"
+1332
View File
File diff suppressed because it is too large Load Diff
+232
View File
@@ -0,0 +1,232 @@
use std::{default, fs, sync::{LazyLock, RwLock}};
use ratatui::style::palette::tailwind;
use serde::{Deserialize, Deserializer};
use std::sync::OnceLock;
use crate::{log, utils::utils::ColorPickerOption};
#[derive(Deserialize, Clone)]
pub struct Config {
pub Anime: AnimeSettings,
pub Manga: MangaSettings,
pub Theme: ThemeSettings,
pub General: GeneralSettings
}
#[derive(Deserialize, Clone)]
pub struct AnimeSettings {
pub source: String,
}
#[derive(Deserialize, Clone)]
pub struct GeneralSettings {
pub askToQuit: bool,
}
#[derive(Deserialize, Clone)]
pub struct MangaSettings {
#[serde(deserialize_with = "manga_reader_deserialize")]
pub reader: MangaReader,
pub source: String,
pub programCmd: String,
pub programArg: String
}
#[derive(Deserialize, Clone)]
pub enum MangaReader {
Sumatra, PDF, CBZ, Okular, Calibre
}
impl MangaReader {
pub fn to_str(&self) -> &str {
match self {
MangaReader::CBZ => "CBZ",
MangaReader::Calibre => "Calibre",
MangaReader::Okular => "Okular",
MangaReader::PDF => "PDF",
MangaReader::Sumatra => "Sumatra"
}
}
}
fn manga_reader_deserialize<'de,D>(deserializer: D) -> Result<MangaReader, D::Error> where D: Deserializer<'de> {
let s = String::deserialize(deserializer)?.to_lowercase();
match s.as_str() {
"sumatra" => Ok(MangaReader::Sumatra),
"pdf" => Ok(MangaReader::PDF),
"cbz" => Ok(MangaReader::CBZ),
"calibre" => Ok(MangaReader::Calibre),
"okular" => Ok(MangaReader::Okular),
_ => {
log!("Invalid manga reader, using default option: PDF");
return Ok(MangaReader::PDF);
}
}
}
// #[derive(Deserialize)]
// pub struct Customization {
// pub ColorPalette: ColorPalette
// }
// #[derive(Deserialize)]
// pub struct ColorPalette {
// pub anime: String,
// pub manga: String,
// pub source: String,
// pub settings: String
// }
static CONFIG: LazyLock<RwLock<Config>> = LazyLock::new(|| {
RwLock::new(load_config())
});
pub fn getConfig() -> Config {
let lock = CONFIG.read().unwrap();
lock.clone()
}
pub fn load_config() -> Config {
let content = fs::read_to_string("config.toml").expect("Can't read config file");
let parsed:Config = toml::from_str(&content).unwrap();
parsed
}
pub fn tailwindPaletteFromString(c: &str, def: tailwind::Palette) -> tailwind::Palette {
let res = match c {
"AMBER" => tailwind::AMBER,
"BLUE" => tailwind::BLUE,
"CYAN" => tailwind::CYAN,
"EMERALD" => tailwind::EMERALD,
"FUCHSIA" => tailwind::FUCHSIA,
"GRAY" => tailwind::GRAY,
"GREEN" => tailwind::GREEN,
"INDIGO" => tailwind::INDIGO,
"LIME" => tailwind::LIME,
"NEUTRAL" => tailwind::NEUTRAL,
"ORANGE" => tailwind::ORANGE,
"PINK" => tailwind::PINK,
"PURPLE" => tailwind::PURPLE,
"RED" => tailwind::RED,
"ROSE" => tailwind::ROSE,
"SKY" => tailwind::SKY,
"SLATE" => tailwind::SLATE,
"STONE" => tailwind::STONE,
"TEAL" => tailwind::TEAL,
"VIOLET" => tailwind::VIOLET,
"YELLOW" => tailwind::YELLOW,
"ZINC" => tailwind::ZINC,
_ => def
};
res
}
pub fn setMangaSource(v:String) {
let mut conf = CONFIG.write().unwrap();
conf.Manga.source = v
}
pub fn setAnimeSource(v:String) {
let mut conf = CONFIG.write().unwrap();
conf.Anime.source = v
}
#[derive(Deserialize, Clone)]
pub struct Color {
red: u8,
green: u8,
blue: u8,
opacity: u8
}
impl Color {
pub fn to_u32(&self) -> u32 {
((self.opacity as u32) << 24) |
((self.red as u32) << 16) |
((self.green as u32) << 8) |
(self.blue as u32)
}
pub fn to_ratatui(&self) -> ratatui::style::Color {
// log!("{}", self.to_u32());
let x = ratatui::style::Color::from_u32(self.to_u32());
x
}
}
pub fn color_deserialize<'de,D>(deserializer: D) -> Result<Color, D::Error> where D: Deserializer<'de> {
let s = String::deserialize(deserializer)?.to_lowercase();
log!("{:?}",s.get(1..3));
let hexred = match s.get(1..3) {
Some(res)=>match u8::from_str_radix(res, 16){
Ok(r) => r,
Err(_) => {
log!("Invalid hex color");
0
},
},
None => {
log!("Invalid hex color");
0
}
};
let hexgre = match s.get(3..5) {
Some(res)=>match u8::from_str_radix(res, 16){
Ok(r) => r,
Err(_) => {
log!("Invalid hex color");
0
},
},
None => {
log!("Invalid hex color");
0
}
};
let hexblu = match s.get(5..7) {
Some(res)=>match u8::from_str_radix(res, 16){
Ok(r) => r,
Err(_) => {
log!("Invalid hex color");
0
},
},
None => {
log!("Invalid hex color");
0
}
};
log!("{} {} {}", hexred, hexgre, hexblu);
return Ok(Color {
red: hexred,
green: hexgre,
blue: hexblu,
opacity: 255
})
}
#[derive(Deserialize, Clone)]
pub struct ThemeSettings {
#[serde(deserialize_with = "color_deserialize")]
pub base_border: Color,
#[serde(deserialize_with = "color_deserialize")]
pub anime_border: Color,
#[serde(deserialize_with = "color_deserialize")]
pub manga_border: Color,
#[serde(deserialize_with = "color_deserialize")]
pub menu_text: Color,
#[serde(deserialize_with = "color_deserialize")]
pub menu_selection: Color,
#[serde(deserialize_with = "color_deserialize")]
pub res_border: Color,
#[serde(deserialize_with = "color_deserialize")]
pub res_text: Color,
#[serde(deserialize_with = "color_deserialize")]
pub res_selection: Color,
#[serde(deserialize_with = "color_deserialize")]
pub cmd_text: Color,
#[serde(deserialize_with = "color_deserialize")]
pub status_loading: Color,
#[serde(deserialize_with = "color_deserialize")]
pub cmd_keys: Color,
#[serde(deserialize_with = "color_deserialize")]
pub list_border: Color,
}
+72
View File
@@ -0,0 +1,72 @@
use std::fs;
use std::result::Result::Ok;
use serde::Deserialize;
use crate::config::getConfig;
use crate::source::mangago::Mangago;
use crate::source::{AnimeSource, MangaSource, SourceType};
use crate::source::animeworld::Animeworld;
use crate::source::mangaworld::Mangaworld;
#[derive(Deserialize, Clone)]
pub struct Extension {
Type: String,
Info: ExtensionInfo,
Sitemanager: ExtensionSitemanager
}
#[derive(Deserialize, Clone)]
struct ExtensionInfo {
Name: String,
Description: String,
Language: String
}
#[derive(Deserialize, Clone)]
struct ExtensionSitemanager {
Hostname: String,
Url: String,
}
impl Extension {
pub fn buildfromName(kind: SourceType,name: &str) -> Result<Extension, &str> {
let f = format!("extensions/{}/{}.toml",kind.to_string(), name.to_lowercase());
if let Ok(exists)= fs::exists(&f) {
if exists {
if let Ok(content)= fs::read_to_string(&f) {
let parsed:Extension = toml::from_str(&content).unwrap();
return Ok(parsed);
}
}
}
return Err(name);
}
pub fn toAnimeSource(self) -> Box<dyn AnimeSource+ Send + Sync> {
match self.Info.Name.trim().to_lowercase().as_str() {
// "animeworld" => Animeworld::new(self.Sitemanager.Hostname, self.Sitemanager.Url),
_ => Box::new(Animeworld::new(self.Sitemanager.Hostname, self.Sitemanager.Url))
}
}
pub fn toMangaSource(self) -> Box<dyn MangaSource+ Send + Sync> {
match self.Info.Name.trim().to_lowercase().as_str() {
// "animeworld" => Animeworld::new(self.Sitemanager.Hostname, self.Sitemanager.Url),
"mangago" => Box::new(Mangago::new(self.Sitemanager.Hostname, self.Sitemanager.Url)),
_ => Box::new(Mangaworld::new(self.Sitemanager.Hostname, self.Sitemanager.Url)),
}
}
pub fn defaultAnimeSource() -> Result<Box<dyn AnimeSource>, String> {
let def = &getConfig().Anime.source;
if let Ok(ext) = Extension::buildfromName(SourceType::Anime, def) {
return Ok(ext.toAnimeSource());
}
return Err(format!("No default file for {}",def));
}
pub fn defaultMangaeSource() -> Result<Box<dyn MangaSource>, String> {
let def = &getConfig().Manga.source;
if let Ok(ext) = Extension::buildfromName(SourceType::Manga, def) {
return Ok(ext.toMangaSource());
}
return Err(format!("No default file for {}",def));
}
}
+67
View File
@@ -0,0 +1,67 @@
use std::{ffi::OsString, fs::{create_dir_all, read_dir}};
use serde::Deserialize;
// const DOWNLOADS: &str = "/downloads";
const ANIME_FOLDER: &str = "./downloads/anime";
const MANGA_FOLDER: &str = "./downloads/manga";
pub fn init_folders() {
let _ = create_dir_all(ANIME_FOLDER);
let _ = create_dir_all(MANGA_FOLDER);
}
pub fn list_folder(folder: &str) -> Result<Vec<OsString>, Box<dyn std::error::Error>> {
let r = read_dir(folder)?
.filter_map(|entry| entry.ok())
.filter(|entry| entry.path().is_dir())
.map(|entry| entry.file_name())
.collect::<Vec<OsString>>();
Ok(r)
}
pub fn read_anime_folder() -> Result<Vec<OsString>, Box<dyn std::error::Error>> {
list_folder(ANIME_FOLDER)
}
pub fn read_manga_folder() -> Result<Vec<OsString>, Box<dyn std::error::Error>> {
list_folder(MANGA_FOLDER)
}
#[derive(Deserialize)]
pub struct Config {
pub Settings: Settings,
pub Extension: Extensions,
}
#[derive(Deserialize)]
pub struct Settings {
pub Folders: Folders
}
#[derive(Deserialize)]
pub struct Extensions {
pub defaultanime: String,
pub defaultmanga: String
}
#[derive(Deserialize)]
pub struct Folders {
pub basedir: String,
pub extensions: String,
pub anime: String,
pub manga: String
}
#[derive(Deserialize)]
pub struct ColorPalette {
pub anime: String,
pub manga: String,
pub source: String,
pub settings: String
}
// impl Config {
// pub fn defaultAnimeSource() -> Source {
// Animeworld::new(hostname, url, sourcetype)
// }
// }
+116
View File
@@ -0,0 +1,116 @@
use std::{fs, u8};
use ratatui::crossterm::event::KeyCode;
use serde::Deserialize;
#[derive(Deserialize)]
pub struct KeybindsString {
pub GoBack:String,
pub Open:String,
pub MenuUp:String,
pub MenuDown:String,
pub MenuLeft:String,
pub MenuRight:String,
pub AddFavourite:String,
}
pub struct Keybinds {
pub GoBack:KeyCode,
pub Open:KeyCode,
pub MenuUp:KeyCode,
pub MenuDown:KeyCode,
pub MenuLeft:KeyCode,
pub MenuRight:KeyCode,
pub AddFavourite:KeyCode,
}
impl KeybindsString {
pub fn build() -> Result<Self, String> {
let f = "keybinds.toml";
if let Ok(exists)= fs::exists(f) {
if exists {
if let Ok(content)= fs::read_to_string(f) {
let parsed:KeybindsString = toml::from_str(&content).unwrap();
return Ok(parsed);
}
}
}
return Err(f.to_owned());
}
}
impl Keybinds {
pub fn build() -> Result<Keybinds, String> {
let x: Result<Keybinds, String> = match KeybindsString::build() {
Ok(ks) => Ok(
Self {
GoBack: ks.GoBack.to_key_code()?,
Open: ks.Open.to_key_code()?,
MenuUp: ks.MenuUp.to_key_code()?,
MenuDown: ks.MenuDown.to_key_code()?,
MenuLeft: ks.MenuLeft.to_key_code()?,
MenuRight: ks.MenuRight.to_key_code()?,
AddFavourite: ks.AddFavourite.to_key_code()?,
}
),
Err(f) => Err(f),
};
x
}
pub fn buildOrDefault() -> Result<Keybinds, String> {
let x: Result<Keybinds, String> = match KeybindsString::build() {
Ok(ks) => Ok(
Self {
GoBack: ks.GoBack.to_key_code().unwrap_or(KeyCode::Esc),
Open: ks.Open.to_key_code().unwrap_or(KeyCode::Enter),
MenuUp: ks.MenuUp.to_key_code().unwrap_or(KeyCode::Up),
MenuDown: ks.MenuDown.to_key_code().unwrap_or(KeyCode::Down),
MenuLeft: ks.MenuLeft.to_key_code().unwrap_or(KeyCode::Left),
MenuRight: ks.MenuRight.to_key_code().unwrap_or(KeyCode::Right),
AddFavourite: ks.AddFavourite.to_key_code().unwrap_or(KeyCode::Char('p')),
}
),
Err(f) => Err(f),
};
x
}
}
trait StringToKeyCode {
fn to_key_code(self) -> Result<KeyCode,String>;
}
impl StringToKeyCode for String {
fn to_key_code(self) -> Result<KeyCode, String> {
match self.to_uppercase().as_ref() {
"BACKSPACE"=>Ok(KeyCode::Backspace),
"ENTER"=>Ok(KeyCode::Enter),
"LEFT"=>Ok(KeyCode::Left),
"RIGHT"=>Ok(KeyCode::Right),
"UP"=>Ok(KeyCode::Up),
"DOWN"=>Ok(KeyCode::Down),
"HOME"=>Ok(KeyCode::Home),
"END"=>Ok(KeyCode::End),
"PAGEUP"=>Ok(KeyCode::PageUp),
"PAGEDOWN"=>Ok(KeyCode::PageDown),
"TAB"=>Ok(KeyCode::Tab),
"BACKTAB"=>Ok(KeyCode::BackTab),
"DELETE"=>Ok(KeyCode::Delete),
"INSERT"=>Ok(KeyCode::Insert),
"ESC"=>Ok(KeyCode::Esc),
"CAPSLOCK"=>Ok(KeyCode::CapsLock),
"SCROLLLOCK"=>Ok(KeyCode::ScrollLock),
"NUMLOCK"=>Ok(KeyCode::NumLock),
"PRINTSCREEN"=>Ok(KeyCode::PrintScreen),
"PAUSE"=>Ok(KeyCode::Pause),
"MENU"=>Ok(KeyCode::Menu),
"KEYPADBEGIN"=>Ok(KeyCode::KeypadBegin),
a => {
let chars: Vec<char> = a.chars().collect();
return match chars.as_slice() {
[c] => Ok(KeyCode::Char(*c)),
['F', d] if d.is_ascii_digit() =>{
let dg = d.to_digit(10).unwrap() as u8;
Ok(KeyCode::F(dg))
},
_ => Err(format!("{} is invalid", self))
};
}
}
}
}
+38
View File
@@ -0,0 +1,38 @@
use std::collections::{HashMap, HashSet};
use std::fs;
use std::result::Result::Ok;
use serde::Deserialize;
#[derive(Deserialize,Clone)]
pub struct LibraryItem {
pub name: String,
pub episodes: String
}
impl LibraryItem {
pub fn buildfromName(name: &str) -> Result<HashMap<String, Vec<LibraryItem>>, &str> {
let f = format!("library/{}", name);
if let Ok(exists)= fs::exists(&f) {
if exists {
if let Ok(content)= fs::read_to_string(&f) {
let mut data: HashMap<String, Vec<LibraryItem>> = toml::from_str(&content).unwrap();
for items in data.values_mut() {
let mut hs = HashSet::new();
items.retain(|item| {
hs.insert(item.name.clone())
});
items.sort_by(|a,b| {
a.name.cmp(&b.name)
});
}
return Ok(data);
}
}
}
return Err(name);
}
}
Executable
+233
View File
@@ -0,0 +1,233 @@
mod config;
mod mangatab;
mod extensions;
use color_eyre::{Result};
use ratatui::{
DefaultTerminal, buffer::Buffer, crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, layout::{Constraint, Layout, Rect}, macros::horizontal, style::{Color, Stylize, palette::tailwind}, text::{Line, Text}, widgets::{Block, List, ListDirection, ListItem, Padding, Paragraph, Tabs, Widget}
};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
use crate::{mangatab::MangaTab, config::{getConfig, load_config, tailwindPaletteFromString}};
#[tokio::main]
async fn main() -> Result<()> {
load_config();
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
}
#[derive(Default)]
struct App {
state: AppState,
selected_tab: SelectedTab
}
#[derive(Default, Clone, Copy, PartialEq, Eq)]
enum AppState {
#[default]
Running,
Quitting,
}
#[derive(Default, Clone, Copy, Display, FromRepr, EnumIter)]
enum SelectedTab {
#[default]
#[strum(to_string = "Anime")]
Tab1,
#[strum(to_string = "Manga")]
Tab2,
#[strum(to_string = "Source")]
Tab3,
#[strum(to_string = "Settings")]
Tab4,
}
impl App {
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
while self.state == AppState::Running {
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
self.handle_events()?;
}
Ok(())
}
fn handle_events(&mut self) -> std::io::Result<()> {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Esc => self.quit(),
_ => {}
}
if key.modifiers.contains(KeyModifiers::CONTROL) {
match key.code {
KeyCode::Char('c') => self.quit(),
_=>{}
}
}
if key.modifiers.contains(KeyModifiers::SHIFT) {
match key.code {
KeyCode::Right => self.next_tab(),
KeyCode::Left => self.previous_tab(),
_ => {}
}
} else {
}
}
}
Ok(())
}
pub fn next_tab(&mut self) {
self.selected_tab = self.selected_tab.next();
}
pub fn previous_tab(&mut self) {
self.selected_tab = self.selected_tab.previous();
}
pub fn subnext_tab(&mut self) {
self.selected_tab = self.selected_tab.subnext();
}
pub fn subprevious_tab(&mut self) {
self.selected_tab = self.selected_tab.subprevious();
}
pub fn quit(&mut self) {
self.state = AppState::Quitting;
}
}
impl SelectedTab {
/// Get the previous tab, if there is no previous tab return the current tab.
fn previous(self) -> Self {
let current_index: usize = self as usize;
let previous_index = current_index.saturating_sub(1);
Self::from_repr(previous_index).unwrap_or(self)
}
/// Get the next tab, if there is no next tab return the current tab.
fn next(self) -> Self {
let current_index = self as usize;
let next_index = current_index.saturating_add(1);
Self::from_repr(next_index).unwrap_or(self)
}
}
impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
use Constraint::{Length, Min};
let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
let [header_area, inner_area, footer_area] = vertical.areas(area);
let horizontal = Layout::horizontal([Min(0), Length(20)]);
let [tabs_area, title_area] = horizontal.areas(header_area);
render_title(title_area, buf);
self.render_tabs(tabs_area, buf);
self.selected_tab.render(inner_area, buf);
render_footer(footer_area, buf);
}
}
impl App {
fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
let titles = SelectedTab::iter().map(SelectedTab::title);
let highlight_style = (Color::default(), self.selected_tab.palette().c700);
let selected_tab_index = self.selected_tab as usize;
Tabs::new(titles)
.highlight_style(highlight_style)
.select(selected_tab_index)
.padding("", "")
.divider(" ")
.render(area, buf);
}
}
fn render_title(area: Rect, buf: &mut Buffer) {
"Anime&Manga Scraper CLI".bold().render(area, buf);
}
fn render_footer(area: Rect, buf: &mut Buffer) {
Line::raw("Shift + ArrowKeys to choose the Tab | Press ESC to quit")
.centered()
.render(area, buf);
}
impl Widget for SelectedTab {
fn render(self, area: Rect, buf: &mut Buffer) {
// in a real app these might be separate widgets
match self {
Self::Tab1 => self.render_Anime(area, buf),
Self::Tab2 => self.render_Manga(area, buf),
Self::Tab3 => self.render_Sources(area, buf),
Self::Tab4 => self.render_Settings(area, buf),
}
}
}
impl SelectedTab {
/// Return tab's name as a styled `Line`
fn title(self) -> Line<'static> {
format!(" {self} ")
.fg(tailwind::SLATE.c200)
// .bg(self.palette().c900)
.into()
}
fn render_Anime(self, area: Rect, buf: &mut Buffer) {
Paragraph::new("Anime Home Library: View saved and search for new one in all sources")
.block(self.block())
.render(area, buf);
}
fn render_Manga(self, area: Rect, buf: &mut Buffer) {
// Paragraph::new("Manga Home Library: View saved and search for new one in all sources")
// .block(self.block())
// .render(area, buf);
// INNER TAB FOR OPTIONS
let titles = MangaTab::iter().map(MangaTab::title);
let highlight_style = (Color::default(), self.selected_tab.palette().c700);
let selected_tab_index = self.selected_tab as usize;
Tabs::new(titles)
.highlight_style(highlight_style)
.select(selected_tab_index)
.padding("", "")
.divider(" ")
.render(area, buf);
}
fn render_Sources(self, area: Rect, buf: &mut Buffer) {
Paragraph::new("Sources to source the web")
.block(self.block())
.render(area, buf);
}
fn render_Settings(self, area: Rect, buf: &mut Buffer) {
Paragraph::new("For now just credits")
.block(self.block())
.render(area, buf);
}
/// A block surrounding the tab's content
fn block(self) -> Block<'static> {
Block::bordered()
.padding(Padding::horizontal(1))
.border_style(self.palette().c500)
}
fn palette(self) -> tailwind::Palette {
match self {
Self::Tab1 => tailwindPaletteFromString(&getConfig().Customization.ColorPalette.anime,tailwind::PURPLE),
Self::Tab2 => tailwindPaletteFromString(&getConfig().Customization.ColorPalette.manga,tailwind::AMBER),
Self::Tab3 => tailwindPaletteFromString(&getConfig().Customization.ColorPalette.source,tailwind::EMERALD),
Self::Tab4 => tailwindPaletteFromString(&getConfig().Customization.ColorPalette.settings,tailwind::RED),
}
}
}
+1268
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
use ratatui::{
DefaultTerminal, buffer::Buffer, crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, layout::{Constraint, Layout, Rect}, macros::horizontal, style::{Color, Stylize, palette::tailwind}, text::{Line, Text}, widgets::{Block, List, ListDirection, ListItem, Padding, Paragraph, Tabs, Widget}
};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
#[derive(Default)]
pub struct Manga {
selected_tab: MangaTab
}
#[derive(Default, Clone, Copy, Display, FromRepr, EnumIter)]
pub enum MangaTab {
#[default]
#[strum(to_string = "Latest")]
Tab1,
#[strum(to_string = "Top")]
Tab2,
#[strum(to_string = "Search")]
Tab3
}
impl MangaTab {
pub fn title(self) -> Line<'static> {
format!(" {self} ")
.fg(tailwind::SLATE.c200)
// .bg(self.palette().c900)
.into()
}
}
+73
View File
@@ -0,0 +1,73 @@
use color_eyre::Result;
use ratatui::{
DefaultTerminal, buffer::Buffer, crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}, layout::{Constraint, Layout, Rect}, style::{Color, Stylize, palette::tailwind}, symbols, text::Line, widgets::{Block, List, Padding, Paragraph, Tabs, Widget}
};
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let app_result = App::default().run(terminal);
ratatui::restore();
app_result
}
#[derive(Default)]
struct App {
state: AppState,
}
#[derive(Default, Clone, Copy, PartialEq, Eq)]
enum AppState {
#[default]
Running,
Quitting,
}
impl App {
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
while self.state == AppState::Running {
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
self.handle_events()?;
}
Ok(())
}
fn handle_events(&mut self) -> std::io::Result<()> {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
if key.modifiers.contains(KeyModifiers::CONTROL) {
match key.code {
KeyCode::Char('c') => self.quit(),
_ => {}
}
} else {
match key.code {
KeyCode::Esc => self.quit(),
// KeyCode::Up => self.up,
// KeyCode
_ => {}
}
}
}
}
Ok(())
}
pub fn quit(&mut self) {
self.state = AppState::Quitting;
}
}
impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
let li = List::new([
"1. Anime",
"2. Manga",
])
.highlight_symbol(">>")
.render(area, buf);
}
}
+123
View File
@@ -0,0 +1,123 @@
use ratatui::{crossterm::event::KeyEvent, style::Style, widgets::{List, StatefulWidget, Widget}};
use crate::{App, AppState, components::ListOption::{Item, ListOption, ListOptionState, Section}, config::{getConfig, load_config, setAnimeSource, setMangaSource}, source::getSources, utils::utils::ColorPickerOption};
// pub static testSection: crate::components::ListOption::Section = ;
#[derive(Clone)]
pub struct SettingsState {
pub listoption_sections: Vec<Section>,
listoptionstate: ListOptionState
// tx: tokio::sync::mpsc::UnboundedSender<ApplySettingsChange>
}
pub struct Settings {
}
impl Settings {
pub fn new() -> Settings {
Settings {
}
}
}
impl SettingsState {
pub fn new(tx: tokio::sync::mpsc::UnboundedSender<ApplySettingsChange>) -> SettingsState {
let mut v: Vec<Section> = vec![];
v.push(Self::buildSettings(tx.clone()));
v.push(Self::buildColorCustomization(tx.clone()));
SettingsState {
listoptionstate: ListOptionState::default(),
listoption_sections: v
}
}
pub fn buildSettings(tx: tokio::sync::mpsc::UnboundedSender<ApplySettingsChange>) -> Section {
let tx_a = tx.clone();
let tx_m = tx.clone();
let s = Section::new("Generali".to_owned(), Style::default())
.add_item(Item {
label: "Sorgente Anime predefinita".to_owned(),
option: crate::components::ListOption::ItemOption::new_select_option(Box::new(|| {
getConfig().Anime.source
}),Box::new( || {
getSources("anime")
}),Box::new(move |v| {
setAnimeSource(v.clone());
tx_a.send(ApplySettingsChange::setAnime(v));
true
})),
})
.add_item(Item {
label: "Sorgente Manga predefinita".to_owned(),
option: crate::components::ListOption::ItemOption::new_select_option(Box::new(|| {
getConfig().Manga.source
}),Box::new( || {
getSources("manga")
}),Box::new(move |v| {
setMangaSource(v.clone());
tx_m.send(ApplySettingsChange::setManga(v));
true
})),
});
s
}
pub fn buildColorCustomization(tx: tokio::sync::mpsc::UnboundedSender<ApplySettingsChange>) -> Section {
let tx_a = tx.clone();
let tx_m = tx.clone();
let s = Section::new("Theme".to_owned(), Style::default())
.add_item(Item {
label: "".to_owned(),
option: crate::components::ListOption::ItemOption::new_color_picker(Box::new(|| {
return ColorPickerOption::default(); // TODO
}), Box::new(move |c| {
tx_m.send(ApplySettingsChange::setColorTheme("example".to_owned(), c));
true
})),
});
s
}
pub fn selectnext(&mut self) {
self.listoptionstate.selectnext();
}
pub fn selectprev(&mut self) {
self.listoptionstate.selectprev();
}
pub fn select_sec_next(&mut self) {
self.listoptionstate.select_sec_next();
}
pub fn select_sec_prev(&mut self) {
self.listoptionstate.select_sec_prev();
}
pub fn is_editing(&self) -> bool {
self.listoptionstate.edit_option
}
pub fn handle_inputs(&mut self, k: ratatui::crossterm::event::KeyCode) -> bool {
self.listoptionstate.handle_input(k)
}
pub fn turn_editing_mode(&mut self, b:bool) {
self.listoptionstate.edit_option = b;
}
pub fn handle_control_inputs(&mut self, k:ratatui::crossterm::event::KeyCode) {
self.listoptionstate.handle_control_inputs(k);
}
}
impl StatefulWidget for Settings {
type State = SettingsState;
fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer, state: &mut Self::State) {
// Mutable but one creation only,
ListOption::new().add_sections(state.listoption_sections.clone())
.render(area, buf, &mut state.listoptionstate);
}
}
pub enum ApplySettingsChange {
setManga(String),
setAnime(String),
setAppStatus(AppState),
setColorTheme(String, crate::utils::utils::ColorPickerOption)
}
+237
View File
@@ -0,0 +1,237 @@
use std::{collections::HashMap, fs, path::Path, process::{Command, Stdio}, sync::Arc};
use async_trait::async_trait;
use crate::{config::getConfig, log, utils::utils::AsyncError};
pub mod animeworld;
pub mod mangaworld;
pub mod mangago;
#[derive(Clone, Copy)]
pub enum SourceType {
Anime, Manga
}
impl SourceType {
pub fn to_string(self) -> String {
match self {
SourceType::Anime => "anime".to_owned(),
SourceType::Manga => "manga".to_owned()
}
}
}
pub enum SourceLanguages {
It, Jp, Eng
}
pub struct Series<S: AnimeSource> {
episodes: Vec<Episode>,
title: String,
lang: SourceLanguages,
url: String,
source: S
}
pub struct Episode {
pub title: String,
pub index: usize,
pub section: usize,
pub url: String
}
#[derive(Clone)]
pub struct EpisodeListItem {
pub title: String,
pub url: String
}
pub struct Comic<S: MangaSource> {
episodes: Vec<Chapter>,
title: String,
lang: SourceLanguages,
url: String,
source: S
}
pub struct Chapter {
pub title: String,
pub index: usize,
pub section: usize,
pub url: String
}
#[async_trait]
pub trait AnimeSource: Send + Sync {
fn has_update_page(&self) -> bool;
/*let mut items = hostname.split(".").collect::<Vec<&str>>();
if let Some(f) = items.first() {
if *f != "www" {
items.insert(0, "www");
}
}
Self {
url: items.join("."),
host: items[1..items.len() - 1].join("."),
hostname: "https://".to_owned()+&items[1..].join("."),
sourcetype
} */
fn new(hostname: String, url: String) -> Self where Self: Sized;
fn get_hostname(&self)->String;
fn get_url(&self)->String;
/** {
let p = std::cmp::max(1, page);
let u = format!("{}/updated?page={}",self.url,p);
let a = get(u).await?.error_for_status()?.text().await?;
return Html::parse_document(&a);
}*/
async fn fetch_updated_page(&self, page: usize)-> Result<String, AsyncError>;
fn extract_episode_from_updated(&self, document: String) -> Result<Vec<Episode>, AsyncError>;
async fn fetch_search_page(&self, query: String, page: usize) -> Result<String, AsyncError>;
fn extract_episode_from_search(&self, document: String) -> Result<Vec<Episode>, AsyncError>;
async fn listupdated(&self, page: usize) -> Result<Vec<Episode>, AsyncError> {
let body = self.fetch_updated_page(page).await?;
self.extract_episode_from_updated(body)
}
async fn search(&self, query: String, page: usize) -> Result<Vec<Episode>, AsyncError>{
let body = self.fetch_search_page(query, page).await?;
self.extract_episode_from_search(body)
}
async fn episode_openable_link(&self, card: Episode) -> Result<String, Box<dyn std::error::Error>>;
async fn episode_play_url(&self, url: &str) -> Result<String, Box<dyn std::error::Error>>;
async fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, AsyncError>;
async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), AsyncError>;
}
#[async_trait]
pub trait MangaSource: Send + Sync {
/*let mut items = hostname.split(".").collect::<Vec<&str>>();
if let Some(f) = items.first() {
if *f != "www" {
items.insert(0, "www");
}
}
Self {
url: items.join("."),
host: items[1..items.len() - 1].join("."),
hostname: "https://".to_owned()+&items[1..].join("."),
sourcetype
} */
fn new(hostname: String, url: String) -> Self where Self: Sized;
fn get_hostname(&self)->String;
fn get_url(&self)->String;
fn has_update_page(&self) -> bool;
/** {
let p = std::cmp::max(1, page);
let u = format!("{}/updated?page={}",self.url,p);
let a = get(u).await?.error_for_status()?.text().await?;
return Html::parse_document(&a);
}*/
async fn fetch_updated_page(&self, page: usize)-> Result<String, AsyncError>;
fn extract_episode_from_updated(&self, document: String) -> Result<Vec<Chapter>, AsyncError>;
async fn fetch_search_page(&self, query: String, page: usize) -> Result<String, AsyncError>;
fn extract_episode_from_search(&self, document: String) -> Result<Vec<Chapter>, AsyncError>;
async fn listupdated(&self, page: usize) -> Result<Vec<Chapter>, AsyncError> {
let body = self.fetch_updated_page(page).await?;
self.extract_episode_from_updated(body)
}
async fn search(&self, query: String, page: usize) -> Result<Vec<Chapter>, AsyncError>{
let body = self.fetch_search_page(query, page).await?;
self.extract_episode_from_search(body)
}
async fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, AsyncError>;
// async fn chapter(&self, card: Episode) -> Result<String, Box<dyn std::error::Error>>;
// async fn episode_play_url(&self, url: &str) -> Result<String, Box<dyn std::error::Error>>;
// async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), AsyncError>;
async fn extract_pages_url(&self, chapter_url: String) -> Result<Vec<String>, AsyncError>;
async fn to_pdf(&self, metadata: MangaMetadata, img:Vec<String>) -> Result<String, AsyncError>;
async fn to_cbz(&self, metadata: MangaMetadata, img:Vec<String>) -> Result<String, AsyncError>;
// async fn chapterMetadata(&self, url: String) -> MangaMetadata;
}
pub fn openManga(so: Arc<Box<dyn MangaSource + Send + Sync>>, metadata: MangaMetadata) {
let c = getConfig().Manga.reader;
// log!("1");
tokio::spawn(async move {
// log!("2");
if let Ok(imgs) = so.extract_pages_url(metadata.url.clone()).await {
let path = match c {
crate::config::MangaReader::CBZ | crate::config::MangaReader::Sumatra | crate::config::MangaReader::Okular => {
so.to_cbz(metadata, imgs).await
},
crate::config::MangaReader::PDF | crate::config::MangaReader::Calibre => {
so.to_pdf(metadata, imgs).await
},
};
match path {
Err(k) => {
log!("{}",k);
// ex.send(k); // TODO Send error string to a tx<string>
},
Ok(s) => {
let command = getConfig().Manga.programCmd.clone();
let pargs = getConfig().Manga.programArg;
let args = pargs.split(" ").filter(|x| !x.is_empty());
let cmd = match c {
crate::config::MangaReader::Sumatra => {
let mut a = Command::new(command);
a.args(args).arg(s.clone());
Some(a)
},
crate::config::MangaReader::Okular => {
let mut a = Command::new(command);
a.args(args).arg(s.clone());
Some(a)
},
crate::config::MangaReader::Calibre => {
let mut a = Command::new(command);
a.args(args).arg(s.clone());
Some(a)
}
crate::config::MangaReader::PDF => None,
crate::config::MangaReader::CBZ => None,
};
match cmd {
Some(mut c) => {
let _ = c.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
}
None => {},
};
}
};
}
});
}
pub struct MangaMetadata {
pub title: Option<String>,
pub url: String,
pub chapter: Option<String>,
pub volume: Option<String>
}
pub fn getSources(folder: &str) -> Vec<String> {
let mut p = Path::new("./extensions").join(folder);
let mut res = vec![];
for files in fs::read_dir(p) {
for rf in files {
if let Ok(f) = rf {
if let Some(name) = f.path().file_stem() {
if let Some(s) = name.to_str() {
res.push(s.to_owned());
}
}
}
}
}
res
}
+153
View File
@@ -0,0 +1,153 @@
use std::{collections::HashMap, process::{Command, Stdio}};
use reqwest::get;
use scraper::{Html, Selector};
use crate::{log, source::{AnimeSource, Episode, EpisodeListItem, SourceType}, utils::utils::AsyncError};
pub struct Animeworld {
url: String,
hostname:String,
sourcetype: SourceType
}
impl Animeworld {
fn id_extract( s:&str) -> Option<&str> {
s.split("/").last()
}
}
#[async_trait::async_trait]
impl AnimeSource for Animeworld {
fn has_update_page(&self) -> bool {true}
fn get_hostname(&self)->String {
self.hostname.clone()
}
fn get_url(&self) -> String {
self.url.clone()
}
fn new(hostname: String, url: String) -> Self {
Self {
url,
hostname,
sourcetype: crate::source::SourceType::Anime
}
}
async fn listupdated(&self, page: usize) -> Result<Vec<crate::source::Episode>, AsyncError> {
let body = self.fetch_updated_page(page).await?;
self.extract_episode_from_updated(body)
}
fn extract_episode_from_search(&self, body: String) -> Result<Vec<crate::source::Episode>, AsyncError> {
// let sel = Selector::parse("div.film-list").unwrap();
let document = Html::parse_document(&body);
let sel_inner = Selector::parse("div.inner").unwrap();
let sel_episode_number = Selector::parse("div.ep").unwrap();
let sel_anime_name = Selector::parse("a.name").unwrap();
let cards = document.select(&sel_inner)
.filter_map(|card| {
let carde = card.select(&sel_episode_number).next()?;
let index = carde.inner_html().trim().split(" ").last().unwrap().parse::<usize>().unwrap_or(0);
// log!("index: {}", index);
let carda = card.select(&sel_anime_name).next()?;
// log!("Card a found");
let url = carda.value().attr("href").unwrap_or("").trim().to_owned();
// log!("Card url: {}",url);
let title = carda.inner_html().trim().to_owned();
// log!("Card title: {}", title);
Some(Episode {
title,
index,
url,
section: 0
})
});
Ok(cards.collect::<Vec<Episode>>())
}
fn extract_episode_from_updated(&self, body: String) -> Result<Vec<crate::source::Episode>, AsyncError> {
self.extract_episode_from_search(body)
}
async fn episode_openable_link(&self, card: Episode) -> Result<String, Box<dyn std::error::Error>> {
let id= Self::id_extract(&card.url).unwrap();
let u = format!("{}/api/episode/serverPlayerAnimeWorld?id={}", self.url, id);
let r = get(u).await?.error_for_status()?.text().await?;
let b = Html::parse_document(&r);
let select_source = Selector::parse("source").unwrap();
Ok(b.select(&select_source).next().unwrap().value().attr("src").unwrap_or("").to_owned())
}
async fn episode_play_url(&self, url: &str) -> Result<String, Box<dyn std::error::Error>> {
if let Some(id) = Self::id_extract(url) {
let u = format!("{}/api/episode/serverPlayerAnimeWorld?id={}", self.url, id);
match Command::new("curl")
.arg("-L")
.arg(u).output() {
Ok(output) => {
// log!("{}");
if let Ok(html) = String::from_utf8(output.stdout) {
let b = Html::parse_document(&html);
let select_source = Selector::parse("source").unwrap();
if let Some(c) = b.select(&select_source).next() {
return Ok(c.attr("src").unwrap_or("").to_owned());
}
}
}
_ => {}
}
}
Ok(String::new())
}
async fn fetch_search_page(&self, query: String, page: usize) -> Result<String, AsyncError> {
// if page < 1 { page = 1};
let u = format!("{}?keyword={}&page={}",self.get_url(), query, page);
let r = reqwest::get(u).await?.error_for_status()?.text().await?;
return Ok(r);
}
async fn search(&self, query: String, page: usize) -> Result<Vec<crate::source::Episode>, AsyncError>{
let body = self.fetch_search_page(query, page).await?;
self.extract_episode_from_search(body)
}
async fn fetch_updated_page(&self, page: usize)-> Result<String, AsyncError> {
let p = std::cmp::max(1, page);
let u = format!("{}/updated?page={}",self.get_url(),p);
let a = get(u).await?.error_for_status()?.text().await?;
return Ok(a);
}
async fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, AsyncError>{
let mut hs = HashMap::<String, EpisodeListItem>::new();
if let Ok(a) = get(url).await?.error_for_status()?.text().await {
let b = Html::parse_document(&a);
let select_episodes = Selector::parse("li.episode a").unwrap();
for ep in b.select(&select_episodes) {
let alink = ep.value();
hs.insert(ep.inner_html(), EpisodeListItem {
title: format!("Ep. {:>4}",ep.inner_html()),
url: alink.attr("href").unwrap_or("").trim().to_owned()
});
}
}
return Ok(hs);
}
async fn lib_ep_select(&self, eli: EpisodeListItem) -> Result<(), AsyncError> {
if let Some(url) = Self::id_extract(&eli.url) {
if let Ok(vlc_url) =self.episode_play_url(url).await {
if !vlc_url.is_empty() {
let _ = Command::new("vlc").arg(vlc_url)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn();
}
}
}
Ok(())
}
// async fn fetch_html(url: String) -> Result<String, reqwest::Error> {
// get(url).await?.error_for_status()?.text().await
// }
}
File diff suppressed because it is too large Load Diff
+232
View File
@@ -0,0 +1,232 @@
use std::{collections::HashMap, f64::consts::E, fmt::format, io::Read, sync::Arc};
use crc32fast::Hasher;
use reqwest::get;
use scraper::{ElementRef, Html, Selector};
use zip::write::SimpleFileOptions;
use crate::{log, source::{Chapter, EpisodeListItem, MangaMetadata, MangaSource, SourceType, openManga}, utils::utils::{AsyncError, MangaCbz}};
pub struct Mangaworld {
url: String,
hostname:String,
sourcetype: SourceType
}
#[async_trait::async_trait]
impl MangaSource for Mangaworld {
fn get_hostname(&self)->String {
self.hostname.clone()
}
fn get_url(&self) -> String {
self.url.clone()
}
fn has_update_page(&self) -> bool {true}
fn new(hostname: String, url: String) -> Self {
Self {
url,
hostname,
sourcetype: crate::source::SourceType::Manga
}
}
async fn listupdated(&self, page: usize) -> Result<Vec<crate::source::Chapter>, AsyncError> {
let body = self.fetch_updated_page(page).await?;
self.extract_episode_from_updated(body)
}
//https://www.mangaworld.mx/archive?keyword=
fn extract_episode_from_search(&self, body: String) -> Result<Vec<crate::source::Chapter>, AsyncError> {
let document = Html::parse_document(&body);
let sel_inner = Selector::parse("div.entry").unwrap();
// let sel_episode_number = Selector::parse("a.xanh").unwrap();
let sel_anime_name = Selector::parse("a.manga-title").unwrap();
let cards = document.select(&sel_inner)
.filter_map(|card| {
// let carde = card.select(&sel_episode_number).next()?;
// let index = carde.inner_html().trim().split(" ").last().unwrap().parse::<usize>().unwrap_or(0);
// log!("index: {}", index);
let carda = card.select(&sel_anime_name).next()?;
// log!("Card a found");
let url = carda.value().attr("href").unwrap_or("").trim().to_owned();
// log!("Card url: {}",url);
let title = carda.inner_html().trim().to_owned();
// log!("Card title: {}", title);
Some(Chapter {
title,
index: 0,
url,
section: 0
})
});
Ok(cards.collect::<Vec<Chapter>>())
}
//https://www.mangaworld.ac/
fn extract_episode_from_updated(&self, body: String) -> Result<Vec<crate::source::Chapter>, AsyncError> {
let document = Html::parse_document(&body);
let sel_inner = Selector::parse("div.entry").unwrap();
let sel_episode_number = Selector::parse("a.xanh").unwrap();
let sel_anime_name = Selector::parse("a.manga-title").unwrap();
let cards = document.select(&sel_inner)
.filter_map(|card| {
let carde = card.select(&sel_episode_number).next()?;
let index = carde.inner_html().trim().split(" ").last().unwrap().parse::<usize>().unwrap_or(0);
let url = carde.value().attr("href").unwrap_or("").trim().to_owned();
// log!("index: {}", index);
let carda = card.select(&sel_anime_name).next()?;
// log!("Card a found");
// log!("{}\t{}\t{}\t{}");
// log!("Card url: {}",url);
let title = carda.inner_html().trim().to_owned();
// log!("Card title: {}", title);
Some(Chapter {
title,
index,
url,
section: 0
})
});
Ok(cards.collect::<Vec<Chapter>>())
}
async fn fetch_search_page(&self, query: String, page: usize) -> Result<String, AsyncError> {
let u = format!("{}?keyword={}&page={}",self.get_url(), query, page);
let r = reqwest::get(u).await?.error_for_status()?.text().await?;
return Ok(r);
}
async fn fetch_updated_page(&self, page: usize)-> Result<String, AsyncError> {
let p = std::cmp::max(1, page);
let u = format!("{}?page={}",self.get_url(), p);
let a = get(u).await?.error_for_status()?.text().await?;
return Ok(a);
}
async fn lib_url_parse(&self, url: String) -> Result<HashMap<String, EpisodeListItem>, AsyncError> {
let mut hs = HashMap::<String, EpisodeListItem>::new();
if let Ok(a) = get(url).await?.error_for_status()?.text().await {
let b = Html::parse_document(&a);
let select_volume_element = Selector::parse(".chapters-wrapper .volume-element").unwrap();
let select_volume_name = Selector::parse(".volume .volume-name").unwrap();
let select_episodes = Selector::parse("div.chapter a.chap").unwrap();
let select_span = Selector::parse("span").unwrap();
for vol in b.select(&select_volume_element) {
if let Some(volname) = vol.select(&select_volume_name).next() {
let vv:Vec<&str> = volname.text().collect();
let volume = vv.join(" ");
for ep in vol.select(&select_episodes) {
if let Some(span) = ep.select(&select_span).next() {
let alink = ep.value();
let chapter = span.inner_html();
let format_part = |input: &str, prefix: &str| -> String {
input.split_whitespace().nth(1)
.map(|num_str| {
match num_str.split_once('.') {
Some((a, b)) if !b.is_empty() => {
format!("{} {:>4}.{}", prefix, a, b)
},
Some((_, _)) | None => {
let m = num_str.trim_end_matches('.');
format!("{} {:>4}", prefix, m)
}
}
})
.unwrap_or_default()
};
let title_vol = format_part(&volume, "Volume");
let title_cha = format_part(&chapter, "Capitolo");
let title = vec![title_vol, title_cha]
.into_iter()
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ");
hs.insert(title.clone(), EpisodeListItem {
title,
url: alink.attr("href").unwrap_or("").trim().to_owned()
});
}
}
}
}
}
return Ok(hs);
}
async fn extract_pages_url(&self, chapter_url: String) -> Result<Vec<String>, AsyncError> {
let url = format!("{}/1?style=list", chapter_url);
let a = get(url).await?.error_for_status()?.text().await?;
let b = Html::parse_document(&a);
let sel = Selector::parse("#page img").unwrap();
let pages =b.select(&sel).filter_map(|img| img.value().attr("src")).map(|x| x.to_owned());
Ok(pages.collect())
}
async fn to_pdf(&self, metadata: MangaMetadata, images:Vec<String>) -> Result<String, AsyncError> {
Ok("".to_owned())
}
async fn to_cbz(&self, metadata: MangaMetadata, images:Vec<String>) -> Result<String, AsyncError> {
let bp = format!("./downloads/manga/{}/{}", metadata.title.unwrap_or("manga".to_owned()).replace(".", "_").replace("/", "_").to_ascii_lowercase(),metadata.chapter.unwrap_or("none".to_owned()));
let cbz = MangaCbz::new(bp.clone(), false)?;
let mut i:usize = 0;
std::fs::create_dir_all(&bp)?;
for img in images {
let mut h = Hasher::new();
let png = get(img).await?.bytes().await?;
h.update(&png); // TODO
let hs= h.finalize();
let b = cbz.has_page(i,hs);
log!("has page i={} b={}",i,b);
if !b {
let image_path = format!("{}/{}.png",bp,i);
std::fs::File::create(&image_path)?;
std::fs::write(&image_path, png)?;
cbz.add_page_path(image_path.clone())?;
std::fs::remove_file(image_path)?;
}
i=i+1;
}
std::fs::remove_dir_all(&bp)?;
Ok(cbz.path)
}
// async fn chapterMetadata(&self, url: String) -> MangaMetadata {
// //url = https://www.mangaworld.mx/manga/1637/chainsaw-man/read/5f9cc1d81db7241f07086b78
// if let Ok(res) = get(&url).await {
// if let Ok(body) = res.text().await {
// let html = Html::parse_document(&body);
// let reader_a = Selector::parse("#reader a").unwrap();
// let volopt = Selector::parse(".volume option").unwrap();
// let chaopt = Selector::parse(".chapter option").unwrap();
// let title = match html.select(&reader_a).next() {
// Some(t) => {
// let a:Vec<&str> = t.text().collect();
// let b = a.join(" ");
// let c = b.clone();
// let prefix = "Ritorna a";
// Some(b.clone().strip_prefix(prefix).map(|f| f.to_owned()).unwrap_or(c))
// },
// None => None,
// };
// let cv_num = |a: ElementRef| {
// let v:Vec<&str> = a.text().collect();
// let vs = v.join(" ");
// let last = vs.split(" ").last();
// last.map(|n| n.trim_start_matches("0").to_owned())
// };
// let volume = html.select(&volopt).find(|e| e.attr("selected").is_some()).and_then(cv_num);
// let chapter = html.select(&chaopt).find(|e| e.attr("selected").is_some()).and_then(cv_num);
// return MangaMetadata {
// title,
// url: Some(url),
// chapter: chapter,
// volume: volume
// };
// }
// }
// MangaMetadata { title: None, url: Some(url), chapter: None, volume: None }
// }
}
+193
View File
@@ -0,0 +1,193 @@
pub mod utils {
use block_padding::ZeroPadding;
use ratatui::style::palette::tailwind;
use regex::Regex;
pub type AsyncError = Box<dyn std::error::Error + Send + Sync>;
use aes::{Aes128, cipher::BlockDecryptMut}; // Use Aes256 if your key is 32 bytes
use cbc::{Decryptor, cipher::KeyIvInit};
use std::collections::HashMap;
use std::panic;
use std::fs::OpenOptions;
use std::io::Write;
use std::backtrace::Backtrace;
use std::process::{Command, Stdio};
#[cfg(feature = "debug")]
#[macro_export]
macro_rules! log {
($($arg:tt)*) => {{
let file_result = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open("./debug.log");
if let Ok(mut file) = file_result {
use std::io::Write;
let _ = writeln!(file, $($arg)*);
}
}};
}
#[cfg(not(feature = "debug"))]
#[macro_export]
macro_rules! log {
($($arg:tt)*) => {{}};
}
pub static JS_FILTERS: &[&str] = &[
"jQuery",
"document",
"getContext",
"toDataURL",
"getImageData",
"width",
"height",
];
pub fn first_capital(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
}
pub fn hash_sort(res: &HashMap<String, String>) -> Vec<&String> {
let mut items = res.keys().collect::<Vec<&String>>();
items.sort_by(|a,b|{
let aname = a.splitn(2, "di ").nth(1).unwrap_or("");
let bname = b.splitn(2, "di ").nth(1).unwrap_or("");
match aname.cmp(bname) {
std::cmp::Ordering::Equal => a.cmp(b),
o => o
}
});
items
}
// pub fn image_regex() -> Regex {
// return Regex::new(r#"var imgsrcs\s*=\s*['"]([a-zA-Z0-9+=/]+)['"]"#).unwrap();
// }
pub struct MangaCbz {
pub path: String,
pub pages: HashMap<usize, u32>
}
impl MangaCbz {
pub fn new(mut path: String, truncate: bool) -> Result<Self, AsyncError>{
if !path.ends_with(".cbz") {
path = path+".cbz";
}
let mut pages = HashMap::new();
let ex = std::fs::exists(&path).unwrap_or(true);
if truncate || !ex {
let p = std::path::Path::new(&path).parent().unwrap();
std::fs::create_dir_all(&p)?;
let file = std::fs::File::create(&path)?;
let zip = zip::ZipWriter::new(file);
zip.finish()?;
} else {
let file = std::fs::File::open(&path)?;
let mut zip = zip::ZipArchive::new(file)?;
for i in 0..zip.len() {
if let Ok(img) = zip.by_index(i) {
if let Some(s) = img.name().rsplitn(2,".").nth(0) {
if let Ok(u) = s.parse::<usize>() {
pages.insert(u, img.crc32());
}
}
}
};
}
return Ok(MangaCbz {
path,
pages
});
}
pub fn add_page_path(&self, image_path: String) -> Result<(),AsyncError>{
let sta = Command::new("7z")
.arg("a")
.arg(&self.path)
.arg(image_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()?;
log!("{}",sta);
Ok(())
}
pub fn has_page(&self, i: usize, hs: u32) -> bool {
match self.pages.get(&i) {
Some(u) => u == &hs,
_ => false
}
}
}
// Found on stackoverflow
#[cfg(feature = "debug")]
pub fn setup_panic_hook() {
panic::set_hook(Box::new(|info| {
let backtrace = Backtrace::capture();
let msg = format!(
"=== PANIC ===\nMessage: {}\nLocation: {}\n\nBacktrace:\n{}\n",
info.payload()
.downcast_ref::<&str>()
.copied()
.or_else(|| info.payload().downcast_ref::<String>().map(|s| s.as_str()))
.unwrap_or("<no message>"),
info.location()
.map(|l| format!("{}:{}:{}", l.file(), l.line(), l.column()))
.unwrap_or_else(|| "<unknown location>".to_owned()),
backtrace,
);
// also print to stderr so you see it in the terminal
eprintln!("{msg}");
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open("./panic.log")
{
let _ = writeln!(file, "{msg}");
}
}));
}
#[cfg(not(feature = "debug"))]
pub fn setup_panic_hook() {}
pub struct ColorPickerOption {
pub _type:ColorPickerOptionType,
red: u8,
green: u8,
blue: u8
}
pub enum ColorPickerOptionType {
RGB, HEX
}
impl ColorPickerOption {
pub fn default() -> ColorPickerOption {
ColorPickerOption { _type: ColorPickerOptionType::HEX, red: 0, green: 0, blue: 0 }
}
pub fn redString(&self) -> String {
match self._type {
ColorPickerOptionType::RGB => format!("{:0<3}", self.red.to_string()),
ColorPickerOptionType::HEX => format!("{:0x?}", self.red).get(2..).unwrap_or("00").to_owned(),
}
}
pub fn greenString(&self) -> String {
match self._type {
ColorPickerOptionType::RGB => format!("{:0<3}", self.green.to_string()),
ColorPickerOptionType::HEX => format!("{:0x?}", self.green).get(2..).unwrap_or("00").to_owned(),
}
}
pub fn blueString(&self) -> String {
match self._type {
ColorPickerOptionType::RGB => format!("{:0<3}", self.blue.to_string()),
ColorPickerOptionType::HEX => format!("{:0x?}", self.blue).get(2..).unwrap_or("00").to_owned(),
}
}
}
}