lesspub 1.0.2

CLI tool for downloading Sequences from LessWrong and exporting them as EPUB format ebooks
Documentation
use std::error::Error;
use std::fs::{self, File};
use std::io::BufReader;

use anyhow::anyhow;
use reqwest::Client;

use lesspub_query::{get_ea_post, get_lw_post, get_lw_sequence};
use lesspub_schema::{Post, Sequence};

use crate::cli::CacheOptions;

pub fn cache_data<C: AsRef<[u8]>>(
    subdirectory: Option<&str>,
    filename: &str,
    data: C,
    cache_options: &CacheOptions,
) -> Result<(), Box<dyn Error>> {
    let dir_path = match subdirectory {
        Some(subdir) => cache_options.cache_dir.join(subdir),
        None => cache_options.cache_dir.clone(),
    };
    fs::create_dir_all(&dir_path)?;
    fs::write(dir_path.join(filename), data)?;
    Ok(())
}

pub fn cache_crosspost(
    post: &Post,
    id: &str,
    cache_options: &CacheOptions,
) -> Result<(), Box<dyn Error>> {
    let slug = slug::slugify(id);
    cache_data(
        None,
        &format!("post_{slug}.json"),
        serde_json::to_string_pretty(post)?.as_bytes(),
        cache_options,
    )
}
pub fn uncache_crosspost(
    id: &str,
    cache_options: &CacheOptions,
) -> Result<Option<Post>, Box<dyn Error>> {
    let slug = slug::slugify(id);
    if cache_options.ignore_cache {
        Ok(None)
    } else if let Ok(file) = File::open(cache_options.cache_dir.join(format!("post_{slug}.json"))) {
        let post: Post = serde_json::from_reader(BufReader::new(file))?;
        Ok(Some(post))
    } else {
        Ok(None)
    }
}
pub async fn get_crosspost(
    client: &Client,
    id: &str,
    cache_options: &CacheOptions,
) -> Result<Post, Box<dyn Error>> {
    match uncache_crosspost(id, cache_options) {
        Ok(Some(cached_post)) => {
            log::debug!("Cached post pulled");
            Ok(cached_post)
        }
        _ => {
            log::debug!("Downloading post");
            let post = get_ea_post(client, id).await?;
            cache_crosspost(&post, id, cache_options)?;
            Ok(post)
        }
    }
}

pub fn cache_post(
    post: &Post,
    id: &str,
    cache_options: &CacheOptions,
) -> Result<(), Box<dyn Error>> {
    let slug = slug::slugify(id);
    cache_data(
        None,
        &format!("lw_post_{slug}.json"),
        serde_json::to_string_pretty(post)?.as_bytes(),
        cache_options,
    )
}
pub fn uncache_post(
    id: &str,
    cache_options: &CacheOptions,
) -> Result<Option<Post>, Box<dyn Error>> {
    let slug = slug::slugify(id);
    if cache_options.ignore_cache {
        Ok(None)
    } else if let Ok(file) =
        File::open(cache_options.cache_dir.join(format!("lw_post_{slug}.json")))
    {
        let post: Post = serde_json::from_reader(BufReader::new(file))?;
        Ok(Some(post))
    } else {
        Ok(None)
    }
}
pub async fn get_post(
    client: &Client,
    id: &str,
    cache_options: &CacheOptions,
) -> Result<Post, Box<dyn Error>> {
    match uncache_post(id, cache_options) {
        Ok(Some(cached_post)) => {
            log::debug!("Cached post pulled");
            Ok(cached_post)
        }
        _ => {
            log::debug!("Downloading post");
            let post = get_lw_post(client, id).await?;
            cache_post(&post, id, cache_options)?;
            Ok(post)
        }
    }
}

pub fn cache_sequence(
    sequence: &Sequence,
    cache_options: &CacheOptions,
) -> Result<(), Box<dyn Error>> {
    let slug = slug::slugify(sequence._id.clone().ok_or(anyhow!("no sequence id"))?);
    cache_data(
        None,
        &format!("{slug}.json"),
        serde_json::to_string_pretty(sequence)?.as_bytes(),
        cache_options,
    )
}
pub fn uncache_sequence(
    sequence_info: &lesspub_schema::SequenceListInfo,
    cache_options: &CacheOptions,
) -> Result<Option<Sequence>, Box<dyn Error>> {
    let id = sequence_info._id.clone().ok_or("no id in sequence")?;
    let slug = slug::slugify(&id);

    if cache_options.ignore_cache {
        Ok(None)
    } else if let Ok(file) = File::open(cache_options.cache_dir.join(format!("{slug}.json"))) {
        let sequence: Sequence = serde_json::from_reader(BufReader::new(file))?;
        if sequence_info.postsCount.is_some_and(|new_count| {
            sequence
                .postsCount
                .is_none_or(|old_count| new_count > old_count)
        }) || sequence_info.lastUpdated.is_some_and(|new_date| {
            sequence
                .lastUpdated
                .is_none_or(|old_date| new_date > old_date)
        }) {
            Ok(None)
        } else {
            Ok(Some(sequence))
        }
    } else {
        Ok(None)
    }
}
pub async fn get_sequence(
    client: &Client,
    sequence_info: &lesspub_schema::SequenceListInfo,
    cache_options: &CacheOptions,
) -> Result<Sequence, Box<dyn Error>> {
    match uncache_sequence(sequence_info, cache_options) {
        Ok(Some(cached_sequence)) => {
            log::debug!("Cached sequence pulled");
            Ok(cached_sequence)
        }
        _ => {
            log::debug!("Downloading sequence");
            let id = sequence_info._id.clone().ok_or("no id in sequence")?;
            let sequence = get_lw_sequence(client, &id).await?;
            cache_sequence(&sequence, cache_options)?;
            Ok(sequence)
        }
    }
}