# 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.