StringsTest covers both hex cases, paired and lone surrogate halves, other and malformed escapes being left untouched, and the escaped-backslash case the KDoc describes. CollectionsTest covers chunkedPairs on empty, even-length and odd-length lists and on a sequence. Only tests are added, so nothing needs bumping or building.
8.2 KiB
AGENTS.md
@CONTRIBUTING.md
Import note:
@CONTRIBUTING.mdis expanded automatically by agents that support@-imports (Claude Code, Gemini CLI, OpenCode, Pi, etc. – see agents.md#faq and pi#6291). If your harness does not expand it, readCONTRIBUTING.mddirectly – it is the source of truth. This file is a concise, agent-oriented summary.
Project
Yuzono Anikku/Aniyomi anime extensions – Kotlin + Jsoup/OkHttp scrapers. Each extension is a Gradle module src/<lang>/<source>; shared CMS logic lives as themes in lib-multisrc/<theme>; shared helpers in lib/ and core (keiyoushi.utils, keiyoushi.network).
Commands
Always scope a task to a module. The build has ~330 modules (250 extensions + 73 libs + 9 themes);
an unscoped ./gradlew <task> configures every one of them.
- Verify a change (fast – no APK link/package step):
./gradlew :src:<lang>:<source>:compileDebugKotlin(e.g../gradlew :src:en:anizone:compileDebugKotlin,./gradlew :lib-multisrc:anikototheme:compileDebugKotlin) - Build an installable APK:
./gradlew :src:<lang>:<source>:assembleDebug - Lint/format:
./gradlew :<module>:spotlessCheck(CI) /:<module>:spotlessApply(local) - Build all:
./gradlew assembleDebug– avoid; narrowsettings.gradle.ktsinstead (see below)
Module paths are colon-separated and mirror the directory layout: :src:en:miruro,
:lib-multisrc:anikototheme, :core.
Tests
The repo's only unit tests are in core/src/test/kotlin/keiyoushi/utils/, lib/unpacker/test/kotlin/
and lib/megaextractor/test/. Extensions have none – they are verified by compiling and running
against the live site (CONTRIBUTING.md#submitting-the-changes ~ 1369).
./gradlew --continue :core:testDebugUnitTest :lib:unpacker:testDebugUnitTest :lib:megaextractor:testDebugUnitTest
./gradlew :core:testDebugUnitTest --tests 'keiyoushi.utils.UrlUtilsTest'
Loading a subset of modules
settings.gradle.kts calls loadAllIndividualExtensions(), which includes every extension under
src/. To work on a few, comment it out and list them instead – do not commit the narrowed file:
// loadAllIndividualExtensions()
loadIndividualExtension("en", "miruro")
lib/ and lib-multisrc/ are always loaded wholesale.
Structure
src/<lang>/<source>/build.gradle– extension metadata (extName,extClass,extVersionCodeorthemePkg+overrideVersionCode,isNsfw, optionallibVersion)src/<lang>/<source>/src/eu/kanade/tachiyomi/animeextension/<lang>/<source>/– source code (package must match)lib-multisrc/<theme>/build.gradle.kts– theme base (baseVersionCode,alias(kei.plugins.multisrc))lib-multisrc/<theme>/src/.../multisrc/<theme>/– abstract theme classextends AnimeHttpSourcelib/– reusable libs (lib-cryptoaes,lib-playlistutils, extractors, etc.)core–keiyoushi.utils(parseAs,toJsonRequestBody,tryParse,extractNextJs,absUrl) andkeiyoushi.network(rateLimit,addCookie,OkHttpClient.get/post) – use these, no custom JSON/regex/date/interceptor helpers
Conventions
See CONTRIBUTING.md for full rules. Critical for agents:
- Check
lib/andcore(keiyoushi.utilsfor parsing/prefs,keiyoushi.networkfor clients) first – reuse existing libs/helpers instead of custom boilerplate (parseAs/toJsonRequestBody/tryParse/extractNextJs/absUrl,rateLimit/addCookie,lib-cryptoaes/etc.). SeeCONTRIBUTING.md#core-dependencies. - Kotlin + Android; web scraping via CSS selectors, OkHttp, Jsoup.
- Do not use
data classfor@SerializableDTOs unless needed; camelCase fields,@SerialNameonly when JSON key differs. - Use
response.parseAs<T>(),response.asJsoup(),SimpleDateFormat(...).tryParse(),element.absUrl("href")+setUrlWithoutDomain(). - No hardcoded
User-Agent, noThread.sleep(), no manual Cloudflare checks, nobuildJsonObjectfor requests. - Preserve
idwhen renamingname/lang; keep package name stable. - New or migrated extensions target extensions-lib 16: extend
AnimeHttpSourcedirectly (not thekeiyoushi.utils.AnimeHttpLegacySource/ParsedAnimeHttpLegacySourceshims), implement hosters (getHosterList/getVideoList(hoster)), use the namedVideo(videoUrl =, videoTitle =, ...)constructor, and setlibVersion = 16inbuild.gradle. SeeCONTRIBUTING.md#extensions-lib-16.
keiyoushi.network client helpers
rateLimit(permits, period, interval) { url -> Boolean }replaces the deprecatedeu.kanade.tachiyomi.network.interceptor.rateLimit/rateLimitHost. The predicate receives anHttpUrl, so compare against a host (it.host == "api.example.com"), never a full URL.addCookie(...)replaces the removedlib-cookieinterceptor.OkHttpClient.get/post/put/headare suspend helpers;get/headdefault to a 10-minuteCacheControl, and all default toensureSuccess = true(throws on non-2xx).- Many overloads use Kotlin context parameters (
context(source: AnimeHttpSource)). Inside a class that is anAnimeHttpSourcethe implicit receiver satisfies them, soclient.get(url)and.addCookie("k" to "v")work with nowith(...)wrapper and pick upheaders/baseUrl. - A source whose
baseUrlis preference-backed must rebuild everything derived from it when the preference changes. Usekeiyoushi.utils.LazyMutable(a read/write lazy delegate) forclient,headersand anything built from them, and reassign them all in thebaseUrlsetter – a plainby lazykeeps serving the stale client. Seelib-multisrc/anikototheme/.../AnikotoTheme.kt. - Do not eagerly initialize
clientin a theme base class: the initializer runs before subclass constructors, so an overriddenopen val rateLimitreads0. - Do not create
android.*objects (e.g.LruCache) in a source's constructor or property initializers – useby lazy. The repo publisher instantiates sources on a plain JVM where they throwStub!, which blocks the publish for every extension. SeeCONTRIBUTING.md#misc-notes.
Versioning – bump once per PR, theme bump propagates
Source of truth: CONTRIBUTING.md:287-309 (individual) + CONTRIBUTING.md:1124-1176 (themes) + gradle/build-logic/src/main/kotlin/PluginExtensionLegacy.kt:61:
versionCode = if (theme == null) extVersionCode else theme.baseVersionCode + overrideVersionCode
Rules:
- Individual extension (no theme): increment
extVersionCodeby 1 if code affecting users changed. Bump once per PR – do not increment multiple times across commits. - Theme (
lib-multisrc/<theme>/build.gradle.kts): incrementbaseVersionCodeby 1 when theme logic changes. - When
baseVersionCodeis bumped, do not bumpoverrideVersionCodefor extensions using that theme in the same PR – the addition already bumps every extension's effectiveversionCode. Only bumpoverrideVersionCodewhen the individual extension itself changed independently of the theme. - Checklist mirrors this:
CONTRIBUTING.md#pull-request-checklist~1376-1377and.github/pull_request_template.md:3-4.
CI – what it bumps and builds for you
From .github/workflows/build_pull_request.yml + .github/scripts/:
- Changing anything under
lib/makes CI runbump-versions.py, which auto-bumps every dependent extension. Do not bump those by hand – only bump the module you actually edited. - CI builds only the changed modules (
generate-build-matrices.py→<module>:assembleDebug), never the whole tree. - Head-commit message prefixes:
[skip bumping version]skips the auto-bump;[skip yuzono ci]skips the bump and the build.
Boundaries
- Do not change
versionNamemanually (generated<libVersion>.<versionCode>, lib version defaults to14). - Do not commit
web_hi_res_512.png(delete after Icon Generator). - Do not push to
upstream(no_pushing– fork workflowCONTRIBUTING.md:170-196). Useorigin(your fork) for PRs. - Never commit secrets, keystore, or
local.properties.
PR Instructions
- Follow
CONTRIBUTING.md#submitting-the-changes~1353-1384checklist; test build in Android Studio. - Title: keep concise; reference issues (
Closes #xyz). - One version bump per module per PR as above.