use std::{collections::HashMap, error::Error, sync::Arc, time::Duration};
use nyaa_html::NyaaTheme;
use reqwest::{cookie::Jar, Proxy};
use serde::{Deserialize, Serialize};
use strum::{Display, VariantArray};
use sukebei_nyaa::SukebeiTheme;
use torrent_galaxy::TgxTheme;
use crate::{
app::{Context, LoadType, Widgets},
results::{ResultResponse, ResultTable, Results},
sync::SearchQuery,
theme::Theme,
util::conv::add_protocol,
widget::{
category::{CatEntry, CatIcon, CatStruct},
sort::SelectedSort,
},
};
use self::{
nyaa_html::{NyaaConfig, NyaaHtmlSource},
sukebei_nyaa::{SukebeiHtmlSource, SukebeiNyaaConfig},
torrent_galaxy::{TgxConfig, TorrentGalaxyHtmlSource},
};
#[cfg(feature = "captcha")]
use ratatui_image::protocol::StatefulProtocol;
pub mod nyaa_html;
pub mod nyaa_rss;
pub mod sukebei_nyaa;
pub mod torrent_galaxy;
#[derive(Clone)]
pub enum SourceResults {
Results(Results),
#[cfg(feature = "captcha")]
Captcha(Box<dyn StatefulProtocol>),
}
#[derive(Clone)]
pub enum SourceResponse {
Results(ResultResponse),
#[cfg(feature = "captcha")]
Captcha(Box<dyn StatefulProtocol>),
}
#[derive(Serialize, Deserialize, Clone, Copy, Default)]
pub struct SourceTheme {
#[serde(default)]
pub nyaa: NyaaTheme,
#[serde(default)]
pub sukebei: SukebeiTheme,
#[serde(default, rename = "torrentgalaxy")]
pub tgx: TgxTheme,
}
#[derive(Serialize, Deserialize, Clone, Default)]
#[serde(default)]
pub struct SourceConfig {
pub nyaa: Option<NyaaConfig>,
#[serde(rename = "sukebei")]
pub sukebei: Option<SukebeiNyaaConfig>,
#[serde(rename = "torrentgalaxy")]
pub tgx: Option<TgxConfig>,
}
#[derive(Clone)]
pub struct SourceInfo {
pub cats: Vec<CatStruct>,
pub filters: Vec<String>,
pub sorts: Vec<String>,
}
impl SourceInfo {
pub fn get_major_minor(&self, id: usize) -> (usize, usize) {
for (major, cat) in self.cats.iter().enumerate() {
if let Some((minor, _)) = cat.entries.iter().enumerate().find(|(_, ent)| ent.id == id) {
return (major, minor);
}
}
(0, 0)
}
pub fn entry_from_cfg(&self, s: &str) -> CatEntry {
for cat in self.cats.iter() {
if let Some(ent) = cat.entries.iter().find(|ent| ent.cfg == s) {
return ent.clone();
}
}
self.cats[0].entries[0].clone()
}
pub fn entry_from_str(self, s: &str) -> CatEntry {
let split: Vec<&str> = s.split('_').collect();
let high = split.first().unwrap_or(&"1").parse().unwrap_or(1);
let low = split.last().unwrap_or(&"0").parse().unwrap_or(0);
let id = high * 10 + low;
self.entry_from_id(id)
}
pub fn entry_from_id(self, id: usize) -> CatEntry {
for cat in self.cats.iter() {
if let Some(ent) = cat.entries.iter().find(|ent| ent.id == id) {
return ent.clone();
}
}
self.cats[0].entries[0].clone()
}
}
pub fn request_client(
jar: &Arc<Jar>,
timeout: u64,
proxy_url: Option<String>,
) -> Result<reqwest::Client, Box<dyn Error>> {
let mut client = reqwest::Client::builder()
.gzip(true)
.cookie_provider(jar.clone())
.timeout(Duration::from_secs(timeout));
if let Some(proxy_url) = proxy_url {
client = client.proxy(Proxy::all(
add_protocol(proxy_url, false).map_err(|e| e.to_string())?,
)?);
}
Ok(client.build()?)
}
#[derive(Default, Clone, Copy)]
pub enum ItemType {
#[default]
None,
Trusted,
Remake,
}
#[derive(Clone, Default)]
pub struct Item {
pub id: String,
pub date: String,
pub seeders: u32,
pub leechers: u32,
pub downloads: u32,
pub size: String,
pub bytes: usize,
pub title: String,
pub torrent_link: String,
pub magnet_link: String,
pub post_link: String,
pub file_name: String,
pub category: usize,
pub icon: CatIcon,
pub item_type: ItemType,
pub extra: HashMap<String, String>,
}
#[derive(Serialize, Deserialize, Display, Clone, Copy, VariantArray, PartialEq, Eq)]
pub enum Sources {
#[strum(serialize = "Nyaa")]
Nyaa = 0,
#[strum(serialize = "Sukebei")]
SukebeiNyaa = 1,
#[strum(serialize = "TorrentGalaxy")]
TorrentGalaxy = 2,
}
pub trait Source {
fn search(
client: &reqwest::Client,
search: &SearchQuery,
config: &SourceConfig,
date_format: Option<String>,
) -> impl std::future::Future<Output = Result<SourceResponse, Box<dyn Error + Send + Sync>>> + Send;
fn sort(
client: &reqwest::Client,
search: &SearchQuery,
config: &SourceConfig,
date_format: Option<String>,
) -> impl std::future::Future<Output = Result<SourceResponse, Box<dyn Error + Send + Sync>>> + Send;
fn filter(
client: &reqwest::Client,
search: &SearchQuery,
config: &SourceConfig,
date_format: Option<String>,
) -> impl std::future::Future<Output = Result<SourceResponse, Box<dyn Error + Send + Sync>>> + Send;
fn categorize(
client: &reqwest::Client,
search: &SearchQuery,
config: &SourceConfig,
date_format: Option<String>,
) -> impl std::future::Future<Output = Result<SourceResponse, Box<dyn Error + Send + Sync>>> + Send;
fn solve(
solution: String,
client: &reqwest::Client,
search: &SearchQuery,
config: &SourceConfig,
date_format: Option<String>,
) -> impl std::future::Future<Output = Result<SourceResponse, Box<dyn Error + Send + Sync>>> + Send;
fn info() -> SourceInfo;
fn load_config(config: &mut SourceConfig);
fn default_category(config: &SourceConfig) -> usize;
fn default_sort(config: &SourceConfig) -> SelectedSort;
fn default_filter(config: &SourceConfig) -> usize;
fn default_search(config: &SourceConfig) -> String;
fn format_table(
items: &[Item],
sort: &SearchQuery,
config: &SourceConfig,
theme: &Theme,
) -> ResultTable;
}
impl Sources {
pub async fn load(
&self,
load_type: LoadType,
client: &reqwest::Client,
search: &SearchQuery,
config: &SourceConfig,
date_format: Option<String>,
) -> Result<SourceResponse, Box<dyn Error + Send + Sync>> {
match self {
Sources::Nyaa => match load_type {
LoadType::Searching | LoadType::Sourcing => {
NyaaHtmlSource::search(client, search, config, date_format).await
}
LoadType::Sorting => {
NyaaHtmlSource::sort(client, search, config, date_format).await
}
LoadType::Filtering => {
NyaaHtmlSource::filter(client, search, config, date_format).await
}
LoadType::Categorizing => {
NyaaHtmlSource::categorize(client, search, config, date_format).await
}
LoadType::SolvingCaptcha(solution) => {
NyaaHtmlSource::solve(solution, client, search, config, date_format).await
}
LoadType::Downloading | LoadType::Batching => unreachable!(),
},
Sources::SukebeiNyaa => match load_type {
LoadType::Searching | LoadType::Sourcing => {
SukebeiHtmlSource::search(client, search, config, date_format).await
}
LoadType::Sorting => {
SukebeiHtmlSource::sort(client, search, config, date_format).await
}
LoadType::Filtering => {
SukebeiHtmlSource::filter(client, search, config, date_format).await
}
LoadType::Categorizing => {
SukebeiHtmlSource::categorize(client, search, config, date_format).await
}
LoadType::SolvingCaptcha(solution) => {
SukebeiHtmlSource::solve(solution, client, search, config, date_format).await
}
LoadType::Downloading | LoadType::Batching => unreachable!(),
},
Sources::TorrentGalaxy => match load_type {
LoadType::Searching | LoadType::Sourcing => {
TorrentGalaxyHtmlSource::search(client, search, config, date_format).await
}
LoadType::Sorting => {
TorrentGalaxyHtmlSource::sort(client, search, config, date_format).await
}
LoadType::Filtering => {
TorrentGalaxyHtmlSource::filter(client, search, config, date_format).await
}
LoadType::Categorizing => {
TorrentGalaxyHtmlSource::categorize(client, search, config, date_format).await
}
LoadType::SolvingCaptcha(solution) => {
TorrentGalaxyHtmlSource::solve(solution, client, search, config, date_format)
.await
}
LoadType::Downloading | LoadType::Batching => unreachable!(),
},
}
}
pub fn apply(self, ctx: &mut Context, w: &mut Widgets) {
ctx.src_info = self.info();
w.category.selected = self.default_category(&ctx.config.sources);
let (major, minor) = ctx.src_info.get_major_minor(w.category.selected);
w.category.table.select(major + minor + 1);
w.category.major = major;
w.category.minor = minor;
w.sort.selected = self.default_sort(&ctx.config.sources);
w.sort.table.select(w.sort.selected.sort);
w.filter.selected = self.default_filter(&ctx.config.sources);
w.filter.table.select(w.filter.selected);
w.search.input.input = self.default_search(&ctx.config.sources);
w.search
.input
.set_cursor(w.search.input.input.chars().count());
ctx.page = 1;
}
pub fn info(self) -> SourceInfo {
match self {
Sources::Nyaa => NyaaHtmlSource::info(),
Sources::SukebeiNyaa => SukebeiHtmlSource::info(),
Sources::TorrentGalaxy => TorrentGalaxyHtmlSource::info(),
}
}
pub fn load_config(self, config: &mut SourceConfig) {
match self {
Sources::Nyaa => NyaaHtmlSource::load_config(config),
Sources::SukebeiNyaa => SukebeiHtmlSource::load_config(config),
Sources::TorrentGalaxy => TorrentGalaxyHtmlSource::load_config(config),
};
}
pub fn default_category(self, config: &SourceConfig) -> usize {
match self {
Sources::Nyaa => NyaaHtmlSource::default_category(config),
Sources::SukebeiNyaa => SukebeiHtmlSource::default_category(config),
Sources::TorrentGalaxy => TorrentGalaxyHtmlSource::default_category(config),
}
}
pub fn default_sort(self, config: &SourceConfig) -> SelectedSort {
match self {
Sources::Nyaa => NyaaHtmlSource::default_sort(config),
Sources::SukebeiNyaa => SukebeiHtmlSource::default_sort(config),
Sources::TorrentGalaxy => TorrentGalaxyHtmlSource::default_sort(config),
}
}
pub fn default_filter(self, config: &SourceConfig) -> usize {
match self {
Sources::Nyaa => NyaaHtmlSource::default_filter(config),
Sources::SukebeiNyaa => SukebeiHtmlSource::default_filter(config),
Sources::TorrentGalaxy => TorrentGalaxyHtmlSource::default_filter(config),
}
}
pub fn default_search(self, config: &SourceConfig) -> String {
match self {
Sources::Nyaa => NyaaHtmlSource::default_search(config),
Sources::SukebeiNyaa => SukebeiHtmlSource::default_search(config),
Sources::TorrentGalaxy => TorrentGalaxyHtmlSource::default_search(config),
}
}
pub fn format_table(
self,
items: &[Item],
search: &SearchQuery,
config: &SourceConfig,
theme: &Theme,
) -> ResultTable {
match self {
Sources::Nyaa => NyaaHtmlSource::format_table(items, search, config, theme),
Sources::SukebeiNyaa => SukebeiHtmlSource::format_table(items, search, config, theme),
Sources::TorrentGalaxy => {
TorrentGalaxyHtmlSource::format_table(items, search, config, theme)
}
}
}
}