Commit Graph
84 Commits
Author SHA1 Message Date
edde746 b27cab4510 build: read Windows libmpv pins from the lock and share the Linux fetch and version parsing
windows/CMakeLists.txt repeated the mpv-build key, asset base, and both
SHA-256 values that mpv-build.lock.json already owns, and the pin mover
never touched it, so a lock bump left Windows silently stale. CMake now
reads artifacts.windows with string(JSON) (floor raised to 3.19) and the
Windows native cache key hashes the lock; a workflow guard rejects
hand-pinned checksums.

The byte-identical Linux libmpv fetch heredoc in build.yml and ci.yml
moves to scripts/fetch_linux_libmpv.py, which verifies the checksum
before extracting and is what the packaging script now points local
builds at. The package-windows and release jobs and
linux/packaging/build-packages.py read the version through
scripts/pubspec_version.py instead of three looser regexes.
2026-09-03 11:36:29 +02:00
edde746 200d896bea build: switch every platform's libmpv supply chain to the unified mpv-build repo
Plezy consumed mpv through four unrelated supply chains: an MPVKit fork
via SwiftPM for the Apple platforms, a libmpv-android fork's AAR for
Android, an in-CI from-source build for Linux, and an unpinned
sourceforge mpv-dev 7z for Windows. All four now consume the same
per-commit, content-addressed binaries from
https://github.com/edde746/mpv-build, pinned to one commit and built
from one set of pinned sources (mpv v0.41.0 on Apple/Android/Windows,
ffmpeg n8.0.1, our libass fork).

- Apple: the SwiftPM package moves from edde746/MPVKit to
  edde746/mpv-build across the ios/macos/tvos projects;
  scripts/set_mpvkit_revision.sh becomes set_native_revision.sh, writes
  every pin site plus the new repo-root mpv-build.lock.json, and
  tvos/scripts/wire_mpv.rb derives the package repo from the locks.
- Android: the mpv Kotlin API and JNI glue move in-app under
  android/libmpv (repackaged com.edde746.plezy.libmpv, exports renamed,
  shrinker rules covered), and the module downloads per-ABI native
  tarballs (lib/*.so incl. libc++_shared.so + include/) driven entirely
  by mpv-build.lock.json, with a PLEZY_LOCAL_MPV_DIR escape hatch for
  locally built artifacts. The fork AAR and its Maven coordinates are
  gone.
- Linux: CI downloads the prebuilt self-relocating libmpv prefix
  (lock-driven, sha256-verified) instead of compiling mpv/ffmpeg/dav1d/
  libplacebo/shaderc from source; linux/packaging/build-libmpv.sh and
  its test are deleted and native-inputs.json shrinks to the simdutf
  entry the CMake builds still fetch.
- Windows: both arches FetchContent the mpv-build dev zips with
  URL_HASH enforcement, replacing the checksum-less sourceforge
  download and its ARM64 7-Zip special case; guard scripts updated.

The lock plus the Apple pin sites all point at mpv-build commit
d7c3d559, whose manifest was verified asset-by-asset against the
published release digests (17/17 match). Verified locally: wire and
pin-script suites green, runtime-input checks green, all four Android
ABI tarballs downloaded/verified/extracted through the real Gradle
tasks, the Windows FetchContent block exercised end to end through
cmake, the Linux asset hash and layout checked against the workflow
contract, and xcodebuild resolved the flipped SwiftPM graph with
SwiftPM validating every binary checksum.
2026-09-02 11:31:47 +02:00
edde746 c9de78697b feat(settings): reorganize settings into general, appearance, and playback homes
Settings were grouped by screen history rather than by what they govern:
the profile-selection prompt, startup section, force-TV mode, and app
language sat under Appearance, Discord Rich Presence and the companion
remote host hid inside Playback > Behavior, and maximum volume lived
under Seek & Timing.

Introduce a General screen (language, startup, desktop window), regroup
Appearance into display/library/home/navigation/Live TV, split the
Playback monolith into engine, video & display, audio, quality,
subtitles, seek, auto-play & skip, and behavior groups, surface
auto-play next episode in settings, rename the Keyboard Shortcuts
section to Controls (now also hosting the companion remote host), move
Discord Rich Presence to Services > Integrations, and move the
performance-overlay switch to Advanced. Pref keys are unchanged, so
stored values, export/import, and reset behavior are unaffected.

TODO markers record the agreed homes for upcoming settings (#1998,
#2064, #1641, #1769, #2149, #1827, #2138, #1810, #1046, #1924, #1413,
#1668, #1386, #1795).
2026-08-27 16:02:10 +02:00
edde746 8f893e7a67 fix(player): demux Android direct play with media3 again
The in-app FFmpeg container demuxer classified every DTS variant as
plain DTS (profile-blind MIME mapping at the demux boundary), so
DTS-HD MA lost its lossless identity downstream, and it ignored
container display-aspect-ratio overrides. media3's extractors get both
right.

Delete the FFmpeg demuxer (JNI, extractor, and its setting); ExoPlayer
direct play demuxes with media3's DefaultExtractorsFactory again, and
mpv - the Android default - reads container display dimensions itself.

close #2124
close #2115
2026-08-26 18:59:28 +02:00
edde746 fe9d9d5e7d test(player): cover saf reads and the demuxer's jni byte source under r8
The random-access demuxer added two surfaces with no gate behind them. A
download stored through SAF arrives as content://, where an index read has to
open a second descriptor while the loader holds one, and the AVIO callbacks
are reached only by name from C++, so R8 may rename them while every debug
check passes -- the shape #1703 shipped with.

FfmpegExtractorSeekTest now runs the cued fixture over a content:// URI
served by a test-only provider, and FfmpegDemuxerReachabilityTest drives
nativeOpen plus a seek through a media3-free byte source so it can run
minified. Wired into the existing R8 reachability target; verified it fails
when the FfmpegDemuxerJni$Input keep rule is removed.
2026-08-24 23:02:23 +02:00
edde746andGitHub cc4eff98c0 build: migrate to Flutter 3.47.1 (#2092) 2026-08-24 12:32:05 +02:00
edde746 adef523894 chore(build): add a SwiftPM refresh script for stale MPVKit pins
After set_mpvkit_revision.sh moves the MPVKit pin, macOS builds die in the
SwiftPM integration migration with "could not find the commit <sha>": Flutter
runs the macOS Xcode steps with -skipPackageUpdates, and its one fetch-capable
step only ever runs for iOS, so the local mirror never learns the new revision.

scripts/refresh_apple_spm.sh runs a single -resolvePackageDependencies per
platform to update the mirror without touching the tracked locks, with --reset
for an internally inconsistent SourcePackages directory. The header of
set_mpvkit_revision.sh now points at it.
2026-08-24 06:12:06 +02:00
edde746 fff1b3090d build(apple): pin MPVKit by commit instead of by version
MPVKit publishes binaries for every commit on its main branch now, so a
version tag is no longer the only resolvable thing. Pinning the revision
means a fork fix is consumable as soon as its binaries land, with no release
ceremony there and no version bump here.

Pinned at 0097fea015fe, which carries the PiP subtitle fix: the iPadOS
Picture-in-Picture window no longer resizes when SRT subtitles appear and
disappear. Nine pin sites move together -- three project.pbxproj
requirements plus the six SwiftPM locks that check_apple_spm_locks.py keeps
in agreement.

tvos/scripts/wire_mpv.rb no longer hardcodes the version it writes; it reads
the revision out of the tvOS project's committed lock, so re-wiring the
tvOS target cannot silently revert the pin. Its test asserts the shape and
that all nine sites name one commit, rather than a literal sha, so moving
the pin stays one command:

    scripts/set_mpvkit_revision.sh <40-char sha>

close #2078
2026-08-24 01:15:28 +02:00
edde746 37446e2d8e fix(android): fail loud on non-literal JVM reflection in the shrinker check
The JVM-reflection scan only recognized string-literal lookups, so a
Class.forName or getDeclaredField written with a constant, variable, or
concatenated name was silently skipped and a release build could lose the
member while the check stayed green — inverting the fail-loud behavior the
checker applies to every native lookup it cannot trace.

Unclaimed reflective occurrences now error like the cannot-trace paths, and
the forName pattern requires the closing paren so a concatenated argument is
not half-claimed as a bare literal prefix. Production reflection stays
all-literal, so repository validation is unchanged.
2026-08-21 19:23:45 +02:00
edde746 da21481ff3 fix(android): keep the reflected media3 MatroskaExtractor fields under R8
AssMatroskaExtractor and MatroskaLatmSupport reflect MatroskaExtractor's private extractorOutput/subtitleSample fields by name, but no keep rule covered them: R8 renames the fields and every MKV direct-play in a release build fails constructing the extractor. Adds the keepclassmembernames rule (descriptors verified against media3 1.11.0 in the Gradle cache) and extends check_shrinker_rules.py to scan Kotlin/Java reflection across all android src/main roots including libass.
2026-08-21 19:23:40 +02:00
edde746 e0122feb2b fix(release): address the Amazon submission API by app id instead of package name
The App Submission API stopped resolving apps by package name and now
returns 400 "No app found with the entered Inputs"; it requires the
amzn1.devportal.mobileapp app id from the console URL. Read the id from
AMAZON_APPSTORE_APP_ID, validate it in preflight, and keep the package
name for console-facing messages.
2026-08-20 00:29:21 +02:00
edde746 2a6f752860 fix(release): make multi-channel deployment resumable 2026-08-18 18:00:52 +02:00
edde746 ad1a12c998 fix(release): reset fresh state and preserve deferred phases 2026-08-18 14:45:32 +02:00
edde746 947fddf540 fix(ci): resolve the repository root after the symbols script moved
The scripts reorganization moved upload_symbols.dart from scripts/ into
scripts/release/ but kept the single-.parent root resolution, so the
script searched scripts/build and scripts/debug-info and every CI symbol
upload failed with "no symbols found". The sibling checks scripts were
updated to .parent.parent in the same move; match them.
2026-08-17 02:46:36 +02:00
edde746 aeb6108654 feat(scripts): add the deploy.py multi-channel release pipeline
One command releases to Play, Amazon, App Store Connect (iOS + tvOS), the GitHub build farm, Microsoft Store, and the GitHub release + cask, with checkpointed resume. Replaces the fastlane release flow; first used for 2.14.0.
2026-08-17 01:40:54 +02:00
edde746 f622ba8efe chore(scripts): group scripts into checks, codegen, maestro and release subdirectories
scripts/ had ~80 flat files. Entry points (ci_*.sh, codegen.sh, run_tests.sh, format_native.sh, setup_hooks.sh, upload-symbols.*) and the shared pubspec_version.py stay at the root; checkers, generators, maestro tooling and release tooling move into subdirectories with their tests. Updated every reference: workflow steps, guard-test glob, Docker COPY paths and .dockerignore whitelist, website audit path, dart test imports, and regenerated the five outputs whose headers embed generator paths.
2026-08-17 01:40:54 +02:00
edde746 ae001d9ff3 fix(linux): restore VAAPI hardware decode and AV1 software fallback in the bundled libmpv
Hardware decoding stopped working for Linux users on 2.13.0 (Fedora 44
report): every source decodes in software, and AV1 plays black video with
audio. Two defects in the pinned libmpv build.

First, mpv's meson 'drm' feature silently disabled itself because the CI
builder lacks libdisplay-info, and every VAAPI path that does not depend
on a display server is derived from it: vaapi-copy's standalone render-node
device (the path 2.12.1 worked on) and the GL dmabuf interop for direct
vaapi. With only the Wayland VA provider compiled in, a machine whose
Wayland VA display fails to initialize has no fallback, and vaapi-copy
has an empty provider list - every source lands on software decoding.
Pin -Ddrm=enabled, -Dvaapi-drm=enabled, -Degl=enabled and
-Dvaapi-wayland=enabled, and add libdisplay-info-dev to the CI package
lists, so a missing piece fails the build instead of shipping silent
software decode.

Second, the bundled static FFmpeg has no AV1 software decoder: its native
av1 codec is hardware-accelerated only, so once hwdec fails there is no AV1
path at all - every packet errors, video hits EOF, the plane goes black
while audio keeps playing. Pin dav1d 1.5.4 (both VideoLAN remotes agree on
the tag object and root commit), build it static before ffmpeg, and pass
--enable-libdav1d.

The build-plan stub test now asserts the hwdec feature flags, the dav1d
static build, and ffmpeg's libdav1d. Verified in an ubuntu:24.04 container
with the production flag sets: meson reports drm, vaapi-drm, vaapi-wayland,
egl and dmabuf-interop-gl enabled, and ffmpeg configures CONFIG_LIBDAV1D=yes
with the AV1 VAAPI hwaccel.

close #1874
2026-08-14 21:49:41 +02:00
edde746 69fadc220d chore: clean up code comments 2026-08-10 20:28:41 +02:00
edde746 369c6279d6 fix(i18n): translate the player, downloads and server-setup text left in English
A Portuguese user reported "Skip Intro" rendering in English on Android TV.
The locale files were not the problem - all 22 were structurally complete.
skip_marker_button.dart simply never imported strings.g.dart and assigned
'Skip Intro' / 'Skip Credits' / 'Next Episode' as plain literals. An audit of
lib/ found ~120 more sites in the same state, in four shapes that need
different fixes:

A literal in a file that never imported the i18n layer is the easy one -
skip_marker_button, performance_stats, track_label_builder and codec_utils all
render text with no `t` in the file at all. TrackLabelBuilder._compose now takes
a fallbackLabel builder instead of an English fallbackPrefix, so the caller
supplies t.audioTracks.track / t.videoControls.subtitleTrack and every unnamed
audio and subtitle row in the track menus is localized.

English reaching the user through an exception message is the widest one, and
it needs care: MediaServerException.message feeds both toString() - logs and
Sentry grouping - and verbatim UI display. Localizing it in place would make
bug-report logs follow the user's locale and split one Sentry issue into 22.
The MediaServer and Seerr families instead gain a nullable `display` alongside
the English `message`, and the six screens that print these errors read
`display ?? message`. PlaybackException keeps the opposite rule, because it
already carries a PlaybackFailureReason for logic and classifyPlaybackFailure
already builds it from t.messages: its stragglers are localized at the throw
site. That also removes the literal "Exception: " prefix Live TV users saw on
a tune failure, since PlaybackException.toString() returns the bare message.

Localized parts hand-concatenated with bare English are the shape no search for
Text('...') can find: '${t.common.pause} auto-scroll' on the home carousel,
'${day} at ${time}' on the Live TV schedule row, and an actor-screen count that
hand-rolled its plural as `n == 1 ? 'title' : 'titles'` - wrong for ru and pl
regardless of translation, now a real Slang plural.

Finally a literal assigned to provider state that a widget renders later:
DownloadProgress.errorMessage, and the four background_downloader notification
bodies, which sit inside a plugin config call where no widget-shaped search
reaches them.

Two things surfaced while converting. track_chapter_controls compared a track
label against 'Audio Track N' to swap in a localized version; once the builder
localized its own fallback that branch became unreachable, so it and the
orphaned _joinTrackLabel are gone. And discovery_view's PeerError fallback arm
looks like a leak but is not - its producers already localize, and a test says
so - so it stays as it is.

All 21 non-base locales are translated, including the 21 keys left empty by
earlier commits that were falling back to English. No locale has an empty value.

scripts/check_hardcoded_strings.py guards the three shapes a structural check
can see, and runs in ci_checks.sh after translation hygiene. Its first draft
passed its own tests while missing this very bug, because 'Skip Intro' is bound
to a local rather than handed to Text(); the name-bound rule that closes that
gap is restricted to phrase-shaped literals, or it cannot tell copy from the
identifiers this codebase binds constantly ('cast_row', 'auto', 'liveTv'). It
cannot see English inside a throw or assigned to a provider field - neither is
distinguishable from a log message without dataflow analysis - and the docstring
says so. label: and actionLabel: are deliberately unscanned: here they name a
diagnostic operation, and a check that is chronically red is a check that gets
switched off.

One commit rather than one per area: the keys, the 22 locale files and the
generated output are a single unit, and any partial split fails the repo's own
unused-key scan on the way through.

close #1856
2026-08-10 15:32:43 +02:00
edde746 e9a213807f ci(linux): check the runner's libraries reach the package metadata
The plane added three runtime libraries that bundle-libs.sh deliberately does not
bundle, so they have to be declared per distro by hand - and two hand-maintained
lists drifting apart is the failure this guard exists to prevent.

check_linux_package_deps.py parses the runner's CMake for every pkg-config module
it links, follows target_link_libraries to prove each one actually reaches the
binary, and requires a package name for it in every distro's depends list. It
fails closed on the shapes a naive parser gets wrong: a pkg_check_modules call
naming several modules, options preceding the module name, and version
constraints like mpv>=0.40 that would otherwise be read as a package nobody
ships.

The smoke job builds the three packages and reads the dependencies back out of
the artifacts, deriving what to expect from build-packages.py rather than
restating it - so a library is declared once and verified everywhere. That job is
off by default, which is exactly why it must not carry its own copy of the list.

The Linux native job names libwayland-dev and libegl-dev instead of riding
GTK's and epoxy's transitive dev dependencies, matching the CMake comment's own
rationale. In CI the host-dependency guard runs once: the named step covers the
staged bundle, and build-packages.py's internal run - which exists for by-hand
packaging - is skipped. The smoke job also drops patchelf, which nothing
invokes.
2026-08-10 08:48:14 +02:00
edde746 bb3762ed63 ci: remove the Android Maestro e2e workflow
The Maestro suites remain runnable locally through scripts/run_maestro.py
and scripts/run_maestro_ci.py; drop the workflow, the test that parsed it,
and the CONTRIBUTING reference to automatic PR coverage.
2026-08-08 12:36:49 +02:00
edde746 db4f7a643b test: prune low-value coverage 2026-08-06 05:33:18 +02:00
edde746 fb0613e3db feat(player): toggle playback on a two-finger tap without raising the chrome
A touch viewer had to raise the chrome to pause, which dims the picture and
covers the subtitle line they were trying to finish reading. A two-finger tap
now toggles playback with the chrome left down, so the frame that pauses is the
frame that was on screen. It fires the moment the chord resolves, in every
player state.

The two-finger double tap no longer resets the video zoom. Keeping it would mean
holding this toggle back for the double-tap window before acting, and pausing
late is pausing on the wrong frame. Zoom reset stays in the video settings sheet,
its presets and the keyboard shortcut, and pinching back to 100% now snaps
exactly within three percent so touch has a one-gesture path too.

Both chord actions share _mobileTouchGesturesAllowed, so the chord is inert
under screen lock, in PiP and while the content strip is open; the zoom reset
previously fired straight through a locked screen.

close #1505
2026-08-03 00:04:03 +02:00
edde746 944a8d89f5 feat(windows): package for the Microsoft Store as an MSIX bundle
The Store's unpackaged EXE path would require Authenticode-signing the
installer and every PE file inside it. MSIX submissions are re-signed by the
Store instead, so this route needs no code-signing certificate. build-msix.ps1
mirrors build-installer.ps1 and consumes the same per-architecture build
artifacts, leaving the installer, portable archives and WinSparkle appcast
untouched.

One template generates the manifest for both architectures, carrying the
identity reserved in Partner Center. check_windows_msix.py recomputes the
package family name from the publisher DN, so a mistyped identity fails CI
rather than a submission, and it parses the script rather than running it
because root CI is Linux. Qualified logo assets are indexed into
resources.pri; without the altform-unplated variants the shell draws the
taskbar icon on an accent-coloured plate.

PlatformDetector.isPackagedInstall gates the in-app updater and the Liberapay
tile, which the read-only package directory and Store commerce policy
respectively rule out. Gating at runtime keeps one Windows build feeding both
the installer and the Store package.
2026-07-30 20:03:04 +02:00
edde746 1165998dae fix(windows): elevate the installer when the install directory is read-only
PrivilegesRequired=lowest makes Inno Setup "always run in non
administrative install mode" — the launching token is irrelevant. So a
copy that ended up in C:\Program Files, which the destination page still
lets an elevated wizard run pick, is registered under HKCU while living
somewhere an ordinary process cannot write. UsePreviousAppDir then aims
every later run straight back at that directory.

WinSparkle launches the downloaded installer with plain ShellExecuteEx
and no verb, so nothing along the in-app update path ever asks for
elevation: the silent installer starts, cannot replace a single file, and
the only way out was to quit Plezy, fetch the installer by hand and pick
"Run as administrator". Inno's own PrivilegesRequiredOverridesAllowed
plus UsePreviousPrivileges does not help here, because it reads the
recorded install mode — which is exactly the non-administrative one that
cannot write.

Decide on write access instead. InitializeSetup probes the registered
install directory and, when it is not writable, relaunches setup through
ShellExec 'runas' pinned to that directory with /ALLUSERS, so the update
lands in place instead of forking a second per-user copy. The relaunch
carries a guard parameter and drops any conflicting mode override, and a
refused UAC prompt now explains itself and points at the releases page
rather than failing mutely. A machine-wide install that takes over a
per-user directory also clears the stale uninstall entry and Start Menu
group that would otherwise list Plezy twice in Apps & Features.

Fresh installs are unchanged: still per-user, still no prompt. Only
commandline is added to PrivilegesRequiredOverridesAllowed, since
allowing dialog would make a silent install with no previous copy stop
for the install-mode question — which is how winget installs.

The script carried two near-identical copies of the whole .iss, one per
architecture shape, so both would have needed this code. Collapse them
into one template parameterised by architecture, add -EmitScriptOnly to
generate the .iss without 7-Zip or Inno Setup, and guard the contract
with check_windows_installer.py so the elevation path, the single-source
AppId and the winget marker cannot rot.

close #1705
2026-07-28 23:57:59 +02:00
edde746 8aa836d106 ci(android): gate name-based reachability on an R8-minified variant
R8 only ever ran on `release`, so every automated gate in this repository
exercised code the shipped APK does not contain. Reflective lookups, JNI
callbacks and native library loading can all break under shrinking while
`flutter test`, the Robolectric suites and `connectedDebugAndroidTest`
stay green — which is exactly how #1703 shipped, with the bundled FFmpeg
audio renderer shrunk out of release builds for TrueHD and DTS-HD.

Add a `minified` build type that inherits release's shrinker
configuration but stays debuggable and debug-signed, so it is an ordinary
test artifact and never a publishable one. Three integration details
took a run each to find: the Flutter plugin copies app build types into
every plugin module, so library-level shrinking deleted the plugin entry
points that only GeneratedPluginRegistrant references; the harness must
not be shrunk or the runner disappears; and androidx.test has to survive
in the app under test, or the runner cannot link its own supertype and
the run reports zero tests instead of failing.

Instrumentation still defaults to `debug`, because only one build type
can host androidTest and the existing playback suites drive media3
builder APIs the app never calls, which R8 shrinks legitimately. The new
reachability test opts into the minified variant instead and touches no
builder API, so the only keeps it depends on are the ones under test.
Emptying proguard-rules.pro was verified to fail it.
2026-07-28 20:13:40 +02:00
edde746 ae331b217c fix(android): keep the FFmpeg audio decoder through R8
Flutter enables minification for every release build, and nothing but a
keep rule reaches androidx.media3.decoder.ffmpeg. DefaultRenderersFactory
instantiates FfmpegAudioRenderer with Class.forName, media3's consumer
rules only -keepclassmembers its constructor, and this project had no
proguard-rules.pro at all, so R8 shrank the renderer out of the shipped
dex and the reflective lookup failed with ClassNotFoundException. The
same pass dropped FfmpegAudioDecoder.growOutputBuffer, which ffmpeg_jni
resolves in JNI_OnLoad and whose absence fails the whole
System.loadLibrary("ffmpegJNI") call.

Release builds therefore lost every codec that decoder adds. TrueHD and
DTS-HD fell through to MediaCodecAudioRenderer, which has no decoder for
them, so a 4K Dolby Vision file died with NO_SUITABLE_DECODER_ERROR and
handed off to the mpv fallback — losing ExoPlayer's Profile 7 to 8.1
conversion on hardware that could have direct-played it. Only debug
builds, where R8 never runs, exercised the working path.

Keep the package and the type named in the JNI callback descriptor, and
guard the invariant so it cannot silently rot again: check_shrinker_rules
fails when an app class in a reflected namespace, a FindClass target, a
native callback member, or a descriptor type has no keep covering it.
Also record the built audio renderers, because whether the extension
loaded is otherwise indistinguishable in an uploaded log.

close #1703
2026-07-28 19:44:04 +02:00
edde746 0ed0ebf22a fix(tvos): derive the RunnerTests roster from the test directory
Read the sources from disk instead of a hand-maintained allow-list that the
script uses destructively, and add a guard that fails when the project and
directory disagree. FlutterNativeTextInputTests.mm was the second test the
list would have silently unwired.
2026-07-27 17:44:52 +02:00
edde746 41c6389cd4 test(e2e): guard the sheet, track, locale, and TV settings behaviour
Four regression flows, each run on a Pixel 7 and, where relevant, a real
Android TV box.

Discriminating — the baseline fails, HEAD passes:

`07_sheet_back_dismiss` pins both halves of the hosted-sheet fix on touch: the
barrier removes the rows behind it from the semantics tree, and one Back closes
only the sheet while Settings stays the current route, including the nested
per-library options page. It fails on c48cbf70, the commit before 8e1904dd.
`03_tv_library_focus` gains the same occlusion assertion for the TV sort sheet
and fails there too.

`08_track_choice_survives_pending_pass` picks a non-default audio and subtitle
track, lets playback outlive the automatic pass's 5s attempt and 25s deadline,
and asserts the choice is still selected. On a56b9a3d the audio reverts to the
container's default. Both new flows onboard from a cleared install: the TV
regressions in the same group leave "Force TV mode" enabled, and a remembered
track selection would pre-select the rows under test.

Coverage without a comparable baseline:

`09_language_picker_locales` asserts the four new endonyms, switches to Turkish,
reads root navigation labels from the generated locale, and restores English.
The locales do not exist before 7677d159/100d7729, so there is nothing to fail
against — this is forward coverage, not a reproduction.

`10_tv_settings_navigation` runs on real Android TV hardware, which no existing
flow covers: the rail layout a device reports on its own, the TV dialog path for
Manage Libraries, the Apple-only Atmos gate staying closed on Android, and the
D-pad-only route to the number spinner's accessibility labels, which a single
tap would hide. It passes on either side of the range and is verified against
both an empty server and one with a resume position, since rail order shifts
with that. It does not assert the 15b54e2e row density: that change is invisible
to a semantics-tree driver, and a pixel `height` assertion would only hold for
one DPR. It registers under a new `android-tv-device` group no workflow
dispatches.

No TV playback flow is included. Entering content on the TV Recommended view has
no stable anchor: the accessibility `focused` flag sits on the hero backdrop
rather than the rail card, and a resume position anywhere in the library pushes
"Recently Added" below the fold and out of the semantics tree entirely. Search
is not a way around it either — inputText does not reach the TV search field.
Covering TV playback needs a testID on the rail card, not a cleverer selector.

Also repairs three assertions that could never fail:

`03_tv_library_focus` gated the sort sheet closing on `notVisible: "Sort by"`,
but the header renders "Sort By" and Maestro selectors are case-sensitive
regexes, so the wait returned immediately and the next D-pad press landed in the
sheet's close animation.

`open_codec_sample` matched a card's watch state as `watched|unwatched` only. A
codec sample keeps a resume position once any earlier flow has played it, so the
row announces "N percent watched" and the subflow stopped finding it on a
fixture container that outlives one suite.

`06_playback_recovery` tapped "Zulu Zone" out of the Recently Added rail, but
every seeded alphabet title shares one dateadded, so which of them the rail
returns is a tie-break. The flow only needs some playable movie.
2026-07-27 13:31:51 +02:00
edde746 41a2e996e1 perf(test): scale test concurrency and stop re-onboarding every Maestro flow
The Dart suite spent 77% of its cost compiling one isolate per test file
while `flutter test` used half the cores, and every Maestro flow replayed
a full Jellyfin onboarding before its first real assertion.

- Add scripts/run_tests.sh, which runs `flutter test` with -j set to the
  cores the process may actually use instead of the ncpu/2 default.
  Measured on 8 cores: 190s -> 136s; -j 12 regresses to 165s, so it scales
  to the core count rather than hard-coding one. CI and CONTRIBUTING use it.
  A cgroup v2 quota, a cgroup v1 quota, and the cpuset/affinity nproc
  reports can each be the binding limit independently, so the detector
  takes the smallest; trusting whichever it found first would oversubscribe
  4x on a container holding an 8-CPU quota while pinned to 2. Covered by
  scripts/test_run_tests.py, which the ci_guard_checks.sh glob picks up.
- Add .maestro/subflows/ensure_onboarded.yaml: cold-start the app and only
  onboard when no session is stored. Flows that just need a signed-in Home
  use it; 02_onboarding_home, 08_logout, 09_download_offline_playback and
  the profile regressions keep clearing state. 59s -> 16s per flow.
- Guard onboarding's two optional taps behind visibility checks. A missed
  `optional: true` tap still runs the full element search, costing 3.0s
  and 7.8s per onboarding to find nothing.
- Disable device animation scales in run_maestro.py, restored by the
  existing cleanup path. CI's emulator got this from the runner flag;
  physical devices never did.
- Shorten the watch_together setup-timeout replacement from 500ms to the
  10ms the same file already proves sufficient, and shorten the retry
  backoff at the one site that missed it: 8.04s -> 1.59s of execution.
- Make the LAN discovery waits deadline-based and resend the beacon while
  polling. Loopback UDP drops datagrams under load, which timed out a
  wait that could never be satisfied; this was the suite's one flaky test.
- Fix 08_logout, which searched for "Logout" and "Are you sure you want to
  logout?" after both strings became "Log out". The flow had been failing
  and aborting the suite before 09 ever ran.

flutter test 190s -> 131s. Maestro's Android suite 621s -> 385s across the
eight flows the baseline reached, and now runs all nine green.
2026-07-27 03:59:27 +02:00
edde746 de4ed3cd2e fix(e2e): reach Jellyfin over adb reverse on the API 28 suite
The legacy playback group repeatedly failed to see any Jellyfin server
while the container reported healthy, because the API 28 image routes
the 10.0.2.2 host alias unreliably. Uses the runner's existing reverse
mapping, as the media suite already does on API 35, so the app connects
over 127.0.0.1 instead. No assertion is weakened.
2026-07-26 23:05:53 +02:00
edde746 0c58b2dc55 fix(ci): follow the guard roster and Flutter pin to their current homes
Two workflow guards had drifted from the code they describe, so
`scripts/ci_guard_checks.sh` failed on a clean tree.

The Flutter release-tag pin moved out of build.yml into the shared
setup-flutter-git composite action, but the checker read that action
from a fixed repository path while its test mutated a workflow fixture.
The mutation could not reach the checker, so the rejection test asserted
against an unmodified run. The checker now resolves the action beside
the workflow it is given, and the test materialises a `.github` tree so
the pin is genuinely exercised.

The script-test roster likewise moved into ci_guard_checks.sh, which
discovers `scripts/test_*.py` by glob; the dispatch guard still expected
each one to be named explicitly in ci_checks.sh and ci.yml. It now reads
that glob and checks both aggregates delegate to the shared roster.
2026-07-26 23:05:17 +02:00
edde746 a56b9a3dfb Merge the deduplication and dead-code removal pass
Consolidates duplicated logic behind shared implementations — paginated
grid tabs, focus chrome, cached remote stores, sheet selection columns,
the server artifact store and a test fixture layer — and removes code
that had become unreachable. Net reduction of about 5,500 lines with no
behaviour change.

Where a fix had landed separately in code that moved into a shared
helper, the fix was re-applied inside the helper rather than left behind
in the copy that went away.
2026-07-26 19:41:23 +02:00
edde746 7e05686a7c fix(windows): restore x64 and arm64 builds 2026-07-26 15:32:02 +02:00
edde746 352b88109b refactor: extract shared mixins and helpers, drop dead abstractions
Introduces shared seams for paginated views, D-pad reorder, media control
routing, async singletons and the device method channel, then points the
open-coded copies at them.

Also removes unused models and duplicated provider/server plumbing, folds
the twice-implemented artifact store in the server, and factors the
repeated Flutter toolchain prologue in CI into a composite action.
2026-07-26 06:09:48 +02:00
edde746 77da82b648 fix(ci): make cross-platform checks deterministic 2026-07-25 16:17:25 +02:00
edde746 db7bc17c49 chore(website): remove tests and update SvelteKit 2026-07-24 20:30:45 +02:00
edde746 40833e65ec fix(ci): align toolchains and isolate platform tests 2026-07-24 10:06:50 +02:00
edde746 0c6dab01b3 build(deps): upgrade Flutter package dependencies 2026-07-24 03:56:40 +02:00
edde746 09656fa4d3 fix(supply-chain): verify CI and production inputs
Pin external actions, images, toolchains, native archives, and tvOS engine artifacts; enforce fail-closed CI checks and keep website privacy disclosures aligned with shipped behavior.
2026-07-24 03:56:40 +02:00
edde746 b41fb4fe75 fix(ui): harden settings focus and semantics 2026-07-24 03:46:50 +02:00
edde746 43a8fe020d fix(relay): secure reconnect and room ownership 2026-07-24 03:46:50 +02:00
edde746 658da37b48 build: make toolchain inputs reproducible 2026-07-24 03:40:06 +02:00
edde746 1d1c301f61 test: stabilize deterministic integration coverage 2026-07-24 03:40:06 +02:00
edde746 fa901be328 fix(e2e): run emulator suites through Python 2026-07-15 08:25:55 +02:00
edde746 0bcd00bcb6 ci: harden privileged workflow trust boundaries 2026-07-15 07:57:42 +02:00
edde746 139cf83507 feat(test): add Maestro end-to-end coverage 2026-07-15 06:40:35 +02:00
edde746 cbb0a01c5f fix(release): create untagged build drafts 2026-07-14 00:33:12 +02:00
edde746 a0013323f2 chore(ui): enforce icon consistency 2026-07-13 23:13:53 +02:00
edde746 7400a10c59 fix(release): prevent published asset replacement 2026-07-13 21:48:44 +02:00