lesspub 1.0.2

CLI tool for downloading Sequences from LessWrong and exporting them as EPUB format ebooks
Documentation
use std::{path::Path, time::Duration};

use clap::Parser;

use console::Term;
use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget};
use indicatif_log_bridge::LogWrapper;
use lesspub::{
    cache::{get_post, get_sequence},
    cli::{CacheOptions, Command, IdOrUrl, ParsedCliOptions},
    parse::{build_and_save_post_epub, build_and_save_sequence_epub},
};
use lesspub_query::get_lw_sequences;
use lesspub_schema::SequenceListInfo;
use reqwest::Client;

async fn process_sequence(
    sequence: SequenceListInfo,
    client: &Client,
    cache_options: &CacheOptions,
    output_dir: &Path,
    main_progressbar: ProgressBar,
    multi_progress: MultiProgress,
) {
    let progress = multi_progress.insert(0, ProgressBar::new_spinner());
    progress.enable_steady_tick(Duration::from_secs_f32(0.5));
    let id = sequence._id.clone().expect("Error: no id in sequence");
    progress.set_message(format!("Parsing sequence {id}"));
    match get_sequence(client, &sequence, cache_options).await {
        Ok(sequence) => {
            if let Some(title) = &sequence.title {
                progress.set_message(format!("Parsing sequence {id}: {title}"));
            }
            if let Err(e) = build_and_save_sequence_epub(
                client,
                sequence,
                &id,
                cache_options,
                output_dir,
                true,
                progress.clone(),
            )
            .await
            {
                log::error!("Error converting sequence {id} to epub: {e:?}");
            } else {
                progress.finish_with_message(format!("Finished processing sequence {id}"));
                progress.finish_and_clear();
            }
        }
        Err(e) => log::error!("Error fetching sequence data: {e:?}"),
    }
    main_progressbar.inc(1);
}

#[tokio::main]
pub async fn main() {
    let command = Command::parse();

    let ParsedCliOptions {
        ignore_cache,
        delete_cache,
        output_dir,
        cache_dir,
    } = command.options().into();

    let cache_options = CacheOptions {
        ignore_cache,
        cache_dir: cache_dir.clone(),
    };

    let logger =
        env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).build();
    let level = logger.filter();
    let progress =
        MultiProgress::with_draw_target(ProgressDrawTarget::term(Term::buffered_stderr(), 30));
    LogWrapper::new(progress.clone(), logger)
        .try_init()
        .unwrap();
    log::set_max_level(level);

    let client = reqwest::ClientBuilder::new()
        .user_agent(concat!(
            env!("CARGO_PKG_NAME"),
            "/",
            env!("CARGO_PKG_VERSION")
        ))
        .build()
        .expect("Failed to construct HTTP client");

    match command {
        Command::All { .. } => {
            let sequences = get_lw_sequences(&client)
                .await
                .expect("Failed to fetch sequence IDs from server");

            let main_progress = progress.add(ProgressBar::new(sequences.len() as u64));

            let sequence_futures = sequences.into_iter().map(|sequence| {
                process_sequence(
                    sequence.clone(),
                    &client,
                    &cache_options,
                    &output_dir,
                    main_progress.clone(),
                    progress.clone(),
                )
            });

            futures::future::join_all(sequence_futures).await;
            let _ = progress.clear();
            log::info!("Done!");
        }
        Command::Sequence { id_options, .. } => {
            let id = IdOrUrl::from(id_options).get_id().unwrap();

            let pg = progress.insert(0, ProgressBar::new_spinner());
            pg.enable_steady_tick(Duration::from_secs_f32(0.5));

            pg.set_message(format!("Parsing sequence {id}"));
            let sequence = lesspub_schema::SequenceListInfo {
                _id: Some(id.clone()),
                lastUpdated: None,
                postsCount: None,
            };

            match get_sequence(&client, &sequence.clone(), &cache_options).await {
                Ok(sequence) => {
                    if let Some(title) = &sequence.title {
                        pg.set_message(format!("Parsing sequence {id}: {title}"));
                    }
                    if let Err(e) = build_and_save_sequence_epub(
                        &client,
                        sequence,
                        &id,
                        &cache_options,
                        &output_dir,
                        false,
                        pg.clone(),
                    )
                    .await
                    {
                        log::error!("Error converting sequence {id} to epub: {e:?}");
                    } else {
                        pg.set_message(format!("Sequence {id} converted and saved."));
                    }
                }
                Err(e) => log::error!("Error fetching sequence {id} data: {e:?}"),
            }
            pg.finish_and_clear();
        }
        Command::Post { id_options, .. } => {
            let id = IdOrUrl::from(id_options).get_id().unwrap();

            let pg = progress.insert(0, ProgressBar::new_spinner());
            pg.enable_steady_tick(Duration::from_secs_f32(0.5));

            pg.set_message(format!("Parsing post {id}"));
            match get_post(&client, &id, &cache_options).await {
                Ok(post) => {
                    if let Some(title) = &post.title {
                        pg.set_message(format!("Parsing post {id}: {title}"));
                    }
                    if let Err(e) = build_and_save_post_epub(
                        &client,
                        post,
                        &id,
                        &cache_options,
                        &output_dir,
                        pg.clone(),
                    )
                    .await
                    {
                        log::error!("Error converting post {id} to epub: {e:?}");
                    } else {
                        pg.set_message(format!("Post {id} converted and saved."));
                    }
                }
                Err(e) => log::error!("Error fetching post {id} data: {e:?}"),
            }
            pg.finish_and_clear();
        }
    };

    if delete_cache {
        std::fs::remove_dir_all(cache_dir).unwrap();
        log::info!("Deleted cache.");
    }
}