# Contributing
This guide has some instructions and tips on how to create a new Keiyoushi extension. Please **read
it carefully** if you're a new contributor or don't have any experience on the required languages
and knowledges.
This guide is not definitive and it's being updated over time. If you find any issues in it, feel
free to report it through a [Meta Issue](https://github.com/yuzono/cursed-manga-extensions/issues/new?assignees=&labels=Meta+request&template=06_request_meta.yml)
or fixing it directly by submitting a Pull Request.
## Table of Contents
- [Contributing](#contributing)
- [Table of Contents](#table-of-contents)
- [Prerequisites](#prerequisites)
- [Tools](#tools)
- [Cloning the repository](#cloning-the-repository)
- [Getting help](#getting-help)
- [Writing an extension](#writing-an-extension)
- [Setting up a new Gradle module](#setting-up-a-new-gradle-module)
- [Loading a subset of Gradle modules](#loading-a-subset-of-gradle-modules)
- [Extension file structure](#extension-file-structure)
- [AndroidManifest.xml (optional)](#androidmanifestxml-optional)
- [build.gradle](#buildgradle)
- [Core dependencies](#core-dependencies)
- [Extension API](#extension-api)
- [lib tools](#lib-tools)
- [Available libs](#available-libs)
- [Adding a lib dependency](#adding-a-lib-dependency)
- [Creating a new lib](#creating-a-new-lib)
- [keiyoushi.utils (core utilities)](#keiyoushiutils-core-utilities)
- [JSON parsing - `parseAs`](#json-parsing---parseas)
- [JSON serialization - `toJsonString` / `toJsonRequestBody`](#json-serialization---tojsonstring--tojsonrequestbody)
- [JSON models (DTOs) and serialization](#json-models-dtos-and-serialization)
- [Protobuf parsing and serialization - `parseAsProto` / `toRequestBodyProto`](#protobuf-parsing-and-serialization---parseasproto--torequestbodyproto)
- [Date parsing - `tryParse`](#date-parsing---tryparse)
- [Filter helpers - `firstInstance` / `firstInstanceOrNull`](#filter-helpers---firstinstance--firstinstanceornull)
- [Next.js data extraction - `extractNextJs` / `extractNextJsRsc`](#nextjs-data-extraction---extractnextjs--extractnextjsrsc)
- [Extracting URLs - `setUrlWithoutDomain` + `absUrl`](#extracting-urls---seturlwithoutdomain--absurl)
- [Additional dependencies](#additional-dependencies)
- [Extension main class](#extension-main-class)
- [Main class key variables](#main-class-key-variables)
- [HTML and Image Processing](#html-and-image-processing)
- [OkHttp and Network](#okhttp-and-network)
- [Extension call flow](#extension-call-flow)
- [Popular Manga](#popular-manga)
- [Latest Manga](#latest-manga)
- [Manga Search](#manga-search)
- [Filters](#filters)
- [Manga Details](#manga-details)
- [Chapter](#chapter)
- [Chapter Pages](#chapter-pages)
- [Misc notes](#misc-notes)
- [Advanced Extension features](#advanced-extension-features)
- [Extension logic and app features](#extension-logic-and-app-features)
- [Configurable Sources and Preferences](#configurable-sources-and-preferences)
- [URL intent filter](#url-intent-filter)
- [Update strategy](#update-strategy)
- [Renaming existing sources](#renaming-existing-sources)
- [Multi-source themes](#multi-source-themes)
- [Creating a new theme](#creating-a-new-theme)
- [Theme directory structure](#theme-directory-structure)
- [Theme build.gradle.kts](#theme-buildgradlekts)
- [Theme main class](#theme-main-class)
- [Using a Theme](#using-a-theme)
- [Running](#running)
- [Debugging](#debugging)
- [Android Debugger](#android-debugger)
- [Logs](#logs)
- [Inspecting network calls](#inspecting-network-calls)
- [Using external network inspecting tools](#using-external-network-inspecting-tools)
- [Setup your proxy server](#setup-your-proxy-server)
- [OkHttp proxy setup](#okhttp-proxy-setup)
- [Building](#building)
- [Submitting the changes](#submitting-the-changes)
- [Pull Request checklist](#pull-request-checklist)
## Prerequisites
Before you start, please note that the ability to use following technologies is **required** and
that existing contributors will not actively teach these to you.
- Basic [Android development](https://developer.android.com/)
- [Kotlin](https://kotlinlang.org/)
- Web scraping
- [HTML](https://developer.mozilla.org/en-US/docs/Web/HTML)
- [CSS selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors)
- [OkHttp](https://square.github.io/okhttp/)
- [JSoup](https://jsoup.org/)
### Tools
- [Android Studio](https://developer.android.com/studio)
- Emulator or phone with developer options enabled and a recent version of Komikku installed
- [Icon Generator](https://as280093.github.io/AndroidAssetStudio/icons-launcher.html)
- [Try jsoup](https://try.jsoup.org/)
### Cloning the repository
Some alternative steps can be followed to skip unrelated sources, which will make it faster to pull,
navigate and build. This will also reduce disk usage and network traffic.
**Due to the large size of this repository, it is highly recommended to do a partial clone to save network traffic and disk space.**
Steps
1. Do a partial clone.
```bash
git clone --filter=blob:none --sparse
cd cursed-manga-extensions/
```
2. Configure sparse checkout.
There are two modes of pattern matching. The default is cone mode.
Cone mode enables significantly faster pattern matching for big monorepos
and the sparse index feature to make Git commands more responsive.
In this mode, you can only filter by file path, which is less flexible
and might require more work when the project structure changes.
You can skip this code block to use legacy mode if you want easier filters.
It won't be much slower as the repo doesn't have that many files.
To enable cone mode together with sparse index, follow these steps:
```bash
git sparse-checkout set --cone --sparse-index
# add project folders
git sparse-checkout add common core gradle lib lib-multisrc utils
# add a single source
git sparse-checkout add src//
```
To remove a source, open `.git/info/sparse-checkout` and delete the exact
lines you typed when adding it. Don't touch the other auto-generated lines
unless you fully understand how cone mode works, or you might break it.
To use the legacy non-cone mode, follow these steps:
```bash
# enable sparse checkout
git sparse-checkout set --no-cone
# edit sparse checkout filter
vim .git/info/sparse-checkout
# alternatively, if you have VS Code installed
code .git/info/sparse-checkout
```
Here's an example:
```bash
/*
!/src/*
!/multisrc-lib/*
# allow a single source
/src//
# allow a multisrc theme
/lib-multisrc/
# or type the source name directly
```
Explanation: the rules are like `gitignore`. We first exclude all sources
while retaining project folders, then add the needed sources back manually.
3. Configure remotes.
```bash
# add upstream
git remote add upstream
# optionally disable push to upstream
git remote set-url --push upstream no_pushing
# optionally fetch master only (ignore all other branches)
git config remote.upstream.fetch "+refs/heads/master:refs/remotes/upstream/master"
# update remotes
git remote update
# track master of upstream instead of fork
git branch master -u upstream/master
```
4. Useful configurations. (optional)
```bash
# prune obsolete remote branches on fetch
git config remote.origin.prune true
# fast-forward only when pulling master branch
git config pull.ff only
# Add an alias to sync master branch without fetching useless blobs.
# If you run `git pull` to fast-forward in a blobless clone like this,
# all blobs (files) in the new commits are still fetched regardless of
# sparse rules, which makes the local repo accumulate unused files.
# Use `git sync-master` to avoid this. Be careful if you have changes
# on master branch, which is bad practice.
git config alias.sync-master '!git switch master && git fetch upstream && git reset --keep FETCH_HEAD'
```
5. Later, if you change the sparse checkout filter, run `git sparse-checkout reapply`.
Read more on
[Git's object model](https://github.blog/2020-12-17-commits-are-snapshots-not-diffs/),
[partial clone](https://github.blog/2020-12-21-get-up-to-speed-with-partial-clone-and-shallow-clone/),
[sparse checkout](https://github.blog/2020-01-17-bring-your-monorepo-down-to-size-with-sparse-checkout/),
[sparse index](https://github.blog/2021-11-10-make-your-monorepo-feel-small-with-gits-sparse-index/),
and [negative refspecs](https://github.blog/2020-10-19-git-2-29-released/#user-content-negative-refspecs).
## Getting help
- Join [the Discord server](https://discord.gg/85MZhUX688) for online help and to ask questions while
developing your extension. When doing so, please ask them in the `#programming` channel.
- There are some features and tricks that are not explored in this document. Refer to existing
extension code for examples.
## Writing an extension
The quickest way to get started is to copy an existing extension's folder structure and renaming it
as needed. We also recommend reading through a few existing extensions' code before you start.
### Setting up a new Gradle module
Each extension should reside in `src//`. Use `all` as `` if your target
source supports multiple languages or if it could support multiple sources.
The `` used in the folder inside `src` should be the major `language` part. For example, if
you will be creating a `pt-BR` source, use `` here as `pt` only. Inside the source class, use
the full locale string instead.
### Loading a subset of Gradle modules
By default, all individual and multisrc extensions are loaded for local development.
This may be inconvenient and can drastically slow down your system when working on a single extension.
To adjust which modules are loaded, make adjustments to the `settings.gradle.kts` file as needed. You can specify the single extension you want to work on in the `load individual extension` function. This helps avoid loading unnecessary modules, making the build process more efficient and preventing your CPU from being overworked.
#### Extension file structure
The simplest extension structure looks like this:
```console
$ tree src///
src///
├── AndroidManifest.xml (optional)
├── build.gradle
├── res
│ ├── mipmap-hdpi
│ │ └── ic_launcher.png
│ ├── mipmap-mdpi
│ │ └── ic_launcher.png
│ ├── mipmap-xhdpi
│ │ └── ic_launcher.png
│ ├── mipmap-xxhdpi
│ │ └── ic_launcher.png
│ └── mipmap-xxxhdpi
│ └── ic_launcher.png
└── src
└── eu
└── kanade
└── tachiyomi
└── extension
└──
└──
├── .kt
├── .kt (optional)
├── .kt (optional)
└── .kt (optional)
```
`` should be an ISO 639-1 compliant language code (two letters or `all`). ``
should be adapted from the site name, and can only contain lowercase ASCII letters and digits.
Your extension code must be placed in the package `eu.kanade.tachiyomi.extension..`.
> [!TIP]
> Additional files in the extension package (like `Dto.kt`, `Filters.kt`, `UrlActivity.kt`)
> should NOT repeat the extension name (e.g. use `Dto.kt` instead of `MySourceNameDto.kt`).
> Note: While older extensions might use the repeated name pattern, avoiding it is a newly enforced convention to maintain consistency across the repository.
#### AndroidManifest.xml (optional)
You only need to create this file if you want to add deep linking to your extension.
See [URL intent filter](#url-intent-filter) for more information.
#### build.gradle
Make sure that your new extension's `build.gradle` file follows the following structure:
```groovy
ext {
extName = ''
extClass = '.'
extVersionCode = 1
isNsfw = true
}
apply plugin: "kei.plugins.extension.legacy"
```
| Field | Description |
|------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `extName` | The name of the extension. Should be romanized if site name is not in English. |
| `extClass` | Points to the class that implements `Source`. You can use a relative path starting with a dot (the package name is the base path). This is used to find and instantiate the source(s). |
| `extVersionCode` | The extension version code. This must be a positive integer and incremented with any change to the code. Do not bump for changes that do not affect users, such as changing a private function to a public function. |
| `isNsfw` | Flag to indicate that a source contains NSFW content. Should always be set explicitly to either `true` or `false`. Falls back to `false` if not set. |
The extension's version name is generated automatically by concatenating `1.4` and `extVersionCode`.
With the example used above, the version would be `1.4.1`.
### Core dependencies
#### Extension API
Extensions rely on [extensions-lib](https://github.com/komikku-app/extensions-lib), which provides
some interfaces and stubs from the [app](https://github.com/komikku-app/komikku) for compilation
purposes. The actual implementations can be found [in the Komikku source code](https://github.com/komikku-app/komikku/tree/master/app/src/master/java/eu/kanade/tachiyomi/source).
Referencing the actual implementation will help with understanding extensions' call flow.
#### lib tools
The `lib/` directory contains reusable Gradle modules that solve common problems shared across
multiple extensions, such as cookie injection, image descrambling, JavaScript deobfuscation, and
more. Before implementing something from scratch, check whether an existing lib already covers your
use case. Each lib is self-documented via KDoc comments and/or a README in its own folder.
#### Available libs
| Module | Description |
|----------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| [`lib-cookieinterceptor`](https://github.com/yuzono/cursed-manga-extensions/tree/master/lib/cookieinterceptor) | Injects cookies into OkHttp requests for a given domain |
| [`lib-cryptoaes`](https://github.com/yuzono/cursed-manga-extensions/tree/master/lib/cryptoaes) | AES-CBC decryption compatible with CryptoJS; JSFuck deobfuscation |
| [`lib-randomua`](https://github.com/yuzono/cursed-manga-extensions/tree/master/lib/randomua) | Fetches and rotates real-world User-Agent strings |
| [`lib-synchrony`](https://github.com/yuzono/cursed-manga-extensions/tree/master/lib/synchrony) | JavaScript deobfuscation via the Synchrony engine (QuickJS sandbox) |
| [`lib-textinterceptor`](https://github.com/yuzono/cursed-manga-extensions/tree/master/lib/textinterceptor) | Renders plain text or HTML as a PNG image page |
| [`lib-unpacker`](https://github.com/yuzono/cursed-manga-extensions/tree/master/lib/unpacker) | Unpacks Dean Edwards-packed JavaScript; substring extraction helpers |
> [!NOTE]
> The table above highlights the most commonly used libraries. Check the `lib/` directory for the full list of available modules and their specific READMEs.
#### Adding a lib dependency
Declare the module in your extension's `build.gradle`:
```groovy
dependencies {
implementation(project(':lib:'))
}
```
For example:
```groovy
dependencies {
implementation(project(':lib:dataimage'))
}
```
Gradle resolves transitive dependencies automatically, so you only need to declare the lib you are
directly using.
#### Creating a new lib
If no existing lib fits your needs and the functionality is generic enough to be shared across
multiple extensions, you can create a new one.
A lib follows this structure:
```console
lib//
├── build.gradle.kts
└── src
└── keiyoushi
└── lib
└──
└── MyLib.kt
```
The `build.gradle.kts` must apply the `kei.plugins.library` plugin:
```kotlin
plugins {
alias(kei.plugins.library)
}
```
If your lib depends on another lib, declare it in the same file:
```kotlin
plugins {
alias(kei.plugins.library)
}
dependencies {
implementation(project(":lib:"))
}
```
Place your code in the package `keiyoushi.lib.`. Document public API with KDoc so
contributors can understand the lib without needing to read `CONTRIBUTING.md`.
#### keiyoushi.utils (core utilities)
The `core/utils` module provides a set of shared extension functions that are available to all extensions
without any extra Gradle dependency. Prefer using these helpers instead of implementing your own equivalents, as they provide standardized and maintained solutions.
The utilities live in the `keiyoushi.utils` package and are imported individually.
##### JSON parsing - `parseAs`
Use `keiyoushi.utils.parseAs` to deserialize JSON. It works on `String`, `Response`, `InputStream`, and `JsonElement` receivers and uses the shared `jsonInstance` (a pre-configured `Json` with `ignoreUnknownKeys = true`). The `Response` and `InputStream` variants use efficient stream decoding and automatically close the stream after reading.
```kotlin
import keiyoushi.utils.parseAs
// From a Response (uses streaming and consumes the body):
val dto = response.parseAs()
// From a String:
val dto = jsonString.parseAs()
// With a transform applied before parsing (e.g., stripping JSONP callbacks):
val dto = response.parseAs { it.substringAfter("callback(").dropLast(1) }
```
**Do not** create a local `private val json: Json by injectLazy()` unless you specifically need a custom JSON configuration (e.g., `isLenient = true` or custom serializers). For standard parsing, the global instance is already available via `jsonInstance` and the `parseAs` helpers use it automatically.
##### JSON serialization - `toJsonString` / `toJsonRequestBody`
Use `keiyoushi.utils.toJsonString` to serialize an object to a JSON string. If you are sending a POST/PUT request, use `keiyoushi.utils.toJsonRequestBody` to directly convert your object into an OkHttp `RequestBody` with the correct `application/json` media type.
```kotlin
import keiyoushi.utils.toJsonRequestBody
import keiyoushi.utils.toJsonString
// To a RequestBody for OkHttp (recommended for APIs):
val body = myRequestDto.toJsonRequestBody()
// To a simple String:
val jsonString = myRequestDto.toJsonString()
```
##### JSON models (DTOs) and serialization
When defining `@Serializable` classes for JSON parsing, **do not** use `data class` unless you actually need data class features (like `copy()` or destructuring). Use a regular `class` instead to reduce the generated bytecode size.
Always use camelCase for Kotlin properties. Only use `@SerialName` when the JSON key does not match the property name (e.g., mapping a snake_case JSON key like `cover_img` to `coverImg`, or an invalid Kotlin identifier like `_count` to `count`). If the JSON key already matches the property name exactly, `@SerialName` is redundant and should be omitted. It is also recommended to make fields `private` if they are only used internally (for instance, when mapping directly to `SManga` or `SChapter` within the DTO).
```kotlin
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
// Bad: Using data class and snake_case variable names
@Serializable
data class MyDto(val manga_id: Int, val cover_img: String)
// Good: Regular class, camelCase variables mapped with @SerialName only when names differ, and private fields
@Serializable
class MyDto(
@SerialName("manga_id") private val mangaId: Int,
@SerialName("cover_img") private val coverImg: String,
private val title: String, // No @SerialName needed if JSON key is "title"
@SerialName("_count") private val count: Int, // Needed for invalid Kotlin identifiers
) {
fun toSManga() = SManga.create().apply {
url = mangaId.toString()
thumbnail_url = coverImg
this.title = title
}
}
```
- **Use `@Serializable` classes instead of `JsonObject`:** Do not manually traverse `JsonObject` or `JsonArray`. Define `@Serializable` classes and use `parseAs()`.
- **Map only used fields:** Do not map all fields from the JSON response in your DTOs if they are not used. Omit unused fields to keep the class clean and reduce bytecode.
- **Mandatory fields should not have defaults:** Do not provide default empty/null values to mandatory fields (like a manga's ID or title) in DTOs just to avoid parsing exceptions. Let the parser fail early so broken entries are detected.
- **Avoid `buildJsonObject` for requests:** Instead of manually building `JsonObject` with `buildJsonObject { put(...) }`, define a `@Serializable` request DTO class and use `toJsonRequestBody()`.
- **Avoid manual JSON string reading:** Avoid manually reading the response body as a string to parse JSON (e.g., `response.body.string()` or `response.peekBody(Long.MAX_VALUE).string()` outside of interceptors). Use `response.parseAs()` directly, which handles efficient stream decoding and automatically closes the response body.
##### Protobuf parsing and serialization - `parseAsProto` / `toRequestBodyProto`
If a source's API uses Protocol Buffers (Protobuf) instead of JSON, use the `keiyoushi.utils` helpers to decode and encode the data. These extensions use a shared `protoInstance` and automatically handle resource management.
```kotlin
import keiyoushi.utils.parseAsProto
import keiyoushi.utils.toRequestBodyProto
import keiyoushi.utils.decodeProtoBase64
// From a Response (automatically closes the body):
val dto = response.parseAsProto()
// From a Response with a transform applied before decoding:
val dto = response.parseAsProto { bytes -> bytes.drop(4).toByteArray() }
// Decoding a Base64-encoded Protobuf string:
val dto = base64String.decodeProtoBase64()
// Creating a RequestBody for a POST request (defaults to application/protobuf):
val requestBody = myRequestDto.toRequestBodyProto()
````
If you only need to work with raw bytes, you can also use `.decodeProto()` and `.encodeProto()` directly on a `ByteArray`.
Do not create a local `private val proto: ProtoBuf by injectLazy()` unless you specifically need a custom configuration. For standard parsing, the global instance is already available and the `parseAsProto` helpers use it automatically.
##### Date parsing - `tryParse`
Use `keiyoushi.utils.tryParse` on a `SimpleDateFormat` instance to safely parse a date string.
It returns `0L` on failure or when the input is `null`, which is exactly what the app expects.
```kotlin
import keiyoushi.utils.tryParse
// Declare dateFormat at class/file level - creating SimpleDateFormat is expensive:
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
chapter.date_upload = dateFormat.tryParse(dateStr)
```
**Do not** write manual try/catch blocks or null-guards around `SimpleDateFormat.parse()` -
`tryParse` handles both. Also, always declare your `SimpleDateFormat` as a class-level or
file-level `val` so it is not reconstructed for every chapter.
Two common mistakes to avoid:
- **Always set `Locale.ROOT`**, unless the pattern contains locale-sensitive text (such as month names) - in which case use the appropriate locale.
- **Set the timezone** if known. Either if the site's region is known, or because the pattern uses a literal `'Z'`.
```kotlin
// Wrong: 'Z' is treated as a literal character, timezone defaults to device local time
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT)
// Correct:
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
// Also correct (Z without quotes parses the timezone offset from the string):
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.ROOT)
```
##### Filter helpers - `firstInstance` / `firstInstanceOrNull`
Use these instead of `filterIsInstance().first()` / `filterIsInstance().firstOrNull()`.
```kotlin
import keiyoushi.utils.firstInstance
import keiyoushi.utils.firstInstanceOrNull
val genreFilter = filters.firstInstanceOrNull()
```
**SharedPreferences - `getPreferences` / `getPreferencesLazy`**
Use these instead of accessing `Injekt` manually.
```kotlin
import keiyoushi.utils.getPreferences
import keiyoushi.utils.getPreferencesLazy
// Eager:
private val preferences = getPreferences()
// Lazy (recommended for most cases):
private val preferences by getPreferencesLazy()
```
##### Next.js data extraction - `extractNextJs` / `extractNextJsRsc`
If the site is built with Next.js, use `keiyoushi.utils.extractNextJs` on a `Document` or `Response`,
or `keiyoushi.utils.extractNextJsRsc` on a raw RSC response string to pull typed data out of the
hydration payload without fragile HTML scraping.
```kotlin
import keiyoushi.utils.extractNextJs
val data = response.extractNextJs()
// Or with an explicit predicate:
val data = document.extractNextJs { element ->
element is JsonObject && "slug" in element
}
```
For client-side navigation responses (`text/x-component` content type), pass the `rsc: 1`
request header and use `extractNextJsRsc` on the response body string.
See [#14266](https://github.com/keiyoushi/extensions-source/pull/14266) and
[#14446](https://github.com/keiyoushi/extensions-source/pull/14446) for real-world usage.
##### Extracting URLs - `setUrlWithoutDomain` + `absUrl`
When extracting URLs from HTML, prefer `element.absUrl("href")` or `element.attr("abs:href")` over manually concatenating `baseUrl` + `path`. Combined with `setUrlWithoutDomain()`, this safely handles both absolute and relative links.
```kotlin
// Risky - setUrlWithoutDomain cannot resolve all relative URLs:
setUrlWithoutDomain(element.attr("href"))
// Safe:
setUrlWithoutDomain(element.absUrl("href"))
```
#### Additional dependencies
If you find yourself needing additional functionality, you can add more dependencies to your `build.gradle`
file. Many of [the dependencies](https://github.com/komikku-app/komikku/blob/master/app/build.gradle.kts)
from the app are exposed to extensions by default.
> [!NOTE]
> Several dependencies are already exposed to all extensions via Gradle's version catalog.
> To view which are available check the `gradle/libs.versions.toml` file.
Notice that we're using `compileOnly` instead of `implementation` if the app already contains it.
You could use `implementation` instead for a new dependency, or you prefer not to rely on whatever
the main app has at the expense of app size.
> [!IMPORTANT]
> Using `compileOnly` restricts you to versions that must be compatible with those used in
> [the latest stable version of the app](https://github.com/komikku-app/komikku/releases/latest).
### Extension main class
The class which is referenced and defined by `extClass` in `build.gradle`. This class should implement
either `SourceFactory` or `HttpSource`.
| Class | Description |
|--------------------|----------------------------------------------------------------------------------------------------------------------------------|
| `SourceFactory` | Used to expose multiple `Source`s. Use this in case of a source that supports multiple languages or mirrors of the same website. |
| `HttpSource` | For online source, where requests are made using HTTP. |
| `ParsedHttpSource` | Deprecated, use `HttpSource` instead. |
#### Main class key variables
| Field | Description |
|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `name` | Name displayed in the "Sources" tab in the app. |
| `baseUrl` | Base URL of the source without any trailing slashes. |
| `lang` | An ISO 639-1 compliant language code (two letters in lower case in most cases, but can also include the country/dialect part by using a simple dash character). |
| `id` | Identifier of your source, automatically set in `HttpSource`. It should only be manually overridden if you need to copy an existing autogenerated ID. |
### HTML and Image Processing
- **Parsing partial HTML:** If an API returns a JSON response containing an HTML string, use `Jsoup.parseBodyFragment(html, baseUrl)` instead of `Jsoup.parse(html)`. Passing the `baseUrl` ensures that `abs:href` and `absUrl()` can correctly resolve relative links.
- **Formatting Chapter Numbers:** Do not write custom `DecimalFormat` logic just to remove trailing zeros from float chapter numbers. Simply use `.toString().removeSuffix(".0")`.
- **Generating Page lists:** The app ignores the `index` passed to the `Page` object, but you must ensure the list itself is sorted correctly according to the source. You can use Kotlin's `mapIndexed` to easily instantiate `Page` objects, or rely on the index provided by the source API if available:
```kotlin
return document.select(".pages img").mapIndexed { index, img ->
Page(index, imageUrl = img.attr("abs:src"))
}
```
- **Memory-efficient Image Interceptors:** When implementing interceptors for descrambling, stitching, or decrypting images, avoid loading the entire image into a `ByteArray`, as this can cause `OutOfMemoryError` on low-end devices. Prefer stream-based processing instead:
- **Read:** Use `response.body.byteStream()` with `BitmapFactory.decodeStream()` to decode images directly from the stream.
- **Write:** Write the processed bitmap into an Okio `Buffer` via `output.outputStream()` and convert it using `asResponseBody(mediaType)`.
- **Decryption:** Use Okio's `cipherSource` extension for stream-based decryption rather than decrypting a full byte array in memory.
- Note: `readByteArray()` should generally be avoided here because it forces full in-memory buffering of the image. Streaming directly keeps memory usage lower and more stable.
- Always wrap network responses in `response.use { ... }` to ensure the response body is properly closed and to prevent memory leaks.
- If applicable, call `bitmap.recycle()` after you're done with it to free native memory early.
- **Do not manually check for Cloudflare:** Do not manually check for Cloudflare challenges (e.g., checking for "Just a moment..." text) in `parse` methods. The app handles this before calling the parser.
- **Prefer stable selectors:** Avoid relying on volatile auto-generated CSS class names (e.g., `styles_Card__jN8og`) or complex regex for parsing. Prefer stable structural selectors.
- **Use `ownText()` to avoid mutation:** To get text from an element without including text from its children, use `.ownText()`. This avoids having to select and remove child elements (`.select().remove()`) or mutate the document.
- **Parse status using `.lowercase()`:** When comparing strings for status parsing (e.g., `contains("ongoing")`), prefer calling `.lowercase()` on the source string once instead of using `ignoreCase = true` on multiple `contains` checks.
### OkHttp and Network
- **Always pass `headers`:** Every `GET()` and `POST()` call must include `headers` (or a custom headers object). Omitting headers will send the request without the app's default User-Agent and other expected headers.
- **Referer header trailing slash:** When setting a `Referer` header pointing to the site root, always include a trailing slash: `.add("Referer", "$baseUrl/")`. This matches what browsers send and is required by some servers.
- **Static URLs don't need `HttpUrl.Builder`:** Use string interpolation directly for URLs with no dynamic query parameters. Only use `HttpUrl.Builder` (or `.toHttpUrl().newBuilder()`) when query parameters need URL-encoding or the URL is built conditionally.
```kotlin
// Unnecessary builder for a static URL:
val url = "$baseUrl/manga".toHttpUrl().newBuilder().build()
// Prefer:
return GET("$baseUrl/manga", headers)
```
- **GraphQL Queries:** If you are sending GraphQL requests, use Kotlin's raw multi-dollar string interpolation (`$$"""..."""`) for your queries. This prevents having to escape every JSON variable `$` symbol manually.
- **Empty checks on `.text()`:** Because Jsoup's `.text()` automatically trims whitespace, you can use `.isNotEmpty()` instead of `.isNotBlank()` when checking for empty strings. The same applies to `.ownText()`. This also means you should not use `.trim()` with these functions.
- **Use `network.client` for Cloudflare:** When overriding the client for sources protected by Cloudflare, simply use `override val client = network.client.newBuilder()...`. The default `client` now handles Cloudflare challenges automatically. Do **not** use `network.cloudflareClient`, as it is deprecated.
- **Never use `Thread.sleep()`:** Do not use `Thread.sleep()` for rate limiting. Use OkHttp's `rateLimitHost` interceptor instead.
- **Avoid synchronous calls in `parse` methods:** Do not call `client.newCall(...).execute()` inside parsing methods like `pageListParse` or `chapterListParse`. Make the request part of the standard flow by overriding the corresponding request method (e.g., `pageListRequest`) or `fetchImageUrl`.
- **Pass `HttpUrl` directly:** The `GET()` and `POST()` helpers accept an `HttpUrl` object. Do not call `.toString()` on a built `HttpUrl` before passing it.
- **Use `HttpUrl` for URL manipulation:** When parsing or extracting parts of a URL, prefer using `HttpUrl` methods (like `pathSegments()` or `queryParameter()`) over manual string splitting or regex. It is safer and handles edge cases better.
- **Use `CookieInterceptor` for custom cookies:** When you need to inject custom cookies into requests, use the `lib-cookieinterceptor` dependency instead of manually adding `Cookie` headers. Manually setting the `Cookie` header overrides all cookies (including Cloudflare cookies set via WebView), breaking login and challenge solving.
### Extension call flow
#### Popular Manga
a.k.a. the Browse source entry point in the app (invoked by tapping on the source name).
- The app calls `fetchPopularManga` which should return a `MangasPage` containing the first batch of
found `SManga` entries.
- This method supports pagination. When user scrolls the manga list and more results must be fetched,
the app calls it again with increasing `page` values (starting with `page=1`). This continues while
`MangasPage.hasNextPage` is passed as `true` and `MangasPage.mangas` is not empty.
- To show the list properly, the app needs `url`, `title` and `thumbnail_url`. You **must** set them
here. The rest of the fields could be filled later (refer to Manga Details below).
#### Latest Manga
a.k.a. the Latest source entry point in the app (invoked by tapping on the "Latest" button beside
the source name).
- Enabled if `supportsLatest` is `true` for a source
- Similar to popular manga, but should be fetching the latest entries from a source.
#### Manga Search
- When the user searches inside the app, `fetchSearchManga` will be called and the rest of the flow
is similar to what happens with `fetchPopularManga`.
- If search functionality is not available, return `Observable.just(MangasPage(emptyList(), false))`
- `getFilterList` will be called to get all filters and filter types.
##### Filters
The search flow has support for filters that can be added to a `FilterList` inside the `getFilterList`
method. When the user changes the filters' state, they will be passed to the `searchRequest`, and they
can be iterated to create the request (by getting the `filter.state` value, where the type varies
depending on the `Filter` used). You can check the [filter types available in Filter.kt](https://github.com/komikku-app/komikku/blob/master/source-api/src/commonMain/kotlin/eu/kanade/tachiyomi/source/model/Filter.kt)
and in the table below.
| Filter | State type | Description |
|--------------------|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `Filter.Header` | None | A simple header. Useful for separating sections in the list or showing any note or warning to the user. |
| `Filter.Separator` | None | A line separator. Useful for visual distinction between sections. |
| `Filter.Select` | `Int` | A select control, similar to HTML's `