Skip to content

fix(hstream/hanime) - Modify hStream to group series episodes together & fix hAnime covers#96

Open
PineappleTwilight wants to merge 13 commits intoyuzono:masterfrom
PineappleTwilight:master
Open

fix(hstream/hanime) - Modify hStream to group series episodes together & fix hAnime covers#96
PineappleTwilight wants to merge 13 commits intoyuzono:masterfrom
PineappleTwilight:master

Conversation

@PineappleTwilight
Copy link
Copy Markdown

@PineappleTwilight PineappleTwilight commented Mar 25, 2026

Checklist:

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

Fixes 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:

  • Add support for listing multiple episodes per Hstream series by probing and assembling a full episode list from individual episode pages.

Bug Fixes:

  • Prevent duplicate Hstream entries in popular, latest, and search results by grouping items with the same title into a single series entry.

Enhancements:

  • Update Hstream source URL handling to use a normalized series base URL and bump the source version to trigger user migration.

amnesia added 3 commits March 25, 2026 17:15
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.
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 25, 2026

Reviewer's Guide

Groups 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 process

sequenceDiagram
    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
Loading

Class diagram for updated Hstream extension behavior

classDiagram
    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
Loading

Flow diagram for grouping series entries in list parsing

flowchart 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
Loading

File-Level Changes

Change Details Files
Group popular, latest, and search results by series title so only one entry appears per series.
  • Adjust URL handling in popularAnimeFromElement to derive thumbnail from original href while normalizing to the series base URL for navigation.
  • Override popularAnimeParse, latestUpdatesParse, and searchAnimeParse to build their lists from the respective selectors, then group by title and keep the first entry per title.
  • Reuse existing next-page selectors to compute hasNextPage in the new parse overrides.
src/en/hstream/src/eu/kanade/tachiyomi/animeextension/en/hstream/Hstream.kt
Change episode list handling to fetch and aggregate multiple episodes for a series instead of treating each URL as a single-episode entry.
  • Override episodeListRequest to normalize any incoming anime URL to its series base URL and always request episode 1.
  • Implement episodeListParse to parse episode 1 from the initial response, then probe sequential episode URLs (2..50) via HTTP, stopping when a non-200 or error occurs, accumulating SEpisode objects.
  • Introduce parseEpisodeFromDoc helper to construct SEpisode with upload date, canonical URL, numeric episode_number, and standardized name based on episode index.
  • Add getSeriesBaseUrl utility that strips a trailing -/ from URLs to derive a series-level path, and reuse it across popular listing and episode fetching.
  • Update versionId from 2 to 3 so existing users are prompted to migrate to the new source behavior.
src/en/hstream/src/eu/kanade/tachiyomi/animeextension/en/hstream/Hstream.kt

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

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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

  • Series Grouping: Implemented logic to group individual anime episodes into their respective series across popular, latest updates, and search listings, ensuring only one entry per series is displayed.
  • Episode List Parsing: Refactored the episode parsing mechanism to dynamically fetch and list multiple episodes for a series by iteratively probing for episode URLs, rather than just parsing a single episode.
  • Version ID Bump: Incremented the versionId to 3, which will prompt existing users to migrate to this updated source, addressing potential issues with old invalid URLs.
  • Utility Function: Added a new utility function, getSeriesBaseUrl, to extract the base URL for a series from an episode URL, facilitating consistent series identification.

🧠 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 Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

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:

  • 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.
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>

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 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.

PineappleTwilight and others added 4 commits March 25, 2026 17:28
…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
@PineappleTwilight
Copy link
Copy Markdown
Author

I know some people prefer each episode as a unique type of "series", but it defeats the entire purpose of the parent app imo.

@PineappleTwilight PineappleTwilight changed the title Modify hStream to group series episodes together fix(hstream) - Modify hStream to group series episodes together Mar 29, 2026
Amnesia added 3 commits March 29, 2026 21:51
…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.
@PineappleTwilight PineappleTwilight changed the title fix(hstream) - Modify hStream to group series episodes together fix(hstream) - Modify hStream to group series episodes together & fix hAnime covers Mar 30, 2026
@PineappleTwilight PineappleTwilight changed the title fix(hstream) - Modify hStream to group series episodes together & fix hAnime covers fix(hstream/hanime) - Modify hStream to group series episodes together & fix hAnime covers Mar 30, 2026
Amnesia added 2 commits March 29, 2026 23:41
…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
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.

1 participant