Commit Graph
98 Commits
Author SHA1 Message Date
edde746 4fe4f7b1d7 fix(mpv): stop loading the ytdl hook for media-server streams
Every URL the player opens is a media-server stream or a local file, so mpv's
bundled ytdl_hook has nothing to resolve. It still ran an on_load hook per
open and, whenever an open failed, spawned yt-dlp with the full stream URL in
its argv — access token included, readable through /proc on Linux. It also
added ~700ms to every failed open and buried the real "[stream] Failed to
open" line under three ytdl_hook errors.

mpv decides whether to load the builtin script inside mpv_initialize, so this
has to be an option set beforehand rather than a property set from Dart.
Verified against mpv 0.41: --ytdl=yes logs "Loading lua script
@ytdl_hook.lua", --ytdl=no never loads it.

Apple is deliberately excluded: the bundled libmpv is built without Lua, so
the option does not exist there and setting it would only print an mpv error
on every player init.
2026-07-31 21:45:33 +02:00
edde746 7f0cad339c fix(startup): report and repair a failed launch instead of showing "Error"
Since 2.10.0 the whole app sits behind one all-or-nothing initialization
gate, and that gate discarded the only evidence of its own failure. It
caught the error, logged nothing but `error.runtimeType`, rendered an
icon plus the word "Error" plus Retry, and never reported the error
because catching it kept the crash reporter from ever seeing it. There
is no log file on any platform, the buffer is in memory only, a
double-clicked Windows release build has no console, and the log viewer
lives in Settings, behind the gate that just failed. #1732 is the result:
a Windows 11 user whose app will not boot and who cannot produce a single
byte of diagnostic detail.

The gate now names its phases. Each step is wrapped so a throw carries
the phase it came from, replacing a `Future.wait` that discarded every
error but the first and could not attribute it to any of four concurrent
steps. The failure screen renders the phase, the exception type, the
message and an expandable stack, plus copy and upload actions that reuse
the existing log-relay flow. The record is persisted next to the database
so the next successful launch can surface it in Settings > Logs, and it
is reported to the crash reporter explicitly.

Only preferences and the database still gate the launch. Window chrome,
locale, crash-reporting init, TV/performance detection, the image-cache
budget and download storage are best-effort and time-bounded, so a
stalled platform thread degrades instead of holding the splash forever.
Sentry no longer receives the startup work as its `appRunner`: that made
a startup failure indistinguishable from a Sentry failure, and the guard
would then have re-run migrations and the database open a second time.

The two remaining fatal steps become recoverable. Preference reads
tolerate a value whose stored type no longer matches, dropping the key
and defaulting instead of failing the boot. A store that cannot be parsed
is detected before either desktop plugin backend can memoise it, which is
what makes an in-process repair possible at all. Repair is never
automatic: it states what it will cost, salvages the credential-vault key
and every tracker and Seerr session it can validate out of the damaged
bytes, reseeds them, and moves the original aside rather than deleting
it. Servers and profiles survive a salvaged key because their tokens are
ciphertext in the database; tracker and Seerr sessions are plaintext
preference entries, so the copy says they may still need reconnecting.

Nothing derived from the store reaches a diagnostic. `FormatException`
prints an excerpt of whatever it failed to parse, and during startup that
document holds the vault key, refresh tokens and session cookies while
the redaction manager still has nothing registered, so the wrapper keeps
only the cause's type and offset and the record is an allowlist of
already-redacted fields. The quarantined copy is labelled as containing
credentials, is never offered for upload, and can be deleted from the
dialog.

Also self-heals orphaned WAL/SHM sidecars on desktop rather than only
tvOS, makes every `createTable` migration step idempotent, keeps MSVC
link by-products out of the Windows bundle, and asserts bundle contents
in CI.

Refs #1732
2026-07-31 21:45:32 +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 35f7a12d7c fix(linux): keep the mpv node builder clear of X11's Bool macro
The Linux native reliability job stopped compiling mpv_player.cc: the
node-conversion builder exposed a leaf named `Bool`, and X11's Xlib.h —
reached through epoxy/egl.h -> EGL/eglplatform.h — defines `Bool` as a
macro for `int`, so the declaration was rewritten into nonsense.

Renames that leaf to `Boolean` across the shared walk and all three
builders. The name is the only thing that changes; no conversion
behaviour differs.
2026-07-26 23:05:16 +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 9e8cfbc7dd fix(windows): keep video-child input on the Flutter view
Create the mpv host window with WS_DISABLED so Windows skips the video subtree
when it picks the window that owns a contact and hands the input to the parent
Flutter view instead. Touch over the video never reached Flutter before: mpv's
inner window owns the contact from its own thread, and neither relay worked
from there - Flutter resolves WM_POINTER with GetPointerInfo, which only
answers for a message the calling thread retrieved, and the system discards a
cross-thread pointer send outright. WS_EX_TRANSPARENT and an HTTRANSPARENT
WM_NCHITTEST reply are both same-thread-only, so disabling the subtree is the
one hit-test opt-out that applies across threads. The mouse relay stays for
input that still reaches mpv's window.

Repair the contract test that covers this. It drove its pointer assertions with
cross-thread sends that Windows drops, so every touch assertion had been dead
since it was added and the suite fails "primary touch must press once" on main.
Relaying those sends through the window's own thread runs all eight tests, and
injected mouse and touch presses over the disabled host now assert delivery to
the parent view; removing WS_DISABLED fails the suite.

close #1556
2026-07-26 19:04:36 +02:00
edde746 7e05686a7c fix(windows): restore x64 and arm64 builds 2026-07-26 15:32:02 +02:00
edde746 8ef977d890 fix(windows): route touch input over video 2026-07-26 14:58:37 +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 1ae58f676b fix(windows): recover failed display handoffs 2026-07-25 17:38:09 +02:00
edde746 f8f366a513 fix(native): harden player teardown and shelf recovery 2026-07-25 16:16:24 +02:00
edde746 b7d5922b0a fix(ci): repair Windows native checks 2026-07-25 00:22:23 +02:00
edde746 d1cec00b51 fix(ci): repair native and Maestro checks 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 9f2e050797 fix(native): bound cross-platform lifecycle ownership 2026-07-24 03:56:40 +02:00
edde746 e0bf66eea8 fix(runtime): harden application service boundaries 2026-07-24 03:46:46 +02:00
edde746 a1b6a89714 fix(player): load subtitle sidecars with media
close #1583
2026-07-17 23:09:07 +02:00
edde746 54a4b8e414 refactor(mpv): move common code to shared 2026-07-15 07:57:02 +02:00
edde746 dfeffad044 fix(windows): forward touch input over video 2026-07-13 18:48:58 +02:00
edde746 6506f36843 refactor(app): share client and presentation scaffolds 2026-07-12 17:31:12 +02:00
edde746 f7f4e3a992 fix(windows): support structured mpv observations 2026-07-12 08:42:24 +02:00
edde746 3d36ecf094 fix(mpv): harden native initialization and logging 2026-07-12 08:42:23 +02:00
edde746 0ddd0a6c25 refactor(mpv): remove unused seek events 2026-07-12 08:42:21 +02:00
edde746 bc55aea701 refactor: remove superseded code paths 2026-07-12 08:42:19 +02:00
edde746 048fb77c8c fix(windows): move mpv teardown off platform thread 2026-07-12 08:42:19 +02:00
edde746 422db75b5b feat(music): mpv audio playback engine with gapless queue service
Audio-only mpv core on every platform (dedicated
com.plezy/mpv_audio_player channels): parameterized android/windows/
linux mpv plugins and a new apple MpvAudioPlayerCore, all skipping
video/window paths (vid=no, audio-display=no, gapless-audio=weak).
MusicPlaybackService drives an in-memory queue with shuffle/repeat,
file-loaded-event gapless arming (property edges coalesce and the
android bridge drops them), per-track progress reporting, OS media
controls, audio focus, sleep timer, and error auto-skip.
PlaybackCoordinator enforces one live native player: starting video
disposes the audio core first.
2026-07-05 19:26:28 +02:00
edde746 25c7893a8e fix(windows): report native crashes through the inproc sentry backend
Crashpad uploads minidumps straight to the DSN's /minidump endpoint,
which our tracker does not implement — every native Windows crash
(e.g. faults inside libmpv threads, #1490) was silently dropped.
The inproc handler stackwalks at crash time and reports through the
regular envelope endpoint, and crashpad_handler.exe/crashpad_wer.dll
leave the bundle.
2026-07-05 09:10:46 +02:00
edde746 c4184c6148 fix(ci): restore sanity checks 2026-07-05 08:34:55 +02:00
edde746 2e4ba352f4 fix: multi-trigger audio recovery after Windows sleep/wake
The two prior attempts at #783 failed for lack of a reliable trigger:
re-setting audio-device to its own value is a no-op, and gating
recovery on audio-device-list changes while current-ao is null never
fires in the real flows (a paused player only discovers the dead
WASAPI session at unpause, when the device list is already stable;
a zombie session that consumes samples silently never goes null).

Replace the single gate with a recovery scheduler on the mpv event
thread, ticked every wait-event timeout:

- WM_POWERBROADCAST resume (both PBT variants, via the plugin's
  existing window-proc delegate) requests unconditional ao-reloads at
  +1.5s and +6s after wake — the only trigger that reaches zombie
  sessions; the second shot covers a first reload landing while the
  audio stack is still restoring. The handoff is a single atomic
  flag: no mpv calls or timers from the platform thread, so there is
  nothing to clean up on dispose.
- current-ao falling to null arms clock-driven retries (budget 5,
  x2 backoff capped at 8s); retries must be clock-driven because a
  failed ao-reload falls back to null without a property change
  event. A device-list change while null refreshes the budget, and
  audio-device-list is now observed natively so recovery does not
  depend on the Dart side.

Every trigger, attempt, and outcome logs as [mpv:audio-recovery] at
warn level so exported logs from the reporter are actionable if this
still misses. Verified with synthetic PBT_APMRESUMEAUTOMATIC while
playing and while paused: full WASAPI reinit both times, audio
restarts on unpause.

Ref #783
2026-07-05 06:49:38 +02:00
edde746 7972743a2f fix(windows): stop mpv from double-handling hardware media keys 2026-07-02 18:30:20 +02:00
edde746 0e3c592205 fix(windows): marshal mpv events onto the platform thread
close #1453

Events were delivered to the Flutter event sink directly on the mpv event
thread while method replies were already marshaled; concurrent messenger
use from two threads intermittently crashed guests during the event burst
of an in-place media switch. Also joins the event thread before draining
platform tasks in the plugin destructor (reverse member destruction order
would otherwise let the event thread enqueue into a destroyed queue) and
coalesces the wakeup PostMessage behind a retry-safe flag.
2026-07-02 12:43:10 +02:00
edde746 ba112d7aa6 refactor(windows): remove legacy mpv compositing, require DComp engine 2026-07-01 06:37:35 +02:00
edde746 a4f0b92b4e fix(windows): stop taskbar icon flashing on minimize after playback 2026-05-30 03:01:48 +02:00
edde746 6a74ff083f fix(mpv): avoid forcing HDR colorspace hint 2026-05-21 16:01:15 +02:00
edde746 73735c5a00 fix(ci): unblock analyzer and windows build 2026-05-20 18:10:21 +02:00
edde746 e71c39271c fix(windows): recover mpv audio output 2026-05-06 10:22:04 +02:00
edde746 5010833da9 fix(mpv): avoid blocking runtime calls 2026-05-03 08:23:44 +02:00
edde746 024af35bf5 chore: add native formatting checks 2026-05-01 05:56:49 +02:00
Cameron 6371d7a36e fix(windows): fixed black screen while navigating between videos in full screen 2026-04-26 16:59:50 +01:00
edde746 e63920e23b fix: native monitor-aware fullscreen on windows 2026-04-16 23:13:15 +02:00
edde746 d04781e4b3 fix: use baseline x86_64 mpv build for older CPU compat 2026-04-16 04:19:43 +02:00
edde746 062fd82de1 fix: recover audio after Windows sleep via ao-null-reload 2026-04-03 12:19:36 +02:00
edde746 da95901d39 fix: Windows native code bugs and dedup
- fix unsigned underflow in Utf8FromUtf16 when WideCharToMultiByte fails
- fix inverted freopen_s logic in console attach
- remove misleading WS_EX_APPWINDOW from GWL_STYLE mask
- close mutex handle on normal exit
- extract SetHDRStateForTarget, EnableComposition/DisableComposition, DisplayModeToMap helpers
- cache GetWindowsVersion result
2026-04-02 15:02:07 +02:00
edde746 29101c310b fix: clean up power-resume timer on dispose and destroy 2026-04-02 14:42:14 +02:00
edde746 91db6d0e57 fix: reload audio output on Windows wake from sleep 2026-04-02 14:36:50 +02:00
edde746 4a54b897ac fix: guard sdk_26100.h for newer Windows SDKs 2026-04-02 13:22:27 +02:00
edde746 9565682eda feat: Windows display mode matching (refresh rate, HDR) 2026-04-02 13:05:58 +02:00
edde746 dbb3ae38b3 chore: bump sentry_flutter to 9.15.0 2026-03-19 19:56:36 +01:00