Files
yuzuno-anime-extensions/gradle.properties
PineappleTwilight 51fa7e2b07 Fixes/miruro: Squash Merge (#629)
* Fix miruro issues

* Revert version

* Fix null titles (again)

* Update src/en/miruro/src/eu/kanade/tachiyomi/animeextension/en/miruro/Miruro.kt

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Fix review issues

* Add provider toggle

* Improve Miruro provider robustness with retry, fallback, and graceful error handling

- Make decryptResponse graceful (Log.e + return empty instead of throw)
- Eliminate volatile mutable state (currentEpisodeData, currentAnilistId)
- Embed episode data and anilistId in pipe URL JSON for thread-safety
- Derive subTypes dynamically from episodesObj.keys()
- Add safePipeApiCall with retry for 444/5xx errors (2 retries, 500ms delay)
- Build cross-provider episode map at episodeListParse time
- Embed fallback provider data in episode URLs for zero-cost fallback
- Add video-level provider fallback in videoListParse
- Remove dead parseEpisodesFromProvider code

* Bump Miruro extVersionCode to 3

* Refactor Miruro: eliminate volatile state, deduplicate episode paths, refine retry logic

- Replace cachedAnimeMeta @Volatile var with thread-safe LRU LinkedHashMap
  cache (max 16 entries, access-order eviction) via getOrCreateMeta/getMeta
  helpers - eliminates race conditions from shared mutable state
- Unify three nearly-identical episode building blocks (primary, merge
  fallback, non-merge fallback) into a single provider loop with
  merge/non-merge guards
- Extract stripHtml() private function from inline HTML stripping chain
- Refine safePipeApiCall to only retry on 429/444/502/503/504 instead
  of all 5xx codes
- Bump extVersionCode 3 -> 4

* Fix Miruro: cascade fallback on provider errors, skip 444 retries

- 444 is a permanent 'content not available' from provider, not transient
  - Remove 444 from retryable codes in safePipeApiCall
  - Return immediately on 444 instead of wasting time retrying
- Primary provider failure now triggers fallback chain immediately
  - Track primaryFailed flag from 444 or parse exceptions
  - Fallback runs on primaryFailed OR empty videos
- Exponential backoff for transient errors (429/502/503/504)
  - 1s, 2s, 3s delays instead of flat 500ms
  - maxRetries bumped from 2 to 3
- Fallback tries all sub-types per provider, not just preferred
- Log empty/null streams arrays for easier debugging
- Bump extVersionCode 4 -> 5

* Fix Miruro: handle unreleased series and provider 444 errors gracefully

- Add validateResponse() to catch 500 (unreleased series) and 444
  (provider doesn't have content) before decrypting
- animeDetailsParse + episodeListParse now show clear error messages
  instead of crashing on garbage response bodies
- IOException messages are user-facing in the host app
- Bump extVersionCode 5 -> 6

* Refactor Miruro: extract DTO and extractor into separate files, remove duplicate code

Split monolithic Miruro.kt into MiruroDto.kt (serialization DTOs and Json parser) and
MiruroExtractor.kt (video extraction, circuit breaker, decryption, fallback traversal).
Remove duplicate code block (second copies of buildPipeRequest, buildPipeQuery,
safePipeApiCall, decryptResponse, etc.), fix nested lambda ambiguity in subtitle
mapping, add missing resolveTitleFromDto method, fix kotlinx.serialization.Json
import path, add jsonObject import, remove unused imports.

* Fix Miruro: use dynamic year generation in filters

Replace hardcoded 2026 year with Calendar.getInstance().get(Calendar.YEAR) so
filter year options stay current without manual updates.

* Bump Miruro extVersionCode to 7

Version bump for refactor and filter improvements.

* Align Miruro with live site: dynamic config, fix provider order, remove kuz

* Fix version

* Fix Miruro: extract media object from /info response to restore descriptions

* Fix Miruro: wire dub filter, make cache thread-safe, fix resource leaks, improve robustness

- Wire DubLanguageFilter into searchAnimeRequest (dub query param)

- Make animeMetaCache thread-safe (Collections.synchronizedMap + synchronized)

- Convert AnimeMeta to regular class with @Volatile on mutable fields

- Fix resource leak in videoListParse (nullable primaryResponse, finally close, 444 early exit)

- Add toIntOrNull guard in episodeListRequest with descriptive IOException

- Add synchronized(this) double-check locking in fetchConfig

- Pre-compile regex patterns as companion constants (BR_REGEX, CLOSE_P_REGEX, HTML_TAG_REGEX, QUALITY_REGEX)

- Extract truncateDescription() and formatSubTypeLabel() helpers (dedup)

- Add TRANSIENT_RETRY_CODES companion constant

- Add explicit JSONArray/JSONObject branches in buildPipeQuery

- Add logging to 5 swallowed exception catch blocks

* Fix Miruro: remove unused code, fix extractor leak, harden types and null safety

- Remove unused baseUrl from MiruroExtractor constructor

- Fix safePipeApiCall resource leak (close before new call, null guard on return)

- Add h-sub/embed cases to extractor subType labels

- Add TRANSIENT_RETRY_CODES companion constant in extractor

- Remove unused DTOs (PipePayload, AnimeBrowseResponse, PageInfoDto, EpisodesResponseDto, ProviderEpisodeDto)

- Remove unused JsonElement import from MiruroDto

- Convert AnilistMalIdResponse and JikanEpisodesDto nested classes to data class

- Make jsonParser internal visibility

- Replace !! with ?. null-safe handling in MiruroFilters.parseCheckbox

- Remove exitProcess(0) from MiruroUrlActivity (was killing host app process)

* Fix Miruro: add genre/tag exclusion filters, remove invalid Hentai genre

Convert GenreFilter and TagsFilter from CheckBox to TriState to support include/exclude/ignore. Wire excludedGenre and excludedTag API params in searchAnimeRequest. Remove Hentai from genre list (not valid per AniList). Bump extVersionCode to 4.

* Fix Miruro: remove unused JsonObject import

* fix(en/miruro): replace provider aliases with real display names

Replace coded aliases (kiwi, bee, hop, etc.) with actual streaming service names (AnimePahe, Anikoto, Zoro, etc.) in preferences and episode scanlator labels. Add PROVIDER_DISPLAY_NAMES map and providerDisplayName() function. Sort videos by preferred provider first. Bump extVersionCode to 3.

* refactor(en/miruro): add provider names to video quality labels and improve extractor logging

Prefix Video.quality with provider display name (e.g. 'AnimePahe - 1080p Sub'). Add providerDisplayName() and PROVIDER_DISPLAY_NAMES to extractor companion object. Improve circuit breaker log messages to show both display name and alias key.

* fix(en/miruro): show display names in preference summary and all providers in scanlator

* fix(en/miruro): default show provider names in scanlator to false and improve title

* fix(en/miruro): aggregate sub-type labels across all providers in scanlator

* fix(en/miruro): filter scanlator to sub/dub only and clean up serialization roundtrip

- Add SCANLATOR_SUB_TYPES whitelist (sub, dub) to filter out non-display
  sub-type keys like 'es', 'ssub', 'h-sub' from scanlator label
- Add SUB_TYPE_DISPLAY_ORDER for consistent scanlator label ordering
- Replace org.json ↔ kotlinx.serialization double-serialization with
  direct buildFallbackProvidersJson/buildFallbackSubTypesJson helpers
- Fix MutableMap type (Map→MutableMap) resolving 'No set operator' error
- Remove duplicate PROVIDER_DISPLAY_NAMES from MiruroExtractor, delegate
  to Miruro.providerDisplayName()
- Bump extVersionCode 2 → 3

* Fix version

* feat(en/miruro): add stream format preference and label HLS/MP4 in video quality

Add Preferred Stream Format preference (HLS/MP4/All, default HLS) that sorts preferred format first as a fallback - non-preferred formats are never discarded, just ranked lower. Include all stream types instead of silently skipping non-HLS. Label stream type (HLS/MP4) in quality string so users can distinguish them. Bump extVersionCode to 3.

* fix(en/miruro): rename MP4 stream preference to Embed to match Miruro terminology

* fix(en/miruro): improve stream format preference labels

* refactor(en/miruro): remove embed providers and stream format preference

- Remove 5 embed providers (bun, nun, twin, cog, telli) from defaultConfig
- Update provider list to HLS-only: kiwi, bee, bonk, ally, moo, hop, dune
- Remove stream format preference (HLS/Embed/First available) — now always HLS
- Remove embed from SUB_TYPE_DISPLAY_ORDER and formatSubTypeLabel
- Remove TRANSIENT_RETRY_CODES and preferredStreamFormat delegate
- Simplify videoListParse — direct parseStreamsFromResponse call, no retry/fallback
- Remove buildFallbackProvidersJson and buildFallbackSubTypesJson helpers
- Bump extVersionCode 3 → 4

* refactor(en/miruro): remove proxy, retry, and fallback logic from extractor and DTOs

- Remove buildProxiedUrl, obfuscateUrl, fnv1a32, PROXY_* constants
- Remove circuit breaker (providerFailureState, FailureState, isProviderCircuitOpen)
- Remove recordProviderFailure/recordProviderSuccess
- Remove safePipeApiCall (retry with exponential backoff)
- Remove fetchFallbackVideos and buildPipeQueryMap
- Simplify parseStreamsFromResponse — remove proxyConfig, cors, episodeId params
- Remove ProxyConfigDto from DTOs
- Simplify ProviderConfigDto — remove parent, relationship, visible, player, proxy, cors, fallback
- Fix referer check: != null → isNotEmpty() (field has default value, never null)

* feat(en/miruro): add AniTaku (twin) and AnimeGG (cog) providers

* feat(en/miruro): dynamically fetch providers from config API

Fetch available providers from the site's config API on extension load
instead of hardcoding them. Filters out embed providers (identified by
relationship=embed + player=iframe in config) so only native providers
appear in preferences and episode resolution.

- Add parent, relationship, visible, player fields to ProviderConfigDto
- Replace static PROVIDER_DISPLAY_NAMES/PREF_PROVIDER_ENTRIES/VALUES
  with mutable instance vars populated from config API
- MiruroExtractor now takes resolveDisplayName lambda instead of
  calling static companion method
- Auto-reset stale preferred provider when config changes
- Episode fallback respects config provider priority order
- findMaxEpisodeNumber scans all providers when preferred is missing
- Remove twin/cog from defaults (they are embeds of bonk/moo)
- Bump extVersionCode to 6

* fix(en/miruro): remove duplicate providerOrder local variable

The same val providerOrder was declared twice in the same function
scope (line 549 and 592), causing a compile error. Removed the second
declaration since the first is already in scope.

* Spotless

* fix(en/miruro): extract m3u8 from kwik embed pages via JsUnpacker

Embed streams returned kwik.cx HTML page URLs as playable video URLs, causing
unsupported file format errors. Now fetches the kwik /e/ page, unpacks the packed
JavaScript, and extracts the actual .m3u8 URL. Also adds isActive field to StreamDto.

* feat(en/miruro): add stream type preference, highest available quality, and more truncation options

- Add Preferred Stream Type preference (HLS/Embed/All, default HLS) that filters
  videos in sort with fallback to full list if filter yields empty
- Add Highest Available as default quality option (sorts by actual resolution
  descending instead of pinning a specific quality)
- Add 750 and 75 character description truncation options

* feat(en/miruro): async config fetch, dynamic provider mapping, and debug logging

Migrate synchronous blocking config fetch to cached async system with
SharedPreferences persistence. Config is fetched on extension load and
re-fetched on mirror change; cold starts use cached prefs until fresh
data arrives.

Add dynamic provider alias-to-display-name mapping using config API data:
- Parent-aware naming for embed providers (e.g. 'Bun (via Bee)')
- Respect visible flag to filter out hidden providers
- Append capability labels to preference entries (e.g. 'AnimePahe (Sub, Download)')

Fix ProxyConfigDto deserialization — API returns boolean for embed
providers but object for native providers. Add ProxyConfigSerializer to
handle both forms gracefully.

Add comprehensive Log.d debug logging throughout Miruro.kt and
MiruroExtractor.kt. Use TAG constant instead of hardcoded strings.

Bump extVersionCode 4 → 6.

* feat(en/miruro): add MAL episode title support via Jikan API

Fetch episode titles from Jikan alongside filler data in a single pagination
pass. Cache titles in AnimeMeta, resolve via new resolveEpisodeTitles()
method with cache-first logic that shares Jikan calls with filler resolution.
Add Episode Title Source preference (MAL/Provider) defaulting to MAL.
Provider titles serve as fallback when no MAL title is available for a
given episode number.

* chore(en/miruro): bump extVersionCode for MAL episode titles

* fix(en/miruro): use lenient Json parser for proxy config deserialization

The ProxyConfigSerializer used the default Json instance which does not
ignore unknown keys, causing a warning when the API returns new fields
like 'segments' in the proxy config object. Switch to jsonParser which
has ignoreUnknownKeys = true.

* feat(en/miruro): add dynamic mirror fetching from status page API

Fetch available mirrors from status.miruro.com API, filter by heartbeat
health status, persist to SharedPreferences, and populate mirror
preference dynamically. Migrate rateLimitHost calls to kotlin.time
Duration API.

* chore(en/miruro): bump extVersionCode for dynamic mirrors

* fix(en/miruro): place episode 0 before episode 1 in all sort orders

Separate episode 0 entries from the main list and prepend them
to the front, so descending sort yields 0, 12, 11, ..., 1 instead
of 12, 11, ..., 1, 0. Ascending order was already correct but is
now explicit about the split.

* miruro: add missing providers (moo, cog) and improve embed extraction

- Add moo (Moon) and cog (Cog) to provider defaults and display names
- Add dune and kuz to known display names
- Replace custom kwik embed extraction with OmniEmbedExtractor
- Add fallback provider support when primary provider returns no videos
- Bump extVersionCode to 10

* lib: add omniembedextractor and kwikextractor libraries

* Integrate all video extractor libs into OmniEmbedExtractor

Add 26 extractors beyond the original 8 (34 total):
- Tier 1 (simple): Voe, Streamlare, StreamHub, VidGuard, Sendvid, StreamDav, StreamSilk, Vido, Vudeo, Upstream, Sibnet, Rumble, Amazon, Fusevideo, Lulu, Buzzheavier, Fastream, VidBom, PixelDrain, MixDrop
- Tier 2 (suspend): VidMoly, VidHide, StreamPlay, StreamUp, Uqload, Blogger

Domain mappings cover 50+ embed domains used across extensions.

* miruro: address code review feedback

- Remove unused companion providerDisplayName (instance method handles fallback)
- Remove unused referer param from OmniEmbedExtractor.extractVideos
- Derive Jikan maxPages from maxEpisode instead of hard-coding 10
- Gate Log.d calls behind BuildConfig.DEBUG via logD helper
- Track mirror fetch job and add close() to cancel CoroutineScope

* Revert unnecessary changes

* Revert "Revert unnecessary changes"

This reverts commit c05093891f.

* Revert UrlActivitiy

* Cleanup

* Suggestion

* refactor

* Potential fix for pull request finding

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

* PR changes

* Fix okhttp

* lib: add anilib library for AniList GraphQL API

* refactor(en/miruro): replace Jikan and inline AniList code with anilib

* Init

* fix(cloudscraper): address code review compilation and runtime issues

- Evaluate URL_HELPER before shims that reference __cf_parseUrl (BrowserEnvironment)

- Add missing kotlinx.serialization.Serializable import for CvParams (JsdSolver)

- Fix isLenientOnJson -> isLenient in Json builder (JsdSolver)

- Fix key-quoting regex to produce valid JSON with opening quotes (JsdSolver)

- Replace non-existent Cookie.EXPIRES_AT_OTHER with direct expiresAt comparison (CookieCache)

- Remove unused defaultTtlMillis constructor parameter (CookieCache)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(en/miruro): add "Include all provider streams" preference

Adds a settings toggle (default off) that controls whether video streams
from all available providers are included in the player, or only the
preferred provider's streams.

When off (default): only the preferred provider's streams are shown
(fallbacks still used when primary is empty).
When on: streams from all providers are fetched and shown.

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(lib/anilib): expand AniLib with rate-limit handling, caching, paginated queries, ani.zip, and description builder

- Add rate-limit handling: throttle() enforces min interval (667ms normal,
  2000ms degraded), handle429() reads Retry-After, handleRateLimitHeaders()
  reads X-RateLimit-Remaining, state persisted to SharedPreferences
- Add inline SharedPreferences caching with configurable TTL (default 15min)
- Add fetchSearch, fetchTrending, fetchSeasonal, fetchRelatedMedia queries
- Add fetchEpisodeTitles() for ani.zip integration returning EpisodeTitlesResult
- Expand MediaSnapshot with format, episodes, duration, season, seasonYear,
  startDate, endDate, synonyms, averageScore, popularity, tags, relations,
  characters, trailer, countryOfOrigin, source, siteUrl
- Add PageData, PageInfo, FuzzyDate, MediaTag, MediaConnection, MediaEdge,
  RelatedMediaNode, CharacterNode, CharacterName, CharacterImage, Trailer,
  AnizipResponse, AnizipEpisode, EpisodeTitlesResult DTOs
- Migrate executeQuery() to graphQLPost()/parseGraphQLAs() from core
- Add buildDescription() extension on MediaSnapshot with DescriptionOptions

* refactor(en/miruro): prioritize AniLib data over pipe API in anime details, enrich episodes with ani.zip titles

- animeDetailsParse now tries AniLib.fetchMediaDetails() first for richer
  AniList GraphQL data, falling back to pipe API DTO then raw JSONObject
- Add buildFromSnapshot() using AniLib.resolveTitle/resolveCoverUrl/
  resolveMainStudio and buildDescription()
- Add buildFromDto() using same AniLib helpers via MediaTitle/MediaCoverImage/
  MediaStudios construction
- Extract mapStatus() as shared helper for all three parse paths
- Remove resolveTitleFromDto() (fully replaced by AniLib.resolveTitle())
- Replace FQCN aniyomi.lib.anilib.X with short-form imports
- Enrich episode names from ani.zip when only "Episode X" is available

* fix(en/miruro): bump extVersionCode to 6

* feat(lib/anilib): add AniFiller integration with episode classification types and cached dataset fetch

* feat(en/miruro): add filler episode handling preferences and integrate AniFiller data in episode list

* fix(lib/anilib): fix GraphQL deserialization, add unified filter/browse method, migrate Miruro browse to AniLib

Replace AniListResponse<T> with correct DTO types for parseGraphQLAs.
parseGraphQLAs already unwraps the GraphQL {data} envelope, so
AniListResponse<T> (which has its own data field) caused a
mismatch — after unwrapping, JSON is {Page:{...}} not
{data:{Page:{...}}}.

- Add PageDataWrapper with @SerialName("Page") property matching
  post-unwrapping JSON shape (same pattern as MediaData with
  @SerialName("Media"))
- Change all executeQuery calls: Page queries use PageDataWrapper,
  Media queries use MediaData directly
- Remove AniListResponse<T> (no longer needed)
- Add MediaFilter data class with toVariables() for parameterized
  GraphQL queries
- Add fetchFilteredMedia with FILTERED_MEDIA_QUERY supporting all
  AniList filter parameters
- Refactor fetchSearchAnime/fetchTrending/fetchSeasonal to delegate
  to fetchFilteredMedia
- Add convenience wrappers: fetchPopular, fetchUpcoming,
  fetchRecentlyUpdated
- Migrate Miruro browse/search from pipe API to AniLib directly
- Add toMediaFilter() extension on FilterSearchParams
- Add animeFromMediaSnapshot() converter and applyDubPostFilter()
- Bump extVersionCode to 8

* refactor(lib/anilib): restyle description builder with markdown formatting and star ratings

Use markdown bold labels, star ratings, and structured line-per-field
layout instead of plain Unicode symbols (⊛⊞│) and cramped single-line
info. This matches the styling patterns used by Animetsu, Cineby, and
AnikotoTheme in the repo.

- Add 5-star score visualization (★★★★☆ 85%) replacing plain ★ 85%
- Replace Unicode bullets with **Bold:** labels for genres/tags/studio
- Replace │ inline separator with line-per-field layout for readability
- Use [linked titles] for relations when AniList siteUrl is available
- Use [▶ Watch Trailer](url) instead of raw Trailer: URL
- Use *italic* for synonyms section
- Use --- horizontal rule before synopsis instead of bare em-dash
- Use Airing instead of Releasing for RELEASING status
- Use ordinal episodes (12 episodes) instead of abbrev (12 eps)
- DescriptionOptions API unchanged — fully backward-compatible

* Fix title pref

* fix(lib/anilib): fix AnizipEpisode deserialization, add airing dates and filler episode data

- AnizipEpisode.title was String? but API returns JSON object (en/x-jat/ja), causing silent null. Add AnizipTitle DTO and resolvedTitle computed property.
- AnizipEpisode had no @SerialName for aired field; add airDate/airdate with correct SerialNames and resolvedAirDate computed property.
- Make EpisodeTitlesResult @Serializable with pre-parsed airDates map.
- Add FillerEpisodeData(type, airDate, title) DTO with parsed epoch millis.
- Add @Serializable to FillerType enum (was missing, blocked compilation of FillerEpisodeData).

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(lib/anilib): add caching and date parsing to fetchAiringSchedule, fetchEpisodeTitles, fetchFillerData

- Add SharedPreferences caching (15-min TTL) to fetchAiringSchedule via executeQuery with cacheKey.
- Extract extractAiringSchedule(snapshot) and buildScheduleMap(media) as public helpers to reuse schedule data from already-fetched fetchMediaDetails cache.
- Add SharedPreferences caching (15-min TTL) to fetchEpisodeTitles; parse resolvedAirDate via anizipDateFormat.tryParse() into airDates map.
- Rewrite fetchFillerData to return FillerEpisodeData with parsed airDate (epoch millis via anifillerDateFormat.tryParse) and title from AniFillerEpisode.
- Move anizipDateFormat and anifillerDateFormat lazy vals above KDoc blocks for correct doc-comment association.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(en/miruro): fix episode airing dates, titles, and filler marks with enriched fallback chains

- Rewrite episodeListParse enrichment: try fetchMediaDetails cache first for schedule data (reuses 15-min cache), fall back to standalone fetchAiringSchedule only on failure.
- Date fallback chain: AniList airingSchedule → ani.zip airDates → AniFiller airDate.
- Title enrichment: try ani.zip resolvedTitle first, then AniFiller title as last resort.
- Filler marking via FillerEpisodeData with [Filler], [Mixed Canon], [Anime Canon] scanlator tags.
- Use Math.round() instead of .toInt() for Int map lookups to fix Float truncation bug on fractional episodes.
- Bump extVersionCode 8 → 10.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* https://developer.android.com/studio/known-issues#android-studio-quail-1-parallel-sync-execution-issue

* fix(lib/anilib): make AniFillerMappings.malId nullable to handle null JSON values

AniFiller dataset can contain null mal_id for anime without a MyAnimeList entry. The non-nullable Int type caused kotlinx.serialization to throw 'Unexpected symbol n in numeric literal' at path $[175].mappings.mal_id.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(en/miruro): remove brackets from filler episode scanlator marks

Change filler mark labels from [Filler], [Mixed Canon], [Anime Canon] to Filler, Mixed Canon, Anime Canon in episode scanlator field.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(en/miruro): add CloudScraperInterceptor and zstd decompression for pipe API requests

Miruro.tv pipe API requests were failing with HTTP 403 due to Cloudflare
TLS fingerprinting blocking OkHttp's non-browser TLS stack. Additionally,
the server now responds with content-encoding: zstd instead of gzip.

Changes:
- Add CloudScraperInterceptor (QuickJS-based headless CF bypass) to OkHttp client
- Add ZstdDecompressInterceptor as a network interceptor for zstd decompression
- Bump extVersionCode 10 -> 11
- Add dependencies: lib:cloudscraper, com.github.luben:zstd-jni:1.5.6-9@aar

The CloudScraperInterceptor solves JSD, legacy IUAM, and managed CF challenges
without a WebView, and caches cf_clearance cookies across requests. The
ZstdDecompressInterceptor strips the Content-Encoding header and decompresses
the response body before OkHttp's BridgeInterceptor processes it.

* fix(cloudscraper): resolve compilation errors in cloudscraper library

Cloudscraper library from feature/cflib merge had several compilation issues:

- BrowserEnvironment: const val with trimMargin() requires runtime
  evaluation; changed URL_HELPER to @JvmField val
- ChallengeDetector/JsdSolver: $cv and $params interpreted as Kotlin
  string templates; escaped as ${'$'}cv and ${'$'}params
- JsdSolver: extractCookies returns immutable Map<String,String>;
  changed to toMutableMap() for putAll support
- LegacyIuamSolver/ManagedChallengeSolver: non-local return statements
  inside withQuickJsTimeout lambda (not inline); restructured to use
  captured result vars returned after the lambda completes

Also fixes in Miruro.kt:
- Buffer.writeTo(zstd) corrected to Buffer.readFrom(zstd) (InputStream vs OutputStream)
- Removed broken .addHeader() call on OkHttpClient.Builder (not a valid API)

* fix(cloudscraper): resolve concurrent QuickJS stack overflow

- Replace per-call Executors.newSingleThreadExecutor() with shared
  ThreadPoolExecutor(4, 4, 60s) daemon threads in QuickJsExt
- Add explicit StackOverflowError catch mapped to CloudscraperException
- Ensure engine is always closed in finally block
- Remove @Synchronized from CloudScraperInterceptor.intercept() which
  was serializing all HTTP requests unnecessarily
- Per-host locking via CookieCache.lockForHost() already handles
  concurrent access correctly

* fix(cloudscraper): harden browser environment and JS solvers

BrowserEnvironment:
- Add JS-level recursion guard (RECURSION_GUARD_SHIM) with
  __cf_callDepth counter and __cf_maxCallDepth=256 limit
- Timer callbacks reset call depth before execution for fresh budget
- Add WebGLRenderingContext and WebGL2RenderingContext stubs with
  full extension object hierarchy
- Add AudioContext and OfflineAudioContext stubs with realistic
  method signatures (createOscillator, createAnalyser, createGain)
- Add Intl.DateTimeFormat, Intl.NumberFormat, and Intl.Collator stubs
  with functional format/compare methods

Security hardening:
- Fix JS injection in ManagedChallengeSolver and LegacyIuamSolver:
  escape backslashes and double quotes in domain string interpolation
- Add protocol whitelist (javascript:/data:) to resolveUrl() in all
  three solvers (JsdSolver, ManagedChallengeSolver, LegacyIuamSolver)
  to prevent SSRF via crafted challenge URLs

* feat(cloudscraper): add TLS/JA3 fingerprint spoofing via impersonator-bctls

Integrate zhkl0228/impersonator (BouncyCastle-based TLS fingerprint
spoofing) to make CloudScraper's HTTP requests appear as real Chrome
browser TLS handshakes to Cloudflare.

- Add impersonator-bctls 1.0.13 dependency (pure Java, no native code,
  no OkHttp classpath conflicts)
- Create impersonated OkHttpClient internally using ImpersonatorFactory
  .chrome() with spoofed SSLContext for Chrome's JA3 fingerprint
- All challenge-solving requests and cookie-carrying retries now use the
  impersonated client, bypassing Cloudflare's TLS fingerprint checks
- Add impersonatedClient constructor parameter for users who want to
  provide their own pre-configured client
- Use solveClient.newCall() directly for retry instead of chain.proceed()
  to ensure the TLS fingerprint is applied to the final request

* fix(cloudscraper): use impersonator-bctls 1.0.12 (latest on JitPack)

* fix(cloudscraper): use ImpersonatorFactory.android() (chrome() doesn't exist)

* feat(en/miruro): shape requests with browser-fingerprint interceptor to bypass WAF block; drop zstd-jni native dep

Miruro's Cloudflare edge enforces a WAF custom-rule block (not a solvable
challenge — the body is the 'Sorry, you have been blocked' page with
cf-error-details and no cf-mitigated header) on pipe-API requests whose
header set does not match a real Chrome browser's fetch(). Live curl
against /api/secure/pipe?e=<base64url> with only UA+Accept+Referer
returns 403; the same probe with the full browser-shape set
(sec-fetch-*, sec-ch-ua-*, Origin, Accept-Language q=0.9, zstd opt-in)
returns 200 + x-obfuscated: 2.

CloudScraperInterceptor cannot fix custom-rule blocks — its isBlockPage
path passes through unresolved by design (no challenge to solve). Rewiring
miruro through the lib was a dead end.

Fix: add MiruroBrowserFingerprintInterceptor, an internal network
interceptor that browser-shapes every miruro request with a Chrome 148
desktop fingerprint. Classifies pipe-context (path /api/*: Sec-Fetch-Dest
empty, Sec-Fetch-Mode cors, Sec-Fetch-Site same-origin, Origin set) vs
navigate-context (warmup GET / and /watch/<id>: Sec-Fetch-Dest document,
Sec-Fetch-Mode navigate, Sec-Fetch-Site none on cold, no Origin).
Overwrites WAF-fingerprint headers (Accept-Encoding/Accept-Language/
User-Agent/Sec-Ch-Ua/Sec-Ch-Ua-Mobile/Sec-Ch-Ua-Platform/Sec-Fetch-*/
Origin) so caller intent cannot leak a bot signal; fill-if-absent on
body-shape headers (Accept, Referer).

Stripped the now-redundant manual UA/Accept/Accept-Language/Referer
from ensureBaseVisit, ensureWatchPageVisited, and buildPipeRequest
(now bare GET(url) + bare Request.Builder().url(...).build()) — the new
interceptor is the single source of truth for fingerprint shaping.
Bumped USER_AGENT from private to internal so the same-package
interceptor can read it.

Second prod crash unmasked by the WAF fix: java.lang.NoClassDefFoundError
clinit failed for com.github.luben.zstd.ZstdInputStreamNoFinalizer. The
zstd-jni native library (libzstd-jni-*.so) wasn't being packaged into the
extension APK under kei.plugins.extension.legacy — the static initializer
that calls System.loadLibrary fails on real Android devices (confirmed on
Samsung SM-S928U, Android 16). Before this fingerprint fix, the WAF
returned 403 HTML (Content-Encoding: identity), so ZstdDecompressInterceptor
short-circuited and never instantiated ZstdInputStream; now that the WAF
passes us through, server honored our zstd advertisement and the broken
native-lib path executed for the first time in prod.

Fix for the crash: drop the zstd token from Accept-Encoding (advertise
only gzip, deflate, br), remove the ZstdDecompressInterceptor class
entirely, and drop the zstd-jni AAR dependency. The server responds with
content-encoding: br instead, which aniyomi's host okhttp3.
CompressionInterceptor handles transparently (no native lib). Verified
via live curl + full decode walk: brotli-decompress → base64url-decode →
XOR with PIPE_KEY → gzip-decompress (XOR'd payload has gzip magic
1f 8b 08 00) → valid UTF-8 JSON {streaming:{bonk:{...},...}}.
MiruroExtractor.decryptResponse already does base64url→XOR→GZIPInputStream,
so it needs zero changes.

Also dropped the now-unused project(':lib:cloudscraper') dep from
miruro gradle — the lib itself is untouched and still available for
other extensions that need challenge-solving.

Build verification: cleaned and assembled successfully on Windows.
Real-device test on Samsung SM-S928U / Android 16 / WebView 149.0.7827.163
passed (no NoClassDefFoundError, pipe API returns real JSON)

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* refactor(lib/cloudscraper): remove obsolete LegacyIuam and ManagedChallenge solvers

These solver paths produced zero production wins on modern Cloudflare edge
configs and were already documented as removed in the parent interceptor's
own KDoc. Modern CF managed v2/v3 challenges ('Just a moment…') require
fingerprint signals (real canvas/WebGL rendering, pointer events, focus
state) that are not reproducible in a headless QuickJS DOM shim — the
math-based JSchl computation that LegacyIuamSolver/ManagedChallengeSolver
performed is no longer sufficient to earn cf_clearance on edges that have
escalated to challenge v2/v3.

The CloudScraperInterceptor now relies on its WebView fallback path
(aniyomi.lib.cloudflareinterceptor.CloudflareInterceptor) for unclassifiable
or managed challenges; the JSD fast-path (JsdSolver) handles the simpler
window.__CF$cv$params variants that DO solve in headless environments.

Files removed:
* ManagedChallengeSolver.kt — QuickJS-DOM-shim attempt at managed v2/v3
  challenges (the approach pioneered by go-cfscraper). Failed on modern
  edges for the canvas/WebGL/pointer/focus reasons cited above.
* LegacyIuamSolver.kt — Cloudflare IUAM (I'm Under Attack Mode) and V1
  classic JS challenges (jschl_vc / jschl_answer / pass). Both mostly
  extinct on modern CF; legacy edges that still serve them are typically
  solvable by JsdSolver's pre-challenge detection path instead.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* refactor(lib/cloudscraper): rewrite JSD solver, browser environment, and QuickJS worker config

Rewrites the three coupled JSD-solver pieces as one unit so each one
compiles against a consistent sibling API. This commit would not build
if split (each file references members the others expose in their new
shape), so all three land together.

* BrowserEnvironment.kt — Refactored from one monolithic shim block into
  modular constant blocks (URL_HELPER, RECURSION_GUARD_SHIM, CONSOLE_SHIM,
  TIMERS_SHIM, ATOB_BTOA_SHIM, TEXT_CODEC_SHIM, windowShim, NAVIGATOR_SHIM,
  SCREEN_SHIM, documentShim, PERFORMANCE_SHIM, CRYPTO_SHIM, MISC_SHIM).
  Each block is evaluated in its own QuickJs.evaluate call so a syntax
  error in one block surfaces with a clear boundary instead of aborting
  the entire install. Fingerprint values (navigator.userAgentData.brands,
  navigator.platform, sec-ch-ua, screen dimensions, language) are now
  derived from the supplied userAgent string so the navigator/brands/
  platform all agree — Cloudflare flags mismatches between reported UA
  and navigator.userAgentData.brands / navigator.platform.

* JsdSolver.kt — Adds initialCookies passthrough so cookies warmed via
  the host client's jar reach the QuickJS script execution; tracks the
  sensor POST URL separately (SensorResult.challengeUrl) for telemetry;
  tightens retry to RETRYABLE_ERRORS only (script-content errors like
  NO_SENSOR_PAYLOAD / NO_CLEARANCE_COOKIE are deterministic for a fixed
  script+env so retrying them only burns another scriptTimeoutMs);
  drops scriptTimeoutMs default from 10s to 8s for faster fallback to
  WebView.

* QuickJsExt.kt — Worker thread stack size raised to 16 MB (private
  WORKER_STACK_SIZE) so both QuickJs.create() and evaluate() run on the
  same 16 MB-stack thread. Without this, the CashApp binding sizes
  QuickJS's internal 1 MB C-stack limit from the creating thread, so
  creating the engine on a small-stack thread and evaluating on a
  large-stack worker causes an instant stack overflow on the very first
  evaluate call (related to the prior 'resolve concurrent QuickJS stack
  overflow' commit but stabilizing the worker thread itself too).

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* refactor(lib/cloudscraper): harden interceptor + detector + cookie cache and add diagnostics

Pairs together the runtime-side hardening flow + a new debug helper.

* CloudScraperInterceptor.kt — Removed the broken impersonator-bctls
  SSLContext path (BCJSSE raised TlsFatalAlert during TLS 1.3 handshake
  on every modern CF edge — see upstream issues zhkl0228/impersonator#8/#9)
  and now layers keiyoushi.lib.tlsspoof.SpoofedTlsSupport on the host
  client builder instead. Restated the KDoc as a hybrid solver: headless
  JSD fast path + WebView fallback for unclassifiable/managed challenges.
  The X-Cloudscraper-Skip marker header on internal retry leg is preserved
  for downstream consumer diagnostics.

* ChallengeDetector.kt — Reduced the challenge taxonomy to binary
  JSD / UNSOLVABLE. The old multi-type taxonomy (MANAGED / LEGACY_IUAM /
  TURNSTILE) carried metadata only the dead solvers consumed; modernized
  to match the 'no managed/legacy solver' reality.

* CookieCache.kt — make lockForHost non-reentrant (a second attempt by
  the same thread returns false so the lock now guards against duplicate
  solve attempts within the same call stack). unlockForHost remove the
  ReentrantLock from the hostLocks map when its queue is empty, so the
  map no longer grows one entry per host ever visited.

* CloudScraperDebug.kt (NEW) — Runtime-flag-gated diagnostic tiers
  (always / verbose / trace) for log-noisy JSD sensor debugging. Flags
  are runtime not compile-time, so a developer can flip them on without
  rebuilding cloudscraper itself.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* chore(lib/cloudscraper): wire test sourceset and add JSD solver unit + integration tests

Splits build config + tests + fixtures together because the test
sourceset needs the new build.gradle.kts deps to compile against
cloudscraper's main sourceset; splitting would leave the new test
sourceset non-compiling and the build config change meaningless
without tests.

build.gradle.kts:
* Adds the 'test' android sourceSet wired to test/java + test/kotlin
  so JVM unit tests can run against this android library module.
* testOptions: isIncludeAndroidResources=false, isReturnDefaultValues=true
  so Log.w/Log.e calls in the unit under test don't try to load the
  Android framework (we are testing parsing/cache/regex logic, not
  real-device logging).
* Replaces implementation(libs.impersonator) with
  implementation(project(":lib:cloudflareinterceptor")) and
  implementation(project(":lib:tlsspoof")) — the runtime path now uses
  keiyoushi.lib.tlsspoof.SpoofedTlsSupport instead of the broken
  impersonator-bctls SSLContext path (BouncyCastle JSSE raised
  TlsFatalAlert during TLS 1.3 handshake on every modern CF edge —
  see upstream issues zhkl0228/impersonator#8/#9).
* Adds testImplementation deps for kotlin-test-junit, okhttp,
  kotlinx.serializaton.json, and quickjs so tests can construct
  HttpUrl/Cookie instances, parse JSON, and run the real sensor
  script under QuickJS for integration coverage.

Tests (test/kotlin/keiyoushi/lib/cloudscraper/, 6 files):
* ChallengeDetectorTest.kt — verifies JSD detection, UNSOLVABLE
  classification of managed/legacy/turnstile/403/503 variants, and
  regex-pattern extraction of __CF$cv$params metadata.
* CookieCacheTest.kt — covers cache hit/miss/stale/expiry, per-host
  lock non-reentrancy (the new lockForHost behavior from the prior
  commit), and hostLocks-map cleanup when the queue is empty.
* JsdSolverExtractCookiesTest.kt — Set-Cookie parsing from sensor
  response, including the cf_clearance cookie and any sibling cookies
  the challenge endpoint emits.
* JsdSolverExtractDeobfuscationPiecesTest.kt — verifies the
  deobfuscation-piece extraction (D-array entries, handler chains)
  from a real captured cf_jsd_main.js sensor script.
* JsdSolverParseCvParamsTest.kt — parses a real __CF$cv$params JSON
  blob into the typed struct the solver consumes.
* JsdSolverRealScriptTest.kt — end-to-end integration: loads the
  real captured cf_jsd_main.js fixture + cf_jsd_sensor_payload.txt,
  runs the full QuickJS install-evaluate-sensor-POST path, asserts
  a non-empty sensor payload is produced. Marked as the slowest test
  in the suite but kept in the regular unit run for regression
  coverage on the JSD fast path.

Fixtures (test/fixtures/, 4 files):
* cf_challenge_403.html — a captured managed-challenge 403 page used
  by ChallengeDetectorTest to verify UNSOLVABLE classification of
  non-JSD challenges.
* cf_jsd_main.js — the real captured JSD sensor script used by
  JsdSolverExtractDeobfuscationPiecesTest and JsdSolverRealScriptTest.
  Single-line file (minified source) so line is long; fixture kept
  as one file rather than reformatted so byte-for-byte matches what
  Cloudflare serves.
* cf_jsd_sensor_payload.txt — the request body the sensor script
  POSTs to /__cf_chl_rcv, used by JsdSolverRealScriptTest as the
  expected output shape for the replicated sensor generation.
* cf_jsd_sensor_response.txt — empty placeholder documenting the
  response shape from the challenge endpoint (real responses are
  server-generated and short-lived; left empty so we don't pin to
  one CF variant's content).

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(lib:tlsspoof,lib:cloudflareinterceptor,gradle): add TLS spoofing support and update CloudflareInterceptor fallback

Adds the new :lib:tlsspoof module + wires it into both the Cloudflare
fallback path and the version catalog as one atomic unit. The five
files land together because each pair would otherwise be broken:

  - tlsspoof's build.gradle.kts consumes the conscrypt dep that the
    decorator-domain KDoc in TlsSpoof.kt references
  - SpoofedTlsSupport.applyTo() calls TlsSpoof.install()
  - CloudflareInterceptor now imports SpoofedTlsSupport to install
    the spoofed stack on the WebView-bypass client; references the
    new keiyoushi.lib.tlsspoof package
  - libs.versions.toml drops the failed impersonator-bctls entry
    (now superseded by :lib:tlsspoof via Conscrypt)

lib/tlsspoof (NEW):
* build.gradle.kts — aliases kei.plugins.library; depends on
  org.conscrypt:conscrypt-android:2.5.2 (the Maven-published
  Conscrypt build that ships GREASE / ALPS / X25519MLKEM768,
  giving a Chromium-adjacent JA3 instead of Android's default).
* TlsSpoof.kt — pure-JVM Android TLS spoofing library. Installs the
  upstream Conscrypt JSSE provider at JCA position 1 (idempotent —
  Security.insertProviderElement returns the prior position if
  already installed). Every SSLContext.getInstance("TLS") OkHttp
  then creates internally uses Conscrypt instead of Android's bundled
  variant. Background KDoc documents why this exists: prior
  impersonator-bctls attempt failed (BCJSSE TLS 1.3 TlsFatalAlert on
  modern CF edges — upstream issues zhkl0228/impersonator#8/#9), and
  modern CF edge configs (e.g. Miruro.tv) reject Android-default JA3
  at the TLS handshake layer before challenge JS even runs. Includes
  the design-tradeoff honestly: pure-JVM cannot reproduce a per-byte
  Chrome JA3 (Chrome's ClientHello byte ordering is owned by
  Conscrypt's internal ClientHello builder, not by OkHttp or by the
  JCA caller), but the Chromium-adjacent JA3 is enough to pass CF's
  fingerprint gate for the sites tested.
* SpoofedTlsSupport.kt — applyTo(OkHttpClient.Builder) entry point.
  Calls TlsSpoof.install() then filters the builder's connectionSpecs
  down to OkHttp's MODERN_TLS profile (TLS 1.2/1.3 only, AEAD cipher
  suites, same set Chrome 131 offers). CLEARTEXT preserved so plain
  HTTP calls still work. Mutation is idempotent. Does NOT disable
  cert pinning or trust verification — the spoofed handshake still
  validates against the system trust store.

lib/cloudflareinterceptor (MODIFIED):
* CloudflareInterceptor.kt — rewrote KDoc to be honest about the
  hybrid reality (headless cannot solve modern managed/Turnstile).
  New per-host locks (hostLocks ConcurrentHashMap<String, ReentrantLock>)
  deduplicate concurrent solves for the same host. In-memory
  CookieCache with 25-minute TTL avoids burning a fresh WebView for
  every request; cf_clearance is bound to TLS/JA3+UA+IP and is
  intentionally NOT persisted across process death. Caller-UA
  passthrough — uses request UA if present, falls back to current
  Chrome 131 / Windows UA, because CF re-challenges sessions that
  flip UA between the original request and the WebView. Detects
  cf-mitigated: challenge header alongside the legacy
  Server: cloudflare + 403/503 signaling. Cookie sync to OkHttp
  client uses the REAL request URL (scheme + host + path), not a
  fake http:// URL — the old code's cookies were silently discarded.
  Caller-contract note: must be added as a network interceptor
  (not app interceptor) so the WebView solve and post-solve retry
  both flow through the rest of the chain (decompression, rate
  limiting, etc.).

gradle/libs.versions.toml (MODIFIED):
* Drops the impersonator entry (com.github.zhkl0228:impersonator-bctls)
  now that nothing in the tree depends on it. :lib:tlsspoof is the
  replacement.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(lib:m3u8server): add full-buffer AutoDetector, AES-128 HLS, header threading, fallback retry, and UpstreamStatusException

AutoDetector: detectInterleavedSkips() full-buffer scan for JPEG/PNG/GIF junk at any offset (not just prefix). M3u8HttpServer: full-buffer download + non-junk chunk assembly, processSegmentUrl takes keyUrl+iv for AES-128-CBC decryption, modifyM3u8Content parses #EXT-X-KEY and injects key/iv on segment URLs, createLocalUrl accepts referer+userAgent encoded as URL params, handleM3u8Request/handleSegmentRequest read from session.parameters, fetchM3u8Content catches IOException and retries via fallbackClient, UpstreamStatusException maps upstream codes to NanoHTTPD Status via passThroughStatus. M3u8Integration: processM3u8Video extracts Referer+UA from video.headers, constructor accepts fallbackClient. M3u8ServerManager: processM3u8Url threads referer+userAgent, constructor accepts fallbackClient.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(lib:megacloudextractor,lib:rapidcloudextractor,lib:dopeflixextractor): graceful 410 handling via requestNewKeyOrNull

Replace hard-fail requestNewKey() with null-tolerant requestNewKeyOrNull() across MegaCloud, RapidCloud, and DopeFlix extractors. When the keys.json GitHub raw endpoint returns 410 ( Gone) or fails, sources are silently skipped via mapNotNull instead of crashing the entire extraction path with IllegalStateException.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(en/miruro): route HLS streams through ultracloud.cc proxy with XOR obfuscation

Add buildProxiedUrl() that wraps raw owocdn.top stream URLs through vault01/02.ultracloud.cc using XOR obfuscation with VITE_PROXY_OBF_KEY and base64url encoding. Proxy server selection via FNV-1a hash of episodeId|anilistId mod 2, matching the frontend's Xb() function. The proxy URL encodes the raw stream.referer from the pipe API (e.g. https://kwik.cx/) — not the resolveHlsReferer output — because the proxy uses the encoded referer to fetch upstream. Video headers carry Referer+Origin set to mirrorBaseUrl so the m3u8 server sends browser-like CORS headers to the proxy. parseStreamsFromResponse accepts episodeId+anilistId params for the hash seed. build.gradle wires m3u8server, cloudflareinterceptor, megacloudextractor, and rapidcloudextractor dependencies.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(en/miruro): thread episodeId/anilistId and PROXY_KEY from Miruro.kt into extractor

Add PROXY_KEY constant (hex decode of a54d389c18527d9fd3e7f0643e27edbe) alongside existing PIPE_KEY. Pass PROXY_KEY as new constructor param to MiruroExtractor. In videoListParse, extract episodeId and anilistId from the episodeData JSON and thread them through all three parseStreamsFromResponse call sites (primary, sub-types, and fallback providers) so the extractor can select the correct proxy server via FNV-1a hash.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* docs(en/miruro): add frontend reverse-engineering research artifacts

Bundle analysis from miruro.tv WatchRoute JS: proxy URL construction (ix/ax XOR+base64url), FNV-1a proxy selection (Xb), deobfuscation (ox/dx), recursive m3u8 fetch (px), and URL verification (gx). Secrets dump from env2.js with both XOR keys and proxy URLs. Discovered endpoints, security report, and README with critical findings.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* refactor(en/miruro): remove dead code

Remove unused parseAnimeListResponse (deprecated, never called),
activeMirrorReferer (only consumer was resolveHlsReferer), and
resolveHlsReferer (private, never called).

* feat(lib:anilib): extend with includeNSFW filter, new query methods, and utility extensions

- Add includeNSFW parameter to all fetching methods (fetchFilteredMedia, fetchSearchAnime, fetchTrending, fetchSeasonal, fetchPopular, fetchUpcoming, fetchRecentlyUpdated)
- Add fetchTopAnime, fetchAiringThisSeason, fetchGenres, fetchTags, fetchStudioProductions, fetchMediaTrends methods
- Add MediaSnapshot extension properties (isAdultContent, formattedStatus, streamingLinks, externalLinks, rankings, stats, etc.)
- Add MediaFilter.Builder for fluent filter construction
- Add new DTOs: MediaStreamingEpisode, MediaExternalLink, MediaRank, MediaStats, MediaTrend, Studio, GenreCollectionData
- Extend MediaSnapshot with meanScore, trending, favourites, isLicensed, hashtag, streamingEpisodes, externalLinks, rankings, stats fields
- Update fetchMediaDetails query to include new fields

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(en/miruro): add Show NSFW preference toggle

Adds a toggle to include adult/NSFW anime in search and browse results. Defaults to off. Includes disclaimer that Miruro providers seldom have fully NSFW anime (hentai) available regardless.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* docs(miruro): Remove falsely committed research artifacts

* fix(lib:anilib): pass isAdult=true instead of null to include adult content

AniList API defaults to excluding adult content when the isAdult variable is omitted. Setting isAdult=null (which caused omission) was therefore equivalent to having the NSFW toggle off. Now explicitly sends isAdult=true when the user enables the toggle.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* refactor(lib:anilib): move MediaFilter.Builder into MediaFilter companion class

Relocates the Builder class from file-level to inside the MediaFilter data class body, improving discoverability and encapsulation.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(en/miruro): wire includeNSFW to all AniLib fetch calls and parallelize episode metadata

Threads the includeNSFW preference into fetchRecentlyUpdated, fetchSearchAnime, and fetchFilteredMedia so the NSFW toggle takes effect across latest, text search, and filter search. Also parallelizes three independent AniLib network calls (airing schedule, episode titles, filler data) via runBlocking/coroutineScope/async, switches animeMetaCache from synchronized LinkedHashMap to ConcurrentHashMap, uses parallelCatchingFlatMapBlocking for fallback provider requests, and fixes label variable ordering in the debug interceptor.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(en/miruro): optimize XOR decryption in-place and disable m3u8 server for multi streams

Replaces mapIndexed + toByteArray with an in-place XOR loop to avoid allocating an intermediate array. Also sets withM3u8Server=false for MegaCloud multi-type streams so extraction uses the direct video URL instead of routing through the m3u8 server proxy.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* refactor(lib:anilib): relocate to lib-multisrc with multisrc plugin and improve error handling

Move anilib from lib/ to lib-multisrc/ using the multisrc plugin format with baseVersionCode. Package changed from aniyomi.lib.anilib to eu.kanade.tachiyomi.multisrc.anilib. Added proper non-2xx HTTP status code handling: executeQuery now reads response bodies on failure, extracts GraphQL error messages, and logs them with status codes. Similar improvements applied to fetchEpisodeTitles and loadAniFillerIndex.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* refactor(en/miruro): update anilib dependency and imports for new multisrc location

Update build.gradle dependency from lib:anilib to lib-multisrc:anilib. Update all imports from aniyomi.lib.anilib to eu.kanade.tachiyomi.multisrc.anilib in Miruro.kt and MiruroFilters.kt.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(lib:anilib): show all content when NSFW toggle is enabled

When includeNSFW is true, omit isAdult parameter to show all anime
(adult + non-adult). Previously set isAdult=true which showed only
adult content.

* anilib: improve rate limit handling

- Fix throttle() race condition with synchronized block
- Only backoff when X-RateLimit-Remaining == 0 (was <= 1)
- Add degraded mode recovery after 5-minute cooldown
- Persist degraded state to SharedPreferences

* Delete cf_challenge_403.html

* Delete cf_jsd_main.js

* Delete cf_jsd_sensor_payload.txt

* Delete cf_jsd_sensor_response.txt

* Delete ChallengeDetectorTest.kt

* Delete CookieCacheTest.kt

* Delete JsdSolverExtractCookiesTest.kt

* Delete JsdSolverExtractDeobfuscationPiecesTest.kt

* Delete JsdSolverParseCvParamsTest.kt

* Delete JsdSolverRealScriptTest.kt

* Remove tests

* refactor(anilib): Move anilib again

* refactor(anilib): Move anilib again again

* feat(en/miruro): route browse/search through Miruro pipe API with AniLib fallback

getPopularAnime, getLatestUpdates, and getSearchAnime (both text-search and filter-browse paths) now hit Miruro's own pipe 'search' and 'search/browse' endpoints first, keeping AniList GraphQL (AniLib) as a synchronous fallback so a Miruro backend hiccup never blanks the page. The filter UI now drives the pipe's native genre/tag/format/status/season/year/dubLanguage query params — dub filtering happens at the source instead of as a client-side post-filter on the pipe path. Pipe responses are decoded with the existing XOR+gzip path and parsed via the same parseAnimeFromMediaObj used by the info endpoint; media shape is identical.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* Remove broken libraries and fix miruro build chores

* Remove bad miruro lib

* Remove bad miruro lib (again)

---------

Co-authored-by: Amnesia <amnesia@example.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Amnesia <anon@example.com>
Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-07-11 23:12:13 -04:00

9 lines
221 B
INI

org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx6144m
org.gradle.parallel=true
kotlin.stdlib.default.dependency=false
org.gradle.tooling.parallel=true