lesspub 1.0.2

CLI tool for downloading Sequences from LessWrong and exporting them as EPUB format ebooks
Documentation
use anyhow::anyhow;
use clap::Parser;
use reqwest::Url;
use std::{error::Error, path::PathBuf, str::FromStr};

/// Download and parse Lesswrong posts and sequences into epub files.
#[derive(Debug, Parser)]
pub enum Command {
    /// Download a specific post
    Post {
        #[command(flatten)]
        id_options: IdOptions,
        #[command(flatten)]
        options: CliOptions,
    },
    /// Download a specific sequence
    Sequence {
        #[command(flatten)]
        id_options: IdOptions,
        #[command(flatten)]
        options: CliOptions,
    },
    /// Download all sequences
    All {
        #[command(flatten)]
        options: CliOptions,
    },
}
impl Command {
    pub fn options(&self) -> CliOptions {
        match self {
            Command::Post { options, .. }
            | Command::Sequence { options, .. }
            | Command::All { options } => options.clone(),
        }
    }
}

#[derive(Debug, Clone, Parser)]
pub struct IdOptions {
    /// The URL at which the desired sequence or post can be found
    #[clap(long, short = 'u')]
    url: Option<String>,
    /// The ID of the desired sequence or post
    #[clap(long)]
    id: Option<String>,
}
#[derive(Debug, Clone)]
pub enum IdOrUrl {
    Url { url: String },
    Id { id: String },
}
impl From<IdOptions> for IdOrUrl {
    fn from(value: IdOptions) -> IdOrUrl {
        if let Some(id) = value.id {
            IdOrUrl::Id { id }
        } else {
            IdOrUrl::Url {
                url: value
                    .url
                    .ok_or(anyhow!("No ID or URL was specified"))
                    .unwrap(),
            }
        }
    }
}
impl IdOrUrl {
    pub fn get_id(self) -> Result<String, Box<dyn Error>> {
        Ok(match self {
            IdOrUrl::Id { id } => id,
            IdOrUrl::Url { url } => {
                let url = Url::from_str(&url)?;
                url.path_segments()
                    .ok_or(anyhow!("Failed to parse URL segments"))?
                    .nth(1)
                    .ok_or(anyhow!("Failed to find ID in URL"))?
                    .to_string()
            }
        })
    }
}

#[derive(Debug, Clone, Parser)]
pub struct CliOptions {
    /// Don't use the cache during this usage
    #[clap(long, short = 'i')]
    pub ignore_cache: bool,

    /// Delete the application's cache after use
    #[clap(long, short = 'd')]
    pub delete_cache: bool,

    /// The directory in which to output the resulting ebook file(s)
    #[clap(long, default_value = "./", short = 'o')]
    pub output_dir: PathBuf,

    /// The directory in which to store cached data for reuse
    #[clap(long, short = 'c')]
    pub cache_dir: Option<PathBuf>,
}
#[derive(Debug, Clone)]
pub struct ParsedCliOptions {
    pub ignore_cache: bool,
    pub delete_cache: bool,
    pub output_dir: PathBuf,
    pub cache_dir: PathBuf,
}
impl From<CliOptions> for ParsedCliOptions {
    fn from(value: CliOptions) -> Self {
        Self {
            ignore_cache: value.ignore_cache,
            delete_cache: value.delete_cache,
            output_dir: value.output_dir,
            cache_dir: value.cache_dir.unwrap_or(
                dirs::cache_dir()
                    .unwrap_or(PathBuf::from_str("./cache").unwrap())
                    .join("lesspub"),
            ),
        }
    }
}

pub struct CacheOptions {
    pub ignore_cache: bool,
    pub cache_dir: PathBuf,
}