zantetsu 0.2.0

Ultra-fast, intelligent library for anime metadata extraction and normalization
Documentation
# zantetsu

Ultra-fast anime metadata extraction and canonical title matching for Rust.

[![crates.io](https://img.shields.io/crates/v/zantetsu.svg)](https://crates.io/crates/zantetsu)
[![docs.rs](https://docs.rs/zantetsu/badge.svg)](https://docs.rs/zantetsu)
[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

## What it does

`zantetsu` parses anime release filenames into structured metadata and
optionally resolves the parsed title to a canonical entry with cross-referenced
Kitsu / AniList / MAL identifiers.

```
[SubsPlease] Frieren - Beyond Journey's End - 01 (1080p) [A1B2C3D4].mkv
     │                  │                     │    │
  group              title                episode  resolution
```

No machine learning. No network calls required for parsing. No runtime
downloads. Entirely self-contained.

## Install

```toml
[dependencies]
zantetsu = "0.2"
```

Requires Rust 1.85+.

## Quick start

### Parse a filename

```rust
use zantetsu::{EpisodeSpec, Zantetsu};

let engine = Zantetsu::new()?;
let result = engine.parse("[SubsPlease] Cowboy Bebop - 01 [1080p][HEVC].mkv")?;

assert_eq!(result.title.as_deref(),  Some("Cowboy Bebop"));
assert_eq!(result.group.as_deref(),  Some("SubsPlease"));
assert_eq!(result.episode,           Some(EpisodeSpec::Single(1)));
println!("resolution: {:?}", result.resolution); // Some(P1080)
# Ok::<(), Box<dyn std::error::Error>>(())
```

### Match to a canonical title (local Kitsu dump)

```rust,no_run
use zantetsu::{MatchSource, TitleMatcher};

let matcher = TitleMatcher::new(
    MatchSource::kitsu_dump("/home/user/.local/share/zantetsu/kitsu-dumps"),
)?;

if let Some(hit) = matcher.match_title("Sousou no Frieren")? {
    println!("canonical: {}", hit.canonical_title);   // "Frieren: Beyond Journey's End"
    println!("score    : {:.2}", hit.score);           // 0.97
    println!("anilist  : {:?}", hit.ids.anilist);      // Some(154587)
}
# Ok::<(), Box<dyn std::error::Error>>(())
```

### Match to a canonical title (remote GraphQL)

```rust,no_run
use zantetsu::{MatchSource, TitleMatcher};

let matcher = TitleMatcher::new(
    MatchSource::remote_endpoint("https://graphql.anilist.co"),
)?;

if let Some(hit) = matcher.match_title("Spy x Family")? {
    println!("{} ({})", hit.canonical_title, hit.ids.anilist.unwrap_or(0));
}
# Ok::<(), Box<dyn std::error::Error>>(())
```

## Parsed fields

| Field | Type | Example |
|---|---|---|
| `title` | `Option<String>` | `"Cowboy Bebop"` |
| `group` | `Option<String>` | `"SubsPlease"` |
| `episode` | `Option<EpisodeSpec>` | `Single(1)`, `Range(1,3)`, `Special(0.5)` |
| `resolution` | `Option<Resolution>` | `P1080`, `P720`, `P2160` |
| `video_codec` | `Option<VideoCodec>` | `H265`, `H264`, `AV1` |
| `audio_codec` | `Option<AudioCodec>` | `AAC`, `FLAC`, `AC3` |
| `source` | `Option<MediaSource>` | `BluRay`, `WebDL`, `HDTV` |
| `confidence` | `f32` | `0.92` |

## Quality scoring

Score a parse result against a configurable quality profile:

```rust
use zantetsu::{QualityProfile, Zantetsu};

let engine = Zantetsu::new()?;
let result = engine.parse("[EMBER] Violet Evergarden - 01 [BD 1080p HEVC FLAC].mkv")?;
let profile = QualityProfile::default();
let scores  = engine.score(&result, &profile);

println!("video score: {}", scores.video);
println!("audio score: {}", scores.audio);
# Ok::<(), Box<dyn std::error::Error>>(())
```

## Crate family

`zantetsu` is a façade over two focused sub-crates. Depend on the façade for
most use cases; depend on the sub-crates directly if you need finer control.

| Crate | Purpose |
|---|---|
| [`zantetsu`]https://crates.io/crates/zantetsu | **This crate** — unified public API |
| [`zantetsu-core`]https://crates.io/crates/zantetsu-core | Heuristic filename parser, quality scoring, types |
| [`zantetsu-vecdb`]https://crates.io/crates/zantetsu-vecdb | Canonical title matching, `TitleMatcher` |

## Error handling

All fallible functions return `Result<T, ZantetsuError>`. Errors implement
`std::error::Error` via `thiserror`.

```rust
use zantetsu::{Zantetsu, ZantetsuError};

let engine = Zantetsu::new()?;
match engine.parse("") {
    Err(ZantetsuError::EmptyInput) => eprintln!("filename is empty"),
    Err(e)  => eprintln!("error: {e}"),
    Ok(res) => println!("{:?}", res.title),
}
# Ok::<(), Box<dyn std::error::Error>>(())
```

## License

MIT