fix(hstream/hanime) - Modify hStream to group series episodes together & fix hAnime covers#96
fix(hstream/hanime) - Modify hStream to group series episodes together & fix hAnime covers#96PineappleTwilight wants to merge 13 commits intoyuzono:masterfrom
Conversation
hstream.moe lists each episode as a separate series entry. This change deduplicates search/popular/latest results by title and probes for all episodes when viewing a series.
Reviewer's GuideGroups hStream episodes by series so duplicate entries are collapsed, and updates episode fetching to iterate over series episode URLs while bumping the source version ID. Sequence diagram for the updated episode fetching processsequenceDiagram
actor User
participant AniyomiApp
participant Hstream
participant HttpClient
participant HstreamServer
User->>AniyomiApp: Open anime details
AniyomiApp->>Hstream: episodeListRequest(anime)
Hstream->>AniyomiApp: Request for series_base_url-1/
AniyomiApp->>HttpClient: GET series_base_url-1/
HttpClient->>HstreamServer: GET series_base_url-1/
HstreamServer-->>HttpClient: 200 HTML (episode 1)
HttpClient-->>AniyomiApp: Response
AniyomiApp->>Hstream: episodeListParse(response)
Hstream->>Hstream: getSeriesBaseUrl(current_url)
Hstream->>Hstream: parseEpisodeFromDoc(doc, 1, series_base_url-1/)
loop For epNum from 2 to 50
Hstream->>HttpClient: GET baseUrl + series_base_url-epNum/
HttpClient->>HstreamServer: GET series_base_url-epNum/
alt Response code is 200
HstreamServer-->>HttpClient: 200 HTML (episode epNum)
HttpClient-->>Hstream: Response
Hstream->>Hstream: parseEpisodeFromDoc(epDoc, epNum, series_base_url-epNum/)
else Non 200 or error
HstreamServer-->>HttpClient: Non 200 or error
HttpClient-->>Hstream: Response or Exception
Hstream-->>Hstream: Break loop
end
end
Hstream-->>AniyomiApp: List<SEpisode> for all fetched episodes
AniyomiApp-->>User: Display grouped episode list
Class diagram for updated Hstream extension behaviorclassDiagram
class Hstream {
+Int versionId
+String popularAnimeSelector()
+SAnime popularAnimeFromElement(Element element)
+AnimesPage popularAnimeParse(Response response)
+String latestUpdatesSelector()
+AnimesPage latestUpdatesParse(Response response)
+String searchAnimeSelector()
+AnimesPage searchAnimeParse(Response response)
+Request episodeListRequest(SAnime anime)
+List~SEpisode~ episodeListParse(Response response)
-SEpisode parseEpisodeFromDoc(Document doc, Int epNum, String url)
-String getSeriesBaseUrl(String url)
-Long toDate(String dateString)
}
class SAnime
class SEpisode
class AnimesPage
class Element
class Document
class Response
class Request
Hstream --> SAnime : uses
Hstream --> SEpisode : uses
Hstream --> AnimesPage : creates
Hstream --> Element : parses
Hstream --> Document : parses
Hstream --> Response : parses
Hstream --> Request : returns
Flow diagram for grouping series entries in list parsingflowchart TD
A[Response] --> B[Parse HTML with asJsoup]
B --> C[Select elements using selector]
C --> D[Map each element with fromElement]
D --> E[Group list by title]
E --> F[From each group take first item]
F --> G[Check for next page element]
G --> H[Create AnimesPage with grouped list and hasNextPage]
subgraph PopularAnimeParse
C1[Use popularAnimeSelector]
end
subgraph LatestUpdatesParse
C2[Use latestUpdatesSelector]
end
subgraph SearchAnimeParse
C3[Use searchAnimeSelector]
end
C -.popularAnimeParse.-> C1
C -.latestUpdatesParse.-> C2
C -.searchAnimeParse.-> C3
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the Hstream extension by introducing proper series grouping for anime entries across various listing types (popular, latest, search). Previously, individual episodes might have appeared as separate entries, but now they are consolidated under their respective series. Additionally, the episode parsing logic has been revamped to dynamically fetch and display all available episodes for a given series, greatly improving the user experience for multi-episode content. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- The
popularAnimeParse,latestUpdatesParse, andsearchAnimeParseimplementations all repeat the same pattern (select → map → groupBy title → take first → next page check); consider extracting a shared helper that takes the selector and element-mapper as parameters to reduce duplication and the chance of future divergence. - Using
groupBy { it.title }when collapsing episodes into a single series risks merging different shows that share a title; if the site structure allows, grouping by a normalized base URL (e.g.,getSeriesBaseUrlon the href) would be more robust than relying on the title string. - The hardcoded
2..50loop inepisodeListParsemay truncate longer series or make it unclear why 50 is the limit; consider making this a named constant (e.g.,MAX_EPISODE_PROBE) or deriving the range from metadata on the first page if available.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `popularAnimeParse`, `latestUpdatesParse`, and `searchAnimeParse` implementations all repeat the same pattern (select → map → groupBy title → take first → next page check); consider extracting a shared helper that takes the selector and element-mapper as parameters to reduce duplication and the chance of future divergence.
- Using `groupBy { it.title }` when collapsing episodes into a single series risks merging different shows that share a title; if the site structure allows, grouping by a normalized base URL (e.g., `getSeriesBaseUrl` on the href) would be more robust than relying on the title string.
- The hardcoded `2..50` loop in `episodeListParse` may truncate longer series or make it unclear why 50 is the limit; consider making this a named constant (e.g., `MAX_EPISODE_PROBE`) or deriving the range from metadata on the first page if available.
## Individual Comments
### Comment 1
<location path="src/en/hstream/src/eu/kanade/tachiyomi/animeextension/en/hstream/Hstream.kt" line_range="71-73" />
<code_context>
+ override fun popularAnimeParse(response: Response): AnimesPage {
+ val document = response.asJsoup()
+ val elements = document.select(popularAnimeSelector())
+ val animeList = elements.map(::popularAnimeFromElement)
+ .groupBy { it.title }
+ .map { (_, items) -> items.first() }
+ val hasNextPage = document.selectFirst(popularAnimeNextPageSelector()) != null
+ return AnimesPage(animeList, hasNextPage)
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Using the title as the deduplication key may incorrectly merge different series with the same name.
`groupBy { it.title }` will merge all entries sharing a title, even when they’re different series (remakes, movies vs TV, or different categories). If you only want to dedupe true duplicates, use a more stable identifier (e.g., `it.url` or a URL+title pair). The same adjustment likely applies to `latestUpdatesParse` and `searchAnimeParse` for consistency.
Suggested implementation:
```
override fun popularAnimeParse(response: Response): AnimesPage {
val document = response.asJsoup()
val elements = document.select(popularAnimeSelector())
// Deduplicate by a stable identifier (URL) instead of title to avoid merging different series
val animeList = elements
.map(::popularAnimeFromElement)
.distinctBy { it.url }
val hasNextPage = document.selectFirst(popularAnimeNextPageSelector()) != null
return AnimesPage(animeList, hasNextPage)
}
```
You should also update `latestUpdatesParse` and `searchAnimeParse` in the same file for consistency. Wherever they currently do something like:
```kotlin
elements.map(::latestUpdatesFromElement)
.groupBy { it.title }
.map { (_, items) -> items.first() }
```
or similar for search, change those chains to:
```kotlin
elements
.map(::latestUpdatesFromElement)
.distinctBy { it.url }
```
and
```kotlin
elements
.map(::searchAnimeFromElement)
.distinctBy { it.url }
```
respectively, or adjust to use `it.url` (or another stable identifier) as the deduplication key instead of `it.title`.
</issue_to_address>
### Comment 2
<location path="src/en/hstream/src/eu/kanade/tachiyomi/animeextension/en/hstream/Hstream.kt" line_range="180-187" />
<code_context>
+ // Probe for more episodes (2..50), break on first failure
+ for (epNum in 2..50) {
+ val epPath = "$seriesPath-$epNum/"
+ try {
+ val resp = client.newCall(GET("$baseUrl$epPath")).execute()
+ if (resp.code != 200) {
+ resp.close()
+ break
+ }
+ val epDoc = resp.asJsoup()
+ episodes.add(parseEpisodeFromDoc(epDoc, epNum, epPath))
+ resp.close()
+ } catch (e: Exception) {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** HTTP response handling inside the loop can be tightened to avoid potential resource leaks.
If `asJsoup()` or `parseEpisodeFromDoc` throws, the response isn’t closed and may leak. Using `execute().use { resp -> ... }` around the whole body ensures the response is always closed, even on exceptions, and removes the need for manual `close()` calls.
</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/en/hstream/src/eu/kanade/tachiyomi/animeextension/en/hstream/Hstream.kt
Outdated
Show resolved
Hide resolved
src/en/hstream/src/eu/kanade/tachiyomi/animeextension/en/hstream/Hstream.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Code Review
This pull request updates the Hstream extension, incrementing its version ID. It introduces new parsing methods for popular anime, latest updates, and search results, and significantly refactors the episode listing logic to dynamically probe for episodes up to 50. A new utility function, getSeriesBaseUrl, was also added. Feedback includes addressing a potential resource leak in the episode probing loop, refactoring duplicated parsing logic into a shared function, using a constant for the magic number 50, and compiling the regex in getSeriesBaseUrl for improved performance and readability.
src/en/hstream/src/eu/kanade/tachiyomi/animeextension/en/hstream/Hstream.kt
Outdated
Show resolved
Hide resolved
src/en/hstream/src/eu/kanade/tachiyomi/animeextension/en/hstream/Hstream.kt
Outdated
Show resolved
Hide resolved
src/en/hstream/src/eu/kanade/tachiyomi/animeextension/en/hstream/Hstream.kt
Outdated
Show resolved
Hide resolved
src/en/hstream/src/eu/kanade/tachiyomi/animeextension/en/hstream/Hstream.kt
Outdated
Show resolved
Hide resolved
…am/Hstream.kt Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
…am/Hstream.kt Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
- Extract shared parseAnimeList() method to eliminate triplicated parse logic - Replace groupBy/map/first with distinctBy for efficient deduplication - Use compareByDescending instead of sortedWith().reversed() - Extract hardcoded episode probe limit to MAX_EPISODE_PROBE constant - Use proper JSON serialization for video API request body
# Conflicts: # src/en/hstream/src/eu/kanade/tachiyomi/animeextension/en/hstream/Hstream.kt
|
I know some people prefer each episode as a unique type of "series", but it defeats the entire purpose of the parent app imo. |
…ctor The cover extraction now parses the structured __NUXT__ script data to find the matching video's cover_url by slug, with a fallback to the img.hvpi-cover CSS selector.
…video loading - Make height and url nullable with defaults in both Stream classes to prevent MissingFieldException when API returns null values - Replace servers.get(0) with firstOrNull() to avoid IndexOutOfBoundsException on empty server lists - Use mapNotNull in videoListParse and fetchVideoListPremium to safely skip streams missing url or height
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 extensionFixes issues:
#53
Add a 👍 reaction to pull requests you find important.
Summary by Sourcery
Group Hstream series entries so that multiple episodes of the same title are treated as a single anime in listings and support fetching full episode lists for a series.
New Features:
Bug Fixes:
Enhancements: