lesspub/
cli.rs

1use anyhow::anyhow;
2use clap::Parser;
3use reqwest::Url;
4use std::{error::Error, path::PathBuf, str::FromStr};
5
6/// Download and parse Lesswrong posts and sequences into epub files.
7#[derive(Debug, Parser)]
8pub enum Command {
9    /// Download a specific post
10    Post {
11        #[command(flatten)]
12        id_options: IdOptions,
13        #[command(flatten)]
14        options: CliOptions,
15    },
16    /// Download a specific sequence
17    Sequence {
18        #[command(flatten)]
19        id_options: IdOptions,
20        #[command(flatten)]
21        options: CliOptions,
22    },
23    /// Download all sequences
24    All {
25        #[command(flatten)]
26        options: CliOptions,
27    },
28}
29impl Command {
30    pub fn options(&self) -> CliOptions {
31        match self {
32            Command::Post { options, .. }
33            | Command::Sequence { options, .. }
34            | Command::All { options } => options.clone(),
35        }
36    }
37}
38
39#[derive(Debug, Clone, Parser)]
40pub struct IdOptions {
41    /// The URL at which the desired sequence or post can be found
42    #[clap(long, short = 'u')]
43    url: Option<String>,
44    /// The ID of the desired sequence or post
45    #[clap(long)]
46    id: Option<String>,
47}
48#[derive(Debug, Clone)]
49pub enum IdOrUrl {
50    Url { url: String },
51    Id { id: String },
52}
53impl From<IdOptions> for IdOrUrl {
54    fn from(value: IdOptions) -> IdOrUrl {
55        if let Some(id) = value.id {
56            IdOrUrl::Id { id }
57        } else {
58            IdOrUrl::Url {
59                url: value
60                    .url
61                    .ok_or(anyhow!("No ID or URL was specified"))
62                    .unwrap(),
63            }
64        }
65    }
66}
67impl IdOrUrl {
68    pub fn get_id(self) -> Result<String, Box<dyn Error>> {
69        Ok(match self {
70            IdOrUrl::Id { id } => id,
71            IdOrUrl::Url { url } => {
72                let url = Url::from_str(&url)?;
73                url.path_segments()
74                    .ok_or(anyhow!("Failed to parse URL segments"))?
75                    .nth(1)
76                    .ok_or(anyhow!("Failed to find ID in URL"))?
77                    .to_string()
78            }
79        })
80    }
81}
82
83#[derive(Debug, Clone, Parser)]
84pub struct CliOptions {
85    /// Don't use the cache during this usage
86    #[clap(long, short = 'i')]
87    pub ignore_cache: bool,
88
89    /// Delete the application's cache after use
90    #[clap(long, short = 'd')]
91    pub delete_cache: bool,
92
93    /// The directory in which to output the resulting ebook file(s)
94    #[clap(long, default_value = "./", short = 'o')]
95    pub output_dir: PathBuf,
96
97    /// The directory in which to store cached data for reuse
98    #[clap(long, short = 'c')]
99    pub cache_dir: Option<PathBuf>,
100}
101#[derive(Debug, Clone)]
102pub struct ParsedCliOptions {
103    pub ignore_cache: bool,
104    pub delete_cache: bool,
105    pub output_dir: PathBuf,
106    pub cache_dir: PathBuf,
107}
108impl From<CliOptions> for ParsedCliOptions {
109    fn from(value: CliOptions) -> Self {
110        Self {
111            ignore_cache: value.ignore_cache,
112            delete_cache: value.delete_cache,
113            output_dir: value.output_dir,
114            cache_dir: value.cache_dir.unwrap_or(
115                dirs::cache_dir()
116                    .unwrap_or(PathBuf::from_str("./cache").unwrap())
117                    .join("lesspub"),
118            ),
119        }
120    }
121}
122
123pub struct CacheOptions {
124    pub ignore_cache: bool,
125    pub cache_dir: PathBuf,
126}