commit 3a58d17ca36c0ed13f77c767eda37f0afc1f1e56
parent 21406a80688851194f2659c5bc0ae1cf00c7f906
Author: egor-achkasov <eaachkasov@gmail.com>
Date: Wed, 6 May 2026 17:32:03 +0000
Drop anyhow; add custom Error enum
Diffstat:
9 files changed, 385 insertions(+), 321 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -20,7 +20,6 @@ name = "arhivach-downloader-tui"
path = "src/bin/tui/main.rs"
[dependencies]
-anyhow = "1.0.102"
arboard = "3"
clap = { version = "4.5.57", features = ["derive"] }
ratatui = "0.29"
diff --git a/src/bin/cli/main.rs b/src/bin/cli/main.rs
@@ -1,130 +1,132 @@
-use arhivarch_downloader::config::Config;
-use arhivarch_downloader::event::Event;
-use arhivarch_downloader::export::{html::HtmlExporter, ExporterKind};
-
-use clap::{Parser, ValueEnum};
-
-use std::path::PathBuf;
-
-#[derive(Clone, ValueEnum)]
-enum ExporterArg {
- Html,
-}
-use std::sync::mpsc::channel;
-
-fn main() -> anyhow::Result<()> {
- let config = parse_args();
- let (tx, rx) = channel::<Event>();
- let handle = std::thread::spawn({
- let config = config.clone();
- move || arhivarch_downloader::run(&config, tx)
- });
-
- for event in rx {
- render_event(&event);
- }
-
- let _ = handle.join().map_err(|e| anyhow::anyhow!("{:?}", e))?;
- Ok(())
-}
-
-pub fn parse_args() -> Config {
- #[derive(Parser)]
- #[command(about, long_about)]
- struct Cli {
- /// URL to download
- url: String,
-
- /// Path to download directory
- #[arg(short = 'd', long = "dir", value_name = "DIR", default_value = ".", value_hint = clap::ValueHint::DirPath)]
- dir: PathBuf,
-
- /// Exporter
- #[arg(short = 'e', long = "exporter", value_name = "EXPORTER", default_value = "html")]
- exporter: ExporterArg,
-
- /// Download thumbnail images, default: false
- #[arg(short = 't', long = "thumb", default_value_t = false)]
- thumb: bool,
-
- /// Download files (images, videos, gifs, etc), default: false
- #[arg(short = 'f', long = "files", default_value_t = false)]
- files: bool,
-
- /// Resume files and thumbnails downloading instead of overwriting. Useless if neither -t nor -f are set, default: false
- #[arg(short = 'r', long = "resume", default_value_t = false)]
- resume: bool,
-
- /// Download retries in case of a error
- #[arg(short = 'R', long = "retries", default_value_t = 3)]
- download_retries: u32,
- }
- let cli = Cli::parse();
-
- Config {
- url: cli.url,
- dir: cli.dir,
- exporter: match cli.exporter {
- ExporterArg::Html => ExporterKind::Html(HtmlExporter),
- },
- thumb: cli.thumb,
- files: cli.files,
- resume: cli.resume,
- download_retries: cli.download_retries,
- }
-}
-
-fn render_event(event: &Event) {
- use std::io::Write;
- match event {
- Event::GetStarted => {
- print!("Fetching thread...");
- std::io::stdout().flush().ok();
- }
- Event::GetDone =>
- println!(" Done."),
- Event::GetFailed { error } =>
- eprintln!("\nFailed to fetch thread: {}", error),
-
- Event::DownloadAllStarted =>
- println!("Downloading stuff..."),
- Event::DownloadAllDone =>
- println!("All downloads complete."),
- Event::DownloadAllFailed { error } =>
- eprintln!("Download failed: {}", error),
-
- Event::DownloadStarted { index, max_index } => {
- print!("\r\tDownloading {} / {}...", index, max_index);
- std::io::stdout().flush().ok();
- }
- Event::DownloadDone { index, max_index } => {
- println!("\r\tDownloading {} / {}... Done.", index, max_index);
- }
- Event::DownloadFailed { url, error } =>
- eprintln!("\r\tFailed to download {}: {}", url, error),
- Event::DownloadSkipped { index, max_index } =>
- println!("\r\tDownloading {} / {}... Skipped.", index, max_index),
-
- Event::DownloadFilesStarted => {
- println!("Downloading files...");
- std::io::stdout().flush().ok();
- }
- Event::DownloadFilesDone =>
- println!("Done."),
- Event::DownloadThumbStarted => {
- println!("Downloading thumbnails...");
- std::io::stdout().flush().ok();
- }
- Event::DownloadThumbDone =>
- println!("Done."),
-
- Event::ExportStarted => {
- print!("Exporting...");
- std::io::stdout().flush().ok();
- }
- Event::ExportDone =>
- println!(" Done."),
- Event::ExportFailed { error } =>
- eprintln!("\nExport failed: {}", error),
- }
-}
+use arhivarch_downloader::config::Config;
+use arhivarch_downloader::event::Event;
+use arhivarch_downloader::export::{html::HtmlExporter, ExporterKind};
+
+use clap::{Parser, ValueEnum};
+
+use std::path::PathBuf;
+
+#[derive(Clone, ValueEnum)]
+enum ExporterArg {
+ Html,
+}
+use std::sync::mpsc::channel;
+
+fn main() {
+ let config = parse_args();
+ let (tx, rx) = channel::<Event>();
+ let handle = std::thread::spawn({
+ let config = config.clone();
+ move || arhivarch_downloader::run(&config, tx)
+ });
+
+ for event in rx {
+ render_event(&event);
+ }
+
+ handle.join().map_err(|e| {
+ eprintln!("ERROR: {:?}", e);
+ std::process::exit(1);
+ }).ok();
+}
+
+pub fn parse_args() -> Config {
+ #[derive(Parser)]
+ #[command(about, long_about)]
+ struct Cli {
+ /// URL to download
+ url: String,
+
+ /// Path to download directory
+ #[arg(short = 'd', long = "dir", value_name = "DIR", default_value = ".", value_hint = clap::ValueHint::DirPath)]
+ dir: PathBuf,
+
+ /// Exporter
+ #[arg(short = 'e', long = "exporter", value_name = "EXPORTER", default_value = "html")]
+ exporter: ExporterArg,
+
+ /// Download thumbnail images, default: false
+ #[arg(short = 't', long = "thumb", default_value_t = false)]
+ thumb: bool,
+
+ /// Download files (images, videos, gifs, etc), default: false
+ #[arg(short = 'f', long = "files", default_value_t = false)]
+ files: bool,
+
+ /// Resume files and thumbnails downloading instead of overwriting. Useless if neither -t nor -f are set, default: false
+ #[arg(short = 'r', long = "resume", default_value_t = false)]
+ resume: bool,
+
+ /// Download retries in case of a error
+ #[arg(short = 'R', long = "retries", default_value_t = 3)]
+ download_retries: u32,
+ }
+ let cli = Cli::parse();
+
+ Config {
+ url: cli.url,
+ dir: cli.dir,
+ exporter: match cli.exporter {
+ ExporterArg::Html => ExporterKind::Html(HtmlExporter),
+ },
+ thumb: cli.thumb,
+ files: cli.files,
+ resume: cli.resume,
+ download_retries: cli.download_retries,
+ }
+}
+
+fn render_event(event: &Event) {
+ use std::io::Write;
+ match event {
+ Event::GetStarted => {
+ print!("Fetching thread...");
+ std::io::stdout().flush().ok();
+ }
+ Event::GetDone =>
+ println!(" Done."),
+ Event::GetFailed { error } =>
+ eprintln!("\nFailed to fetch thread: {}", error),
+
+ Event::DownloadAllStarted =>
+ println!("Downloading stuff..."),
+ Event::DownloadAllDone =>
+ println!("All downloads complete."),
+ Event::DownloadAllFailed { error } =>
+ eprintln!("Download failed: {}", error),
+
+ Event::DownloadStarted { index, max_index } => {
+ print!("\r\tDownloading {} / {}...", index, max_index);
+ std::io::stdout().flush().ok();
+ }
+ Event::DownloadDone { index, max_index } => {
+ println!("\r\tDownloading {} / {}... Done.", index, max_index);
+ }
+ Event::DownloadFailed { url, error } =>
+ eprintln!("\r\tFailed to download {}: {}", url, error),
+ Event::DownloadSkipped { index, max_index } =>
+ println!("\r\tDownloading {} / {}... Skipped.", index, max_index),
+
+ Event::DownloadFilesStarted => {
+ println!("Downloading files...");
+ std::io::stdout().flush().ok();
+ }
+ Event::DownloadFilesDone =>
+ println!("Done."),
+ Event::DownloadThumbStarted => {
+ println!("Downloading thumbnails...");
+ std::io::stdout().flush().ok();
+ }
+ Event::DownloadThumbDone =>
+ println!("Done."),
+
+ Event::ExportStarted => {
+ print!("Exporting...");
+ std::io::stdout().flush().ok();
+ }
+ Event::ExportDone =>
+ println!(" Done."),
+ Event::ExportFailed { error } =>
+ eprintln!("\nExport failed: {}", error),
+ }
+}
diff --git a/src/bin/tui/main.rs b/src/bin/tui/main.rs
@@ -305,14 +305,14 @@ fn handle_mouse_click(app: &mut App, col: u16, row: u16) {
// ── Main loop ─────────────────────────────────────────────────────────────────
-fn run_app(terminal: &mut DefaultTerminal, app: &mut App) -> anyhow::Result<()> {
+fn run_app(terminal: &mut DefaultTerminal, app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
execute!(stdout(), EnableMouseCapture)?;
let result = run_loop(terminal, app);
execute!(stdout(), DisableMouseCapture)?;
result
}
-fn run_loop(terminal: &mut DefaultTerminal, app: &mut App) -> anyhow::Result<()> {
+fn run_loop(terminal: &mut DefaultTerminal, app: &mut App) -> Result<(), Box<dyn std::error::Error>> {
loop {
app.poll();
terminal.draw(|f| draw(f, app))?;
@@ -360,9 +360,12 @@ fn run_loop(terminal: &mut DefaultTerminal, app: &mut App) -> anyhow::Result<()>
}
}
-fn main() -> anyhow::Result<()> {
+fn main() {
let mut terminal = ratatui::init();
let result = run_app(&mut terminal, &mut App::new());
ratatui::restore();
- result
+ if let Err(e) = result {
+ eprintln!("ERROR: {e}");
+ std::process::exit(1);
+ }
}
diff --git a/src/lib/download.rs b/src/lib/download.rs
@@ -1,4 +1,4 @@
-use anyhow::{anyhow, Result};
+use crate::error::{Error, Result};
/// Downloads a URL, retrying up to `tries` times.
///
@@ -14,11 +14,11 @@ pub fn download(url: &str, tries: u32) -> Result<ureq::http::Response<ureq::Body
match CLIENT.get(url).call() {
Ok(response) => return Ok(response),
Err(ureq::Error::StatusCode(code)) if code >= 400 && code < 500 => {
- return Err(anyhow!("client error: {}", code));
+ return Err(Error::HttpClientError(code));
}
Err(ureq::Error::StatusCode(_)) => continue,
Err(e) => return Err(e.into()),
}
}
- Err(anyhow!("failed to download {} after {} tries", url, tries))
+ Err(Error::DownloadFailed { url: url.to_string(), tries })
}
diff --git a/src/lib/error.rs b/src/lib/error.rs
@@ -0,0 +1,63 @@
+use std::fmt;
+
+#[derive(Debug)]
+pub enum Error {
+ Io(std::io::Error),
+ Ureq(ureq::Error),
+ ParseInt(std::num::ParseIntError),
+ HttpClientError(u16),
+ DownloadFailed { url: String, tries: u32 },
+ MissingElement(&'static str),
+ EmptyFile(String),
+ NoPosts,
+ UnknownExporter(String),
+ ChannelSend,
+}
+
+pub type Result<T> = std::result::Result<T, Error>;
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Error::Io(e) => write!(f, "I/O error: {e}"),
+ Error::Ureq(e) => write!(f, "network error: {e}"),
+ Error::ParseInt(e) => write!(f, "parse error: {e}"),
+ Error::HttpClientError(code) => write!(f, "client error: {code}"),
+ Error::DownloadFailed { url, tries } => {
+ write!(f, "failed to download {url} after {tries} tries")
+ }
+ Error::MissingElement(name) => write!(f, "missing element: {name}"),
+ Error::EmptyFile(url) => write!(f, "empty file: {url}"),
+ Error::NoPosts => write!(f, "no posts to export"),
+ Error::UnknownExporter(name) => write!(f, "unknown exporter: {name}"),
+ Error::ChannelSend => write!(f, "failed to send event"),
+ }
+ }
+}
+
+impl std::error::Error for Error {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ match self {
+ Error::Io(e) => Some(e),
+ Error::Ureq(e) => Some(e),
+ Error::ParseInt(e) => Some(e),
+ _ => None,
+ }
+ }
+}
+
+impl From<std::io::Error> for Error {
+ fn from(e: std::io::Error) -> Self { Error::Io(e) }
+}
+
+impl From<ureq::Error> for Error {
+ fn from(e: ureq::Error) -> Self { Error::Ureq(e) }
+}
+
+impl From<std::num::ParseIntError> for Error {
+ fn from(e: std::num::ParseIntError) -> Self { Error::ParseInt(e) }
+}
+
+impl<T> From<std::sync::mpsc::SendError<T>> for Error {
+ fn from(_: std::sync::mpsc::SendError<T>) -> Self { Error::ChannelSend }
+}
diff --git a/src/lib/export/html/mod.rs b/src/lib/export/html/mod.rs
@@ -1,27 +1,28 @@
-use crate::{config::Config, post::Post};
-use anyhow::Result;
-use super::Exporter;
-
-mod render;
-
-const TEMPLATE: &str = include_str!("template.html");
-
-#[derive(Clone)]
-pub struct HtmlExporter;
-
-impl Exporter for HtmlExporter {
- fn export(&self, posts: &[Post], config: &Config) -> Result<()> {
- anyhow::ensure!(!posts.is_empty(), "No posts to export");
-
- std::fs::create_dir_all(&config.dir)?;
- let posts_html = posts
- .iter()
- .map(|p| render::render_post(p, config.files, config.thumb))
- .collect::<Vec<String>>()
- .join("\n");
- let index_html = TEMPLATE.replace("{{posts}}", &posts_html);
- std::fs::write(config.dir.join("index.html"), index_html)?;
-
- Ok(())
- }
-}
+use crate::{config::Config, error::{Error, Result}, post::Post};
+use super::Exporter;
+
+mod render;
+
+const TEMPLATE: &str = include_str!("template.html");
+
+#[derive(Clone)]
+pub struct HtmlExporter;
+
+impl Exporter for HtmlExporter {
+ fn export(&self, posts: &[Post], config: &Config) -> Result<()> {
+ if posts.is_empty() {
+ return Err(Error::NoPosts);
+ }
+
+ std::fs::create_dir_all(&config.dir)?;
+ let posts_html = posts
+ .iter()
+ .map(|p| render::render_post(p, config.files, config.thumb))
+ .collect::<Vec<String>>()
+ .join("\n");
+ let index_html = TEMPLATE.replace("{{posts}}", &posts_html);
+ std::fs::write(config.dir.join("index.html"), index_html)?;
+
+ Ok(())
+ }
+}
diff --git a/src/lib/export/mod.rs b/src/lib/export/mod.rs
@@ -1,35 +1,34 @@
-pub mod html;
-
-use super::{config::Config, post::Post};
-
-use anyhow::Result;
-
-use std::str::FromStr;
-
-#[derive(Clone)]
-pub enum ExporterKind {
- Html(html::HtmlExporter),
-}
-
-pub trait Exporter {
- fn export(&self, posts: &[Post], config: &Config) -> Result<()>;
-}
-
-impl Exporter for ExporterKind {
- fn export(&self, posts: &[Post], config: &Config) -> Result<()> {
- match self {
- ExporterKind::Html(html) => html.export(posts, config),
- }
- }
-}
-
-impl FromStr for ExporterKind {
- type Err = anyhow::Error;
-
- fn from_str(s: &str) -> Result<ExporterKind> {
- match s.to_lowercase().as_str() {
- "html" => Ok(ExporterKind::Html(html::HtmlExporter {})),
- _ => anyhow::bail!("unknown exporter: {}", s),
- }
- }
-}
+pub mod html;
+
+use super::{config::Config, post::Post};
+use crate::error::{Error, Result};
+
+use std::str::FromStr;
+
+#[derive(Clone)]
+pub enum ExporterKind {
+ Html(html::HtmlExporter),
+}
+
+pub trait Exporter {
+ fn export(&self, posts: &[Post], config: &Config) -> Result<()>;
+}
+
+impl Exporter for ExporterKind {
+ fn export(&self, posts: &[Post], config: &Config) -> Result<()> {
+ match self {
+ ExporterKind::Html(html) => html.export(posts, config),
+ }
+ }
+}
+
+impl FromStr for ExporterKind {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<ExporterKind> {
+ match s.to_lowercase().as_str() {
+ "html" => Ok(ExporterKind::Html(html::HtmlExporter {})),
+ _ => Err(Error::UnknownExporter(s.to_string())),
+ }
+ }
+}
diff --git a/src/lib/lib.rs b/src/lib/lib.rs
@@ -1,99 +1,97 @@
-pub mod config;
-pub mod event;
-pub mod export;
-
-mod download;
-mod post;
-
-use crate::post::{Post, File};
-use crate::export::Exporter;
-
-use anyhow::{Result, Context};
-
-use std::sync::mpsc::Sender;
-
-pub const BASE_URL: &str = "https://arhivach.vc";
-
-pub fn run(config: &config::Config, tx: Sender<event::Event>) -> Result<()> {
- tx.send(event::Event::GetStarted)?;
- let html = download::download(&config.url, config.download_retries)?
- .body_mut()
- .read_to_string()
- .context("failed to read response body")?;
- let posts = Post::parse_posts(&html)
- .inspect_err(|e| { let _ = tx.send(event::Event::GetFailed { error: format!("{:#}", e) }); })
- .context("failed to parse posts")?;
- tx.send(event::Event::GetDone)?;
-
- tx.send(event::Event::DownloadAllStarted)?;
- run_download(&posts, &config, tx.clone())
- .inspect_err(|e| { let _ = tx.send(event::Event::DownloadAllFailed { error: format!("{:#}", e) }); })
- .context("failed to download files")?;
- tx.send(event::Event::DownloadAllDone)?;
-
- tx.send(event::Event::ExportStarted)?;
- config.exporter.export(&posts, config)
- .inspect_err(|e| { let _ = tx.send(event::Event::ExportFailed { error: format!("{:#}", e) }); })
- .context("failed to export")?;
- tx.send(event::Event::ExportDone)?;
-
- Ok(())
-}
-
-/// Download files and thumbnails. Send DownloadStarted, DownloadDone and DownloadFailed events
-fn run_download(posts: &[Post], config: &config::Config, tx: Sender<event::Event>) -> Result<()> {
- std::fs::create_dir_all(&config.dir)?;
-
- let download_item = |url: &str, filepath: &std::path::PathBuf| -> Result<()> {
- let result = download::download(url, config.download_retries)?;
- let mut bytes = Vec::new();
- std::io::Read::read_to_end(&mut result.into_body().as_reader(), &mut bytes)?;
- anyhow::ensure!(!bytes.is_empty(), "empty file: {}", url);
- std::fs::write(filepath, bytes)?;
- Ok(())
- };
-
- let download_section = |
- subdir: &str,
- get_url: fn(&File) -> (&str, &str),
- | -> Result<()> {
- let dir = config.dir.join(subdir);
- std::fs::create_dir_all(&dir)?;
-
- let mut index: usize = 1;
- let max_index: usize = posts.iter().map(|p| p.files.len()).sum();
- for f in posts.iter().flat_map(|p| &p.files) {
- tx.send(event::Event::DownloadStarted { index, max_index })?;
- let (url, fallback) = get_url(f);
- let filename = url.rsplit("/").next().unwrap_or(fallback).trim();
- let filepath = dir.join(filename);
- if config.resume && filepath.exists() {
- tx.send(event::Event::DownloadSkipped { index, max_index })?;
- index += 1;
- continue
- }
- match download_item(url, &filepath) {
- Ok(()) => tx.send(event::Event::DownloadDone{ index, max_index })?,
- Err(e) => tx.send(event::Event::DownloadFailed {
- url: url.to_string(),
- error: format!("{:#}", e)
- })?
- };
- index += 1;
- }
- Ok(())
- };
-
- if config.files {
- tx.send(event::Event::DownloadFilesStarted)?;
- download_section("files", |f| (&f.url, &f.name_timestamp))?;
- tx.send(event::Event::DownloadFilesDone)?;
- }
- if config.thumb {
- tx.send(event::Event::DownloadThumbStarted)?;
- download_section("thumb", |f| (&f.url_thumb, &f.name_timestamp))?;
- tx.send(event::Event::DownloadThumbDone)?;
- }
-
- Ok(())
-}
+pub mod config;
+pub mod error;
+pub mod event;
+pub mod export;
+
+mod download;
+mod post;
+
+use crate::error::{Error, Result};
+use crate::post::{Post, File};
+use crate::export::Exporter;
+
+use std::sync::mpsc::Sender;
+
+pub const BASE_URL: &str = "https://arhivach.vc";
+
+pub fn run(config: &config::Config, tx: Sender<event::Event>) -> Result<()> {
+ tx.send(event::Event::GetStarted)?;
+ let html = download::download(&config.url, config.download_retries)?
+ .body_mut()
+ .read_to_string()?;
+ let posts = Post::parse_posts(&html)
+ .inspect_err(|e| { let _ = tx.send(event::Event::GetFailed { error: e.to_string() }); })?;
+ tx.send(event::Event::GetDone)?;
+
+ tx.send(event::Event::DownloadAllStarted)?;
+ run_download(&posts, &config, tx.clone())
+ .inspect_err(|e| { let _ = tx.send(event::Event::DownloadAllFailed { error: e.to_string() }); })?;
+ tx.send(event::Event::DownloadAllDone)?;
+
+ tx.send(event::Event::ExportStarted)?;
+ config.exporter.export(&posts, config)
+ .inspect_err(|e| { let _ = tx.send(event::Event::ExportFailed { error: e.to_string() }); })?;
+ tx.send(event::Event::ExportDone)?;
+
+ Ok(())
+}
+
+/// Download files and thumbnails. Send DownloadStarted, DownloadDone and DownloadFailed events
+fn run_download(posts: &[Post], config: &config::Config, tx: Sender<event::Event>) -> Result<()> {
+ std::fs::create_dir_all(&config.dir)?;
+
+ let download_item = |url: &str, filepath: &std::path::PathBuf| -> Result<()> {
+ let result = download::download(url, config.download_retries)?;
+ let mut bytes = Vec::new();
+ std::io::Read::read_to_end(&mut result.into_body().as_reader(), &mut bytes)?;
+ if bytes.is_empty() {
+ return Err(Error::EmptyFile(url.to_string()));
+ }
+ std::fs::write(filepath, bytes)?;
+ Ok(())
+ };
+
+ let download_section = |
+ subdir: &str,
+ get_url: fn(&File) -> (&str, &str),
+ | -> Result<()> {
+ let dir = config.dir.join(subdir);
+ std::fs::create_dir_all(&dir)?;
+
+ let mut index: usize = 1;
+ let max_index: usize = posts.iter().map(|p| p.files.len()).sum();
+ for f in posts.iter().flat_map(|p| &p.files) {
+ tx.send(event::Event::DownloadStarted { index, max_index })?;
+ let (url, fallback) = get_url(f);
+ let filename = url.rsplit("/").next().unwrap_or(fallback).trim();
+ let filepath = dir.join(filename);
+ if config.resume && filepath.exists() {
+ tx.send(event::Event::DownloadSkipped { index, max_index })?;
+ index += 1;
+ continue
+ }
+ match download_item(url, &filepath) {
+ Ok(()) => tx.send(event::Event::DownloadDone{ index, max_index })?,
+ Err(e) => tx.send(event::Event::DownloadFailed {
+ url: url.to_string(),
+ error: e.to_string()
+ })?
+ };
+ index += 1;
+ }
+ Ok(())
+ };
+
+ if config.files {
+ tx.send(event::Event::DownloadFilesStarted)?;
+ download_section("files", |f| (&f.url, &f.name_timestamp))?;
+ tx.send(event::Event::DownloadFilesDone)?;
+ }
+ if config.thumb {
+ tx.send(event::Event::DownloadThumbStarted)?;
+ download_section("thumb", |f| (&f.url_thumb, &f.name_timestamp))?;
+ tx.send(event::Event::DownloadThumbDone)?;
+ }
+
+ Ok(())
+}
diff --git a/src/lib/post.rs b/src/lib/post.rs
@@ -1,6 +1,5 @@
use super::BASE_URL;
-
-use anyhow::{Context, Result};
+use crate::error::{Error, Result};
#[derive(Debug, Clone)]
pub struct File {
@@ -58,7 +57,7 @@ impl Post {
html: &str,
) -> Result<Vec<Post>> {
let mut posts = Vec::new();
-
+
let document = scraper::Html::parse_document(html);
let selector = scraper::Selector::parse(r#"div.post"#).unwrap();
for node in document.select(&selector) {
@@ -70,7 +69,7 @@ impl Post {
}
/// Parse div class="post"
- ///
+ ///
/// Example element:
/// ```html
/// <div class="post" id="post329274763" postid="329274763">
@@ -89,13 +88,13 @@ impl Post {
let post_head = node
.select(&SEL_POST_HEAD)
.next()
- .context("missing post_head")?;
+ .ok_or(Error::MissingElement("post_head"))?;
let head = Post::parse_post_head(post_head)?;
let post_comment = node
.select(&SEL_POST_IMAGE_BLOCK)
.next()
- .context("missing post_comment")?;
+ .ok_or(Error::MissingElement("post_comment"))?;
let (files, text) = Post::parse_post_comment(post_comment)?;
Ok(Post {
@@ -114,7 +113,7 @@ impl Post {
///
/// Returns (subject, name, mailto, time, num, id)
/// Returns error if no time, num or id is found or if id is not a number
- ///
+ ///
/// Example element:
/// ```html
/// <div class="post_head">
@@ -152,7 +151,7 @@ impl Post {
.next()
.and_then(|el| el.value().attr("href"))
.and_then(|href| href.strip_prefix('#'))
- .context("missing post id")?
+ .ok_or(Error::MissingElement("post id"))?
.parse()?;
let subject = post_head
@@ -175,14 +174,14 @@ impl Post {
let time = post_head
.select(&SEL_SPAN_POST_TIME)
.next()
- .context("missing post_time")?
+ .ok_or(Error::MissingElement("post_time"))?
.text()
.collect::<String>();
let num = post_head
.select(&SEL_SPAN_POST_NUM)
.next()
- .context("missing post_num")?
+ .ok_or(Error::MissingElement("post_num"))?
.text()
.collect::<String>();
@@ -190,9 +189,9 @@ impl Post {
}
/// Parses the sapn post_comment element from a post element
- ///
+ ///
/// Returns (files, text)
- ///
+ ///
/// Example element:
/// <span class="post_comment">
/// <div class="post_image_block" ...>...</div> (see parse_post_image_block function) (can appear 0 to multiple times)
@@ -216,13 +215,13 @@ impl Post {
let text = Post::parse_post_comment_body(node
.select(&SEL_POST_COMMENT_BODY)
.next()
- .context("missing post_comment_body")?);
+ .ok_or(Error::MissingElement("post_comment_body"))?);
Ok((files, text))
}
/// Parses "post_image_block" element
/// Returns File
- ///
+ ///
/// Example element:
/// ```html
/// <div class="post_image_block" id="pib_77_2" pib="77_2" title="537.4 Кб, 946 x 946
@@ -299,7 +298,7 @@ impl Post {
/// - References are plaintext (e.g. >>329274789)
/// - `<br>` is replaced with \n
/// - `<span class="unkfunc">` (greentext) is replaced with >text
- ///
+ ///
/// If the text contains a reference (e.g. >>329274789) it looks like this in the element:
/// ```html
/// <div class="post_comment_body">
@@ -310,7 +309,7 @@ impl Post {
/// text1
/// </div>
/// ```
- ///
+ ///
/// This example returns:
/// ```text
/// >>329274893
@@ -342,20 +341,20 @@ impl std::fmt::Display for Post {
// Header line
let name = self.name.as_deref().unwrap_or("Аноним");
let mailto = self.mailto.as_deref().unwrap_or("");
-
+
if !mailto.is_empty() {
write!(f, "{} ({})", name, mailto)?;
} else {
write!(f, "{}", name)?;
}
-
+
write!(f, " {} {} ID:{}", self.time, self.num, self.id)?;
-
+
// Subject
if let Some(ref subject) = self.subject {
write!(f, "\n{}", subject)?;
}
-
+
// Files
if !self.files.is_empty() {
write!(f, "\n[Files: {}]", self.files.len())?;
@@ -363,12 +362,12 @@ impl std::fmt::Display for Post {
write!(f, "\n - {}", file)?;
}
}
-
+
// Post text
if !self.text.is_empty() {
write!(f, "\n{}", self.text)?;
}
-
+
Ok(())
}
}