Skip to content

fix(nhentai): migrate to v2 and many QoL changes#9

Open
MediocreLegion wants to merge 31 commits intoyuzono:masterfrom
MediocreLegion:fix/nhv2
Open

fix(nhentai): migrate to v2 and many QoL changes#9
MediocreLegion wants to merge 31 commits intoyuzono:masterfrom
MediocreLegion:fix/nhv2

Conversation

@MediocreLegion
Copy link
Copy Markdown
Contributor

@MediocreLegion MediocreLegion commented Mar 29, 2026

Fixes #10
Now supersedes #11 (adds api key support and default sort option in settings)
100% human made

  • Restored last version before removed from keiyoushi.
  • Moved everything to the new api.
  • Gets the api key from the settings or access token from the webview cookies, for the favorites.
  • Everything tested (had to make an account) and actually working this time
  • Adds the RandomUa changes that weren't imported from keiyoushi.
  • Add default sort option to settings.

Obs: Null exception pill will still appear if you are using an outdated SY and KMK delegate source (disable delegate sources and wait for jobobby04/TachiyomiSY#1581 or komikku-app/komikku#1582)

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

Migrate the NHentai extension to use the nhentai v2 JSON API for listing, searching, details, and pages, replacing HTML parsing and updating data models accordingly.

New Features:

  • Add support for nhentai v2 API endpoints for galleries, search, favorites, and configuration.
  • Introduce configuration and search result DTOs to represent v2 API responses, including image and thumbnail paths.

Bug Fixes:

  • Ensure favorites access surfaces a clear error message when the user is not logged in.

Enhancements:

  • Refactor latest, popular, and search flows to use direct API parsing instead of HTML selectors and Rx-based fetching.
  • Simplify page and image handling by using API-provided image paths and server lists from configuration.
  • Reorder sort options to include a 'Recent' sort aligned with the new API.

Build:

  • Bump the NHentai extension version code to 57 to reflect the migration.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 29, 2026

Reviewer's Guide

Migrates the NHentai extension from HTML scraping to the nhentai v2 JSON API for listings, search, gallery details, and page images, while updating data models, networking, and sort options and bumping the extension version code.

Sequence diagram for NHentai v2 gallery page loading

sequenceDiagram
    actor User
    participant TachiyomiApp
    participant NHentaiSource
    participant NHentaiAPI as NHentai_API_v2
    participant ImageServer

    User ->> TachiyomiApp: Open chapter
    TachiyomiApp ->> NHentaiSource: getPageList(chapter)

    Note over NHentaiSource: Lazy init nhConfig if needed
    NHentaiSource ->> NHentaiAPI: GET /api/v2/config
    NHentaiAPI -->> NHentaiSource: NHConfig(image_servers, thumb_servers)
    NHentaiSource ->> NHentaiSource: Cache nhConfig

    NHentaiSource ->> NHentaiAPI: GET /api/v2/galleries/{galleryId}
    NHentaiAPI -->> NHentaiSource: Hentai(pages, thumbnail, ...)

    NHentaiSource ->> NHentaiSource: Build Page list using
    NHentaiSource ->> NHentaiSource: nhConfig.image_servers.random()

    loop For each page
        NHentaiSource ->> ImageServer: Construct URL {image_server}/{image.path}
    end

    NHentaiSource -->> TachiyomiApp: List<Page>
    TachiyomiApp -->> User: Display images
Loading

Sequence diagram for NHentai v2 search and details

sequenceDiagram
    actor User
    participant TachiyomiApp
    participant NHentaiSource
    participant NHentaiAPI as NHentai_API_v2

    User ->> TachiyomiApp: Search or open manga

    alt Search by id (nhentai: or numeric)
        TachiyomiApp ->> NHentaiSource: getSearchManga(page, query, filters)
        NHentaiSource ->> NHentaiAPI: GET /api/v2/galleries/{id}/
        NHentaiAPI -->> NHentaiSource: Hentai
        NHentaiSource ->> NHentaiSource: parseData(Hentai) -> SManga
        NHentaiSource -->> TachiyomiApp: MangasPage(single SManga)
    else Normal search/listing
        TachiyomiApp ->> NHentaiSource: getSearchManga(page, query, filters)
        NHentaiSource ->> NHentaiAPI: GET /api/v2/search or /favorites
        NHentaiAPI -->> NHentaiSource: ResultNHentai(result, per_page)
        NHentaiSource ->> NHentaiSource: parseSearchData(SearchHentai) for each
        NHentaiSource -->> TachiyomiApp: MangasPage(multiple SManga)
    end

    User ->> TachiyomiApp: Open manga details
    TachiyomiApp ->> NHentaiSource: mangaDetailsRequest(manga)
    NHentaiSource ->> NHentaiAPI: GET /api/v2/galleries/{id}/
    NHentaiAPI -->> NHentaiSource: Hentai
    NHentaiSource ->> NHentaiSource: parseData(Hentai) -> SManga
    NHentaiSource -->> TachiyomiApp: SManga(details)
Loading

Updated class diagram for NHentai v2 data models and source

classDiagram
    class NHentai {
        <<open class>>
        - String baseUrl
        - String apiUrl
        - SharedPreferences preferences
        - OkHttpClient client
        - NHConfig nhConfig
        - Boolean displayFullTitle
        + latestUpdatesRequest(page Int) Request
        + latestUpdatesParse(response Response) MangasPage
        + popularMangaRequest(page Int) Request
        + popularMangaParse(response Response) MangasPage
        + getSearchManga(page Int, query String, filters FilterList) MangasPage
        + searchMangaRequest(page Int, query String, filters FilterList) Request
        + searchMangaParse(response Response) MangasPage
        + mangaDetailsRequest(manga SManga) Request
        + mangaDetailsParse(response Response) SManga
        + chapterListRequest(manga SManga) Request
        + chapterListParse(response Response) List~SChapter~
        + getPageList(chapter SChapter) List~Page~
        + parseSearchData(data SearchHentai) SManga
        + parseData(data Hentai) SManga
    }

    class NHConfig {
        <<Serializable>>
        + List~String~ image_servers
        + List~String~ thumb_servers
    }

    class ResultNHentai {
        <<Serializable>>
        + List~SearchHentai~ result
        + String detail
        + Long per_page
    }

    class SearchHentai {
        <<Serializable>>
        + Int id
        + String english_title
        + String thumbnail
    }

    class Hentai {
        <<Serializable>>
        + Int id
        + List~Image~ pages
        + Image thumbnail
        + List~Tag~ tags
        + Title title
        + Long upload_date
        + Long num_favorites
    }

    class Title {
        <<Serializable>>
        + String english
        + String japanese
        + String pretty
    }

    class Image {
        <<Serializable>>
        + String path
    }

    class Tag {
        <<Serializable>>
        + String type
        + String name
        + String url
    }

    NHentai --> NHConfig : uses
    NHentai --> ResultNHentai : parses
    NHentai --> SearchHentai : parses
    NHentai --> Hentai : parses
    Hentai --> Image : contains
    Hentai --> Tag : contains
    Hentai --> Title : has
    ResultNHentai --> SearchHentai : result items
Loading

File-Level Changes

Change Details Files
Switch listing, popular, and search flows from HTML parsing to v2 JSON API responses.
  • Introduce apiUrl base (/api/v2) and use it for latest, popular, search, favorites, and ID-based gallery lookups
  • Replace Jsoup-based selectors and *FromElement methods with JSON-based *Parse(Response) implementations that map v2 API payloads to SManga via helper parsers
  • Adjust search request query parameter names (e.g., qquery) and favorite URL paths to match v2 API
src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHentai.kt
Update gallery details, chapters, and page image loading to use v2 gallery and image endpoints with server config.
  • Add NHConfig lazy loader calling /api/v2/config to obtain image_servers and thumb_servers
  • Refactor mangaDetailsRequest, chapterListRequest, and getPageList to fetch /api/v2/galleries/{id} and build SManga/SChapter/Page objects from the JSON response
  • Remove DOM-based helpers for extracting gallery JSON and CDN URLs; introduce parseData and parseSearchData helpers that use API model classes and NHConfig servers
src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHentai.kt
src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHDto.kt
Align DTOs with the v2 API response schema and simplify image modeling.
  • Introduce NHConfig, ResultNHentai, and SearchHentai DTOs for config and search/listing responses
  • Change Hentai to use pages: List<Image> and thumbnail: Image instead of Images/media_id, and simplify Image to a path field used directly for URLs
  • Remove obsolete Images and extension-decoding Image model tied to the old v1 API structure
src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHDto.kt
Modernize networking and search APIs to coroutine-based flows and add auth error handling.
  • Replace RxJava-based fetchSearchManga with suspending getSearchManga and direct blocking execute() calls internally
  • Use shared parseAs<T>() JSON helper instead of manual Json.decodeFromString and DOM extraction
  • Add a network interceptor to throw a clear "Log in via WebView to view favorites" error on HTTP 401 rather than relying on HTML login page detection
src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHentai.kt
Tune UX by updating sort options and bumping extension version.
  • Reorder sort options so "Recent" appears first in the sort filter list
  • Increment extVersionCode from 54 to 55 and ensure isNsfw = true remains set
src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHentai.kt
src/all/nhentai/build.gradle

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 left some high level feedback:

  • The Image.extension property no longer makes sense after changing the DTO to store a full path; comparing path to "w"/"p"/"g" will never match and likely breaks extension resolution—either keep the original t field or derive the extension from path (e.g., suffix or MIME).
  • Given that pages now contain full path values used directly for images, manually constructing the thumbnail URL with /galleries/${data.media_id}/cover.${data.pages[0].extension} may be inconsistent with the new API shape; consider using a dedicated cover path from the API if available or a consistent derivation from the same data structure.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `Image.extension` property no longer makes sense after changing the DTO to store a full `path`; comparing `path` to "w"/"p"/"g" will never match and likely breaks extension resolution—either keep the original `t` field or derive the extension from `path` (e.g., suffix or MIME).
- Given that `pages` now contain full `path` values used directly for images, manually constructing the thumbnail URL with `/galleries/${data.media_id}/cover.${data.pages[0].extension}` may be inconsistent with the new API shape; consider using a dedicated cover path from the API if available or a consistent derivation from the same data structure.

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 updates the NHentai extension to utilize a new configuration API for image and thumbnail servers and refactors the data models to align with API changes. Specifically, it introduces NHConfig and HentaiData classes while simplifying the Hentai and Image structures. A critical issue was found in the Image class where the extension property logic incorrectly processes the new path field, which will result in incorrect file extensions for images.

@MediocreLegion MediocreLegion marked this pull request as draft March 29, 2026 19:18
@MediocreLegion MediocreLegion marked this pull request as ready for review March 29, 2026 22: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 2 issues, and left some high level feedback:

  • In chapterListParse, url = "/g/$id/" references id which is not defined in that scope; you likely want to derive the gallery ID from the response or chapter/manga instead.
  • The hasNextPage logic in latestUpdatesParse, popularMangaParse, and searchMangaParse compares per_page to the current page's result size; consider using the API's explicit pagination fields (e.g., total pages or next page indicator) if available instead of inferring from per_page.
  • The nhConfig lazy property performs a synchronous network call during first access and will throw if /config fails; consider adding error handling and/or a fallback (e.g., cached defaults) so a temporary config endpoint failure does not break the entire source.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `chapterListParse`, `url = "/g/$id/"` references `id` which is not defined in that scope; you likely want to derive the gallery ID from the response or chapter/manga instead.
- The `hasNextPage` logic in `latestUpdatesParse`, `popularMangaParse`, and `searchMangaParse` compares `per_page` to the current page's result size; consider using the API's explicit pagination fields (e.g., total pages or next page indicator) if available instead of inferring from `per_page`.
- The `nhConfig` lazy property performs a synchronous network call during first access and will throw if `/config` fails; consider adding error handling and/or a fallback (e.g., cached defaults) so a temporary config endpoint failure does not break the entire source.

## Individual Comments

### Comment 1
<location path="src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHentai.kt" line_range="104-108" />
<code_context>
-        thumbnail_url = element.selectFirst(".cover img")!!.let { img ->
-            if (img.hasAttr("data-src")) img.attr("abs:data-src") else img.attr("abs:src")
-        }
+    override fun latestUpdatesParse(response: Response): MangasPage {
+        val res = response.parseAs<ResultNHentai>()
+        val mangas = res.result.map { parseSearchData(it) }
+        return MangasPage(mangas, hasNextPage = res.per_page > mangas.size)
     }

</code_context>
<issue_to_address>
**suggestion (bug_risk):** The `hasNextPage` calculation for latest updates likely does not match the API semantics.

In latest updates you use `hasNextPage = res.per_page > mangas.size`, which is equivalent to the search logic but inverted relative to typical pagination semantics. If `per_page` is the page size limit, returning fewer than `per_page` items usually indicates the last page. You likely want:

```kotlin
val hasNextPage = mangas.size == res.per_page
```

Please confirm the API’s contract for `per_page` and update this condition so `hasNextPage` matches the actual pagination behavior.

```suggestion
    override fun latestUpdatesParse(response: Response): MangasPage {
        val res = response.parseAs<ResultNHentai>()
        val mangas = res.result.map { parseSearchData(it) }
        val hasNextPage = mangas.size == res.per_page
        return MangasPage(mangas, hasNextPage = hasNextPage)
    }
```
</issue_to_address>

### Comment 2
<location path="src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHentai.kt" line_range="60-63" />
<code_context>
+            userAgentType = preferences.getPrefUAType(),
+            customUA = preferences.getPrefCustomUA(),
+            filterInclude = listOf("chrome"),
+        ).rateLimit(4).addNetworkInterceptor { chain ->
+            val response = chain.proceed(chain.request())
+            if (response.code == 401) {
+                throw Exception("Log in via WebView to view favorites")
+            }
+            response
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Throwing a generic `Exception` in the network interceptor can interfere with normal HTTP error handling.

Here the interceptor throws a generic `Exception` on 401, which bypasses OkHttp’s normal error handling and makes it harder to distinguish auth errors from network failures. Prefer throwing an `IOException` (or the auth-specific exception used elsewhere) so it integrates with existing retry/error logic, and close the response before throwing to avoid leaking the connection:

```kotlin
if (response.code == 401) {
    response.close()
    throw IOException("Log in via WebView to view favorites")
}
```
</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.

@mangkoran
Copy link
Copy Markdown

mangkoran commented Mar 30, 2026

I've tested the apk. I still get NullPointerException: null when I open (not read) manga but I'm able to read the manga (chapter/page images are able to load).

@natyoufri
Copy link
Copy Markdown

natyoufri commented Mar 30, 2026

the dev from az said he fixed it for tachi az. az4521/TachiyomiAZ@2b425fd and az4521/TachiyomiAZ@a807888 we should inspire ourself from his code

@AaronDeimos
Copy link
Copy Markdown
Contributor

I've tested the apk. I still get NullPointerException: null when I open (not read) manga but I'm able to read the manga (chapter/page images are able to load).

Test mine 🫣

@natyoufri
Copy link
Copy Markdown

Tried it and found no issue

@MediocreLegion
Copy link
Copy Markdown
Contributor Author

@AaronDeimos I moved the favorites to the new api, if you want to check it out

@natyoufri
Copy link
Copy Markdown

natyoufri commented Mar 30, 2026

Don't work anymore
Screenshot_20260330_224500_Mihon
Reading and populars work but not the latest tab

@MediocreLegion
Copy link
Copy Markdown
Contributor Author

Wait, I forgot to push that

@MediocreLegion
Copy link
Copy Markdown
Contributor Author

MediocreLegion commented Mar 30, 2026

Ok, now it's working lol

@natyoufri
Copy link
Copy Markdown

Worked

@MediocreLegion
Copy link
Copy Markdown
Contributor Author

MediocreLegion commented Mar 30, 2026

@mangkoran that NullPointerException: null comes from the delegate sources in SY or KMK, not this extension. Disable it in the settings if you don't want it appearing.

@MediocreLegion MediocreLegion changed the title fix(nhentai): migrate to v2 fix(nhentai): migrate to v2 and many QoL changes Mar 30, 2026
@MediocreLegion
Copy link
Copy Markdown
Contributor Author

I think that's enough.

I added support for API keys and additional settings for selecting default search ordering.

@MediocreLegion
Copy link
Copy Markdown
Contributor Author

Bumping versionId ensures an incompatible delegate source is not used.

Compatibility is implemented in:
komikku-app/komikku#1582

@AaronDeimos
Copy link
Copy Markdown
Contributor

Bumping versionId ensures an incompatible delegate source is not used.

Compatibility is implemented in: komikku-app/komikku#1582

Do we really need to break extension for specific forks? This seems unnecessary.

Also seems like a hacky way to use versionId to do so when that is for generating new IDs in case URL compatibility breaks.

You also added code to generate old IDs; why not just hardcode like the "all" language? NH doesn't have many languages.

Lastly, why open a PR in the fork before this even got merged?? Also, wouldn't it be better to do it in SY since Komikku stays up-to-date with it?

@natyoufri
Copy link
Copy Markdown

I think groups are broken tho, they are not being fetched

@MediocreLegion
Copy link
Copy Markdown
Contributor Author

MediocreLegion commented Mar 31, 2026

@AaronDeimos I may have not been in the best of minds at 1AM lol.
I'm gonna open a PR to SY too, but it usually slower to merge there.

@natyoufri apparently groups where broken before.

@SheepMan99
Copy link
Copy Markdown

Im still having the nullpointexception:null issue. Im on the most recent version of mihon and it's still happening. I even installed komikku and disabled delegate sources and I was still having the issue. Am I missing something or am I stupid and yall are talking about an update thats not done yet?

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

5 participants