use std::path::PathBuf; use clap::Parser; use tracing_subscriber::EnvFilter; use shanty_db::Database; use shanty_org::{DEFAULT_FORMAT, OrgConfig, organize_from_db, organize_from_directory}; #[derive(Parser)] #[command(name = "shanty-org", about = "Organize music files into a clean directory structure")] struct Cli { /// Source directory of music files (standalone mode, reads tags from files). #[arg(long)] source: Option, /// Organize all tracks known to the database. #[arg(long)] from_db: bool, /// Target root directory for organized files. #[arg(long)] target: PathBuf, /// Format template for directory structure and filenames. #[arg(long, default_value = DEFAULT_FORMAT)] format: String, /// Preview changes without moving files. #[arg(long)] dry_run: bool, /// Copy files instead of moving them. #[arg(long)] copy: bool, /// Database URL. Defaults to sqlite:///shanty/shanty.db?mode=rwc #[arg(long, env = "SHANTY_DATABASE_URL")] database: Option, /// Increase verbosity (-v info, -vv debug, -vvv trace). #[arg(short, long, action = clap::ArgAction::Count)] verbose: u8, } fn default_database_url() -> String { let data_dir = dirs::data_dir() .unwrap_or_else(|| PathBuf::from(".")) .join("shanty"); std::fs::create_dir_all(&data_dir).ok(); let db_path = data_dir.join("shanty.db"); format!("sqlite://{}?mode=rwc", db_path.display()) } #[tokio::main] async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); let filter = match cli.verbose { 0 => "warn", 1 => "info,shanty_org=info", 2 => "info,shanty_org=debug", _ => "debug,shanty_org=trace", }; tracing_subscriber::fmt() .with_env_filter( EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(filter)), ) .init(); if !cli.from_db && cli.source.is_none() { anyhow::bail!("specify either --from-db or --source "); } if cli.from_db && cli.source.is_some() { anyhow::bail!("--from-db and --source are mutually exclusive"); } let config = OrgConfig { target_dir: cli.target, format: cli.format, dry_run: cli.dry_run, copy: cli.copy, }; if config.dry_run { println!("DRY RUN — no files will be moved"); } let stats = if cli.from_db { let database_url = cli.database.unwrap_or_else(default_database_url); tracing::info!(url = %database_url, "connecting to database"); let db = Database::new(&database_url).await?; organize_from_db(db.conn(), &config).await? } else { let source = cli.source.unwrap(); if !source.is_dir() { anyhow::bail!("'{}' is not a directory", source.display()); } organize_from_directory(&source, &config).await? }; println!("\nOrganization complete: {stats}"); Ok(()) }