Adds parse_publicmetadb_segments_json alongside the existing
IntroDB/SkipDB/AniSkip parsers, reusing the shared collect_segments
extractor. Extends that extractor with a "credits" key-prefix alias
(mapped to the canonical "outro" type) since PublicMetaDB names its
outro fields credits_start_ms/credits_end_ms rather than outro_*.
Covers all 58 documented endpoints (resume, watch history, skip
timestamps, ratings, episode ratings, highlights, external ID
mappings, anime seasons, lists, picks, and voting) as stateless
method/url/body plan builders, wired into core_invoke the same way
mdblist_plan is.
IntroDB, SkipDB, TheIntroDB, and AniSkip are all read-only, no-API-key
lookups, so there was no reason to keep four separate on/off prefs that
were always toggled together. AnimeSkip keeps its own flag since it needs
a client id to work at all.
Every skip-segment source (IntroDB, SkipDB, AniSkip, AnimeSkip) now has its
HTTP request (url/method/headers/body) built in fluxa-core and returned as
a plan; the desktop shell just executes it and hands the raw response back
for parsing. This keeps the provider logic reusable for a future Android
consumer instead of reimplementing each integration per platform.
Also adds TheIntroDB.org (v3) as a new skip-segment source (read + submit),
alongside the existing providers, with its own request-plan builders and a
tolerant response parser (credits -> outro, null-timestamp handling).
Adds parsing for SkipDB's intro/recap/outro/preview API responses
(reusing the existing tolerant segment collector), a 'preview' skip
type alongside intro/outro/recap, and a useSkipDb preference threaded
through playback prefs and profile sanitization the same way useIntroDb is.
Adds profileAvatarPackManifestPlan: recognizes a URL that already points
directly at a pack.json/json.pack file (GitHub blob link or any other
HTTPS host) and fetches it as a single pack, instead of requiring a bare
GitHub repository URL and walking its whole tree.
parse_repository_url rejected any GitHub URL with more than two path
segments, so a /blob/<ref>/<path>/pack.json link (pasted to add one
specific pack) failed to parse. The frontend then silently treated the
resulting None as "no packs found" with no error surfaced.
Now the trailing /blob or /tree path is used to scope discovery to
just that pack instead of importing every pack in the repository.
Simkl's DELETE /sync/playback/{id} removes a single saved playback
session, mirroring Trakt's remove-progress endpoint. Continue-watching
drop plans now flag dropSimkl alongside dropTrakt so the desktop shell
can clear the remote record when an item originated from Simkl.
The vidmoly.me case was asserting None, but stream_request_referer
already returns a same-origin referer for http(s) URLs. Also covers the
magnet-link case, which correctly has no referer.
Fixes visibility of items re-exported two module levels up (player/* and
trailer/* to headless_engine) — pub(super) inside the submodules only
reached their immediate parent, not headless_engine, so the crate failed
to build.
mod.rs was 881 lines mixing three execution paths behind two thin
public entrypoints. Split into plugin_runtime/{settings_layout,
scraper_exec,host_functions}.rs:
- settings_layout.rs: run_settings_layout + NoopHttpClient (network
disabled during settings-layout evaluation)
- scraper_exec.rs: run (the real scraper execution path with a live
HTTP client)
- host_functions.rs: native_fetch/to_hex/from_hex + register_host_functions
(the JS-bridge registration, plus the polyfill include_str! constants)
mod.rs keeps PluginHttpRequest/Response/Client, the two public
entrypoints (execute_scraper, get_settings_layout), and the test suite
— 330 lines, down from 881. No behavior change — same 393 tests pass.
external_sync.rs was 855 lines, ~800 of them the #[cfg(test)] module.
Split into external_sync/tests/{calendar,merge,anilist,stremio,trakt,
simkl,mal,cross_provider,wire_fixtures}.rs, mirroring the per-provider
submodule split (and the same pattern used for headless_engine/tests/).
external_sync.rs is now 55 lines of pure module wiring. No behavior
change — same 393 tests pass.
tmdb_plan.rs was 879 lines. Split into tmdb_plan/{helpers,
meta_conversion,request_plans,genres_catalog}.rs:
- helpers.rs: shared content-type/language/image-url/id-hint/query
encoding primitives used across the other three files
- meta_conversion.rs: TMDB meta/video -> internal meta/trailer JSON
conversion (single + bulk + full-detail variants)
- request_plans.rs: builtin meta/detail/season/credits/people request
plan builders
- genres_catalog.rs: genre tables, builtin manifest, builtin catalog URL
tmdb_plan.rs is now 126 lines: module wiring plus the existing test
suite. No behavior change — same 393 tests pass.
watchlist_plan.rs was 974 lines (plus the already-split collections.rs
submodule). Split the remaining hub content into watchlist_plan/
{remote_collection,plans,library_commands}.rs:
- remote_collection.rs: remote collection request/response plans
- plans.rs: watchlist toggle, external merge, collection import
validation, offline grouping, playback progress merge plans
- library_commands.rs: mark-watched application, progress-meta merge,
the library command plan, and playback progress write plan (kept
together since they call into each other)
collections.rs switched from `use super::*` to explicit imports since
it's now a sibling rather than a child of the hub's import scope.
watchlist_plan.rs is now 177 lines: module wiring plus the existing
test suite. No behavior change — same 393 tests pass.
mdblist_plan.rs was 998 lines. Split into mdblist_plan/{helpers,
media_ratings,lists,watchlist_sync,scrobble,discussion,user}.rs,
mirroring the categories already named in the crate's module map
(media/ratings/catalog/lists/watchlist/sync/scrobble/discussion/user):
- helpers.rs: shared bearer/poll-outcome/query-encoding/URL-building
primitives
- media_ratings.rs: media info/ratings/catalog/search/genres request
plans
- lists.rs: list CRUD, item mutation, list discovery URLs
- watchlist_sync.rs: watchlist/sync/up-next request plans
- scrobble.rs: scrobble/checkin plans
- discussion.rs: discussion thread/comment/reply plans
- user.rs: user profile/stats/follow plans
mdblist_plan.rs is now 245 lines: module wiring plus the existing test
suite. No behavior change — same 393 tests pass.
library_state.rs was 1399 lines mixing playback progress, library list
building, continue-watching/next-episode logic, and artwork/diff
helpers. Split into library_state/{helpers,playback_progress,
library_lists,continue_watching,artwork_diff}.rs:
- helpers.rs: shared text()/number() JSON field accessors
- playback_progress.rs: playback progress item plans
- library_lists.rs: continue-watching/watchlist list building, library
document normalization
- continue_watching.rs: badge computation, next-episode resolution
(kept together since they share several private helpers), plus the
UP_NEXT_POSITION_SECONDS/UP_NEXT_DURATION_SECONDS constants
- artwork_diff.rs: continue-watching artwork selection, before/after
diffing helpers
library_state.rs is now 228 lines: module wiring plus the existing test
suite. No behavior change — same 393 tests pass.
stream_policy.rs was 1486 lines mixing stream metadata, magnet-link
building, torrent file matching, torrent runtime status, language
matching, and stream selection. Split into stream_policy/{meta,magnet,
torrent_files,torrent_runtime,language,selection}.rs:
- meta.rs: SourceSelectionMode, stream field accessors, playback-info
and header-forwarding plans
- magnet.rs: magnet link normalization/building
- torrent_files.rs: torrent file index resolution, sibling subtitle
matching
- torrent_runtime.rs: torrent stream URL building, runtime/status plans,
ready-budget policy
- language.rs: audio/subtitle language normalization and matching
- selection.rs: player track state, stream selection/ordering
stream_policy.rs re-exports each submodule's full pub(crate) surface
(rather than a curated subset) since nearly everything is exercised
directly by the existing test suite. No behavior change — same 393
tests pass.
Note: includes the pending torrent_ready_budget_json addition that was
already in stream_policy.rs before this refactor (relocated into
torrent_runtime.rs) — preserved verbatim.
player_policy.rs was 1493 lines mixing several distinct plan builders
alongside the existing dolby_vision.rs submodule. Split into
player_policy/{backend_selection,torrent_fallback,buffer_targets,
retry_and_ordering,playback_close,source_sidebar,next_episode}.rs:
- backend_selection.rs: ExoPlayer/MPV/external backend choice
- torrent_fallback.rs: torrent file-fallback policy
- buffer_targets.rs: buffer/cache size targets
- retry_and_ordering.rs: retry policy, next-retry-source selection,
stream shell/ordering plans (kept together — they share
retry_stream_key/behavior_text/stream_is_p2p helpers)
- playback_close.rs: playback-close scrobble/progress plan
- source_sidebar.rs: source sidebar option state
- next_episode.rs: next-episode prefetch/selection
dolby_vision.rs switched from `use super::*` to explicit imports since
it's now a sibling rather than a child of the hub's import scope.
player_policy.rs is now 642 lines: module wiring plus the existing test
suite. No behavior change — same 393 tests pass.
Note: includes whatever pending edits were already in player_policy.rs
and player_policy/dolby_vision.rs before this refactor — preserved
verbatim, just relocated.
ffi.rs had grown to 1999 lines with ~24 route_* functions still inline
alongside the 6 already-extracted route files (library_routes.rs,
mdblist_routes.rs, external_sync_routes.rs, engine_routes.rs,
addon_resource_routes.rs, addon_support_routes.rs). Extracted the rest
into 15 new ffi/*_routes.rs files following the same convention
(use super::*; pub(super) fn route_x), grouping several small routers
together where a single domain didn't warrant its own file:
- addon_protocol_routes.rs, resource_plan_routes.rs, stream_policy_routes.rs,
search_plan_routes.rs, player_policy_routes.rs, content_identity_routes.rs,
calendar_routes.rs, tmdb_routes.rs, player_scrobble_routes.rs: one router each
- watchlist_offline_routes.rs: watchlist + offline
- anime_nuvio_routes.rs: anime detection + nuvio sync
- intro_plugins_routes.rs: intro segments + plugins
- core_addon_store_routes.rs: core contract + addon store + profile avatar pack
- profile_routes.rs: profile contract + profile prefs
- plan_misc_routes.rs: headless adapter plan, discovery plan, data policy,
dolby vision rpu, player flow
ffi.rs is now 605 lines: core_invoke, the ROUTERS dispatch table, and
the shared arg-parsing helpers/tests. No behavior change — same 393
tests pass, including the fixture test that every known core_invoke
method still routes.
Note: this includes whatever pending edits were already present in
ffi.rs, library_routes.rs, and mdblist_routes.rs before this refactor —
preserved verbatim, just relocated along with everything else.
calendar_plan.rs was 1029 lines with several tightly-coupled but
distinct plan builders. Split into calendar_plan/{helpers,plans,
release_rows,widget_notifications,meta_dates}.rs:
- helpers.rs: shared CalendarItemInput, item identity/detail-score,
artwork resolution, date parsing (used across the other four files)
- plans.rs: visibility/content/desktop-read/candidate plans
- release_rows.rs: release row + season candidate plans
- widget_notifications.rs: widget rows, notification content, release
detection
- meta_dates.rs: meta-to-calendar-items, unaired episode lookup,
this-week partitioning, month matching
calendar_plan.rs is now just module wiring plus the existing test suite.
No behavior change — same 393 tests pass.
search_plan.rs was 1262 lines mixing search, discover, and addon-catalog
concerns. Split into search_plan/{search,discover,addon_catalog,
library_sort,detail_nav}.rs:
- search.rs: search suggestions/screen/result-grouping/merge plans
- discover.rs: discover page merging, selection, and sort plans
- addon_catalog.rs: addon manifest/catalog helpers, metadata feed options,
transport URL resolution, feed genre resolution
- library_sort.rs: library sort plan
- detail_nav.rs: detail series lookup id, season load plan
search_plan.rs is now just module wiring plus the existing test suite.
No behavior change — same 393 tests pass.
mod.rs was 1590 lines, ~700 of which were four JS polyfill strings
(base64, text encoder, crypto/CryptoJS shim, cheerio) embedded as Rust
raw-string constants. Moved them to plugin_runtime/polyfills/*.js,
loaded via include_str! (same pattern external_sync.rs already uses
for JSON test fixtures). mod.rs is now 882 lines of actual Rust logic;
the polyfill JS gets real syntax highlighting/tooling as a side benefit.
No behavior change — same 393 tests pass, including the ones that
execute the crypto/cheerio polyfills through the JS engine.
home_ranking.rs was 1685 lines mixing five fairly distinct concerns.
Split into home_ranking/{helpers,folders,bootstrap,ranking,billboard}.rs:
- helpers.rs: shared meta_text/meta_i64/meta_string_array accessors
- folders.rs: folder paging/source-plan/merge + collection shelf building
- bootstrap.rs: home hero plan + bootstrap preparation/completion plans
- ranking.rs: NativeHomeCategory + personalization/prioritization/optimize
- billboard.rs: candidate/visual scoring, pool building, catalog normalization
home_ranking.rs is now just module wiring plus the existing test suite.
No behavior change — same 393 tests pass.
tests.rs alone was 1424 lines. Broken into one file per feature area
(detail, player, home, library, discover, plugins, calendar,
cross_cutting, engine, wire_fixtures), mirroring the existing
per-feature submodule split in headless_engine itself. No behavior
change — same 393 tests pass.
mod.rs was 2204 lines, ~65% of it the #[cfg(test)] module. Splitting
the ~1424 lines of tests into headless_engine/tests.rs leaves mod.rs
at 777 lines of actual engine logic (struct, lifecycle fns, dispatch/
complete_effect tables). No behavior change — same 393 tests pass.
Breaks the 2403-line external_sync.rs into trakt.rs, simkl.rs, mal.rs,
stremio.rs, calendar.rs, merge.rs, and plan.rs, mirroring the existing
provider_mappers.rs/anilist.rs split. external_sync.rs is now just
module wiring plus the test suite. No behavior changes.
Adds import_apply_plan_json (Trakt/Simkl/Stremio's shared
watchlist+watched merge shape) and extends anilist_entries_to_sync /
nuvio_sync::import_merge_plan_json with categories+dryRun. Each now
decides, from already-fetched provider data and local before-state,
what to merge and what to report as counts — the host only passes the
user's category selection through and applies whatever non-null
fields come back. This was previously TypeScript-only control flow in
fluxa-desktop; centralizing it lets any host platform (e.g. the
Kotlin mobile app) reuse the same category/dry-run decision logic
through the existing core_invoke funnel instead of reimplementing it.
Given a destination provider, selected import categories, and the
local library snapshot (watchlist/completed/dropped/continueWatching),
decides which push operations are needed and in what shape per
provider (per-item watchlist/status adds, bulk watched-ids, Nuvio's
bulk item/history/progress payloads). Centralizes the
provider/category capability matrix that fluxa-desktop's
crossProviderPush.ts previously hand-wrote in TypeScript, so other
host platforms (mobile) can consume the same decision logic through
the existing core_invoke("pushPlan", ...) funnel instead of
re-deriving it.
The activity-timestamp diffing (which resource changed since last sync)
and SIMKL's delta-merge algorithm were living entirely in fluxa-desktop's
TS layer, duplicating pure JSON-in/JSON-out decision logic that a second
platform consumer would otherwise have to reimplement from scratch. Only
the actual HTTP fetch/retry/storage I/O stays in the platform layer.
metaDetail requests carry an optional source addon transportUrl now.
When present, resource_fetch_plan_json filters the meta race down to
just that addon instead of racing every configured meta-capable
addon by priority, so a poster clicked from Cinemeta's catalog shows
Cinemeta's own meta response instead of whichever addon wins the
priority race. Falls back to the full race when the source addon is
unknown (deep links, search, library) or doesn't implement the meta
resource itself (e.g. a torrent-indexing addon).
Also tags search request objects with their addon's transportUrl so
the desktop shell can stamp search results with their source addon
the same way catalog rows already can.
The previous commit only synthesized a YouTube tlang= translation when
preferred/secondary subtitle prefs had no native track — it skipped
the systemLanguage signal (the app's own UI language), which the
selector already uses as a real fallback source for picking a track.
Now all three (preferred, secondary, systemLanguage) are eligible
translation targets, not just the two explicit prefs.
trailer_subtitle_selection_plan_json only ever picked from the
literal caption tracks YouTube's player response lists (usually just
the original-language one). If the user's preferred/secondary
subtitle language had no native track, playback silently fell back to
whatever scored best with no way to get their language — even though
YouTube's timedtext endpoint supports server-side translation via a
tlang= query param on any existing track's URL.
Now, when no available track matches the user's explicit preferred/
secondary language, synthesize a translated variant from the
best-scoring native track by appending tlang=<target> to its URL.
DetailState (headless_engine/detail.rs) whitelists which fields from a
completed fetchDetailSecondary effect get copied into engine state —
mdblistRatings wasn't in that list, so the frontend's fetch always
succeeded but the value was silently dropped before ever reaching the
UI. Added alongside the existing omdb_ratings field it mirrors.
MDBList returns {"value": null, "score": null} for unrated sources
(e.g. myanimelist on non-anime titles) rather than omitting the key.
The previous or_else only triggered when "value" was absent, not when
present-but-null, so a source with null value and a real score never
fell through.
Scrobble/checkin bodies nest under movie/show (Trakt-style), not the
flat {ids, type} shape originally guessed. Discussion bodies use
"content" not "comment". Sync/ratings needs a per-item "rating" field.
mdblistSyncMutatePlan's route now builds the grouped body from a flat
items array like the watchlist/list-items routes, instead of taking a
pre-built body.