Skip to content

NH: refactor to HttpSource and apiV2#11

Open
AaronDeimos wants to merge 5 commits intoyuzono:masterfrom
AaronDeimos:nh
Open

NH: refactor to HttpSource and apiV2#11
AaronDeimos wants to merge 5 commits intoyuzono:masterfrom
AaronDeimos:nh

Conversation

@AaronDeimos
Copy link
Copy Markdown
Contributor

@AaronDeimos AaronDeimos commented Mar 30, 2026

Close: #10
Supersede: #9
Ai assisted

  • Restored last version before removed from keiyoushi
  • Fix and refactor for the new structure
  • Moved everything to api
  • Everything tested and working
  • Version code is 57 to stay consistent with last version but let me know if i should make it 55 instead

Checklist:

  • Updated extVersionCode value in build.gradle for individual extensions
  • Updated overrideVersionCode or baseVersionCode as needed for all multisrc extensions
  • Referenced all related issues in the PR body (e.g. "Closes #xyz")
  • Added the isNsfw = true flag in build.gradle when appropriate
  • Have not changed source names
  • Have explicitly kept the id if a source's name or language were changed
  • Have tested the modifications by compiling and running the extension through Android Studio
  • Have removed web_hi_res_512.png when adding a new extension

Summary by Sourcery

Refactor the NHentai extension to use the v2 HTTP API and HttpSource, updating models, parsing, and favorites handling while integrating shared random user agent helpers and enforcing getMangaUrl overrides.

New Features:

  • Add API v2-based pagination, gallery, and favorites support for NHentai using JSON endpoints.
  • Introduce reusable random user agent helper utilities with preferences for desktop, mobile, and custom user agents.
  • Add a Spotless lint step that enforces getMangaUrl overrides when the random UA library is used.

Bug Fixes:

  • Restore and correct NHentai extension behavior after previous structural changes, including proper favorites authorization via access tokens and 401 handling.

Enhancements:

  • Switch NHentai from ParsedHttpSource with HTML parsing to HttpSource with typed DTOs, simplifying title, chapter, and page mapping.
  • Refine NHentai tag utilities to produce cleaner tag descriptions and sorted tag lists.
  • Use the NHentai config API to dynamically select image and thumbnail servers for pages and covers.
  • Replace legacy random UA extension integration with new header-based helpers and context-receiver usage across modules.

Build:

  • Bump NHentai extension version code from 54 to 57.
  • Add okhttp-brotli as a compileOnly dependency to the randomua library.
  • Enable the -Xcontext-parameters compiler flag in core, lib-android, and lib-multisrc modules.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 30, 2026

Reviewer's Guide

Refactors the NHentai extension to use the JSON-based api/v2 via HttpSource, replaces HTML parsing with typed DTOs, reworks favorites and pagination, introduces a reusable random user agent helper library enforced by a Spotless lint check, and bumps the extension and build tooling to support context receivers and Brotli.

Sequence diagram for NHentai favorites API authorization flow

sequenceDiagram
    participant User
    participant Tachiyomi as TachiyomiApp
    participant NH as NHentaiSource
    participant Client as OkHttpClient
    participant Interceptor as authorizationInterceptor
    participant CookieMgr as CookieManager
    participant API as NHentaiApiV2

    User->>Tachiyomi: Open favorites for NHentai
    Tachiyomi->>NH: fetchSearchManga(page, query, filters with FavoriteFilter)
    NH->>NH: searchMangaRequest(...) builds https://nhentai.net/api/v2/favorites
    NH->>Client: newCall(favoritesRequest)
    Client->>Interceptor: intercept(request)

    alt request.path contains /favorites
        Interceptor->>CookieMgr: getCookie(baseUrl)
        CookieMgr-->>Interceptor: cookies string
        Interceptor->>Interceptor: extract access_token
        Interceptor->>Interceptor: update accessToken cache
        alt accessToken not blank
            Interceptor->>Client: proceed(request with Authorization User accessToken)
        else accessToken blank
            Interceptor->>Client: proceed(original request)
        end
    else other paths
        Interceptor->>Client: proceed(original request)
    end

    Client->>API: GET /api/v2/favorites
    API-->>Client: HTTP 200 with JSON or HTTP 401

    alt response.code == 401 and path contains /favorites
        Client-->>Interceptor: Response 401
        Interceptor->>Interceptor: clear accessToken
        Interceptor->>Client: close response
        Interceptor-->>Tachiyomi: throw IOException(Log in via WebView)
        Tachiyomi-->>User: Show error "Log in via WebView to view favorites"
    else success
        Client-->>NH: Response 200
        NH->>NH: searchMangaParse(response) using PaginatedResponse~GalleryListItem~
        NH-->>Tachiyomi: MangasPage
        Tachiyomi-->>User: Render favorites list
    end
Loading

Sequence diagram for random user agent selection in headersBuilder

sequenceDiagram
    participant Source as HttpSource
    participant Headers as HeadersBuilder
    participant Prefs as SharedPreferences
    participant Helper as UserAgentPreferenceHelpers
    participant UAHelper as HelperKt
    participant Network as NetworkHelper.client
    participant UADB as UA_DB_server

    Source->>Source: headersBuilder()
    Source->>Headers: super.headersBuilder()
    Source->>Helper: setRandomUserAgent(headers, userAgentType=null, filterInclude=["chrome"], filterExclude=[])

    Helper->>Source: getPreferences()
    Source-->>Helper: SharedPreferences
    Helper->>Prefs: getPrefUAType()
    Prefs-->>Helper: UserAgentType
    Helper->>Prefs: getPrefCustomUA()
    Prefs-->>Helper: customUA or null

    alt random type != OFF
        Helper->>UAHelper: getRandomUserAgent(type, filterInclude, filterExclude)
        alt cached userAgent exists
            UAHelper-->>Helper: cached userAgent
        else no cache
            UAHelper->>Network: newCall(GET UA_DB_URL)
            Network->>UADB: GET user-agents.json
            UADB-->>Network: JSON desktop and mobile lists
            Network-->>UAHelper: Response
            UAHelper->>UAHelper: parseAs UserAgentList
            UAHelper->>UAHelper: filter include and exclude
            UAHelper-->>Helper: random user agent
        end
        Helper->>Headers: set(User-Agent, userAgent)
    else random type OFF and customUA not null
        Helper->>Headers: set(User-Agent, customUA)
    else no UA
        Helper-->>Headers: return without changes
    end

    Source-->>Source: headers with possibly randomized User-Agent
Loading

Class diagram for NHentai HttpSource refactor and DTOs

classDiagram
    direction LR

    class NHentai {
        +String lang
        +String nhLang
        +String baseUrl
        +String apiUrl
        +Long id
        +String name
        -Json json
        -CookieManager webViewCookieManager
        -String accessToken
        -SharedPreferences preferences
        +OkHttpClient client
        -Boolean displayFullTitle
        -Regex shortenTitleRegex
        +setupPreferenceScreen(screen PreferenceScreen)
        +headersBuilder() Headers.Builder
        +popularMangaRequest(page Int) Request
        +popularMangaParse(response Response) MangasPage
        +latestUpdatesRequest(page Int) Request
        +latestUpdatesParse(response Response) MangasPage
        +fetchSearchManga(page Int, query String, filters FilterList) Observable~MangasPage~
        +searchMangaRequest(page Int, query String, filters FilterList) Request
        +searchMangaParse(response Response) MangasPage
        -combineQuery(filters FilterList) String
        -searchMangaByIdRequest(id String) Request
        -searchMangaByIdParse(response Response, id String) MangasPage
        +mangaDetailsRequest(manga SManga) Request
        +mangaDetailsParse(response Response) SManga
        +getMangaUrl(manga SManga) String
        +chapterListRequest(manga SManga) Request
        +chapterListParse(response Response) List~SChapter~
        +pageListRequest(chapter SChapter) Request
        +pageListParse(response Response) List~Page~
        +imageUrlParse(response Response) String
        +getFilterList() FilterList
        -authorizationInterceptor(chain Interceptor.Chain) Response
        -shortenTitle(text String) String
        -getThumbServer() String
        -getImageServer() String
    }

    class HttpSource {
        <<abstract>>
        +Headers.Builder headersBuilder()
    }

    class PaginatedResponse~T~ {
        +List~T~ result
        +Int numPages
    }

    class GalleryListItem {
        -Int id
        -String thumbnail
        -String englishTitle
        -String japaneseTitle
        +toSManga(displayFullTitle Boolean, shortenTitle Function, thumbServer String) SManga
    }

    class Hentai {
        +Int id
        -NHTitle title
        -ImageInfo thumbnail
        -Long uploadDate
        +List~NHTag~ tags
        +Int numPages
        -Int numFavorites
        +List~NHPage~ pages
        +toSManga(full Boolean, displayFullTitle Boolean, shortenTitle Function, thumbServer String) SManga
        +toSChapter() SChapter
    }

    class NHTitle {
        +String english
        +String japanese
        +String pretty
    }

    class ImageInfo {
        +String path
    }

    class NHTag {
        +String type
        +String name
    }

    class NHPage {
        +String path
    }

    class Config {
        +List~String~ imageServers
        +List~String~ thumbServers
    }

    class NHUtils {
        +getArtists(data Hentai) String
        +getGroups(data Hentai) String
        +getTagDescription(data Hentai) String
        +getTags(data Hentai) String
    }

    NHentai ..|> HttpSource

    NHentai --> PaginatedResponse : uses
    NHentai --> GalleryListItem : uses
    NHentai --> Hentai : uses
    NHentai --> Config : uses
    NHentai --> NHUtils : uses

    PaginatedResponse o--> Hentai : result
    PaginatedResponse o--> GalleryListItem : result

    Hentai --> NHTitle : has
    Hentai --> ImageInfo : thumbnail
    Hentai --> NHPage : pages
    Hentai --> NHTag : tags

    NHUtils ..> Hentai : extension data
Loading

Class diagram for randomua helper library and Spotless RandomUaCheck

classDiagram
    direction LR

    class HttpSource {
        <<abstract>>
        +Headers.Builder headersBuilder()
    }

    class Headers_Builder {
        +set(name String, value String) Headers.Builder
    }

    class PreferenceScreen {
    }

    class UserAgentPreferenceHelpers {
        <<file-level functions>>
        -SharedPreferences.getPrefUAType() UserAgentType
        -SharedPreferences.getPrefCustomUA() String
        +setRandomUserAgent(builder Headers_Builder, userAgentType UserAgentType, filterInclude List~String~, filterExclude List~String~) Headers_Builder
        +addRandomUAPreference(screen PreferenceScreen)
    }

    class UserAgentType {
        <<enum>>
        MOBILE
        DESKTOP
        OFF
    }

    class HelperKt {
        <<file-level functions>>
        -String UA_DB_URL
        -NetworkHelper client
        -String userAgent
        +getRandomUserAgent(userAgentType UserAgentType, filterInclude List~String~, filterExclude List~String~) String
    }

    class UserAgentList {
        +List~String~ desktop
        +List~String~ mobile
    }

    class NetworkHelper {
        +OkHttpClient client
    }

    class RandomUaCheck {
        +create() FormatterStep
    }

    class FormatterStep {
        +create(name String, state Serializable, toFormatter Function) FormatterStep
    }

    class FormatterFunc {
    }

    class RandomUaCheck_State {
        +toFormatter() FormatterFunc
    }

    HttpSource ..> UserAgentPreferenceHelpers : context(source)
    Headers_Builder ..> UserAgentPreferenceHelpers : extension

    UserAgentPreferenceHelpers ..> UserAgentType : uses
    UserAgentPreferenceHelpers ..> HelperKt : calls getRandomUserAgent

    HelperKt ..> NetworkHelper : uses
    HelperKt ..> UserAgentList : parses

    RandomUaCheck ..> RandomUaCheck_State : creates
    RandomUaCheck_State ..> FormatterFunc : returns
    RandomUaCheck ..> FormatterStep : creates
Loading

File-Level Changes

Change Details Files
Refactor NHentai source from HTML scraping ParsedHttpSource to JSON api/v2-based HttpSource with new DTOs and config-driven image servers.
  • Replace ParsedHttpSource with HttpSource and implement popular, latest, search, details, chapters, and pages using JSON api/v2 endpoints.
  • Introduce PaginatedResponse, GalleryListItem, Hentai, NHTitle, ImageInfo, NHTag, NHPage, and Config DTOs and map them to SManga, SChapter, and Page instances.
  • Use api/v2 config endpoint to dynamically select image and thumbnail servers for covers and page images.
  • Simplify title shortening and tag/artist/group helpers in NHUtils, ensuring stable URLs via getMangaUrl override and consistent /g/{id}/ URLs for all flows.
src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHentai.kt
src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHDto.kt
src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHUtils.kt
Rework favorites handling and filters to use authenticated api/v2 favorites with cookie-derived access tokens and improved filter helpers.
  • Add an OkHttp network interceptor that reads the access_token cookie from the WebView CookieManager and injects an Authorization: User header for /favorites requests.
  • On 401 responses for favorites, clear the cached token and throw a user-facing exception instructing login via WebView.
  • Update search and filter handling to use api/v2 favorites and search parameters (query/sort/page, OffsetPageFilter, SortFilter) and replace custom findInstance with a shared firstInstanceOrNull helper.
src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHentai.kt
Replace the old random user-agent integration with a new randomua helper library using context receivers and header-based configuration, and wire NHentai to it.
  • Remove previous randomua interceptor-based helpers and add a new context(HttpSource) Headers.Builder.setRandomUserAgent extension to apply Random UA or custom UA from preferences.
  • Add context(HttpSource) PreferenceScreen.addRandomUAPreference to build Random UA and custom UA preferences with validation and restart hints.
  • Implement a new Helper.kt in lib/randomua that fetches and caches random user agents from a remote JSON list using OkHttp with Brotli and cache-control tweaks, and exposes getRandomUserAgent filtered by type and substrings.
  • Update NHentai to use headersBuilder().setRandomUserAgent(...) and screen.addRandomUAPreference() instead of the old APIs.
src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHentai.kt
lib/randomua/src/keiyoushi/lib/randomua/UserAgentPreference.kt
lib/randomua/src/keiyoushi/lib/randomua/Helper.kt
Add a Spotless lint step enforcing getMangaUrl when using the randomua library, and enable language features needed by the new helpers.
  • Add RandomUaCheck Spotless FormatterStep that throws if a file imports keiyoushi.lib.randomua but does not override getMangaUrl().
  • Wire RandomUaCheck into the Spotless kotlin configuration in keiyoushi.lint.gradle.kts so it runs on all Kotlin sources.
  • Enable the -Xcontext-parameters compiler flag in core, lib-android, and lib-multisrc Gradle scripts to support context receivers used by the randomua helpers.
buildSrc/src/main/kotlin/RandomUaCheck.kt
buildSrc/src/main/kotlin/keiyoushi.lint.gradle.kts
core/build.gradle.kts
buildSrc/src/main/kotlin/lib-android.gradle.kts
buildSrc/src/main/kotlin/lib-multisrc.gradle.kts
Adjust build configuration for NHentai and randomua to match the restored extension versioning and dependencies.
  • Bump NHentai extVersionCode from 54 to 57 and keep isNsfw = true.
  • Add okhttp-brotli as a compileOnly dependency to lib/randomua to support Brotli-compressed user-agent lists.
src/all/nhentai/build.gradle
lib/randomua/build.gradle.kts

Assessment against linked issues

Issue Objective Addressed Explanation
#10 Fix the NullPointerException when opening an NHentai entry so that tags and chapter information load without errors.
#10 Restore correct NHentai entry details (cover, title, tags, and chapter list) by updating the extension to work with the current NHentai site/API.

Possibly linked issues

  • NHentai Extension error #10: The PR refactors NHentai to API v2 and restores proper tags/chapters display, fixing the NullPointerException issue.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The global userAgent cache in randomua.Helper ignores the requested UserAgentType and filters, so once a value is set all subsequent calls (including mobile vs desktop, different includes/excludes) will reuse the same UA; consider caching per type/filter or removing the global cache.
  • In getRandomUserAgent, runBlocking(Dispatchers.IO) is used even when called from the main thread (the cache-only branch still blocks while reading/parsing), which risks UI freezes; consider deferring UA resolution off the main thread or ensuring the header builder never triggers network/cache IO on the main looper.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The global `userAgent` cache in `randomua.Helper` ignores the requested `UserAgentType` and filters, so once a value is set all subsequent calls (including mobile vs desktop, different includes/excludes) will reuse the same UA; consider caching per type/filter or removing the global cache.
- In `getRandomUserAgent`, `runBlocking(Dispatchers.IO)` is used even when called from the main thread (the cache-only branch still blocks while reading/parsing), which risks UI freezes; consider deferring UA resolution off the main thread or ensuring the header builder never triggers network/cache IO on the main looper.

## Individual Comments

### Comment 1
<location path="lib/randomua/src/keiyoushi/lib/randomua/Helper.kt" line_range="31-36" />
<code_context>
+    }
+    .build()
+
+internal fun getRandomUserAgent(
+    userAgentType: UserAgentType,
+    filterInclude: List<String>,
+    filterExclude: List<String>,
+): String? {
+    if (!userAgent.isNullOrEmpty()) return userAgent
+
+    // avoid network on main thread when webview screen accesses headers
</code_context>
<issue_to_address>
**issue (bug_risk):** User-agent caching ignores type and filters, which can produce inconsistent or unexpected results.

Since `userAgent` is a single global cache, it’s set based only on the first call’s `userAgentType` and filters. All later calls with different types or `filterInclude`/`filterExclude` values will still return that first value, which can break callers that switch between mobile/desktop or update filters. To support multiple combinations, either:
- cache by a key that includes `(UserAgentType, filterInclude, filterExclude)`, or
- remove the global cache and rely on HTTP caching (you already set `Cache-Control`).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the NHentai extension to transition from HTML scraping to using the site's JSON API, involving a complete overhaul of the DTOs and the main source class. Additionally, it introduces a new library for managing random user agents. The review feedback points out critical issues including potential null pointer exceptions due to unsafe assertions in the new DTOs, an invalid dependency version in the build configuration, and a logic bug in the user agent helper where global state prevents the correct application of filtering parameters.

@AaronDeimos AaronDeimos marked this pull request as draft March 30, 2026 15:52
@AaronDeimos AaronDeimos marked this pull request as ready for review March 30, 2026 16:12
Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • getRandomUserAgent() caches the first returned value in a single global userAgent variable without considering UserAgentType or the include/exclude filters, so subsequent calls for different types or filters may reuse an inappropriate UA; consider caching per (type, filterInclude, filterExclude) or not caching at all.
  • The preference keys PREF_KEY_RANDOM_UA and PREF_KEY_CUSTOM_UA in UserAgentPreference.kt are very generic and shared across all sources using the library; consider namespacing them (e.g., by source id or library name) to avoid collisions with other extensions that might also use random UA settings.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- getRandomUserAgent() caches the first returned value in a single global userAgent variable without considering UserAgentType or the include/exclude filters, so subsequent calls for different types or filters may reuse an inappropriate UA; consider caching per (type, filterInclude, filterExclude) or not caching at all.
- The preference keys PREF_KEY_RANDOM_UA and PREF_KEY_CUSTOM_UA in UserAgentPreference.kt are very generic and shared across all sources using the library; consider namespacing them (e.g., by source id or library name) to avoid collisions with other extensions that might also use random UA settings.

## Individual Comments

### Comment 1
<location path="src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHentai.kt" line_range="58-59" />
<code_context>
             .build()
     }

+    override fun headersBuilder() = super.headersBuilder()
+        .setRandomUserAgent(
+            filterInclude = listOf("chrome"),
+        )
</code_context>
<issue_to_address>
**issue (bug_risk):** The context receiver `setRandomUserAgent` is used without a matching `HttpSource` context in scope, which is likely to fail compilation.

Because `setRandomUserAgent` is defined with `context(source: HttpSource)`, it can only be called where an implicit `HttpSource` context is in scope (e.g., inside `with(this@NHentai)` or another `context(HttpSource)` function). Simply being in an `HttpSource` subclass is not enough. You can either wrap the call in a context, for example:

```kotlin
override fun headersBuilder() = with(this) {
    super.headersBuilder()
        .setRandomUserAgent(filterInclude = listOf("chrome"))
}
```

or change the helper’s signature if you meant it to be a normal extension function.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@AaronDeimos
Copy link
Copy Markdown
Contributor Author

@sourcery-ai dismiss

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NHentai Extension error

2 participants