Commit graph

64 commits

Author SHA1 Message Date
KhooLy
85dcc014eb Preserve home data during refresh 2026-07-10 23:57:06 +03:00
KhooLy
194cb4cff3 Always include collection focus GIFs 2026-07-10 23:51:53 +03:00
KhooLy
cf9b0cbe06 Add subtitle delay alignment policy 2026-07-10 23:23:58 +03:00
KhooLy
8ce31e7214 Preserve selected stream provider on completion 2026-07-10 20:22:49 +03:00
KhooLy
05b60e1da4 Avoid repeated torrent file prioritization 2026-07-10 19:41:35 +03:00
KhooLy
c1ef70c9e2 Scope readiness-poll torrent adds to the requested file
The stat-polling "get" action registered a magnet without narrowing
rqbit's only_files to the target index, so progress/readiness could be
driven by unrelated files in the torrent. That let isPlayableEnough
report true before the actual file had any data, handing mpv a stream
URL that wasn't ready yet (Sentry RUST-4). Also re-scope on repeat
lookups of an already-known link so switching files on a shared magnet
(season packs) doesn't keep the first file's scope.
2026-07-10 17:25:20 +03:00
KhooLy
5c07852af7 Wire torrentStatusInfo into the FFI dispatch table
torrent_status_info_json (isPlayableEnough/statusKey/bufferProgress)
existed unreachable from any binding — no ffi.rs arm called it. It's
exactly the policy fluxa-desktop needs to decide when a torrent stream
is actually safe to hand to the player.
2026-07-10 16:39:18 +03:00
KhooLy
e7012c643d Track catalog fetch as a distinct loading state in Discover
is_loading only covered the results fetch, so the frontend had no way
to tell "catalogs still loading" apart from "no catalogs available",
causing a false empty state while addon catalogs were still in flight.
2026-07-10 16:22:28 +03:00
KhooLy
c2b0e610ec Plan Discover per selected catalog 2026-07-10 13:53:39 +03:00
KhooLy
83df69a7dd Stop excluding catalogs from discover when they lack declared genre support
Excluding a catalog entirely whenever it didn't declare genre as a supported
extra was too strict: once genre filtering actually started reaching the
core (previous commit), any user whose installed addons don't declare genre
support at all got zero catalogs and "No content found." Instead, only skip
sending the genre query param to catalogs that don't support it — they still
contribute their normal item list, which then goes through the existing
post-fetch genre filter in discover_sort_plan_json (checks each item's own
genres tag), so mismatched items still get filtered out without starving
the catalog list entirely.

Also switched to the canonical, case-insensitive catalog_supports_extra in
addon_protocol.rs instead of the ad-hoc case-sensitive duplicate added
earlier, matching the pattern already used by search_plan.rs.
2026-07-10 13:28:32 +03:00
KhooLy
913ac0fdcd Exclude catalogs without genre support from genre-filtered discover requests
Discover was sending genre=X to every installed catalog regardless of
whether its manifest declared genre as a supported extra. Addons commonly
ignore unsupported extras and return their default/unfiltered list, so
picking e.g. "Animation" pulled in unrelated items from catalogs (Action,
Sport, ...) that don't actually support genre filtering.
2026-07-10 13:12:41 +03:00
KhooLy
b2c9c4f439 Save outgoing episode progress atomically with playerLoadStreamsRequested
Episode auto-advance (playerLoadStreamsRequested) was fully decoupled
from progress saving: nothing in the core persisted the outgoing
episode's position/metadata before switching, so it relied entirely on
the host remembering to fire a separate savePlaybackProgressRequested
dispatch first. If the host missed that or the app was killed in the
gap between the two dispatches, the last watched position (and its
season/episode metadata) for the finished episode was lost.

playerLoadStreamsRequested now accepts an optional outgoingProgress
payload; when present and the video id is actually changing,
dispatch_load_streams emits a writePlaybackProgress effect for the
outgoing video in the same response as the stream-load effects, so a
single host dispatch covers both.
2026-07-10 12:29:50 +03:00
KhooLy
766d988d67 Fix continue-watching progress merge losing episode season/number
lastEpisodeSeason and lastEpisodeNumber were only taken from the
incoming write when the video changed, with no fallback to the
existing value, same flaw as 54e5fff fixed for name/thumbnail. If
an auto-advanced episode's metadata was incomplete, this nulled out
season/number on the continue-watching record, which downstream
consumers then displayed as "Season 0".
2026-07-10 12:19:15 +03:00
KhooLy
4f90c99117 chore: format Rust sources 2026-07-10 11:18:01 +03:00
KhooLy
b196e61f48 Add Anilist auth provider to profile token merge 2026-07-10 11:11:41 +03:00
KhooLy
7ac59c9bc9 Update README links to FluxaMedia org repos 2026-07-10 11:11:28 +03:00
KhooLy
da3a45a6d5 Ignore AGENTS.md 2026-07-10 11:11:18 +03:00
KhooLy
54e5fffe17 Fix continue-watching progress merge losing episode name/thumbnail
lastEpisodeName and lastEpisodeThumbnail were only taken from the
incoming write when the video changed, with no fallback to the
existing value (unlike continueWatchingPoster/Background). If an
auto-advanced episode's metadata was incomplete, this nulled out a
previously-good title/thumbnail on the continue-watching card.
2026-07-10 02:40:42 +03:00
KhooLy
c5a98f0473 Fire continue-watching badge refresh in parallel with home bootstrap
dispatch_load now also fires RefreshContinueWatching alongside
ReadHomeBootstrap, so continueWatching badges (new episode / up next)
can arrive and update state independently instead of only ever coming
from bootstrap.

Note: fluxa-desktop's readHomeBootstrap still computes badges inline
via refreshReleasedContinueWatching, so home load doesn't yet get
faster from this alone — it currently just computes badges twice.
Realizing the perf win needs a follow-up to drop that inline call and
rely solely on the parallel refresh effect.
2026-07-09 21:29:52 +03:00
KhooLy
69e4c0974c Don't filter anime discover results by native item type
"anime" is a virtual content-type bucket in Discover that doesn't map
to Stremio's native movie/series type field, so items from
anime-flagged catalogs were being dropped when their underlying type
didn't literally equal "anime". Treat it like the empty-filter case.
2026-07-09 21:29:41 +03:00
KhooLy
90d13d9983 Consolidate addon resource parse+plan into a single core call
Extracts parse_addon_body (returns a ParsedAddonBody enum instead of
always serializing to a JSON string) so the existing three-hop path
(parseAddonResourceResult -> wrapAddonResourceResponse ->
resourceParsePlan, still used by subtitles.ts) and the new single-call
parseAndPlanAddonResource share the same parsing logic instead of
duplicating error-envelope construction.

Removes the now-redundant wrapAddonResourceResponse FFI method. Tests
assert the combined call produces byte-identical output to the old
three-step pipeline.

Pairs with fluxa-desktop's matching addonManifest.ts/fetchPlanning.ts
change.
2026-07-09 21:29:29 +03:00
KhooLy
e6d7ad75f6 Serve higher-resolution YouTube trailers via HLS variants and adaptive audio pairing
YouTube's muxed mp4 formats cap out around 720p. Prefer, in order:
the highest-resolution muxed-audio HLS variant from the master
playlist, then the best adaptive video paired with the best adaptive
audio track (returned as a separate audioUrl), then falling back to
muxed mp4 as before. try_all_clients now keeps the best adaptive/muxed
fallback seen across client contexts instead of returning on first
success, since an earlier client can return a worse stream than a
later one.

Pairs with the audio-track sync already added to the desktop hero
trailer player, which was waiting on a resolvedTrailer.audioUrl this
enables.
2026-07-09 21:29:15 +03:00
KhooLy
58266b32e7 chore: format Rust sources 2026-07-08 15:04:56 +03:00
KhooLy
9391b49c99 Expose prewarm_youtube_watch_config for early visitor_data scraping
Companion to the desktop-side prewarm call: lets the frontend trigger
the one-time YouTube watch-page scrape in the background as soon as
Home mounts, instead of paying that cost inline on the first real
trailer resolution.
2026-07-08 14:59:09 +03:00
KhooLy
f7aa810ed8 Scrape YouTube visitor_data and add ANDROID_VR client for higher-res trailers
Without a visitor_data header, YouTube strips the url/cipher from every
adaptive format and only serves the lowest-quality progressive stream
(itag 18, ~360p). Fetch a real watch page once (cached, 3h TTL) to grab
INNERTUBE_API_KEY/VISITOR_DATA and send it as X-Goog-Visitor-Id, and add
the ANDROID_VR client ahead of ANDROID/IOS, matching the approach used by
other Stremio-ecosystem clients (Nuvio) to unlock full adaptive quality.
2026-07-08 13:58:09 +03:00
KhooLy
85b54e1179 Treat blank episode thumbnail as missing when caching continue-watching art
apply_next_episode_badge only filtered out null, not empty string, so
an addon returning "thumbnail": "" for an episode would silently
overwrite a previously-cached good thumbnail and then read as missing,
forcing a fallback to series art until a real thumbnail reappeared.
Now matches the read-side str_field blank check.
2026-07-08 13:57:59 +03:00
KhooLy
3d4cfd3294 Normalize hero/home meta trailers like detail screen already does
Home billboard and category items kept raw addon trailer fields
(source/ytId) instead of the resolved YouTube URL that detail.rs
already produces via normalize_meta_trailer, so hero autoplay never
had a usable trailer URL to play.
2026-07-08 13:16:35 +03:00
KhooLy
5d57fde599 chore: format Rust sources 2026-07-07 22:46:39 +03:00
KhooLy
7964ec45ef feat(streaming): add subtitle tracks and a JNI export to the trailer resolver
Extends resolve_youtube_trailer_json to also extract caption tracks and
distinguish geo-blocked vs. failed outcomes, then exposes it over JNI
(Java_..._FluxaStreamingNative_resolveYoutubeTrailerJsonNative) so the
Android app's TrailerResolver can delegate to this instead of keeping
its own separate innertube client in sync.
2026-07-07 20:34:52 +03:00
KhooLy
3b955c0de4 feat(streaming): resolve YouTube trailer stream URLs via innertube
Adds a minimal innertube client (ANDROID primary, IOS fallback) that
POSTs to /youtubei/v1/player and extracts a direct progressive mp4 URL
from streamingData.formats, with a disk cache (6h TTL) keyed by video
id. Lets the desktop shell play a muted/looping trailer through a
plain <video> element instead of embedding YouTube's own web player.
2026-07-07 20:16:21 +03:00
KhooLy
d3e550baef feat(player): support unbounded buffer cache via negative cacheSizeMb sentinel
Lets the desktop UI offer an "Infinite" buffer tier without changing the
BufferTargetsRequest contract; a negative cacheSizeMb maps to a 64GB cap.
2026-07-07 19:30:31 +03:00
KhooLy
016514a1a3 feat(sync): tag progress entries by origin so Local and Nuvio can be pinned separately
Native on-device playback now stamps source:"local" on every PlaybackProgress
write, and Nuvio's sync already writes into the same progress bucket so it
gets source:"nuvio" from the desktop side. merge_continue_watching_lists_json
reads this tag (defaulting to "local" for pre-existing entries with no tag)
so source-of-truth pinning can target Local and Nuvio independently instead
of treating them as one inseparable group.
2026-07-07 19:29:11 +03:00
KhooLy
83ff0408e9 feat(sync): add configurable source-of-truth and ranking for continue-watching conflicts
merge_continue_watching_lists_json and replace_external_continue_watching_json
accept optional sourceOfTruth/rankingMode params so a caller can pin a
specific provider or rank by episode number instead of always taking the
most recent savedAt. Defaults preserve prior behavior exactly.

Also tags Stremio's continue-watching items with reason:"stremio", matching
Trakt/Simkl/AniList, so it can participate in provider pinning and so stale
Stremio entries are actually evicted on re-sync instead of lingering forever.
2026-07-07 19:20:49 +03:00
KhooLy
4acb603bc8 feat: parse scrobble action and auth provider strings into enums at the boundary
Follow-up to c9b37c1's fallback/source-selection mode work, per the same
robustness-plan item: two more closed sets were being compared as string
literals in real decision logic.

player_scrobble.rs: should_enqueue_durable's action param was matched
against "pause"/"stop" inline; ScrobbleAction (Start/Pause/Stop/Unknown)
via an infallible From<&str> makes the set explicit without changing the
lenient fallback behavior (unrecognized actions still count as durable,
same as before).

profile_contract.rs: token_merge_plan_json's provider match arms
("trakt"/"mal"/"simkl"/"stremio"|"account"/_) become AuthProvider, same
infallible-fallback style so an unrecognized provider still no-ops
instead of hard-erroring.

Scope note: auth.rs's provider/mode fields and headless_engine's scrobble
action_name are NOT converted — neither is branched on in Rust, they're
pass-through values forwarded verbatim to platform-executed effects, so a
strict enum there would only add a rejection path for legitimate values
the platform side defines, not catch a real bug the way the two above do.
2026-07-07 16:01:23 +03:00
KhooLy
e9fce57434 refactor: replace remaining json!({...}) object literals with typed structs
helpers.rs's addon trailer normalization and player.rs's prefetched-next-
episode cache were the only two production call sites left building JSON
object literals by hand in headless_engine; everything else under json!(
turned out to be either empty-array/object defaults or test fixtures
simulating platform effect results (which stay untyped on purpose, since
that's the actual external FFI boundary).
2026-07-07 15:37:38 +03:00
KhooLy
603a94bf6c docs: mark §11 (desktop JSON tax) resolved after verifying fluxa-desktop
Per CLAUDE.md's rule to verify against fluxa-desktop/src-tauri call
sites before touching FluxaCore: the plan's premise for this section
no longer holds. dispatchAction/completeEffect/getSnapshot in
fluxa-desktop's src/core/engine.ts already go through dedicated Tauri
commands (engine_dispatch/engine_complete_effect/engine_snapshot) that
call FluxaCore::* directly in Rust, not through core_invoke. FluxaCore
itself has also grown from the "8 methods" this doc assumed to 21
(cast/AirPlay/Chromecast/Roku helpers), all with real call sites
verified via grep. The remaining core_invoke Tauri command is a
generic fallback for infrequent methods where a dedicated command
per method would be boilerplate with no measurable win, since Tauri
IPC serializes as JSON at the JS<->Rust boundary either way. No code
change needed; also corrected the stale "8 methods" claim in
CLAUDE.md's core_api description to reflect the current count.
2026-07-07 15:10:23 +03:00
KhooLy
4ce837b125 test(ffi): add checked-in core_invoke method fixture + routing regression test
Per the robustness plan's §7. The plan's main suggestion — a
core_methods! table macro generating ffi.rs's route_* functions — was
skipped: ffi.rs's ~115 methods have heterogeneous call shapes (0-arg,
single string arg, multi-field object arg, some routed straight to a
domain function, some with inline argument massaging first), and
CLAUDE.md already records the project's own assessed judgment that
consolidating the three FFI surfaces isn't worth the rewrite risk —
that reasoning applies just as much to a macro-generated version of
one of those three surfaces.

Implemented the plan's other, explicitly "cheap middle ground"
suggestion instead: tests/wire/core_invoke_methods.txt checks in the
current list of all 114 method names route() recognizes (extracted
from ffi.rs's match arms), and a new test
(ffi::tests::every_known_core_invoke_method_still_routes) calls
core_invoke(method, "{}") for each and asserts the error kind isn't
unknown_method. This doesn't validate each method's business logic —
only that the name is still wired to some router — but that's exactly
enough to turn a renamed or deleted method from a runtime "no such
method" surprise on a platform this repo can't see into a test failure
here. Verified it actually catches drift by temporarily adding a bogus
method name to the fixture and confirming the test fails.

core_capabilities_json generation from the method table (the plan's
second §7 bullet) doesn't apply here: core_contract.rs's
CoreCapabilitySet is a small hand-curated set of platform feature
flags (http/storage/auth/player/plugins/torrent/local_stream/
notifications), not a per-method availability list — there's no
natural 1:1 mapping from the 114 routed methods to those 8 flags to
generate from.
2026-07-07 15:06:54 +03:00
KhooLy
c9b37c1123 feat: parse fallback/source-selection mode strings into enums at the boundary
Per the robustness plan's §6: fallback_mode and source_selection_mode
were closed sets compared as string literals scattered across match
arms. Parse each once where the raw string enters real decision logic
instead of re-comparing spellings everywhere a typo could hide.

player_policy.rs: DvFallbackMode enum (Off/Auto/Dv8/ConvertDv81/Hdr10)
replaces the String field on DvProxyPlanRequest, deserialized directly
via serde (snake_case matches the existing wire strings exactly).
Caught a real bug in the process: the existing test suite already
covered "hdr10" as a value real requests send (matching the field's
own doc comment, which the string-comparison code had silently drifted
from — "hdr10" never hit any specific match arm, just fell through to
the same catch-all as "off"). Missing that variant broke a passing
test immediately, which is exactly the point of parsing at the
boundary: a typo/missed variant now fails loudly in one place instead
of silently in whichever comparison forgot about it.

stream_policy.rs: SourceSelectionMode enum (Regex/First/Manual) via an
infallible `From<&str>` (unlike DV mode, any unrecognized value should
keep behaving as "manual" like it always has, not become a hard
parse error) replaces the STREAM_SOURCE_MODE_* string constants in
select_stream_index/select_stream_index_values. Callers (jni.rs,
player_flow.rs) now convert with `.into()` at the same point they used
to pass the raw string through.

Scope notes for what's NOT done here, and why:
- Did not change EffectEnvelope.kind from String to EffectKind as §6
  also suggests. EffectEnvelope is embedded in EngineState.pending_effects,
  which round-trips through create_headless_engine's
  serde_json::from_str(initial_json). Today an unrecognized kind string
  in restored state is survivable (kind is a plain String; only the
  from_str() lookup in complete_effect can fail, and it already drops
  that one stale effect gracefully). Making kind a strict enum would
  make one unrecognized effect kind fail deserialization of the entire
  restored EngineState instead of just that effect — a resilience
  regression across app-version upgrades that remove/rename a variant,
  which runs directly against this plan's own goal. Left as String
  pending a design for tolerant partial-state restore.
- scrobble action_name and auth provider/mode enums, and the ~80
  json!({...}) payload literals in headless_engine, are deferred to a
  follow-up pass — each is mechanical but independent of this change.
2026-07-07 15:04:13 +03:00
KhooLy
16c907b927 feat(types): add typed Profile wire struct, harden MetaItem/Stream for reuse
Per the robustness plan's §4. Found that types/resource.rs's MetaItem/
Stream/Video already existed as fully-typed wire structs (grep showed
zero callers anywhere in the crate — dead scaffolding, like the
FluxaEnv trait CLAUDE.md already flags) but weren't safe to wire in as
written: fields like MetaItem.id/name were required Strings with no
#[serde(default)], and none of the three had a #[serde(flatten)] extra
catch-all — exactly the "crucial constraint" the plan calls out, since
effect payloads echo these objects back to the platform and a dropped
unknown field would be silent data loss. Added default + flatten to
all three so they're actually safe to adopt.

New types/profile.rs adds Profile (id + flatten extra) and wires it
into the first AppAction field per the plan's priority order:
ProfileActivated { profile: Value } -> { profile: Profile }. Updated
library::dispatch_profile_activated and profile::activate to take the
typed value; `profile["id"]` string-indexing becomes `profile.id`.
profile::update_active and everything downstream (home state, effect
payloads) still work in terms of Value, converted once via
Profile::to_value() — this is a one-field migration, not a rewrite of
ProfileState's storage, so the wire format is provably unchanged (the
golden fixture from a prior commit asserts byte-identical dispatch
output and passes without modification).

Remaining actions with `profile`/`meta`/`streams`/`item: Value` fields
(~20 call sites) are intentionally left for follow-up, one at a time,
per the plan's own incremental sequencing.
2026-07-07 14:55:30 +03:00
KhooLy
5a7865573e perf: cache/statics for regex compilation in stream ranking hot paths
Per the robustness plan's §10: stream_policy.rs's subtitle language
matching built a Regex from the user's language preference on every
call, and those calls run per-stream inside per-stream ranking loops.
Add a one-entry Mutex<Option<(String, Regex)>> cache keyed by the
normalized preference string — it changes once per settings edit, not
per stream — matching the plan's suggested approach.

external_sync.rs's IMDB id extraction compiled r"tt\d+" per history
item; it already exists as a shared OnceLock static
(content_identity::imdb_regex()), so just reuse that instead of adding
a second copy. watchlist_plan.rs's GitHub blob-URL rewrite compiled its
regex per call; moved to its own OnceLock static following the same
house pattern already used by imdb_regex()/year_regex().
2026-07-07 14:49:49 +03:00
KhooLy
fdf91ceb19 perf(headless-engine): replace clone-and-diff dispatch with dirty tracking
Per the robustness plan's §9 (flagged as the single highest-leverage
perf change in the crate): headless_engine_dispatch_json used to clone
the entire EngineState before the reducer ran, clone it again after,
then StatePatch::diff did a deep PartialEq walk across all 16 domains
and cloned each changed one a third time — cost scaled with everything
the user has ever loaded (full catalogs, episode lists), not with what
the action actually touched.

Introduces Tracked<T> (src/headless_engine/state.rs): wraps each
EngineState domain field so any mutable access — a field write, a
whole-value replacement via DerefMut, a method call taking &mut —
flips a dirty bit automatically. Serialize/Deserialize delegate
straight to the wrapped value, so the wire format is byte-for-byte
unchanged (verified by the golden wire fixtures added in the prior
commit, which pass unmodified). EngineState::diff_dirty() replaces
StatePatch::diff(before, after): it clones only domains whose dirty
bit is set and clears it, so both the pre-dispatch full clone and the
post-dispatch PartialEq walk are gone entirely. The ~16 call sites that
did whole-domain replacement (`engine.state.detail = DetailState {
...}`) needed `*engine.state.detail = ...` instead — the compiler
catches any site the migration missed as a type error, so there's no
way for this rewrite to silently skip a domain the way a hand-added
mark_dirty() call could.

Also fixes headless_engine_snapshot_json, which used to serialize
while still holding the engines mutex; it now clones the state and
drops the lock before calling serde_json::to_string.

Adds benches/ (criterion, `--features bench` dev-only surface via a
new bench_targets re-export module mirroring the existing fuzz_targets
pattern): headless_engine_dispatch benches a NavigationRequested
dispatch against a 500-stream detail state (a domain the action
doesn't touch, so the win is directly visible), and stream_ranking
benches player_source_sidebar_plan_json grouping 500 streams. CI gets
a bench-check job compiling and smoke-running both (`--test`) without
paying for full timing runs on every push.
2026-07-07 14:46:52 +03:00
KhooLy
b171adf721 feat(core-error): add CoreError + host-pollable log sink, harden effect completion status
Per the robustness plan's §2/§3: introduce a pub(crate) CoreError enum
(BadInput/NotFound) plus a LogAndDiscard trait converting
Result<T, CoreError> back to the existing Option<T> FFI shims while
recording the failure. Rather than a push callback (no such pattern
exists anywhere in this codebase — checked jni.rs/uniffi.rs/ffi.rs and
the streaming engine, which all use poll-based Arc<Mutex<..>> state
instead), the sink is a bounded ring buffer drained via a new
`core.drainErrorLog` core_invoke method, a UniFFI
`drain_core_error_log_json` export, and a JNI
`drainCoreErrorLogJsonNative` extern — the host polls it (e.g. once per
dispatch tick) and forwards lines to logcat/desktop logs. This follows
the codebase's own established poll-not-push convention instead of
inventing a new one.

Wired into headless_engine's two real FFI boundary functions
(headless_engine_dispatch_json / headless_engine_complete_effect_json)
for both bad JSON and unknown-handle cases, and applied end-to-end
across the player_policy module (6 functions) as the first fully
migrated module, demonstrating the pattern for the remaining ~290
Option<String> functions to pick up incrementally.

Also hardens EffectResultInput per §3: effect_id is now a required
field (previously #[serde(default)] silently produced effect_id: ""
that matched no in-flight effect), and status is a closed EffectStatus
enum (Ok/Error/Cancelled) instead of a bare String — an unrecognized
status string now fails deserialization (and gets logged) instead of
comparing false against every "== \"ok\"" call site. Updated the ~39
`result.status == "ok"` / `!= "ok"` comparisons across headless_engine
to `result.status.is_ok()`; the wire strings themselves are unchanged
(serde's rename_all = "camelCase" already produces "ok"/"error").
2026-07-07 14:35:49 +03:00
KhooLy
2439bb3ba6 test: add golden wire fixtures and a headless-engine dispatch fuzz target
Per the robustness plan's §8: a tests/wire/ directory holds one real
AppAction input per representative action family, each paired with a
checked-in golden DispatchResult captured from an actual dispatch (a
new #[cfg(test)] wire_fixtures_match_golden_dispatch_output test
compares against it, with UPDATE_WIRE_FIXTURES=1 to regenerate when a
change is intentional). Any future camelCase/field drift on the
dispatch/completeEffect wire now fails in this repo instead of surfacing
as a silent Android/desktop regression.

Also add fuzz/fuzz_targets/engine_dispatch.rs, feeding arbitrary bytes
into both headless_engine_dispatch_json and
headless_engine_complete_effect_json against one engine handle — this
is the one path in the crate that runs global engine-mutating logic
without a catch_unwind guard on the desktop call path, so it's the
highest-value fuzz target missing from fuzz/. Exposes the four
headless_engine entry points as `pub` (still unreachable outside the
crate except through the fuzzing-gated `fuzz_targets` re-export module)
following the same pattern already used for parse_manifest and the
content_identity helpers.
2026-07-07 14:28:48 +03:00
KhooLy
e05afb2761 chore(ci): add GitHub Actions workflow and clippy lint policy
Add warn-level unsafe_op_in_unsafe_fn / unwrap_used / expect_used /
indexing_slicing / panic clippy lints (test code exempted via cfg_attr),
per the robustness plan's ratchet-to-deny strategy. Add a CI workflow
covering workspace test/clippy/fmt plus a wasm-only cargo check. Run
`cargo fmt` to clear pre-existing formatting drift so the fmt gate starts
clean, and drop a now-unnecessary `mut` flagged by clippy.
2026-07-07 14:22:15 +03:00
KhooLy
cf701723be fix(player-policy): update stale HLS DV test for segment RPU convert
b0bb52a added real HLS segment-level RPU conversion (fluxa-streaming-engine's
OkHttp interceptor) for P7 streams with a DV decoder, but left behind a test
that still expected the old manifest-only passthrough. Split it into two
cases: decoder present -> hls_rpu_convert, decoder absent -> manifest_handled.
2026-07-07 14:19:13 +03:00
KhooLy
689f024d3d docs: robustness & performance improvement plan from full crate review 2026-07-07 14:05:52 +03:00
KhooLy
f00a09a4ce fix(search-plan): JSON-encode transport-url/genre FFI returns
resolve_transport_url_json and resolve_feed_option_genre_json feed
opt_json() at the FFI boundary, which requires valid JSON — a bare
URL/genre string isn't, so resolveTransportUrl/resolveFeedOptionGenre
would fail from the frontend whenever they had a result. Finishes an
in-progress fix that JSON-encoded the transport-url return but missed
the early-return genre branch, and updates home_ranking.rs's direct
(non-FFI) callers to unwrap the now JSON-encoded string before
embedding it in a json!() literal, since json!() already does its own
string encoding.
2026-07-07 13:58:18 +03:00
KhooLy
ca749a8d00 fix(torrent): prevent stale stop from killing a replayed session
start_torrent_server is a process-wide singleton that fully tears
down and recreates the engine on every play. A stop invoked for an
old session could race a fast replay's start and land after the new
session was already up, silently killing the just-started server and
leaving mpv with a dead socket (MPV_ERROR_LOADING_FAILED / "Loading
failed" on the same torrent). Each server generation is now tagged;
stop is a no-op unless it targets the currently active generation.
2026-07-07 13:14:31 +03:00
KhooLy
4db5701d80 chore(companion): document required OAuth env vars for the web companion server
.env.example lists the Trakt/Simkl/MAL client id/secret vars companion_server
and oauth_proxy read at runtime; ignore fluxa-streaming-engine/.env so a real
copy never gets committed.
2026-07-06 21:56:32 +03:00
KhooLy
d614de3341 fix(torrent): fix startup ordering, add metadata timeouts, track real streamed bytes
Bind the listener only after the torrent session is up (with an 18s init
timeout) instead of before, wrap add_lock/api_add_torrent in a 45s timeout
so a stuck peer/tracker can't hang requests forever, and stop stat-only
polls from triggering ensure_torrent so they can't contend with the real
stream request. Also track bytes actually streamed to the client
(CountingReader) so the preload/stat readiness check reflects playback
progress past a seek, not just bytes written to disk.
2026-07-06 21:56:26 +03:00