fix(nhentai): migrate to v2 and many QoL changes#9
fix(nhentai): migrate to v2 and many QoL changes#9MediocreLegion wants to merge 31 commits intoyuzono:masterfrom
Conversation
Reviewer's GuideMigrates 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 loadingsequenceDiagram
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
Sequence diagram for NHentai v2 search and detailssequenceDiagram
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)
Updated class diagram for NHentai v2 data models and sourceclassDiagram
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
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- The
Image.extensionproperty no longer makes sense after changing the DTO to store a fullpath; comparingpathto "w"/"p"/"g" will never match and likely breaks extension resolution—either keep the originaltfield or derive the extension frompath(e.g., suffix or MIME). - Given that
pagesnow contain fullpathvalues 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.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
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.
src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHDto.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- In
chapterListParse,url = "/g/$id/"referencesidwhich is not defined in that scope; you likely want to derive the gallery ID from the response or chapter/manga instead. - The
hasNextPagelogic inlatestUpdatesParse,popularMangaParse, andsearchMangaParsecomparesper_pageto 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 fromper_page. - The
nhConfiglazy property performs a synchronous network call during first access and will throw if/configfails; 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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHentai.kt
Show resolved
Hide resolved
src/all/nhentai/src/eu/kanade/tachiyomi/extension/all/nhentai/NHentai.kt
Outdated
Show resolved
Hide resolved
|
I've tested the apk. I still get |
|
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 |
Test mine 🫣 |
|
Tried it and found no issue |
|
@AaronDeimos I moved the favorites to the new api, if you want to check it out |
|
Wait, I forgot to push that |
|
Ok, now it's working lol |
|
Worked |
|
@mangkoran that |
|
I think that's enough. I added support for API keys and additional settings for selecting default search ordering. |
|
Bumping Compatibility is implemented in: |
Do we really need to break extension for specific forks? This seems unnecessary. Also seems like a hacky way to use 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? |
|
I think groups are broken tho, they are not being fetched |
|
@AaronDeimos I may have not been in the best of minds at 1AM lol. @natyoufri apparently groups where broken before. |
|
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? |

Fixes #10
Now supersedes #11 (adds api key support and default sort option in settings)
100% human made
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:
extVersionCodevalue inbuild.gradlefor individual extensionsoverrideVersionCodeorbaseVersionCodeas needed for all multisrc extensionsisNsfw = trueflag inbuild.gradlewhen appropriateidif a source's name or language were changedweb_hi_res_512.pngwhen adding a new extensionSummary 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:
Bug Fixes:
Enhancements:
Build: