use clap::Parser;
use std::{
fmt::Display,
path::{Path, PathBuf},
};
use glowpub::{
Board, Thread,
api::BoardPosts,
cached::write_if_changed,
generate::Options,
types::{Continuity, Section},
};
const DEFAULT_OUTPUT_DIR: &str = "./books";
#[derive(Debug, Parser)]
enum Command {
Post {
post_id: u64,
#[command(flatten)]
options: CliOptions,
},
Board {
board_id: u64,
#[command(flatten)]
options: CliOptions,
#[clap(long)]
single_file: bool,
},
}
impl Command {
fn options(&self) -> CliOptions {
match self {
Command::Post { options, .. } | Command::Board { options, .. } => options.clone(),
}
}
}
#[derive(Debug, Clone, Parser)]
struct CliOptions {
#[clap(long)]
use_cache: bool,
#[clap(long)]
text_to_speech: bool,
#[clap(long)]
flatten_details: Option<FlattenDetails>,
#[clap(long)]
jpeg: bool,
#[clap(long)]
resize_icons: Option<Option<u32>>,
#[clap(long)]
output_dir: Option<PathBuf>,
#[clap(long, default_value_t = OutputDirLayout::default())]
output_dir_layout: OutputDirLayout,
#[clap(long, default_value_t = OutputFormat::default())]
output_format: OutputFormat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
enum FlattenDetails {
#[default]
None,
All,
Mixed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
enum OutputDirLayout {
#[default]
Nested,
Flat,
}
impl Display for OutputDirLayout {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Nested => write!(f, "nested"),
Self::Flat => write!(f, "flat"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
enum OutputFormat {
#[default]
Both,
Epub,
Html,
None,
}
impl OutputFormat {
fn epub(self) -> bool {
matches!(self, Self::Epub | Self::Both)
}
fn html(self) -> bool {
matches!(self, Self::Html | Self::Both)
}
}
impl Display for OutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Both => write!(f, "both"),
Self::Epub => write!(f, "epub"),
Self::Html => write!(f, "html"),
Self::None => write!(f, "none"),
}
}
}
pub async fn main() {
simple_logger::init_with_level(log::Level::Info).unwrap();
let command = Command::parse();
let CliOptions {
use_cache,
text_to_speech,
flatten_details,
jpeg,
resize_icons,
output_dir,
output_dir_layout,
output_format,
} = command.options();
let resize_icons = resize_icons.map(|r| r.unwrap_or(100));
let mut html_output_dir = output_dir
.clone()
.unwrap_or_else(|| PathBuf::from(DEFAULT_OUTPUT_DIR));
let mut epub_output_dir = output_dir
.clone()
.unwrap_or_else(|| PathBuf::from(DEFAULT_OUTPUT_DIR));
if output_format == OutputFormat::Both || output_dir.is_none() {
html_output_dir = html_output_dir.join("html/");
epub_output_dir = epub_output_dir.join("epub/");
}
let epub_options = Options {
text_to_speech,
flatten_details: match flatten_details.unwrap_or_default() {
FlattenDetails::All | FlattenDetails::Mixed => true,
FlattenDetails::None => false,
},
jpeg,
resize_icons,
};
let html_options = Options {
text_to_speech,
flatten_details: match flatten_details.unwrap_or_default() {
FlattenDetails::All => true,
FlattenDetails::None | FlattenDetails::Mixed => false,
},
jpeg,
resize_icons,
};
match command {
Command::Post { post_id, .. } => {
log::info!("Downloading post {post_id}");
let thread = Thread::get_cached(post_id, !use_cache)
.await
.unwrap()
.unwrap();
log::info!("Downloaded post {post_id} - {}", &thread.post.subject);
log::info!("Caching all the icons...");
thread.cache_all_icons(false).await;
let name = {
let board = Board::get_cached(thread.post.board.id, !use_cache)
.await
.unwrap()
.unwrap();
let board_posts = BoardPosts::get_all_cached(board.id, !use_cache)
.await
.unwrap()
.unwrap();
thread_filename(
&thread,
&board,
board_posts.iter().map(|p| p.section.clone()),
OutputDirLayout::Flat,
)
};
if output_format.html() {
log::info!("Generating html document {name}...");
let path = html_output_dir.join(format!("{name}.html"));
write(path, thread.to_single_html_page(html_options));
}
if output_format.epub() {
log::info!("Generating epub document {name}...");
let path = epub_output_dir.join(format!("{name}.epub"));
write(path, thread.to_epub(epub_options).await.unwrap());
}
}
Command::Board {
board_id,
single_file: false,
..
} => {
log::info!("Downloading board/continuity {board_id}...");
let continuity = Continuity::get_cached(board_id, !use_cache)
.await
.unwrap()
.unwrap();
log::info!(
"Downloaded continuity {board_id} - {}",
&continuity.board.name
);
log::info!("Caching all the icons...");
continuity.cache_all_icons(false).await;
for thread in &continuity.threads {
let name = thread_filename(
thread,
&continuity.board,
continuity.threads.iter().map(|t| t.post.section.clone()),
output_dir_layout,
);
if output_format.html() {
log::info!("Generating html document {name}...");
let path = html_output_dir.join(format!("{name}.html"));
write(path, thread.to_single_html_page(html_options));
}
if output_format.epub() {
log::info!("Generating epub document {name}...");
let path = epub_output_dir.join(format!("{name}.epub"));
write(path, thread.to_epub(epub_options).await.unwrap());
}
}
}
Command::Board {
board_id,
single_file: true,
..
} => {
if output_format.html() {
log::warn!("HTML output is not supported in single-file mode.");
if output_format == OutputFormat::Html {
return;
}
}
log::info!("Downloading board/continuity {board_id}...");
let continuity = Continuity::get_cached(board_id, !use_cache)
.await
.unwrap()
.unwrap();
log::info!(
"Downloaded continuity {board_id} - {}",
&continuity.board.name
);
log::info!("Caching all the icons...");
continuity.cache_all_icons(false).await;
let name = {
let board_id = continuity.board.id;
let name = slug::slugify(&continuity.board.name);
format!("[{board_id}] {name}")
};
log::info!("Generating epub document {name}...");
let path = epub_output_dir.join(format!("{name}.epub"));
write(path, continuity.to_epub(epub_options).await.unwrap());
}
}
log::info!("Done");
}
fn thread_filename(
thread: &Thread,
board: &Board,
board_thread_sections: impl Iterator<Item = Option<Section>>,
layout: OutputDirLayout,
) -> String {
let board_folder = {
let board_id = board.id;
let board_name = slug::slugify(&board.name);
format!("[{board_id}] {board_name}/")
};
let section_folder = thread
.post
.section
.clone()
.map(|Section { id, name, order }| {
let width = Ord::max(board.board_sections.len().to_string().len(), 2);
let name = slug::slugify(name);
format!("Section #{order:0width$} [{id}] {name}/")
})
.unwrap_or_default();
let post_name = {
let post_id = thread.post.id;
let post_subject = slug::slugify(&thread.post.subject);
let post_order = {
let same_section_count = board_thread_sections
.filter(|s| *s == thread.post.section)
.count();
let width = Ord::max(same_section_count.to_string().len(), 2);
let order = thread.post.section_order;
format!("{order:0width$}")
};
format!("#{post_order} [{post_id}] {post_subject}")
};
format!("{board_folder}{section_folder}{post_name}").replace(
'/',
match layout {
OutputDirLayout::Flat => " ",
OutputDirLayout::Nested => "/",
},
)
}
pub fn write(path: impl AsRef<Path>, contents: impl AsRef<[u8]>) {
std::fs::create_dir_all(path.as_ref().parent().unwrap()).unwrap();
write_if_changed(path, contents).unwrap();
}