Skip to content

Using ani-cli-rs as a Rust Library

ani-cli-rs is primarily distributed as a command-line application, but its core functionality is also exposed as a reusable Rust library crate.

The library is published under the crate name ani-cli-rs and is exposed to Rust code through the ani_lib library target.

This allows other Rust applications to use the provider clients, stream resolution, HLS relay, downloading, history, and player-related functionality without depending on the CLI application itself.

Note: The library API is still under active development. Public APIs may change between releases while the library is being stabilized.

Crate setup

The package is named ani-cli-rs, while its library target is named ani_lib:

[package]
name = "ani-cli-rs"
version = "0.9.4"

[lib]
name = "ani_lib"
path = "src/lib.rs"

Add it to your application's Cargo.toml:

[dependencies]
ani-cli-rs = "0.9.6"

Then import functionality from ani_lib:

use ani_lib::{
    AnikotoCzClient,
    SearchResult,
    StreamLink,
    TranslationType,
};

Repository dependency

If you are developing against a version of ani-cli-rs that has not yet been published to crates.io, Cargo can use a Git repository instead:

[dependencies]
ani-cli-rs = {
    git = "https://github.com/vorlie/ani-cli-rs.git"
}

For applications that need a specific revision, pin the dependency to a commit:

[dependencies]
ani-cli-rs = {
    git = "https://github.com/vorlie/ani-cli-rs.git",
    rev = "a430c6e6a203dcf9fac8817e132bffcbd4bac11f"
}

Pinning a commit is useful when your application depends on an API that is still changing. It also ensures that another developer or CI system builds against exactly the same revision.

What is exposed by the library?

The crate currently exposes the following main components:

API Purpose
AnikotoClient Anikoto API catalog client
AnikotoCzClient Anikoto.cz catalog client
SearchResult Search result metadata
StreamLink Resolved media stream metadata
SubtitleTrack Subtitle track metadata
TranslationType Sub/dub selection
HlsRelay Local HLS relay lifetime
relay_stream Create a local HLS relay
download_stream Download a resolved stream
DownloadOptions Download configuration
HistoryStore Persistent playback history
Player External player integration
PlayerKind Supported player selection
PlayerOptions Player configuration
choose_quality Select an appropriate stream quality
expand_episode_selection Expand episode selections
I18n / Locale Localization support
AniError / Result Library error handling

The exact public API should be treated as version-dependent while the library is still being stabilized.


Basic provider usage

A simple application can create an AnikotoCzClient, search for an anime, and then retrieve its episodes and streams.

use ani_lib::{
    AnikotoCzClient,
    TranslationType,
};

#[tokio::main]
async fn main() -> ani_lib::Result<()> {
    let client = AnikotoCzClient::new()?;

    let results = client
        .search("Mushoku Tensei", TranslationType::Sub)
        .await?;

    for result in results {
        println!("{} ({})", result.name, result.id);
    }

    Ok(())
}

The client is asynchronous, so applications should run it inside a Tokio runtime.

Searching

Search results are returned as SearchResult values.

A typical workflow is:

search
  ↓
SearchResult
  ↓
episodes
  ↓
StreamLink
  ↓
playback / download

For example:

let results = client
    .search("Mushoku Tensei", TranslationType::Sub)
    .await?;

let show = results
    .first()
    .ok_or_else(|| ani_lib::AniError::NotFound(
        "No results found".into()
    ))?;

Applications should generally keep the complete SearchResult rather than only storing its display name, since the provider-specific ID is required for subsequent requests.


Getting episodes

Once a SearchResult has been selected, retrieve its available episodes:

let episodes = client
    .episodes(&show.id, TranslationType::Sub)
    .await?;

for episode in &episodes {
    println!("Episode {episode}");
}

The library represents episode identifiers as strings:

Vec<String>

This is intentional because provider episode identifiers are not guaranteed to always be simple integers.

For example, an application should not assume that every episode can safely be parsed with:

episode.parse::<u32>()

Resolving streams

After selecting an episode, request its available streams:

let streams = client
    .streams(
        &show.id,
        "1",
        TranslationType::Sub,
    )
    .await?;

for stream in &streams {
    println!(
        "{} - {} ({})",
        stream.resolution,
        stream.provider,
        if stream.hls { "HLS" } else { "Direct" }
    );
}

The resulting StreamLink contains the information needed by a player or downloader.

Conceptually:

StreamLink
├── URL
├── resolution
├── provider
├── HLS/direct classification
├── downloadability
├── request headers
└── optional subtitle tracks

Applications should use the StreamLink returned by the library rather than attempting to reconstruct provider URLs themselves.


Subtitles

Streams can contain optional subtitle tracks.

The public model is:

pub struct SubtitleTrack {
    pub label: String,
    pub url: String,
    pub default: bool,
}

For example:

for subtitle in &stream.subtitles {
    println!(
        "{}: {}{}",
        subtitle.label,
        subtitle.url,
        if subtitle.default {
            " (default)"
        } else {
            ""
        }
    );
}

The subtitle URL should be treated as provider-controlled data. Applications should not assume that it uses the same host as the media stream.

If a player requires subtitles to be loaded through a local relay, the application is responsible for providing that relay path.


HLS playback

HLS streams require additional handling when consumed by applications that cannot directly access the provider's media resources.

ani-cli-rs exposes:

pub use hls_relay::{
    HlsRelay,
    relay_stream,
    relay_stream_without_hls_subtitles,
};

A relay can be created from a StreamLink:

use ani_lib::relay_stream;

let (relay, local_stream) = relay_stream(&stream).await?;

println!("Local playback URL: {}", local_stream.url);

The important detail is that the returned HlsRelay must remain alive while the stream is being played:

let (relay, local_stream) = relay_stream(&stream).await?;

// Keep `relay` alive while the player is using `local_stream`.
play(local_stream.url).await?;

// Relay is dropped after playback finishes.
drop(relay);

Dropping the relay shuts down its local server.

Why is a relay needed?

Some provider HLS resources require request-specific handling and cannot be consumed directly by every embedded player or WebView.

The relay provides a loopback URL such as:

http://127.0.0.1:<port>/...

while keeping the upstream provider request handling inside the library.

The relay binds to loopback rather than exposing itself as an internet-facing proxy.


Example: application playback pipeline

A desktop application can combine the APIs into a complete playback flow:

use ani_lib::{
    relay_stream,
    AnikotoCzClient,
    TranslationType,
};

#[tokio::main]
async fn main() -> ani_lib::Result<()> {
    let client = AnikotoCzClient::new()?;

    let results = client
        .search("Mushoku Tensei", TranslationType::Sub)
        .await?;

    let show = results
        .first()
        .ok_or_else(|| ani_lib::AniError::NotFound(
            "No anime found".into()
        ))?;

    let episodes = client
        .episodes(&show.id, TranslationType::Sub)
        .await?;

    let episode = episodes
        .first()
        .ok_or_else(|| ani_lib::AniError::NotFound(
            "No episodes found".into()
        ))?;

    let streams = client
        .streams(
            &show.id,
            episode,
            TranslationType::Sub,
        )
        .await?;

    let stream = streams
        .first()
        .ok_or_else(|| ani_lib::AniError::NotFound(
            "No streams found".into()
        ))?;

    if stream.hls {
        let (_relay, local_stream) = relay_stream(stream).await?;

        println!("Play: {}", local_stream.url);

        // Keep `_relay` alive until playback finishes.
    } else {
        println!("Play: {}", stream.url);
    }

    Ok(())
}

A real application would normally add quality selection, error handling, subtitle handling, and player integration around this flow.


Using ani-cli-rs from Tauri

One practical use case for the library is embedding it into a Rust desktop application.

For example, Kioku uses ani_lib from its Tauri backend to provide its playback functionality.

Its dependency can be pinned directly to a Git revision during development:

[dependencies]
ani_lib = {
    package = "ani-cli-rs",
    git = "https://github.com/vorlie/ani-cli-rs.git",
    rev = "a430c6e6a203dcf9fac8817e132bffcbd4bac11f"
}

The important distinction here is:

package = "ani-cli-rs"

refers to the Cargo package name, while:

use ani_lib::...;

refers to the library target exposed by that package.

Tauri command example

A Tauri backend can expose the provider API to a frontend:

#[tauri::command]
pub async fn playback_search(
    query: String,
    translation: TranslationType,
) -> Result<Vec<SearchResult>, String> {
    let client = AnikotoCzClient::new()
        .map_err(|error| error.to_string())?;

    client
        .search(&query, translation)
        .await
        .map_err(|error| error.to_string())
}

The frontend can then invoke the command without needing to know anything about the provider implementation.

This gives the application a clean separation:

┌─────────────────────────────┐
│          Kioku UI           │
│       React / TypeScript    │
└──────────────┬──────────────┘
               │ Tauri commands
               ▼
┌─────────────────────────────┐
│       Kioku Rust backend    │
│        playback.rs          │
└──────────────┬──────────────┘
               │ ani_lib
               ▼
┌─────────────────────────────┐
│          ani_lib             │
│                             │
│  Anikoto clients             │
│  Stream resolution           │
│  HLS relay                   │
│  Downloads                   │
│  Player integration          │
└─────────────────────────────┘

This is preferable to duplicating provider extraction logic inside the application.


Keeping the dependency reproducible

When using the Git dependency during development, there are two common approaches.

Track a branch

[dependencies]
ani_lib = {
    package = "ani-cli-rs",
    git = "https://github.com/vorlie/ani-cli-rs.git",
    branch = "master"
}

This is convenient when actively developing both projects.

However, the dependency can change whenever the branch moves.

Pin a commit

[dependencies]
ani_lib = {
    package = "ani-cli-rs",
    git = "https://github.com/vorlie/ani-cli-rs.git",
    rev = "a430c6e6a203dcf9fac8817e132bffcbd4bac11f"
}

This is preferable for reproducible builds when depending on an unreleased library API.

Once a compatible library version is published, applications should generally prefer the versioned crate dependency:

[dependencies]
ani-cli-rs = "0.9"

API stability

The library target is currently considered a developing API.

This means applications using ani_lib should expect that:

  • public types may gain or lose fields;
  • functions may change signatures;
  • provider-specific behavior may change;
  • error variants may change;
  • playback and relay APIs may evolve;
  • provider implementations can change independently of the public API.

For applications that need a stable build, pin a known-good release or Git commit rather than tracking the development branch indefinitely.

The CLI and library share the same underlying provider implementation, so provider changes can affect both.


Recommended integration pattern

For a desktop media application, the recommended architecture is:

Application
│
├── Search
│   └── AnikotoCzClient::search()
│
├── Series selection
│   └── AnikotoCzClient::episodes()
│
├── Episode selection
│   └── AnikotoCzClient::streams()
│
├── Quality/source selection
│   └── StreamLink
│
├── Playback
│   ├── direct stream
│   └── relay_stream() for HLS
│
├── Subtitles
│   └── StreamLink.subtitles
│
└── Download
    └── download_stream()

Keep provider-specific logic inside ani_lib whenever possible.

The application should primarily deal with the shared models:

SearchResult
StreamLink
SubtitleTrack
TranslationType

This makes it possible to change the provider implementation without requiring the UI or playback layer to understand how media URLs are extracted.


Kioku

Kioku is one example of an application currently integrating ani_lib.

Kioku uses the library from its Tauri backend rather than implementing the Anikoto provider logic itself. Its playback system uses the shared SearchResult and StreamLink models and the library's HLS relay for streams that require local handling.

Kioku is currently not a finished application and should be considered a development/integration example rather than a reference implementation of a stable ani_lib API.

Its integration is nevertheless useful for demonstrating how the library can be embedded into a larger Rust application with a non-CLI frontend.


Related documentation