Files
yuzuno-anime-extensions/.gitignore
PineappleTwilight 7b41726584 Fixes/hanime: Experimental full extension rewrite for new website obfuscation (#192)
* Docs

* Docs

* Docs

* Docs

* feat(hanime): Chicory WASM signature system with live API integration tests - Replace custom ~5600-line WASM interpreter with Chicory runtime (364KB dep) - WASM binary always fetched fresh from live hanime.tv JS bundle (no disk cache) - Chicory WASM Runtime is now the default signature provider (WebView is fallback) - Add 63 unit + integration tests (all passing): - ChicoryGlueTest (17): WASM import function types - ChicorySignatureProviderTest (11): Provider lifecycle - ChicorySignatureProviderIntegrationTest (5): End-to-end WASM signatures - HanimeApiIntegrationTest (11): Live API endpoint validation - SignatureProviderTest (15): Signature cache/headers/expiry - Base64HelperTest (4): Platform Base64 abstraction - Remove android.util.Log from HanimeWasmBinary (JVM test compat) - Use java.net.HttpURLConnection for integration tests (no Android deps) - Add kotlinx-serialization-json 1.7.3 test dependency - Signature freshness: generate immediately before manifest requests - CDN manifest endpoint uses cached.freeanimehentai.net origin

* Bump version

* fix: avoid IllegalStateException from consumed response body in fetchVideoListWithSignature

videoResponse.body.string() at line 178 consumes the OkHttp response body, making subsequent calls to videoListParse(videoResponse) fail with IllegalStateException: closed. Replace all three fallback calls with a new parseVideoModelString(videoString) helper that reuses the already-read string instead of attempting to re-read the closed response body.

* fix: add missing hv_id field to ManifestStream data class

The ManifestStream data class was missing the hv_id field, which is required by the logic in Hanime.kt to resolve the video ID for manifest requests. Without it, the extension would fail to correctly identify the video ID from manifest streams.

* fix: throw SignatureException when WASM malloc is unavailable

Writing to an arbitrary memory address (endAddr) as a fallback when
malloc is unavailable is dangerous and can lead to memory corruption
or crashes if it overlaps with the WASM stack or other data. It is
safer to throw a SignatureException if the required memory management
functions are not exported by the WASM module.

* fix: avoid runBlocking in lazy signatureProvider initialization

Using runBlocking inside a lazy property initializer is risky.
If signatureProvider is accessed from the main thread (e.g., during
preference setup or UI interactions), it will perform synchronous
network requests on the main thread, leading to a
NetworkOnMainThreadException or an ANR error.

Replace the lazy property with a nullable var initialized via a
suspend helper that uses withContext(Dispatchers.IO) instead of
runBlocking.

* fix: replace blocking execute() with non-blocking await() in suspend functions

Blocking calls like execute() should be avoided in suspend functions.
Use the await() extension function from the network library to perform
non-blocking network requests instead. This prevents thread starvation
and allows proper coroutine cancellation.

* fix: remove unused addSignatureHeaders extension function

The addSignatureHeaders extension function is defined but not used anywhere in the class. The actual signature header logic is handled inline in fetchVideoListWithSignature. Remove the dead code to reduce confusion and maintenance burden.

* feat(en/hanime): harden extension robustness across security, threading, and reliability

Security:
- Fix JSON injection in searchRequestBody/latestSearchRequestBody by replacing string-template interpolation with kotlinx.serialization SearchRequest data class; remove pre-quoted JSON strings from SearchParameters (List<String> instead of ArrayList<String>)

Resource management:
- Fix OkHttp Response body leaks in HanimeWasmBinary.fetchPage, fetchVideoListWithSignature, and fetchManifestVideos by wrapping all responses in .use {} blocks

Thread safety:
- Add Mutex-based double-checked locking to ensureSignatureProvider()
- Make SignatureCache.invalidate() suspend with lock acquisition
- Add @Volatile to ChicoryGlue captured fields (capturedSignature, capturedTimestamp)
- Replace mutableSetOf with ConcurrentHashMap.newKeySet for registeredEventTypes
- Add isClosed flag to ChicorySignatureProvider; getSignature() throws after close(), close() acquires initMutex to prevent TOCTOU races
- Make ChicorySignatureProvider.initialize() private
- Replace instance!!/glue!! with local references in getSignature()
- Add @Volatile to authCookie, replace authCookie!! with safe capture

Reliability:
- Add signature refresh-and-retry on 401 from manifest endpoint (invalidates cache, retries once with fresh signature)
- Fix WebView onReceivedSslError to cancel instead of proceed
- Fix WebView onReceivedError to fast-fail for main frame requests
- Fix WebView pollForSignature race (check result before evaluateJavascript)
- Remove redundant @Synchronized from ChicorySignatureProvider.initialize()

Data integrity:
- Use HttpUrl.resolve() instead of string concatenation for URL resolution
- Add MAX_SIGNATURE_LENGTH bounds check in readEmAsmArgs
- Log warnings for unknown ASM_CONSTS IDs
- Clear registeredEventTypes in ChicoryGlue.reset()
- Add null-safety to fetchVideoListPremium, animeDetailsParse, parseSearchJson (empty hits guard)
- Deduplicate videoListParse -> parseVideoModelStreams
- Update User-Agent from Chrome/120 to Chrome/130

Tests:
- Add missing assertNotEquals in testMultipleSignaturesAreDifferent
- Migrate ChicorySignatureProviderTest from runBlocking to runTest
- Standardize Base64HelperTest from JUnit 4 to kotlin.test
- Update tests for private initialize()

* build(en/hanime): override minSdk to 26 for Chicory WASM runtime

The Chicory WASM runtime library (com.dylibso.chicory:runtime:1.7.5) uses
MethodHandle.invoke/MethodHandle.invokeExact bytecode that D8 cannot
desugar below API 26. Override the project-wide minSdk (21) to 26 in the
hanime extension's defaultConfig. This means the extension requires
Android 8.0+ — a hard constraint of the WASM runtime library with no
workaround available.

* Migrate from dead search.htv-services.com to new v10 search API

The old POST-based search endpoint (search.htv-services.com) returns
403 Forbidden for all requests. Migrated to the new GET-based v10 API
at cached.freeanimehentai.net/api/v10/search_hvs with signature headers.

Key changes:
- Replace POST search with GET /api/v10/search_hvs + signature headers
- HAnimeResponse.hits changed from JSON-in-JSON strings to direct List<HitsModel>
- HitsModel updated with new v10 fields (searchTitles, createdAtUnix, etc.)
- HentaiFranchiseHentaiVideo.id changed from Long? to String?
- Added client-side search: fetchSearchHits() caches full API response,
  paginateHits() handles filtering (text, tags, brands, blacklist) and sorting
- Removed old SearchRequest/SearchParameters data classes and request builders
- Added SearchParameters as private inner class for client-side filtering

* Update integration tests for v10 search API migration

- Changed SEARCH_URL to new v10 endpoint (cached.freeanimehentai.net)
- Replaced POST-based search with GET + signature headers
- Simplified parseHits() - hits are now direct JSON objects (no double-parse)
- Added client-side filter helpers (filterHitsByKeyword, filterHitsByTag)
- Removed nbPages assertions (no server-side pagination)
- Updated pagination test for client-side slicing
- Video/manifest/franchise tests unchanged (those endpoints still work)

* Use slug instead of numeric ID in episode URLs

Episode URLs were constructed with numeric IDs (e.g., ?id=5) which is
fragile — the API happens to accept numeric IDs but slugs are the
canonical identifier. Changed episodeListParse() to use it.slug instead
of it.id, and renamed the misleading variable in fetchVideoListPremium()
from `id` to `slug` since it now holds a slug. This also fixes the
premium path which constructs web page URLs like /videos/hentai/<slug>
— numeric IDs don't resolve on the web frontend, but slugs do.

* Fix JsonDecodingException: parse v10 search API as array not object

The v10 search API returns a raw JSON array [{...}] at the root level,
not a wrapped object {"hits": [{...}]}. Changed fetchSearchHits() to
parse the response directly as List<HitsModel> instead of HAnimeResponse.
Removed the dead HAnimeResponse wrapper class (its pagination fields were
from the old search.htv-services.com API). Updated integration test to
parse the array response format.

* Fix JsonDecodingException: change id/brandId from String? to Long?

The /api/v8/video endpoint returns id and brand_id as unquoted
JSON numbers (e.g., "id":2948, "brand_id":92), but
HentaiFranchiseHentaiVideo declared id as String? and both data
classes declared brandId as String?. This caused "Expected
quotation mark but had '2' instead" crashes.

Changed:
- HentaiFranchiseHentaiVideo.id: String? → Long?
- HentaiFranchiseHentaiVideo.brandId: String? → Long?
- HentaiVideo.brandId: String? → Long?

All three fields are dead data (never read in Hanime.kt) so no
downstream code changes were needed.

* Fix video playback: height type mismatch, missing CDN headers, guest filtering

Root cause: Stream.height was declared as String but the API returns an
integer (e.g., "height":720), causing SerializationException on every
video response parse. Changed height to Int? in all three Stream data
classes (Stream, ManifestStream, WindowNuxt.Stream).

Additional fixes:
- Added Referer/Origin headers to Video constructor for CDN playback
  (servers return 403 without proper Referer)
- Added isGuestAllowed filter in parseVideoModelStreams() so non-premium
  users only get streams they can actually play
- Removed content-type and accept from SignatureHeaders.build()
  (content-type is invalid on GET requests, accept was duplicated with
  base headers)
- Wrapped fallback parseVideoModelStreams() in try-catch to prevent
  double-exception crash when both manifest and direct parsing fail
- Updated tests for removed signature headers

* feat(en/hanime): rewrite video flow with PlaylistUtils and bypass decoy manifest

- Add PlaylistUtils dependency for proper HLS master playlist parsing
- Rewrite fetchVideoListWithSignature() to clearly document 2-step flow: 1) Get hvId from /api/v8/video, 2) Fetch real streams from guest manifest
- Replace parseManifestResponse() with parseManifestStreams() using PlaylistUtils.extractFromHls() for proper multi-quality HLS handling
- Add playerVideoHeaders() with correct Referer: https://player.hanime.tv/ (required for m3u8, AES key, and segment CDN requests)
- Add tryParseDecoyStreams() as last-resort fallback for decoy manifest
- Rewrite fetchVideoListPremium() with PlaylistUtils.extractFromHls()
- Update parseVideoModelStreams() to use playerVideoHeaders()
- Add name field to WindowNuxt.Server for server identification
- Add signing key and Windows artifacts to .gitignore

* chore(en/hanime): remove outdated docs and wasm dump files

* fix(en/hanime): replace decoy fallback with empty list, use CDN manifest for premium

- tryParseDecoyStreams() now returns emptyList() instead of broken streamable.cloud URLs that never play (clean failure > broken videos)
- fetchVideoListPremium() now tries CDN guest manifest first (real Golem streams), falls back to __NUXT__ manifest only if CDN fails
- Added hentai_video field to WindowNuxt.Data.DataVideo for hvId extraction
- Fallback catch in premium path returns emptyList() instead of broken Video

* fix(hanime): label video streams with proper quality

Replace generic "Video" fallback in videoNameGen lambdas with actual stream height (e.g., 720p, 1080p) so streams display as "Golem - 720p" instead of "Golem - Video".

* fix(hanime): filter franchise episodes to current series only

The API's hentai_franchise_hentai_videos returns ALL videos in a franchise (broader than a single series). This caused episodes from different sub-series to appear mixed together (e.g., Immoral Sisters 2 episodes showing under Immoral Sisters 1).

- Filter franchise videos by matching series name via getTitle()
- Use actual episode names from API instead of generic 'Episode N'
- Fix IndexOutOfBoundsException in getTitle() for single-word numeric titles
- Replace .ifEmpty fallback with safe single-episode fallback
- Embed hvid in episode URLs and use it to skip /api/v8/video resolution
- Add extractHvIdFromUrl() helper for direct manifest lookup

* fix(hanime): add unfiltered stream fallback for multi-episode video playback

When fetchManifestVideos fails (CDN guest endpoint returns empty or non-guest-allowed streams), the signature path had no working fallback. Only premium users (with auth cookie from webview) could play videos via the HTML/__NUXT__ fallback.

- Add parseVideoModelStreamsUnfiltered() that parses VideoModel manifest streams without the isGuestAllowed filter (only filters premium_alert and requires .m3u8)
- Update fetchVideoListWithSignature to use this as final fallback instead of tryParseDecoyStreams which always returned empty
- Update fetchVideoListPremium to share the same unfiltered fallback
- Both paths now have consistent 3-level fallback: direct manifest → slug-resolved manifest → unfiltered API streams

* Harden signature generation system - Fix ChicoryGlue.reset() destroying WASM event listener registrations (root cause of intermittent playback failure) - Add ChicoryGlue.fullReset() for complete teardown on re-init - Fix Signature TTL: reduce from 4min to 90s (CDN s-maxage=120) - Add Signature.validate() for format + timestamp verification - Add malloc null-pointer validation in ChicorySignatureProvider - Fix close() deadlock: remove runBlocking from ChicorySignatureProvider - Add reinitialize() + retry logic in ChicorySignatureProvider - Extract generateSignature() method for retry support - Harden WASM binary extraction: retry logic, timeouts, fallback regex - Remove SignatureCache layer (no value — WASM execution is fast) - Replace isStale/markStale with signatureProviderMode comparison - Escalating 401 recovery: fresh sig -> provider recreate - Add search hits cache TTL (10 min) - Preference change forces provider recreation - Add @Volatile to signatureProvider and signatureProviderMode - Capture response code inside use{} block - Extract Regex to companion constant in Signature.validate() - Add unit tests for validate(), fullReset(), reset preserves eventTypes - 66/66 tests pass

* Add comprehensive debug logging to hanime extension for 401 playback diagnosis

* fix: add content-type and sec-ch-ua headers to fix 401 on manifest requests for some videos

* fix: implement __tzset_js to write timezone data to WASM memory for valid signature generation

* Fix WebView signature provider: switch default from WASM to WebView

The Chicory JVM WASM interpreter produces invalid cryptographic signatures because it stubs the embind/emval system (returns 0 for all JS object interactions), causing WASM internal state to diverge from a real browser. The server rejects these with HTTP 401.

Changes:
- Switch default signature provider from wasm to webview
- Update preference labels: WebView (Recommended), Chicory WASM (Experimental)
- Add Mutex to serialize getSignature() calls — prevents concurrent WebView creation that caused Already resumed IllegalStateException crash
- Add AtomicBoolean double-resume guard replacing non-thread-safe var boolean
- Guard onPageFinished against duplicate poll chains from redirects
- Add signature caching with 2-minute TTL — first video ~5-15s, subsequent instant
- Fix ktlint error: replace $JS_INTERFACE_NAME template in raw string with placeholder + runtime replace
- Replace Unicode characters with ASCII equivalents for ktlint compliance

* Delete stacktrace.txt

* feat(hanime): robustness and efficiency improvements

Thread safety:
- Fix race condition in ensureSignatureProvider() double-check locking
- Add mutex for search cache to prevent redundant API fetches
- Make ChicoryGlue.asmConsts thread-safe (ConcurrentHashMap)

Feature fixes:
- Implement AND tag inclusion mode (was collected but ignored)
- Add alphabetical sort support (title_sortable was silently broken)

Code cleanup:
- Remove dead code: searchApiRequest, tryParseDecoyStreams, videoListParse, parseVideoModelStreams
- Consolidate slug extraction into extractSlugFromUrl() helper
- Fix getTitle() to not strip legitimate trailing numbers
- Remove all signature value logging (security concern)
- Reduce excessive debug logging across all source files
- Add blank body check in HanimeWasmBinary

Repository cleanup:
- Remove stray nul file, .class files, __pycache__, empty dirs

* feat(hanime): add native signature provider and fix WASM JS environment mock

- Add NativeSignatureProvider: direct SHA-256 signature computation
  Algorithm: SHA256(,Xkdi29,https://hanime.tv,mn2,)
  Zero WASM dependency, instant init, works on all API levels
  Overridable timestampProvider for deterministic testing

- Fix ChicoryGlue emval environment mock:
  Add EmvalHandleManager with full JS environment simulation
  window.top.location.origin -> https://hanime.tv (was returning undefined)
  Real __emval_get_global, __emval_get_property, __emval_new_cstring
  Real __emval_decref with handle release
  Thread-safe ConcurrentHashMap for JsObject properties
  Transient handle tracking to prevent .length handle leaks

- Update Hanime.kt:
  Add native as default signature provider option
  Fix race condition in ensureSignatureProvider()

- Add comprehensive tests:
  NativeSignatureProviderTest with known-answer vectors
  18 new ChicoryGlueTest emval tests (36 total)

Root cause: WASM reads window.location.origin via emval chain during
signature computation. Stub emval functions returning 0 (undefined)
caused different SHA-256 output than the browser.

* Delete leftovers

* feat(hanime): add QoL extension preferences

Add 4 new user-configurable preferences:
- Censored Content Filter (Show All/Uncensored Only/Censored Only)
- Include Premium Streams toggle
- Search Cache Duration (1/5/10/30 minutes, was hardcoded 10 min)
- Custom CDN Domain override with URL validation

Also migrate existing preferences (Preferred Quality, Signature Provider)
from verbose ListPreference pattern to keiyoushi.utils extension functions.

* clean up

* use helper utilities to close resource

* optimize stream extraction using parallel processing

* clean up test & project files

* fix episode URL & un-used methods

* do not consume the resume guard on `isResumable`

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Amnesia <amnesia@example.com>
Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com>
2026-05-14 00:19:48 +07:00

17 lines
153 B
Plaintext

.gradle
/local.properties
/.idea/workspace.xml
.DS_Store
build/
/captures
.idea/
*.iml
repo/
apk/
gen
generated-src/
.kotlin
.history
signingkey.jks
nul