Skip to content

Your first plugin ​

Copy the starter repo to build your first source plugin. It uses a made-up site at example.org and has offline parser tests. Replace its URL and selectors to support a real site.

If Rhai is new to you, read what it is and why Aikatsu uses it first.

Before you start ​

Install these tools before building the CLI:

  • A current stable Rust toolchain through rustup, including rustc and cargo.
  • On Linux, a C/C++ compiler and linker, CMake, pkg-config, and Clang with its libclang development library. These build the core's native dependencies even though the CLI has no graphical interface.
  • On Windows, the MSVC Rust toolchain, Visual Studio Build Tools with the C++ workload and Windows SDK, CMake, NASM, and LLVM with libclang. Build from a Visual Studio developer command prompt so the compiler and linker are on PATH.
  • Git to clone the Aikatsu repository, or download and extract its source archive.

Keep the full checkout, including Cargo.lock and .cargo/config.toml, and run the commands below from its root. Cargo downloads the Rust dependencies, including Rhai, on the first build. That build needs internet access, even though the starter tests run offline afterward.

The HTTP client's native TLS dependency uses libclang to generate Rust bindings. If the build reports that it can't find libclang, set LIBCLANG_PATH to the folder containing libclang.so on Linux or libclang.dll on Windows.

You don't need Qt, Flutter, Node.js, a separate Rhai installation, or an app signing key for this tutorial. The files you'll edit are index.json, the source script, and its test script, all included in the starter below.

Get the CLI ready ​

From an Aikatsu checkout:

sh
cargo build -p aikatsu-core --bin aikatsu-plugin

The binary is target/debug/aikatsu-plugin, or target/debug/aikatsu-plugin.exe on Windows. This builds the core CLI without the Qt or Flutter interfaces. After building it once, you can run that binary directly while editing your scripts. Script changes don't require rebuilding the CLI.

Run the starter tests:

sh
cargo run -p aikatsu-core --bin aikatsu-plugin -- test docs/public/examples/hello-source

All five tests should pass without making a network request.

Copy the starter ​

The files are included at docs/public/examples/hello-source/ in this checkout. Copy that folder into your own repo, or download each file:

Keep this layout so the CLI can find the tests:

text
hello-source/
├── index.json
├── sources/
│   └── example.rhai
└── tests/
    └── example.rhai

Read the source script ​

The app calls search, episodes, heights, and stream. This starter keeps parsing in separate functions so tests can give them HTML directly.

rust
// A teaching example for a made-up site. Replace the URL and selectors.
const BASE = "https://example.org";

fn absolute(path) { url_join(global::BASE + "/", path) }

fn parse_search(html) {
    let results = [];
    for card in select(html, ".anime-card") {
        let links = select(card.html, "a.title");
        if links.is_empty() { continue; }
        let link = links[0];
        let href = attr(link, "href");
        if href == "" || link.text == "" { continue; }
        let images = select(card.html, "img");
        results.push(#{
            name: clean_sub_indo(link.text),
            url: absolute(href),
            image: if images.is_empty() { "" } else { absolute(attr(images[0], "src")) }
        });
    }
    results
}

fn parse_episodes(html) {
    let episodes = [];
    for link in select(html, "a.episode") {
        let number = regex_match(link.text, #"(?i)episode\s+(\d+)"#);
        let href = attr(link, "href");
        if number == () || href == "" { continue; }
        episodes.push(#{
            num: parse_int(number[1]),
            url: absolute(href),
            title: link.text
        });
    }
    episodes
}

fn parse_sources(html) {
    let sources = [];
    for item in select(html, "video source") {
        let height = height_of(attr(item, "data-quality"));
        let src = attr(item, "src");
        if src == "" || height <= 0 { continue; }
        let url = absolute(src);
        let parsed = url_parse(url);
        if parsed != () && (parsed.scheme == "https" || parsed.scheme == "http") {
            sources.push(#{ height: height, url: url });
        }
    }
    sources
}

fn search(query) {
    parse_search(page(global::BASE + "/?s=" + url_encode(query)))
}

fn episodes(url) { parse_episodes(page(url)) }

fn heights(url) {
    parse_sources(page(url)).map(|source| source.height)
}

fn stream(url, quality) {
    let candidates = sort_by_height(parse_sources(page(url)), quality);
    for candidate in candidates {
        if playable(candidate.url, global::BASE + "/") {
            return #{ url: candidate.url, referer: global::BASE + "/" };
        }
    }
    throw "Example: no playable video for this episode";
}

The example expects search cards with .anime-card, episode links with a.episode, and direct video sources inside <video>. Real sites often use different markup, embeds, or JSON endpoints. Replace the selectors to match your site's pages.

Adapt it to a real site ​

  1. Change BASE to the site's actual URL.
  2. Save small HTML examples from its search, series, and episode pages.
  3. Update each parser to match those pages.
  4. Return absolute URLs. A relative URL alone won't tell the player which host to use.
  5. Resolve the real media URL in stream, not just the watch-page URL.
  6. Keep the site's name in errors so people know which source failed.

page and fetch make HTTP requests. They don't run a site's JavaScript like a browser. If JavaScript builds the video URL, reproduce the requests or parsing needed to get it.

Keep parser tests close ​

The starter includes these parser tests:

rust
fn test_search_cards() {
    let html = `<article class="anime-card"><a class="title" href="/anime/test">Test Show Subtitle Indonesia</a><img src="/cover.jpg"></article>`;
    let results = parse_search(html);
    assert_eq(results.len(), 1);
    assert_eq(results[0].name, "Test Show");
    assert_eq(results[0].url, "https://example.org/anime/test");
    assert_eq(results[0].image, "https://example.org/cover.jpg");
}

fn test_empty_search() {
    assert_eq(parse_search("<p>No matches</p>"), []);
}

fn test_episode_numbers() {
    let html = `<a class="episode" href="/watch/2">Episode 2</a><a class="episode" href="/watch/1">Episode 1</a><a class="episode" href="/about">About</a>`;
    let episodes = parse_episodes(html);
    assert_eq(episodes.len(), 2);
    assert_eq(episodes[0].num, 2);
    assert_eq(episodes[1].url, "https://example.org/watch/1");
}

fn test_video_sources() {
    let html = `<video><source src="/video-480.mp4" data-quality="480p"><source src="/video-720.mp4" data-quality="720p"></video>`;
    let sources = parse_sources(html);
    assert_eq(sources.len(), 2);
    assert_eq(sort_by_height(sources, "720p")[0].height, 720);
    assert_eq(sort_by_height(sources, "best")[0].url, "https://example.org/video-720.mp4");
}

fn test_missing_links_and_video_urls() {
    assert_eq(parse_search(`<article class="anime-card"><a class="title">No link</a></article>`), []);
    assert_eq(parse_episodes(`<a class="episode">Episode 1</a>`), []);
    assert_eq(parse_sources(`<video><source data-quality="720p"><source src="javascript:alert(1)" data-quality="720p"></video>`), []);
}

The tests call parse_search, parse_episodes, and parse_sources with saved HTML, so you can test parser changes offline.

The app sorts episodes by number after reading the result, so a parser doesn't have to return them in order. Still, each number should refer to the right episode.

Refresh the index ​

After editing a script, run the CLI's index command against your repo folder. For the included example, from the Aikatsu root:

sh
cargo run -p aikatsu-core --bin aikatsu-plugin -- index docs/public/examples/hello-source

For your own repo, replace that last path. The command compiles each listed script and recalculates its SHA-256. It doesn't bump your plugin version automatically.

Once your offline tests pass, try a live check, then publish the repo.

Aikatsu is free software, licensed under GPL-3.0.