Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.json file 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
	}
}
FieldDescription
info.idA unique identifier for the source. Conventionally, this should be the language and source name, concatenated with a period.
info.nameThe displayed name of the source.
info.altNamesAn array of additional names that the source should be searchable by.
info.versionThe source’s version number. It must be a positive integer and incremented with any notable changes.
info.urlThe source’s main URL, which can be used for deep linking.
info.urlsAn array of the source’s URLs, If the source has multiple domains, this should be used instead of info.url.
info.contentRatingThe 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.languagesAn array of ISO 639 language codes that the source supports.
info.minAppVersionOptional minimum Aidoku app version supported by the source.
info.maxAppVersionOptional maximum Aidoku app version supported by the source.
listingsAn array of listings that the source supports. Listings can have an id, name, and kind being a grid (0) or list (1).
config.languageSelectTypeEither “single” or “multi”, defaulting to “single”. If the app should allow selecting multiple enabled languages.
config.supportsArtistSearchIf the source should allow filtering by series artists without having provided an “artist” text filter.
config.supportsAuthorSearchconfig.supportsArtistSearch but for authors.
config.supportsTagSearchIf the source should allow filtering by pressing on a tag, even if it doesn’t exist in the source filters.
config.allowsBaseUrlSelectIf selecting a URL from info.urls should be enabled in the source settings.
config.breakingChangeVersionThe source version at which a breaking change that requires migration was made.
config.hidesFiltersWhileSearchingIf 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 TypeDescription
textAn input field for text content (e.g. author or artist searching).
sortA sort filter, optionally supporting both ascending and descending directions.
checkA single toggle option.
selectA list allowing selection of a single value.
multi-selectA list allowing selection of multiple values, optionally supporting exclusion as well as inclusion.
noteNon-editable text for information or explanation.
rangeAn 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 TypeDescription
groupUsed to group other settings into sections with optional header and footer text.
selectA list allowing selection of a single value.
multi-selectA list allowing selection of multiple values.
switchA toggle with an on and off state.
stepperA number stepper
segmentA segmented control for selecting a single value from a few options.
textAn input field for text content.
buttonA button that can trigger an action for the source to execute programmatically.
linkA button that links to a web page.
loginA login setting, allowing for simple username/password entry or oauth flow handling.
pageUsed to group settings into a nested page.
editable-listAn 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 at 1. If the MangaPageResult you return has has_next_page set to true, then the function will be called again with the page number incremented if the user scroll down to load another page of results. Manga returned in the result entries only need to have the id, title, and cover attributes populated.
  • get_manga_update: if the needs_details parameter is true, then as many of the available manga struct attributes should be filled as possible to provide the UI with information to display. If needs_chapters is true, the chapters attribute should be set. This function may be called with either or both of the booleans set to true, 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.