download index
Signed-off-by: Felipe Contreras Salinas <felipe@bstr.cl>
This commit is contained in:
parent
ed05e287b7
commit
7bd717e4db
14 changed files with 301 additions and 338 deletions
52
Cargo.lock
generated
52
Cargo.lock
generated
|
|
@ -985,6 +985,15 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
|
|
@ -1056,8 +1065,11 @@ dependencies = [
|
|||
"fluent-templates",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
|
@ -1394,14 +1406,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.140"
|
||||
version = "1.0.147"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
checksum = "6af14725505314343e673e9ecb7cd7e8a36aa9791eb936235a3567cc31447ae4"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1653,11 +1666,22 @@ dependencies = [
|
|||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.14"
|
||||
version = "0.7.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034"
|
||||
checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
|
|
@ -1749,12 +1773,13 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-serde"
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
|
|
@ -1765,15 +1790,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sharded-slab",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-serde",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2322,3 +2346,9 @@ dependencies = [
|
|||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4af59da1029247450b54ba43e0b62c8e376582464bbe5504dd525fe521e7e8fd"
|
||||
|
|
|
|||
18
Cargo.toml
18
Cargo.toml
|
|
@ -10,7 +10,7 @@ clap = { version = "4.5.53", features = ["derive"] }
|
|||
directories = "6.0.0"
|
||||
fluent-templates = "0.13.2"
|
||||
reqwest = { version = "0.12.26", default-features = false, features = [
|
||||
"brotli",
|
||||
"brotli",
|
||||
"http2",
|
||||
"gzip",
|
||||
"json",
|
||||
|
|
@ -21,12 +21,20 @@ serde = { version = "1.0.228", default-features = false, features = [
|
|||
"derive",
|
||||
"std",
|
||||
] }
|
||||
serde_json = "1.0.147"
|
||||
strum = { version = "0.27.2", features = ["derive"] }
|
||||
tokio = { version = "1.48.0", default-features = false, features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tokio = { version = "1.48.0", default-features = false, features = [
|
||||
"fs",
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
] }
|
||||
tokio-util = { version = "0.7.17", default-features = false, features = ["io"] }
|
||||
tokio-stream = { version = "0.1.17", default-features = false }
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = { version = "0.3.22", default-features = false, features = [
|
||||
"fmt",
|
||||
"json",
|
||||
"ansi",
|
||||
"env-filter",
|
||||
"fmt",
|
||||
"tracing",
|
||||
"tracing-log",
|
||||
] }
|
||||
|
||||
|
|
|
|||
45
src/card.rs
45
src/card.rs
|
|
@ -1,45 +0,0 @@
|
|||
//! Card info
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CardInfo {
|
||||
pub slug: String,
|
||||
pub inner: InnerCardInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InnerCardInfo {
|
||||
pub name: String,
|
||||
pub kind: CardKind,
|
||||
pub card_type: CardType,
|
||||
pub acespec: bool,
|
||||
pub tagteam: bool,
|
||||
pub future: bool,
|
||||
pub ancient: bool,
|
||||
pub specific_info: SpecificInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CardKind {
|
||||
Pokemon,
|
||||
Trainer,
|
||||
Energy,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CardType {
|
||||
Basic,
|
||||
Stage1,
|
||||
Stage2,
|
||||
Item,
|
||||
Tool,
|
||||
Supporter,
|
||||
Stadium,
|
||||
Special,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SpecificInfo {
|
||||
PokemonInfo {},
|
||||
TrainerInfo { effect: Vec<String> },
|
||||
EnergyInfo {},
|
||||
}
|
||||
19
src/cli.rs
19
src/cli.rs
|
|
@ -1,15 +1,18 @@
|
|||
//! CLI parameters
|
||||
|
||||
use clap::Parser;
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Args {
|
||||
/// Edition code
|
||||
pub code: String,
|
||||
/// Card number within the edition
|
||||
pub number: u8,
|
||||
///Override the slug for the card
|
||||
#[arg(short, long)]
|
||||
pub slug: Option<String>,
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand, PartialEq)]
|
||||
pub enum Command {
|
||||
/// Downloads the card data
|
||||
DownloadData,
|
||||
/// Terminal User Interface
|
||||
Tui,
|
||||
}
|
||||
|
|
|
|||
4
src/constants.rs
Normal file
4
src/constants.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
//! Application wide constants.
|
||||
|
||||
pub const APP_NAME: &str = "ptcg-tools";
|
||||
pub const SNAKE_CASE_APP_NAME: &str = "ptcg_tools";
|
||||
47
src/directories.rs
Normal file
47
src/directories.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
//! User directories handling
|
||||
use anyhow::{Result, anyhow};
|
||||
use camino::Utf8PathBuf;
|
||||
use directories::ProjectDirs;
|
||||
|
||||
use crate::constants::APP_NAME;
|
||||
|
||||
/// Returns the path to the user data directory.
|
||||
///
|
||||
/// Post condition: this function ensures the directory is already created when returning.
|
||||
pub async fn data_directory() -> Result<Utf8PathBuf> {
|
||||
let user_directory = ProjectDirs::from("cl", "bstr", APP_NAME)
|
||||
.ok_or_else(|| anyhow!("failed to get ProjectDirs"))?
|
||||
.data_dir()
|
||||
.to_path_buf();
|
||||
let user_directory = Utf8PathBuf::try_from(user_directory)?;
|
||||
tokio::fs::create_dir_all(&user_directory).await?;
|
||||
Ok(user_directory)
|
||||
}
|
||||
|
||||
/// Returns the path to the user data cache directory.
|
||||
///
|
||||
/// Post condition: this function ensures the directory is already created when returning.
|
||||
pub async fn data_cache_directory() -> Result<Utf8PathBuf> {
|
||||
let user_directory = ProjectDirs::from("cl", "bstr", APP_NAME)
|
||||
.ok_or_else(|| anyhow!("failed to get ProjectDirs"))?
|
||||
.cache_dir()
|
||||
.to_path_buf();
|
||||
let user_directory = Utf8PathBuf::try_from(user_directory)?;
|
||||
let user_directory = user_directory.join("data");
|
||||
tokio::fs::create_dir_all(&user_directory).await?;
|
||||
Ok(user_directory)
|
||||
}
|
||||
|
||||
/// Returns the path to the user data cache directory.
|
||||
///
|
||||
/// Post condition: this function ensures the directory is already created when returning.
|
||||
pub async fn image_cache_directory() -> Result<Utf8PathBuf> {
|
||||
let user_directory = ProjectDirs::from("cl", "bstr", APP_NAME)
|
||||
.ok_or_else(|| anyhow!("failed to get ProjectDirs"))?
|
||||
.cache_dir()
|
||||
.to_path_buf();
|
||||
let user_directory = Utf8PathBuf::try_from(user_directory)?;
|
||||
let user_directory = user_directory.join("data");
|
||||
tokio::fs::create_dir_all(&user_directory).await?;
|
||||
Ok(user_directory)
|
||||
}
|
||||
|
|
@ -1,231 +0,0 @@
|
|||
//! Download card information.
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use reqwest::Client;
|
||||
use scraper::{Html, Selector};
|
||||
|
||||
use crate::card::{CardInfo, CardKind, CardType, InnerCardInfo, SpecificInfo};
|
||||
use crate::editions::EditionCode;
|
||||
use crate::lang::Language;
|
||||
|
||||
pub async fn download_card_info(
|
||||
client: Client,
|
||||
lang: Language,
|
||||
code: EditionCode,
|
||||
number: u8,
|
||||
override_slug: Option<&str>,
|
||||
) -> Result<CardInfo> {
|
||||
let url = format!(
|
||||
"{}/{}/{number}/",
|
||||
base_url(lang),
|
||||
code.edition_num().to_lowercase()
|
||||
);
|
||||
let response = client.get(url).send().await?;
|
||||
response.error_for_status_ref()?;
|
||||
let (mut slug, inner) = parse_html(lang, code, response.text().await?)?;
|
||||
if let Some(override_slug) = override_slug {
|
||||
slug = override_slug.into()
|
||||
}
|
||||
Ok(CardInfo { slug, inner })
|
||||
}
|
||||
|
||||
fn base_url(lang: Language) -> &'static str {
|
||||
match lang {
|
||||
Language::Es => "https://www.pokemon.com/el/jcc-pokemon/cartas-pokemon/series",
|
||||
Language::En => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_html(lang: Language, code: EditionCode, html: String) -> Result<(String, InnerCardInfo)> {
|
||||
let html = Html::parse_document(&html);
|
||||
let card = html
|
||||
.select(&selector("div.full-card-information")?)
|
||||
.next()
|
||||
.ok_or(anyhow!("Couldn't find card info"))?;
|
||||
let name = card
|
||||
.select(&selector("h1")?)
|
||||
.next()
|
||||
.ok_or(anyhow!("Failed to get card name"))?
|
||||
.inner_html();
|
||||
let (kind, card_type) = parse_card_type(
|
||||
lang,
|
||||
card.select(&selector("div.card-type > h2")?)
|
||||
.next()
|
||||
.ok_or(anyhow!("Failed to get card type"))?
|
||||
.inner_html(),
|
||||
)?;
|
||||
let slug = match kind {
|
||||
CardKind::Pokemon => slugify_pokemon(lang, code, &name),
|
||||
CardKind::Trainer | CardKind::Energy => slugify_unique(lang, &name),
|
||||
};
|
||||
let specific_info = match kind {
|
||||
CardKind::Pokemon => SpecificInfo::PokemonInfo {},
|
||||
CardKind::Trainer => {
|
||||
let effect = card
|
||||
.select(&selector("div.ability > pre > p")?)
|
||||
.map(|e| e.inner_html())
|
||||
.collect();
|
||||
SpecificInfo::TrainerInfo { effect }
|
||||
}
|
||||
CardKind::Energy => SpecificInfo::EnergyInfo {},
|
||||
};
|
||||
|
||||
Ok((
|
||||
slug,
|
||||
InnerCardInfo {
|
||||
name,
|
||||
kind,
|
||||
card_type,
|
||||
acespec: false,
|
||||
tagteam: false,
|
||||
future: false,
|
||||
ancient: false,
|
||||
specific_info,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn selector(sel: &str) -> Result<Selector> {
|
||||
Selector::parse(sel).map_err(|_| anyhow!("failed to parse selector"))
|
||||
}
|
||||
|
||||
fn parse_card_type(lang: Language, text: String) -> Result<(CardKind, CardType)> {
|
||||
let kind = if text.contains(trainer_pattern(lang)) || text.contains(tool_pattern(lang)) {
|
||||
Ok(CardKind::Trainer)
|
||||
} else if text.contains("Pokémon") {
|
||||
Ok(CardKind::Pokemon)
|
||||
} else if text.contains(energy_pattern(lang)) {
|
||||
Ok(CardKind::Energy)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Failed to get card kind (Pokemon, Trainer or Energy)"
|
||||
))
|
||||
}?;
|
||||
|
||||
let card_type = match kind {
|
||||
CardKind::Pokemon => {
|
||||
if text.contains(basic_pattern(lang)) {
|
||||
Ok(CardType::Basic)
|
||||
} else if text.contains(stage1_pattern(lang)) {
|
||||
Ok(CardType::Stage1)
|
||||
} else if text.contains(stage2_pattern(lang)) {
|
||||
Ok(CardType::Stage2)
|
||||
} else {
|
||||
Err(anyhow!("Failed to get Pokemon type: {text}"))
|
||||
}
|
||||
}
|
||||
CardKind::Trainer => {
|
||||
if text.contains(item_pattern(lang)) {
|
||||
Ok(CardType::Item)
|
||||
} else if text.contains(tool_pattern(lang)) {
|
||||
Ok(CardType::Tool)
|
||||
} else if text.contains(stadium_pattern(lang)) {
|
||||
Ok(CardType::Stadium)
|
||||
} else if text.contains(supporter_pattern(lang)) {
|
||||
Ok(CardType::Supporter)
|
||||
} else {
|
||||
Err(anyhow!("Failed to get Trainer type"))
|
||||
}
|
||||
}
|
||||
CardKind::Energy => {
|
||||
if text.contains(basic_pattern(lang)) {
|
||||
Ok(CardType::Basic)
|
||||
} else if text.contains(special_pattern(lang)) {
|
||||
Ok(CardType::Special)
|
||||
} else {
|
||||
Err(anyhow!("Failed to get Pokemon type"))
|
||||
}
|
||||
}
|
||||
}?;
|
||||
|
||||
Ok((kind, card_type))
|
||||
}
|
||||
|
||||
fn trainer_pattern(lang: Language) -> &'static str {
|
||||
match lang {
|
||||
Language::Es => "Entrenador",
|
||||
Language::En => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn energy_pattern(lang: Language) -> &'static str {
|
||||
match lang {
|
||||
Language::Es => "Energía",
|
||||
Language::En => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn basic_pattern(lang: Language) -> &'static str {
|
||||
match lang {
|
||||
Language::Es => "Básic",
|
||||
Language::En => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn stage1_pattern(lang: Language) -> &'static str {
|
||||
match lang {
|
||||
Language::Es => "Fase 1",
|
||||
Language::En => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn stage2_pattern(lang: Language) -> &'static str {
|
||||
match lang {
|
||||
Language::Es => "Fase 2",
|
||||
Language::En => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn item_pattern(lang: Language) -> &'static str {
|
||||
match lang {
|
||||
Language::Es => "Objeto",
|
||||
Language::En => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_pattern(lang: Language) -> &'static str {
|
||||
match lang {
|
||||
Language::Es => "Herramienta",
|
||||
Language::En => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn supporter_pattern(lang: Language) -> &'static str {
|
||||
match lang {
|
||||
Language::Es => "Partidario",
|
||||
Language::En => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn stadium_pattern(lang: Language) -> &'static str {
|
||||
match lang {
|
||||
Language::Es => "Estadio",
|
||||
Language::En => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn special_pattern(lang: Language) -> &'static str {
|
||||
match lang {
|
||||
Language::Es => "Especial",
|
||||
Language::En => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn slugify_pokemon(lang: Language, code: EditionCode, name: &str) -> String {
|
||||
format!("{}-{code}-{lang}", slugify(name))
|
||||
}
|
||||
|
||||
fn slugify_unique(lang: Language, name: &str) -> String {
|
||||
format!("{}-{lang}", slugify(name))
|
||||
}
|
||||
|
||||
fn slugify(name: &str) -> String {
|
||||
name.to_lowercase()
|
||||
.replace("'s", "")
|
||||
.replace(" ", "-")
|
||||
.replace("á", "a")
|
||||
.replace("é", "e")
|
||||
.replace("í", "i")
|
||||
.replace("ó", "o")
|
||||
.replace("ú", "u")
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
//! Data downloaders
|
||||
|
||||
pub mod card_info;
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
//! Editions information
|
||||
|
||||
use serde::Deserialize;
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
pub enum EditionBlock {
|
||||
|
|
@ -8,10 +9,12 @@ pub enum EditionBlock {
|
|||
Sm,
|
||||
Ssh,
|
||||
Sv,
|
||||
Meg,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Display, Debug, Hash, PartialEq, Eq, EnumString)]
|
||||
#[derive(Clone, Copy, Display, Debug, Hash, PartialEq, Eq, EnumString, Deserialize)]
|
||||
#[strum(ascii_case_insensitive, serialize_all = "lowercase")]
|
||||
#[serde(rename_all = "UPPERCASE")]
|
||||
pub enum EditionCode {
|
||||
/// Sword and Shield
|
||||
Ssh,
|
||||
|
|
@ -39,6 +42,10 @@ pub enum EditionCode {
|
|||
Ssp,
|
||||
/// Prismatic Evolutions
|
||||
Pre,
|
||||
/// Mega Evolution
|
||||
Meg,
|
||||
/// Phantasmal Flames
|
||||
Pfl,
|
||||
}
|
||||
|
||||
impl EditionCode {
|
||||
|
|
@ -57,6 +64,8 @@ impl EditionCode {
|
|||
EditionCode::Scr => "SV07",
|
||||
EditionCode::Ssp => "SV08",
|
||||
EditionCode::Pre => "SV8pt5",
|
||||
EditionCode::Meg => "MEG1",
|
||||
EditionCode::Pfl => "MEG2",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -75,6 +84,8 @@ impl EditionCode {
|
|||
EditionCode::Scr => "stellar-crown",
|
||||
EditionCode::Ssp => "surging-sparks",
|
||||
EditionCode::Pre => "prismatic-evolutions",
|
||||
EditionCode::Meg => "mega-evolution",
|
||||
EditionCode::Pfl => "phantasmal-flames",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -93,6 +104,7 @@ impl EditionCode {
|
|||
| EditionCode::Scr
|
||||
| EditionCode::Ssp
|
||||
| EditionCode::Pre => EditionBlock::Sv,
|
||||
EditionCode::Meg | EditionCode::Pfl => EditionBlock::Meg,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,28 @@
|
|||
use anyhow::{Result, anyhow};
|
||||
use camino::Utf8PathBuf;
|
||||
use directories::ProjectDirs;
|
||||
use anyhow::{Context, Result};
|
||||
use tracing_subscriber::Layer;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
const APP_NAME: &str = "ptcg-tools";
|
||||
use crate::constants::{APP_NAME, SNAKE_CASE_APP_NAME};
|
||||
use crate::directories::data_directory;
|
||||
|
||||
/// Sets up logging for the application. Since the mode we will need logging for is a TUI, we can't
|
||||
/// print our logs into stdout, so we will log into a file instead.
|
||||
pub async fn initialize_logging() -> Result<()> {
|
||||
let user_directory = ProjectDirs::from("cl", "bstr", APP_NAME)
|
||||
.ok_or_else(|| anyhow!("failed to get ProjectDirs"))?
|
||||
.data_dir()
|
||||
.to_path_buf();
|
||||
let user_directory = Utf8PathBuf::try_from(user_directory)?;
|
||||
tokio::fs::create_dir_all(&user_directory).await?;
|
||||
pub enum LogMode {
|
||||
File,
|
||||
Print,
|
||||
}
|
||||
|
||||
/// Sets up logging for the application.
|
||||
pub async fn initialize_logging(mode: LogMode) -> Result<()> {
|
||||
match mode {
|
||||
LogMode::File => initialize_file_logging().await,
|
||||
LogMode::Print => initialize_print_logging(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn initialize_file_logging() -> Result<()> {
|
||||
let user_directory = data_directory()
|
||||
.await
|
||||
.context("While initializing logging")?;
|
||||
let log_path = user_directory.join(format!("{APP_NAME}.log"));
|
||||
let log_file = std::fs::File::create(log_path)?;
|
||||
let file_subscriber = tracing_subscriber::fmt::layer()
|
||||
|
|
@ -25,8 +32,18 @@ pub async fn initialize_logging() -> Result<()> {
|
|||
.with_target(false)
|
||||
.with_ansi(false)
|
||||
.with_filter(tracing_subscriber::filter::EnvFilter::from(format!(
|
||||
"{APP_NAME}=debug"
|
||||
"{SNAKE_CASE_APP_NAME}=debug"
|
||||
)));
|
||||
tracing_subscriber::registry().with(file_subscriber).init();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn initialize_print_logging() -> Result<()> {
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::filter::EnvFilter::from(format!(
|
||||
"{SNAKE_CASE_APP_NAME}=debug"
|
||||
)))
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
39
src/main.rs
39
src/main.rs
|
|
@ -1,11 +1,9 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
|
||||
pub mod card;
|
||||
pub mod cli;
|
||||
pub mod downloader;
|
||||
pub mod constants;
|
||||
pub mod directories;
|
||||
pub mod editions;
|
||||
pub mod lang;
|
||||
pub mod logging;
|
||||
|
|
@ -14,20 +12,21 @@ pub mod malie;
|
|||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = cli::Args::parse();
|
||||
logging::initialize_logging().await?;
|
||||
let client = reqwest::Client::new();
|
||||
let edition =
|
||||
editions::EditionCode::from_str(&args.code).context("Couldn't parse edition code")?;
|
||||
let number = args.number;
|
||||
let slug = args.slug.as_deref();
|
||||
let card_info = downloader::card_info::download_card_info(
|
||||
client.clone(),
|
||||
lang::Language::Es,
|
||||
edition,
|
||||
number,
|
||||
slug,
|
||||
)
|
||||
.await?;
|
||||
println!("{card_info:?}");
|
||||
let log_mode = if args.command == cli::Command::Tui {
|
||||
logging::LogMode::File
|
||||
} else {
|
||||
logging::LogMode::Print
|
||||
};
|
||||
logging::initialize_logging(log_mode).await?;
|
||||
match args.command {
|
||||
cli::Command::DownloadData => download_data().await?,
|
||||
cli::Command::Tui => todo!(),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn download_data() -> Result<()> {
|
||||
let client = malie::client::Client::new().await?;
|
||||
client.download_all_data().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
86
src/malie/client.rs
Normal file
86
src/malie/client.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
//! Client to download data from malie.io
|
||||
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use camino::Utf8PathBuf;
|
||||
use tokio::fs::File;
|
||||
use tokio_stream::StreamExt;
|
||||
use tokio_util::io::StreamReader;
|
||||
use tracing::debug;
|
||||
|
||||
use super::models::Index;
|
||||
use crate::directories::data_cache_directory;
|
||||
|
||||
/// Client to download data from mallie.io
|
||||
pub struct Client {
|
||||
client: reqwest::Client,
|
||||
data_cache_directory: Utf8PathBuf,
|
||||
}
|
||||
|
||||
const TCGL_BASE_URL: &str = "https://cdn.malie.io/file/malie-io/tcgl/export";
|
||||
|
||||
impl Client {
|
||||
/// Create a new `Client`
|
||||
pub async fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
client: reqwest::Client::new(),
|
||||
data_cache_directory: data_cache_directory().await?,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn download_all_data(&self) -> Result<()> {
|
||||
self.download_tcgl_index_json().await?;
|
||||
let index = self.load_tcgl_index().await?;
|
||||
println!("{index:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn download_tcgl_index_json(&self) -> Result<()> {
|
||||
let file_path = self.data_cache_directory.join("tcgl_index.json");
|
||||
let url = format!("{TCGL_BASE_URL}/index.json");
|
||||
self.download_if_not_exists(file_path, &url).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_tcgl_index(&self) -> Result<Index> {
|
||||
let file_path = self.data_cache_directory.join("tcgl_index.json");
|
||||
let index = tokio::fs::read_to_string(&file_path)
|
||||
.await
|
||||
.with_context(|| format!("Failed to read {file_path}"))?;
|
||||
let index: Index =
|
||||
serde_json::from_str(&index).with_context(|| format!("Couldn't parse {file_path}"))?;
|
||||
Ok(index)
|
||||
}
|
||||
|
||||
async fn download_if_not_exists(&self, file_path: Utf8PathBuf, url: &str) -> Result<()> {
|
||||
if let Ok(true) = tokio::fs::try_exists(&file_path).await {
|
||||
debug!("Found {}, skipping download", &file_path);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let response = self.client.get(url).send().await?;
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow!(
|
||||
"Error {} when downloading: {}",
|
||||
response.status(),
|
||||
url
|
||||
));
|
||||
}
|
||||
|
||||
let mut file = File::create_new(&file_path)
|
||||
.await
|
||||
.with_context(|| format!("Couldn't create file {file_path}"))?;
|
||||
tokio::io::copy_buf(
|
||||
&mut StreamReader::new(
|
||||
response
|
||||
.bytes_stream()
|
||||
.map(|result| result.map_err(std::io::Error::other)),
|
||||
),
|
||||
&mut file,
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("While writing to file {file_path}"))?;
|
||||
file.sync_all().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1,4 @@
|
|||
//! Module to interact with the PTCG data from malie.io
|
||||
|
||||
pub mod client;
|
||||
pub mod models;
|
||||
|
|
|
|||
33
src/malie/models.rs
Normal file
33
src/malie/models.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
//! Models for malie.io exports
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::editions::EditionCode;
|
||||
|
||||
pub type Index = HashMap<Lang, HashMap<String, Edition>>;
|
||||
|
||||
#[derive(Debug, Deserialize, Eq, PartialEq, Hash)]
|
||||
pub enum Lang {
|
||||
#[serde(rename = "de-DE")]
|
||||
De,
|
||||
#[serde(rename = "en-US")]
|
||||
En,
|
||||
#[serde(rename = "es-ES")]
|
||||
Es,
|
||||
#[serde(rename = "es-419")]
|
||||
EsLa,
|
||||
#[serde(rename = "it-IT")]
|
||||
It,
|
||||
#[serde(rename = "fr-FR")]
|
||||
Fr,
|
||||
#[serde(rename = "pt-BR")]
|
||||
Pt,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Edition {
|
||||
path: String,
|
||||
abbr: Option<EditionCode>,
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue