# mtag-cli

Music Tag Organizer for self-built media libraries such as Plex, Emby, and Jellyfin.

`mtag` scans a source music folder, reads audio tag metadata, and writes a clean
artist/album folder layout. It is conservative by default: files are copied, existing
destinations cause an error, and `--dry-run` lets you inspect the plan first.

## Install

Requires Rust 1.95 or newer.

```bash
cargo install mtag-cli
```

## Quick Start

Preview the organization plan:

```bash
mtag ./Inbox ./Music --dry-run
```

Copy files into the target library:

```bash
mtag ./Inbox ./Music
```

Keep existing files and write duplicates as `song (1).mp3`, `song (2).mp3`, and so on:

```bash
mtag ./Inbox ./Music --on-conflict rename
```

## Usage

```text
Usage: mtag [OPTIONS] <MUSIC_FOLDER> [TARGET_FOLDER]

Arguments:
  <MUSIC_FOLDER>   Folder that contains source music files
  [TARGET_FOLDER]  Folder where organized files will be written [default: Music]

Options:
      --dry-run                    Print planned file operations without writing files
      --move                       Move files instead of copying them
      --on-conflict <ON_CONFLICT>  How to handle existing destination files [default: fail] [possible values: fail, skip, overwrite, rename]
      --template <TEMPLATE>        Organization template [default: {album_artist}/{album}/{file_name}]
  -h, --help                       Print help
  -V, --version                    Print version
```

## Example Layout

Before:

```text
Inbox/
  compilation/
    01 - Hooked On A Feeling.mp3
    02 - Go All The Way.mp3
  pink-floyd/
    101 - In the Flesh.mp3
    101 - In the Flesh.lrc
    102 - The Thin Ice.mp3
  sanitize/
    01 - Thunder Test.mp3
```

After:

```text
Music/
  AC_DC/
    Live_ 2026_/
      01 - Thunder Test.mp3
  Pink Floyd/
    The Wall/
      101 - In the Flesh.mp3
      101 - In the Flesh.lrc
      102 - The Thin Ice.mp3
  Various Artists/
    Awesome Mix Vol. 1/
      01 - Hooked On A Feeling.mp3
      02 - Go All The Way.mp3
```

## Organization Rules

- `album_artist` is preferred for the top-level folder when the tag provides it.
- If `album_artist` is missing, mtag uses the track artist.
- If multiple artists share the same album in the same source folder, mtag places that album under `Various Artists`.
- Same-named albums from different source folders are not merged into `Various Artists`.
- A same-stem `.lrc` file next to an audio file is copied or moved with that audio file.
- Unsafe path characters such as `/`, `\`, `:`, `?`, and `..` are sanitized before folders are created.

## Conflict Policies

Use `--on-conflict` to choose how existing destination files are handled:

```text
fail       Stop with an error. This is the default.
skip       Keep the existing destination and skip the new file.
overwrite  Replace the existing destination.
rename     Keep both files by writing to the next "name (N).ext" path.
```

## Templates

The default template is:

```text
{album_artist}/{album}/{file_name}
```

Supported variables:

```text
{album_artist}
{album}
{artist}
{disc}
{track}
{title}
{file_name}
```

Example:

```bash
mtag ./Inbox ./Music --template "{album_artist}/{album}/{track} - {title}.mp3"
```

Each slash-separated template segment is rendered and sanitized as one path component.

## Library API

The crate exposes the CLI pipeline as small Rust modules:

```rust,no_run
use std::path::Path;

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

# fn main() -> Result<(), Box<dyn std::error::Error>> {
let files = scan_audio_files(Path::new("./Inbox"))?;
let tracks = files
    .iter()
    .map(|path| read_track_metadata(path))
    .collect::<Result<Vec<_>, _>>()?;
let plan = build_copy_plan(&tracks, Path::new("./Music"), &OrganizationOptions::default())?;
let summary = execute_plan(&plan, &ExecutionOptions::default())?;

println!("planned: {}", summary.planned);
# Ok(())
# }
```

Generate local Rust documentation:

```bash
cargo doc --no-deps --open
```

For stricter documentation checks:

```bash
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --locked
```

## Development

Run the same checks used before publishing:

```bash
cargo fmt --check
cargo test --all-targets --locked
cargo test --doc --locked
cargo clippy --all-targets --all-features --locked -- -D warnings
cargo package --locked
```

See `RELEASE.md` for the release flow and crates.io publishing requirements.
