Introduction
This guide outlines how to create an Aidoku source using the aidoku-rs library.
Asking Questions
If this guide doesn’t answer any questions you may have, feel free to reach out in the #source-dev
channel in the Aidoku Discord server. Someone there will be happy to
help.
Note that Aidoku doesn’t endorse any sources proxying illegal or NSFW content, so please keep discussion limited to technical aspects of source development.
Other Resources
Getting Started
At a high level, Aidoku sources use network requests to read data from websites and parse it into content that the Aidoku application can read and display to users. A source can be written for any website that provides content that can be grouped into “series” (or, manga) and “chapters” that have “pages” that can be read (plain text content, or images).
Aidoku sources are WebAssembly programs, and the aidoku-rs library and aidoku cli tool enable you to write Rust programs that can be compiled into source packages that the Aidoku app runs.
Prerequisites
Prerequisites
Since aidoku-rs is the only maintained wrapper for the Aidoku source API, all sources are written using the Rust programming language. In-depth knowledge of Rust isn’t necessary, but familiarity with other programming concepts is. For a guide to learning more about Rust, check out the Rust book.
Familiarity with HTML and CSS selectors is also necessary when creating sources that rely on web scraping.
Additionally, you will need a physical iOS device or iOS simulator installed through Xcode for running sources.
Tooling
Tooling
The first step is to install Rust, if not already installed. The recommended method is to do so using rustup, which makes updating and managing your installation easy later on.
With Rust installed, the next step is to install the proper toolchain for compiling for WebAssembly. You can do so with the following command:
rustup install wasm32-unknown-unknown
In addition to the Rust tooling, the aidoku cli provides all the other necessary functionality for source development. To install it, run:
cargo install --git https://github.com/Aidoku/aidoku-rs aidoku-cli
Creating a Source
With Rust and the aidoku cli tool installed, you are ready to create an Aidoku source.
To create a new project, you can use aidoku in a similar fashion as cargo to initialize the
required files and directory structure:
aidoku init source_dir
You will then be prompted to enter the following parameters:
- Source name: the name displayed for the installed source.
- Source URL: a link to whatever website you are creating a source for. If the website has multiple
or alternative domains, you can configure more by editing the
source.jsonfile later. - Languages: if the website language(s) are shown in the list, you can select them using the arrow keys and space bar, then press enter to confirm. Otherwise, select the “other” option and you will be prompted to enter any additional (ISO 639) language codes, separated by spaces.
- Content rating: if the website content is largely 18+, select “Primarily NSFW content” (or, if you open the website and can reasonably expect to see NSFW content). If the website contains no NSFW content, select safe. This helps users filter sources they to install or show.
These parameters are used to populate an initial source.json file that you can edit to provide
additional functionality for your source. We will explain this in the next section.
Packaging the Source
To compile the Rust program and package the resource files together in an aix file, you can run
the following command:
aidoku package
If the build succeeds, you will have a resulting package.aix file in your current directory that
can be installed in Aidoku. We will explain in a later chapter the best methods for installing and
iterating through development builds.
Resource Files
Resource Files
Unique to Aidoku source projects is the res directory, which contains additional files that are
packaged with the source when compiled. There are three different JSON files that are used for
configuration, each of which has a corresponding JSON schema that can be used for type hints and
autocompletion in IDEs.
To configure this for Visual Studio Code, you can set the
json.schemas key in settings.json:
"json.schemas": [
{
"fileMatch": ["*/res/source.json"],
"url": "https://raw.githubusercontent.com/Aidoku/aidoku-rs/refs/heads/main/crates/cli/src/supporting/schema/source.schema.json"
},
{
"fileMatch": ["*/res/filters.json"],
"url": "https://raw.githubusercontent.com/Aidoku/aidoku-rs/refs/heads/main/crates/cli/src/supporting/schema/filters.schema.json"
},
{
"fileMatch": ["*/res/settings.json"],
"url": "https://raw.githubusercontent.com/Aidoku/aidoku-rs/refs/heads/main/crates/cli/src/supporting/schema/settings.schema.json"
}
]
Similarly for Zed (recommended for Rust development), you can configure the
json-language-server parameters with the same array of schemas:
"lsp": {
"json-language-server": {
"settings": {
"json": {
"schemas": [
...
]
}
}
}
}
Note that these schemas can also be checked against packaged sources by using aidoku:
aidoku verify package.aix
source.json
The source.json file is the only required configuration file, which has three components: “info”
(required), “listings” (optional), and “config” (optional). Here is an example of how the file
should be structured:
{
"info": {
"id": "en.example-source",
"name": "Example Source",
"version": 1,
"url": "https://aidoku.app",
"contentRating": 0,
"languages": ["en"]
},
"listings": [
{
"id": "test",
"name": "Test"
}
],
"config": {
"supportsTagSearch": true
}
}
| Field | Description |
|---|---|
info.id | A unique identifier for the source. Conventionally, this should be the language and source name, concatenated with a period. |
info.name | The displayed name of the source. |
info.altNames | An array of additional names that the source should be searchable by. |
info.version | The source’s version number. It must be a positive integer and incremented with any notable changes. |
info.url | The source’s main URL, which can be used for deep linking. |
info.urls | An array of the source’s URLs, If the source has multiple domains, this should be used instead of info.url. |
info.contentRating | The NSFW level of the source. 0 for sources with no NSFW content at all, 1 for some NSFW, and 2 for majority NSFW sources. |
info.languages | An array of ISO 639 language codes that the source supports. |
info.minAppVersion | Optional minimum Aidoku app version supported by the source. |
info.maxAppVersion | Optional maximum Aidoku app version supported by the source. |
listings | An array of listings that the source supports. Listings can have an id, name, and kind being a grid (0) or list (1). |
config.languageSelectType | Either “single” or “multi”, defaulting to “single”. If the app should allow selecting multiple enabled languages. |
config.supportsArtistSearch | If the source should allow filtering by series artists without having provided an “artist” text filter. |
config.supportsAuthorSearch | config.supportsArtistSearch but for authors. |
config.supportsTagSearch | If the source should allow filtering by pressing on a tag, even if it doesn’t exist in the source filters. |
config.allowsBaseUrlSelect | If selecting a URL from info.urls should be enabled in the source settings. |
config.breakingChangeVersion | The source version at which a breaking change that requires migration was made. |
config.hidesFiltersWhileSearching | If filters should be disabled with an active search query. |
filters.json (optional)
The filters.json file allows you to configure static filters for Aidoku to display in the UI. In
addition to providing static filters, you can also programatically define “dynamic” filters using
the DynamicFilters trait in your source code, which will be appended to whatever static filters
you have defined. However, static filters are preferred whenever possible due to quicker loading and
better inference for various features.
Filters are defined as JSON objects with varying attributes depending on the “type”:
| Filter Type | Description |
|---|---|
text | An input field for text content (e.g. author or artist searching). |
sort | A sort filter, optionally supporting both ascending and descending directions. |
check | A single toggle option. |
select | A list allowing selection of a single value. |
multi-select | A list allowing selection of multiple values, optionally supporting exclusion as well as inclusion. |
note | Non-editable text for information or explanation. |
range | An editable “from” and “to” range of numeric values. |
settings.json (optional)
Similar to the filters.json file, the settings.json file allows you to define static settings
for your source, with dynamic ones able to be provided through the DynamicSettings trait.
Like filters, settings are JSON objects with varying attributes depending on the “type”:
| Setting Type | Description |
|---|---|
group | Used to group other settings into sections with optional header and footer text. |
select | A list allowing selection of a single value. |
multi-select | A list allowing selection of multiple values. |
switch | A toggle with an on and off state. |
stepper | A number stepper |
segment | A segmented control for selecting a single value from a few options. |
text | An input field for text content. |
button | A button that can trigger an action for the source to execute programmatically. |
link | A button that links to a web page. |
login | A login setting, allowing for simple username/password entry or oauth flow handling. |
page | Used to group settings into a nested page. |
editable-list | An editable list of items, allowing the user to add and remove arbitrary text entries. |
Settings values can be fetched and set programmatically using the defaults functions provided by aidoku-rs.
icon.png
Aidoku expects a PNG image file to display as the source icon. The image should be square, and ideally 128x128 with no transparency.
The Rust Program
The Rust Program
Aidoku sources are fundamentally no_std
Rust programs that compile to WebAssembly. Thus, any Rust crates that support no_std (or no_std
with an allocator) can be utilized as dependencies.
aidoku-rs expects sources to be written by implementing traits that it provides to a struct, then
passing that struct to the register_source! macro.
Traits
All sources must implement the one required Source trait, and then can optionally implement any other traits for more features. Check here for a complete list of traits that aidoku-rs provides.
The Source trait has four required functions:
- new: performs any setup required for the source to function (e.g. setting global rate limit) and initializes the source struct.
- get_search_manga_list:
returns a single page of results, with a query and list of filters if input by the user. Filter
values will correspond to any of the configured filters in
filters.json, and page numbers will start at1. If theMangaPageResultyou return hashas_next_pageset totrue, then the function will be called again with the page number incremented if the user scroll down to load another page of results.Mangareturned in the result entries only need to have theid,title, andcoverattributes populated. - get_manga_update:
if the
needs_detailsparameter istrue, then as many of the availablemangastruct attributes should be filled as possible to provide the UI with information to display. Ifneeds_chaptersistrue, thechaptersattribute should be set. This function may be called with either or both of the booleans set totrue, but not neither. - get_page_list: returns a list of pages for a given manga and chapter.
Most sources should implement the DeepLinkHandler trait, to allow users to replace the “http” or “https” in a url with “aidoku” to open the app directly to whatever series or chapter the web page was displaying. This may also be used in the future by the app to open urls in the app directly.
After implementing a basic source that supports searching and filtering, your next step would be considering if you want to implement listings with the ListingProvider trait and a home page with the Home trait.
Registering
The register_source! macro should be called with the source struct name as the first parameter,
and then all the other traits the struct implements apart from the Source trait. For example:
struct MySource;
impl Source for MySource { ... }
impl ImageRequestProvider for MySource { ... }
impl DeepLinkHandler for MySource { ... }
register_source!(MySource, ImageRequestProvider, DeepLinkHandler);
Under the hood, this macro generates the actual functions that are exposed for Aidoku to call directly, and converts the raw data sent from the app to the source into Rust structs for the source to manipulate, then converts the structs the source returns in its implemented functions back into raw data for the app to read. If you implement a trait for your source, the app won’t take it into account unless it is properly registered.
Error Handling
All aidoku-rs trait functions expect a Result<..., AidokuError> to be returned, and most library
functions return Results with an AidokuError, or errors that can be converted into an
AidokuError. This means that the ? operator can be used liberally, allowing the app to handle
any errors. In addition, there are two helper macros to assist with creating errors yourself:
error! and bail!. Both macros allow you to provide a message (that supports formatting like
format! and println!) that the app will display in the UI if encountered.
error! simply constructs an AidokuError, and is typically used for something like this:
let img_element = html.select("img").ok_or_else(|| error!("img element not found"))?;
Whereas bail! will return immediately. It can be used like this:
let Some(img_element) = html.select("img") else {
bail!("img element not found");
};
Network Requests
Nearly every Aidoku source will need to use network requests to fetch data from its target website, be it HTML content or API responses, such as JSON data. As a simple example:
let response: Response = Request::get("http://example.com")?
.header("User-Agent", "Aidoku")
.send()?;
assert_eq!(response.status_code(), 200);
The other commonly used functions that “send” the network request and return a result are
data to get
the raw byte data,
string to
get the text content, and
html to
automatically parse as HTML. If the “json” feature is enabled for the aidoku-rs dependency, the
json_owned function can be used to automatically parse the response data using
serde_json.
Note that all function calls that send the network request are blocking, and will pause your source execution until the data is available to return. When performing multiple network requests that don’t depend on each other, you can consider parallelizing them with Request::send_all.
HTML Parsing
Aidoku provides APIs for interacting with the SwiftSoup library, which allows you to extract data from HTML elements using CSS selectors. The main functions you should be aware of are Element::select, Element::select_first, Element::text, and Element::attr.
let html = Request::get("http://aidoku.app")?.html()?;
let text = html
.select_first("main.home .features > .feature h2")
.and_then(|element| element.text());
let image = html
.select_first(".image > img")
.and_then(|element| element.attr("abs:src"));
Running
After compiling your source with aidoku package (or aidoku pkg), which generates the
package.aix file, you have a few options for running.
Xcode
If you are running macOS and have Xcode installed, a convenient method may be using the iOS simulator for testing sources, but it requires a more complex setup. Since Aidoku is open source, you can clone the GitHub repo and install it to a simulator. Then, simply drag and drop the “package.aix” file onto the simulator. If Aidoku is attached to the debugger in Xcode, logs will be displayed in the Xcode log viewer.
Physical Device
Otherwise, you can choose to either transfer the aix file to the device manually (e.g. via AirDrop),
or serve a source list with aidoku serve package.aix. The latter method will give you a
localhost URL that you can add as a source list in Aidoku on any device connected to the same
network. Your source can then be installed from the “add source” view inside Aidoku, and will show
as an update if you increment the source version number and recompile. Note that you may need to
pull down to refresh the source lists on the browse tab.
Debugging
Now with a general understanding of what goes into creating and running an Aidoku source, we can discuss methods for debugging issues.
Logging
The age-old solution for debugging is a bunch of print statements. Like any standard Rust program,
you can use the println! macro to do this, but there are a few options for viewing the resulting
logs. The standard method is to use the “Display Logs” button in the Advanced app settings. As
mentioned in the previous chapter, logs will also be displayed in the Xcode log viewer if the
debugger is attached to Aidoku.
However, you may also notice the “Log Server” setting above the “Display Logs” button. Aidoku will
post any log messages to the provided server URL, and you can run your own server with aidoku:
aidoku logcat
Enter the URL given to you by the command execution in the “Log Server” input field on a device connected to the same network and all logs should be streamed.
Writing Tests
While Rust programs targeting WebAssembly programs don’t support the standard Rust test runner, aidoku-rs provides a custom test runner that simulates Aidoku’s environment. This doesn’t provide a complete set of the available source APIs, and there may be a few differences from the Aidoku app, but it should be enough for simple tests. To get started, install the test runner with the following command:
cargo install --git https://github.com/Aidoku/aidoku-rs aidoku-test-runner
And then configure the project to use the test runner in the .cargo/config.toml file:
[build]
target = "wasm32-unknown-unknown"
[target.wasm32-unknown-unknown]
runner = "aidoku-test-runner"
Projects created with aidoku init should already have the config.toml file configured and
aidoku-test added as a dev dependency. To use this, you write tests as you normally would in Rust,
but attach #[aidoku_test] to test functions rather than #[test]:
#![allow(unused)]
fn main() {
mod test {
use super::*;
use aidoku_test::aidoku_test;
#[aidoku_test]
fn test_js_execution() {
use aidoku::imports::js::JsContext;
let context = JsContext::new();
let result = context.eval("1 + 2");
assert_eq!(result, Ok(String::from("3")));
}
}
}
If you run into any issues using the test runner, please create an issue on the GitHub repo so we can improve it.