Fourth beta for v0.13.0, cut from master. 14 PRs since beta.3. No migrations.
Playback quality
- The player's quality switcher works on every platform. It previously rendered only on web, where it set a state field and showed a snackbar without changing the stream.
- The rung ladder is derived from the source height, so a 720p file offers no rung above 720p. The chosen rung persists and restarts the session at the current position.
startStreamingSessionacceptsmaxHeightalongsidemaxBitrate.StreamingSessionResultechoes themaxBitrateandmaxHeightthe server actually applied, which can be lower than requested on a relay connection.- Transcodes preserve aspect ratio and never upscale. Forced transcodes previously emitted
-s 1280x720with no aspect handling, so 2.39:1 and 4:3 sources came out geometrically wrong. - Output height is rounded to even on both the capped and uncapped paths. libx264 with
yuv420prefuses an odd frame height. - Session matching compares position plus both caps. It previously compared position only, so a rung change reused the session already running at the old cap.
- New
MAX_TRANSCODE_HEIGHToperator ceiling, settable through env,config.yml, the DB overlay and the admin UI. - A chosen rung vetoes direct play. Original still direct-plays.
- The playback transport cluster went from 256px to 192px, secondary buttons from 40px to 32px, padding from 20/12 to 12/8, so four discrete buttons fit down to 360px width.
File selection
- The player streams the file you picked. It previously fetched streaming candidates keyed on the media id, so the selection could not be expressed, and the direct-play branch overwrote it with the server's pick. Reproduced on desktop only, because
kIsWebforces web onto HLS. - New
Mydia.Library.FileRankingranks by resolution, then bitrate, then id, with unanalyzed files last. Wired intoMydia.Streaming.Candidates,StreamControllerand the subtitle controller, which previously took the head of an unordered preload. StreamControllerand the subtitle controller now filter trashed files. OnlyCandidatesdid before, so the candidates endpoint and the stream route could name different files.- Interlaced resolutions are handled in file ranking.
- The streaming by-id path honors the trashed-file filter.
- A downloaded item whose local file is missing falls back to asking by media item instead of sending the
offlinesentinel where a UUID was expected.
Playback reliability
- Replacing an episode's file no longer breaks playback of that episode permanently. The streaming candidates query ran
cacheFirstagainst an on-disk Hive store, so a cached entry kept naming a deleted file across restarts, rendering a black screen with an advancing timeline and no error. The query is nownetworkOnly, and poisoned entries correct themselves on the first play after updating. - This also affected every automatic quality upgrade, which writes a new
media_filesrow and deletes the old one. - The player subscribes to
Player.stream.error, so a failure afteropenreaches the error UI instead of a black screen. The subscription is gated on playback never having advanced, so recoverable decoder and network log lines do not replace playing video with an error page. - Audio tracks are re-detected as mpv finishes probing. The track list was sampled once, 500ms after
open, so a file whose non-default audio stream resolved later offered no way to reach it. - The cast button is back on the playback screen.
ChromeTopBarhas acastActionslot thatplayer_screen.dartnever filled, so the pill rendered empty for the whole of playback. The pill is now the tap target rather than the 18px glyph inside it.
Recently added
- Recently added means when content arrived, not when the record was created. A show added two years ago that received an episode last night was previously invisible to the rail.
- Files group into slots, one per episode or movie. A slot's timestamp is its earliest file; an item's is the newest of its slots. Quality upgrades therefore cannot bump an item back to the top.
- Computed at query time in a new
Mydia.Media.RecentlyAdded. No migration and no denormalized column. recentlyAddedgains three nullable fields:newEpisodeCount,latestSeasonNumber,latestEpisodeNumber. Cards showS04E02or3 new episodes.favorites,unwatchedandcollectionItemsreport the correctedaddedAt. Their episode-context fields stay null.- The
/tvand/movies"Added (Newest)" sorts read the same value, so the web UI and the player agree. Fileless wanted items fall back to their owninserted_at. - The rail passes an explicit limit. The resolver previously materialized the whole 30-day window to return 20 items, which crosses SQLite's bind-parameter ceiling past roughly 32k items.
- A GraphQL relay client could bypass the pagination ceiling by passing a zero cap. Fixed.
Linux Flatpak
The Flatpak channels are not serving this build yet. The build itself succeeded, but publishing to the OSTree repositories requires one-time operator setup that has not been completed, so flatpak.mydia.dev has nothing to install from. Use the mydia-player-linux-v0.13.0-beta.4.tar.gz asset for now. The commands below will work once the channels are populated.
-
The Linux player builds as a Flatpak targeting two OSTree repositories:
mydiafor stable,mydia-betafor prereleases. Both shipdev.mydia.player, distinguished by OSTree branch.flatpak remote-add --if-not-exists --from mydia-beta https://flatpak.mydia.dev/mydia-beta.flatpakrepo flatpak install mydia-beta dev.mydia.player -
Built on
org.gnome.Platform50 with libass, libplacebo and mpv as manifest modules, so libmpv ships inside the app. The existing tarball links the system libmpv and dies at load time without it. -
Desktop entry and AppStream metadata are added, so the player appears in GNOME Software and KDE Discover.
-
Signing runs in a separate job from building, so the GPG key is absent from the job that runs build code from the dependency tree.
-
The
mydia-player-linux-*.tar.gzasset stays.
Activity feed
- All 32 recorded event types are labelled from one registry,
Mydia.Events.Presentation. Presentation previously lived in three maps that had drifted: 13 types were unhandled by the feed and 9 had no presentation anywhere, sodownload.stalledrendered as its raw key. Event.changeset/2validates:typeagainst the registry, so an unregistered type cannot be recorded.- Filter chips added for the
playbackandplugincategories, which were reachable only under "All". plugin.http_requestis excluded from the global feed, where it fired once per plugin egress request. It keeps its dedicated viewer in the admin plugins logs modal.- Whole hours render without a trailing decimal.
Admin and settings
- Every typed setting in Admin → Configuration raised a
FunctionClauseErroron blur, so the DB overlay worked for toggles but not for anything you type. - A setting whose value has not changed no longer writes a config row.
- Adding or removing a Direct URL on the Remote Access page persists. The handler called a stub left behind when the WebSocket relay was replaced by P2P, whose entire body returned its input, and flashed "Direct URL added successfully" regardless.
- The Updates section is hidden on iOS, where
PlatformUpdater.forCurrentPlatform()returns null and the tile's actions were no-ops.
Player interface
- Poster cards that only navigate no longer show a hover play button. Affects the library, favorites, recently-added, unwatched and collection grids, the home rails and search results.
EpisodeRailCard, whose tap does start playback, is unchanged. - Search posters use the library's depth treatment. They previously had no resting shadow, a purple hover glow, a 1.03 hover scale, a 12px radius against the library's 8px, and ignored reduced-motion.
- The Movies and TV Shows grids no longer render 128px below the app bar.
_getTopPaddingadded the app bar height to aMediaQuery.padding.topthat Flutter had already rewritten to mean the same thing underextendBodyBehindAppBar. The measured gap goes from 128px to 8px atdevicePixelRatio1.0 with a 47px status bar. - The freshness header overlays the grid instead of sitting beside it in a
Column, where every background refetch pushed the grid down another 161px. - The ambient backdrop stays in step with the hovered poster, resets on the search screen, and its deferred writes are guarded against races on dispose and
didUpdateWidget. - Movie and show cards compose from a shared
PosterFramethat owns the poster depth contract. - The Chromecast glyph is drawn from the rounded icon family.
Filename parsing
- Standalone anime absolute-episode numbering and
(US)-style parentheses in titles are fixed. Corpus pass rate goes from 75.8% to 76.4%. - Both bugs were found by porting a dead parser's test suite onto the live one.
Removed code
Mydia.Library.FileParser, 589 lines with no production callers. Both call sites reached the live parser throughalias Mydia.Library.ReleaseParser, as: FileParser, so edits to it changed nothing at runtime while a 795-line test suite reported confidence about it.Mydia.Settings.QualityMatcher, its coverage audited againstReleaseRankerfirst.- The WebSocket relay stubs, the orphaned
UserFavoriteschema and the old home screen. - The
min_quality_scoreconfig key. Its only reader wasQualityMatcher, so setting it had no effect. - New
mix mydia.dead_codetask, advisory rather than a CI gate. A compiler tracer records module reference edges so alias renames resolve, and liveness is forward reachability from exempt roots grown to a fixpoint.
CI
- The E2E stack runs the shipped production image.
Dockerfile.e2eis deleted; it was a 351-line near-duplicate of the productionDockerfile. Seeding moved toscripts/e2e/and asserts its own postconditions instead of ending each step in|| echo "Warning: ...". player/Dockerfile.testgoes from 291 lines to 66 and no longer bakes player source, so editingplayer/lib/stops invalidating the image.- Player E2E coverage goes from 1 test file to 3, and 8 executing tests.
p2p_streaming_test.dartpasses 6/6 unmodified, having never run in CI before. - Rolling
:mastertags are published for the E2E toolbox and the relay. flutter testruns at--concurrency=1in CI. At the default it silently drops files while exiting 0.
Upgrade notes
- No migrations in this release.
min_quality_scoreis removed from config. It was read only by a module with no callers, so removing it changes no behavior. A leftover value is ignored.- GraphQL additions are additive.
startStreamingSessiongains an optionalmaxHeight,StreamingSessionResultgainsmaxBitrateandmaxHeight, andrecentlyAddedgains three nullable fields. A player build older than this server keeps working. - A player build newer than the server retries a rejected document once with a frozen legacy version, so a new player against an older server keeps its Recently Added rail rather than losing it to one unknown field.
- Until you update the player app, it still asks for streaming candidates by media id. That previously returned an arbitrary file and now returns the highest-resolution one, so a user who picked a lower resolution gets a consistent wrong answer instead of an intermittent one until the app updates. The web player ships with the server and is unaffected.
- Playback pinned to a deleted file corrects itself on the first play after updating the player. No app data needs clearing.
- The Linux Flatpak channels are not live for this build. Install the Linux tarball asset, which still requires libmpv on the system.
Docker is ghcr.io/getmydia/mydia:0.13.0-beta.4, or :beta. Add -pg for the Postgres image.
Full Changelog: v0.13.0-beta.3...v0.13.0-beta.4