#![cfg_attr(not(test), windows_subsystem = "windows")]
use crate::{output::OutputFolder, output::OutputFolderKey, time::UnixEpochTs};
use anyhow::{Context as _, Result, ensure};
use std::path::{Path, PathBuf};
mod changelog;
mod cli_update;
mod db;
mod fetch;
mod gui;
mod gui_dummy;
mod lr2folder;
mod mass_edit;
mod migrations;
mod output;
mod table;
mod time;
mod url;
pub(crate) fn build_reqwest() -> Result<reqwest::Client> {
const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
reqwest::ClientBuilder::new()
.user_agent(APP_USER_AGENT)
.build()
.context("failed to build reqwest client")
}
pub(crate) fn open_db(
path: &Path,
outputs: &[OutputFolder],
) -> Result<(rusqlite::Connection, Vec<table::Table>)> {
fn validate_good_outputs(outputs: &[OutputFolder]) -> Result<()> {
if let Some(p) = OutputFolder::is_any_parent_of_another(outputs) {
anyhow::bail!(
"intersecting output paths found: '{}:{}' and '{}:{}'",
p.0.0,
p.0.1.display(),
p.1.0,
p.1.1.display()
);
}
for OutputFolder(_key, path) in outputs {
lr2folder::validate_good_playlists_folder(path)
.context("bad playlists folder selected")?;
}
Ok(())
}
fn validate_outputs_for_tables(
tables: &[table::Table],
outputs: &[OutputFolder],
) -> Result<()> {
for table::Table(table, add_data) in tables {
ensure!(
outputs.iter().any(|o| o.0 == add_data.output),
"missing --output '{}' required for table '{}'",
add_data.output.0,
table.name,
);
}
Ok(())
}
validate_good_outputs(outputs)?;
let (db, tables) = db::open_from_file(path)?;
validate_outputs_for_tables(&tables, outputs)?;
Ok((db, tables))
}
pub(crate) fn save_db(
db: &mut rusqlite::Connection,
outputs: &[OutputFolder],
tables: &mut Vec<table::Table>,
write_tags: bool,
) -> Result<()> {
table::Table::commit_removals(tables);
let tx = db
.transaction()
.context("failed to start transaction for updating db")?;
db::save_db(&tx, tables)?;
if write_tags {
db::update_tags_inplace(&tx)?;
}
lr2folder::write_files(outputs, tables)?;
tx.commit()
.context("failed to commit after writing playlists")?;
Ok(())
}
fn main() -> Result<()> {
const HELP: &str = concat!(env!("CARGO_PKG_NAME"), " ", env!("CARGO_PKG_VERSION"), "
Usage:
gui:
--db <path> Path to song.db
--output <key:path> Folders to write playlists to (multiple allowed)
--save-on-update-finish <true/false> Enable or disable saving on update completion (default: 'true')
--write-tags <true/false> Enable or disable writing tags (default: 'false')
update:
--db <path> Path to song.db
--output <key:path> Folders to write playlists to (multiple allowed)
--write-tags <true/false> Enable or disable tag writing (default: 'false')");
fn fixup_outputs(mut outputs: Vec<OutputFolder>) -> Vec<OutputFolder> {
outputs.reverse();
outputs.sort_by(|a, b| a.0.0.cmp(&b.0.0)); outputs.dedup_by(|a, b| a.0.0 == b.0.0);
outputs
}
env_logger::Builder::from_env(
env_logger::Env::default().default_filter_or("lr2_oxytabler=info"),
)
.init();
let mut args = pico_args::Arguments::from_env();
if args.contains("--help") {
println!("{HELP}");
return Ok(());
}
match args.subcommand()?.as_deref() {
Some("gui") => {
let app = (|| {
let db = args.value_from_str::<_, PathBuf>("--db")?;
let outputs = fixup_outputs(args.values_from_str::<_, OutputFolder>("--output")?);
let write_tags_on_save = args
.opt_value_from_str::<_, bool>("--write-tags")?
.unwrap_or(false);
let save_on_update_finish = args
.opt_value_from_str::<_, bool>("--save-on-update-finish")?
.unwrap_or(true);
let left = args.finish();
ensure!(left.is_empty(), "Unsupported arguments: {left:?}");
gui::App::new(&db, outputs, save_on_update_finish, write_tags_on_save)
})();
match app {
Ok(app) => app.run(),
Err(e) => {
let e = format!("{e:#}");
log::error!("{e}");
gui_dummy::run(e)
}
}
}
Some("update") => {
let db = args.value_from_str::<_, PathBuf>("--db")?;
let outputs = fixup_outputs(args.values_from_str::<_, OutputFolder>("--output")?);
let write_tags = args
.opt_value_from_str::<_, bool>("--write-tags")?
.unwrap_or(false);
let left = args.finish();
ensure!(left.is_empty(), "Unsupported arguments: {left:?}");
cli_update::run(&db, &outputs, write_tags)
}
Some(cmd) => anyhow::bail!("invalid cmd: {cmd}"),
None => {
let left = args.finish();
ensure!(left.is_empty(), "Unsupported arguments: {left:?}");
log::warn!("hint: see --help:\n\n{HELP}");
gui_dummy::run(HELP.to_string())
}
}
}