mtag-cli 0.2.0

Organize music for self-built media libraries like Plex, Emby, and Jellyfin
Documentation
use std::path::PathBuf;

use anyhow::Context;
use clap::Parser;

use mtag_cli::{
    executor::{execute_plan, ConflictStrategy, ExecutionMode, ExecutionOptions},
    metadata::read_track_metadata,
    planner::{build_copy_plan, OrganizationOptions, DEFAULT_TEMPLATE},
    scanner::scan_audio_files,
};

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Cli {
    #[arg(
        value_name = "MUSIC_FOLDER",
        help = "Folder that contains source music files"
    )]
    music_folder_path: PathBuf,
    #[arg(
        value_name = "TARGET_FOLDER",
        default_value = "Music",
        help = "Folder where organized files will be written"
    )]
    target_folder_path: PathBuf,
    #[arg(long, help = "Print planned file operations without writing files")]
    dry_run: bool,
    #[arg(long = "move", help = "Move files instead of copying them")]
    move_files: bool,
    #[arg(
        long,
        value_enum,
        default_value_t = ConflictStrategy::Fail,
        help = "How to handle existing destination files"
    )]
    on_conflict: ConflictStrategy,
    #[arg(long, default_value = DEFAULT_TEMPLATE, help = "Organization template")]
    template: String,
}

fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();
    let audio_file_list = scan_audio_files(&cli.music_folder_path)?;
    let mut skipped_metadata = 0usize;
    let mut tracks = Vec::new();

    for audio_path in &audio_file_list {
        match read_track_metadata(audio_path) {
            Ok(track) => tracks.push(track),
            Err(error) => {
                skipped_metadata += 1;
                eprintln!("skip: {error}");
            }
        }
    }

    let plan = build_copy_plan(
        &tracks,
        &cli.target_folder_path,
        &OrganizationOptions {
            template: cli.template,
        },
    )
    .context("failed to build organization plan")?;

    if cli.dry_run {
        for task in &plan.tasks {
            println!("{} -> {}", task.from.display(), task.to.display());
        }
    }

    let summary = execute_plan(
        &plan,
        &ExecutionOptions {
            conflict_strategy: cli.on_conflict,
            mode: if cli.move_files {
                ExecutionMode::Move
            } else {
                ExecutionMode::Copy
            },
            dry_run: cli.dry_run,
        },
    )?;

    println!(
        "planned: {}, copied: {}, moved: {}, skipped: {}, renamed: {}, metadata skipped: {}",
        summary.planned,
        summary.copied,
        summary.moved,
        summary.skipped,
        summary.renamed,
        skipped_metadata
    );

    Ok(())
}