github getmydia/mydia v0.13.0

5 hours ago

130 PRs since v0.12.0. Read the upgrade notes at the bottom before pulling this: there is a data-loss fix, an authentication change that closes the GraphQL API, and migrations that delete rows.

Server

Intro and credits detection

Mydia detects intro and credits spans in TV episodes. The skip button and its behavior are under Player.

  • Detection runs over a whole season at once. Files carrying chapter markers are read straight off them; otherwise every episode in the season is fingerprinted with Chromaprint and correlated for the audio span they share, and blackdetect snaps the credits boundary to the fade (#307, #310, #311, #312, #315).
  • New media_segments table and per-file analysis state (#308, #309), exposed over GraphQL (#314), scheduled per season in batches of 20 every five minutes (#316).
  • A season is left alone until enough of its files are present, so a season still downloading is not analyzed (#315).
  • Per-season status with a Re-analyze button in the admin UI (#317).
  • Chapter-derived intros are bounded at 180s or 25% of runtime, whichever is tighter, rejected per chapter rather than per file. Container chapters partition the whole timeline, so a two-chapter release could produce an "intro" covering 96% of the episode. Credits are deliberately left unbounded (#329).
  • End-to-end coverage against real fingerprint detection (#319).

Automatic quality upgrades

The "Allow automatic quality upgrades" setting is implemented (#296). It previously had no effect.

  • A daily sweep finds library files scoring below their profile cutoff and searches for something better. Budget is counted in indexer searches rather than items. Episodes group into season packs where possible.
  • An imported upgrade is re-scored on ffprobe output. If it is worse than what it replaced, it goes to trash and the release is blacklisted.
  • upgrade_until_quality is replaced by a score cutoff plus a minimum margin, so a better codec or audio track can trigger an upgrade on its own.

Quality profiles and release selection

  • One model, one struct, dead config removed, with parity between what the UI shows and what ranking does (#223).
  • Multi-resolution profiles pick the highest resolution checked. They previously picked the lowest, so a profile with 720p and 1080p ticked grabbed 720p (#223).
  • Per-profile excluded_sources with an Exclude tab in the editor. Excluded sources are dropped before ranking rather than penalized. There was no source-quality floor in the automatic search path at all, so CAM and TELESYNC releases were grabbed and imported whenever they outscored the alternatives (#335, closes issue #80).
  • Source detection rewritten as one delimiter-anchored vocabulary shared by the indexer parser and the library scorer, pinned by a false-positive corpus. The old unanchored two-letter alternations classified Ghosts.of.Mars as Telesync, Watchmen as Telecine, Scream as Screener and Cameron as CAM (#335).
  • Cam-tier files already in the library count as a hard violation and score zero, so the upgrade path replaces them (#335).
  • Items with no profile fall back to the default profile (#266).

Downloads

  • External torrents are separated from Mydia's queue. Unmanaged torrents are read off the client on each scan and never persisted; anything parseable goes to Issues → Needs Matching, the rest to a new External tab. Matching from either creates the download at that point (#268).
  • Magnets that arrive without trackers get a public tracker list appended at the single point every magnet path funnels through. Bitmagnet always emits a bare magnet, and Prowlarr does whenever its /download redirects to one; those downloads sat at zero bytes until the stall detector gave up. The stored URL keeps the indexer's original string and the info hash is unchanged (#331).
  • A torrent whose file list contains no video is rejected as soon as its metadata is known, before the payload downloads, through the same path as the manual Reject button: blacklist, remove from client, delete the row, queue a replacement search. Transmission's adapter never requested the files field, so its downloads had no file list at all (#357).
  • Import failures keep the file listing they saw: name, size, why each file was skipped, and the parser's guess. Files rejected on extension get an ffprobe header read. A match-files modal imports any subset, and a reject action blacklists and re-searches. The import path never called Blacklists.add/5, so the blacklist stayed empty and a fake release could be re-grabbed indefinitely (#336).
  • Season packs can fill partially-complete seasons. The existing-files guard blocked a pack if any episode in the season had a file, and two different checks returned the same atom, so the individual-episode fallback was skipped too (#334).
  • qBittorrent 5.2+ support: 204 login, renamed session cookie, plus API-key auth (#234).
  • TorBox upload handling, with the same Req multipart fix applied to Premiumize and AllDebrid (#221).
  • rqbit shared-directory series contamination (#230).
  • Real per-file error detail in partial import failures (#231), and provider-native failure detail carried into debrid blacklist reasons (#247).
  • Two production failure bugs where downloads never imported (#235).
  • Removing a download client adopts or honestly orphans its downloads instead of leaving them pointing at a name that does not resolve (#265).
  • A titleless release no longer crashes DownloadMonitor (#330).
  • Manual-search grabs return immediately and survive closing the modal (#229).
  • Bulk auto search from the selection toolbar (#244).

Streaming and transcoding

  • startStreamingSession accepts maxHeight alongside maxBitrate, and StreamingSessionResult echoes what the server actually applied, which can be lower than requested on a relay connection (#349).
  • Transcodes preserve aspect ratio and never upscale. Forced transcodes previously emitted -s 1280x720 with 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 (#349).
  • 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 (#349).
  • New MAX_TRANSCODE_HEIGHT operator ceiling, settable by env, config.yml, the DB overlay and the admin UI (#349).
  • New Mydia.Library.FileRanking ranks by resolution, then bitrate, then id, with unanalyzed files last, wired into Streaming.Candidates, StreamController and the subtitle controller. The subtitle controller previously took the head of an unordered preload (#346).
  • StreamController and the subtitle controller filter trashed files. Only Candidates did, so the candidates endpoint and the stream route could name different files (#346).
  • New contentRating, trailerUrl, cast and similar fields on Movie and TvShow. contentRating previously always resolved to nil; it now parses TMDB release_dates and content_ratings with a US, GB, first-available fallback. All four ride the existing append_to_response mechanism, so metadata-relay needed no changes (#361).

Library and metadata

  • Recently added means when content arrived, not when the record was created. Files group into slots, one per episode or movie; an item's timestamp is the newest of its slots, so a quality upgrade cannot bump an item back to the top. Computed at query time, no migration (#350).
  • recentlyAdded gains newEpisodeCount, latestSeasonNumber and latestEpisodeNumber. favorites, unwatched, collectionItems and the /tv and /movies "Added (Newest)" sorts report the corrected value (#350).
  • Opt-in scheduled library scanning, per library, off by default (#264).
  • Movie franchise section on the detail page (#248), backed by a new TMDB collections endpoint on metadata-relay (#250, #251).
  • TV show trailers on discover, dashboard and library detail (#257), with hardened and memoized TVDB trailer resolution.
  • Progress filter on library pages (#224).
  • Metadata refresh consolidated into one job (#261), and a shared ffmpeg runner extracted from four generators (#305).
  • Filename parsing: standalone anime absolute-episode numbering and (US)-style parentheses in titles. Corpus pass rate goes from 75.8% to 76.4% (#339).
  • Web UI: infinite scroll loaded only the first page (#217); library grid dates sorted by term order rather than value (#238); browser favicon restored (#225).

Security and data safety

  • Every GraphQL root field goes through deny-by-default middleware, with login and the two refresh mutations allowlisted. Authorization was opt-in per resolver and 19 resolvers never opted in; 14 of those were browse, so anyone reaching the endpoint could read the whole catalog, and four were downloads, which also allowed starting transcode jobs. A test walks every root field and fails the build on a new ungated one (#301).
  • Paired players renew their access token. Pairing minted a 30-day token that nothing renewed, because the player's refresh call was a stub with no endpoint behind it, so every request after expiry arrived unauthenticated. The player now exchanges its pairing token and retries once, over HTTP and p2p. HLS is gated on the same token (#301).
  • SQLite table rebuilds no longer fire foreign-key deletes on child tables. One rebuild shipped in v0.10.0 through v0.12.0 wiped subtitles, media_hashes and transcode_jobs on installs predating v0.10.0. The fix lives in the migration helper, so every rebuild inherits it (#292).
  • The database is backed up before migrations on the unattended upgrade path. The code existed and was tested, but its only caller was a dev mix task. SQLite snapshots with VACUUM INTO; Postgres logs the pg_dump command. A failed backup logs an error and boots anyway. SKIP_BACKUPS is now read (#302).
  • Metadata provider API keys are no longer dropped on query auth (#255).
  • Metadata structs are no longer read with Access syntax, which took down the Issues tab library picker for any item with metadata and crashed guest request search on its first result (#261, #313, closes issue #282).

Release notes in the app

  • Notes ship inside the image. /changelog renders them, and after an upgrade a banner lists the versions not yet read. v0.2.0 through v0.12.0 are backfilled (#323).

Admin and activity

  • 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. Event.changeset/2 validates :type against the registry, and filter chips are added for the playback and plugin categories (#337).
  • Every typed setting in Admin → Configuration raised a FunctionClauseError on blur, so the DB overlay worked for toggles but not for anything typed. A setting whose value has not changed no longer writes a config row (#349).
  • 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 and flashed success regardless (#339).
  • /admin/config no longer 500s when a streaming user has no username (#276).
  • The remaining config pages route through admin_page (#298).
  • A plugin whose new manifest asks for more than was granted says so, and the admin status page shows upgrades that got stuck.

Docs

  • docs.mydia.dev is restructured around Diátaxis. The deployment guide is five how-tos, the two PostgreSQL guides are one, and the OIDC, Cardigann and env var material is no longer duplicated across three places. mkdocs builds with --strict in CI (#300).
  • Planning artifacts are kept out of the published site (#274).
  • Stale metadata-relay.fly.dev references removed; relay.mydia.dev is the default (#364).

Player

Detail pages and chrome

  • Infuse-style movie and TV show detail pages: backdrop hero with a large Play button, action row, tag row, overview, cast rail and similar-titles rail. TV shows drive the hero from a selected episode defaulting to next-up, changeable by season and episode selection (#365).
  • Playback chrome rebuilt into one component, replacing a 3px scrubber at 20% opacity, four Material icon families in one control row and timecodes 1450px apart (#263).
  • Neutral palette with a single gold accent (#273).
  • Season episodes render as a horizontal rail (#219).
  • Watched toggle and a Watched · Aug 2 badge on the movie detail screen, where a watched movie and an unopened one rendered identically (#332).
  • Every show-level Play affordance opens the correct next unplayed episode and resumes silently when partway through. Two copies of the same query disagreed, so the hero Play button rendered permanently greyed. The backing determine_next_episode/2 ignored watched, which affected the web UI too (#333).
  • Compact per-season rows in series downloads, with season grouping in the queue (#356).
  • Poster cards that only navigate no longer show a hover play button (#344). Search posters use the library depth treatment (#347). The 128px dead space above library grids is gone (#345).
  • Library search across movies, shows, episodes and collections. It was unreachable on desktop, the magnifier rendering only in the mobile app bar (#232).
  • Fresh data reaches the UI, and the UI says when it has not (#236).
  • A failed startup step no longer leaves a blank window (#262).
  • Web build branding and a reproducible icon set (#293).

Skip intro and credits

  • Skip Intro and Skip Credits buttons appear when playback reaches a detected segment. Auto-skip is a separate setting, off by default (#318).
  • Skip works while casting. Both were bound to the local player, which the cast path never builds, so the button was gated off and the seek returned early. Local and cast playback share one auto-skip rule (#352).
  • Up Next fires on a detected credits segment instead of a flat 90% of runtime, which landed 7 minutes early on a 70-minute episode. Without a detected segment the fallback is a fixed 60-second window before the end. Up Next no longer offers to auto-play into a next episode during offline playback unless that episode is itself downloaded (#358).
  • Limits: auto-skip fires only while the app is foregrounded, since every cast skip is the phone issuing a seek, and skip is available on the player screen only, not the mini controller.

Casting

Chromecast and DLNA on desktop and mobile (#233).

  • iOS and macOS declare Bonjour. Without it the device picker returned empty on both, which the previous Chromecast path could not have worked around (#233, #239).
  • Selecting a device connects to it, and the cast bar reports connecting and not-connected states (#321).
  • Casting resumes from the saved position, reports real progress, keeps subtitles across a seek that restarts the stream, and restarts the session when you seek past the streamed window instead of stalling (#304).
  • The cast bar mounts in an Overlay with its own Navigator, so controls are reachable from any screen (#239, #270).
  • A denied Apple local-network permission is named as such, with a button to the right settings pane. It previously looked like an empty device list (#306).
  • The cast button is back on the playback screen. ChromeTopBar has a castAction slot that the player screen never filled (#351).
  • Limits: cast sessions do not survive an app restart, and DLNA sidecar subtitles are best-effort.

Playback quality and file selection

  • The quality switcher changes the stream on every platform. It previously rendered only on web, where it set a state field and showed a snackbar (#349).
  • The rung ladder derives from source height, so a 720p file offers nothing above 720p. The choice persists and restarts the session at the current position, and vetoes direct play. Original still direct-plays (#349).
  • The playback transport cluster went from 256px to 192px and secondary buttons from 40px to 32px, so four discrete buttons fit down to 360px width (#349).
  • 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 (#346).
  • The streaming candidates query no longer reads a cache-first on-disk store, so replacing a file no longer breaks playback of that item permanently. A cached entry kept naming a deleted file across restarts, rendering a black screen with an advancing timeline and no error. This affected every automatic quality upgrade, which writes a new file row and deletes the old one (#343).
  • The player subscribes to playback stream errors, so a failure after open reaches the error UI instead of a black screen, gated on playback never having advanced (#343).
  • Audio tracks are re-detected as mpv finishes probing. The list was sampled once, 500ms after open (#341).
  • Progress on downloaded media is recorded locally, so resume works offline and syncs back when the server is reachable. A record counts as synced only once the server confirms it (#304).

Desktop windows

  • Window geometry is remembered and restored, including when the monitor it lived on is gone, and sizes to the video's aspect on playback until you resize it yourself (#320).
  • Hold anywhere to drag the window; chrome hides on mouse exit (#294).
  • The macOS traffic-light strip is reserved once at the app root by injecting it into MediaQuery.padding.top, so the back button no longer sits under the window buttons on detail and player routes. It drops to zero in native fullscreen (#328).
  • The macOS traffic lights hide with the playback controls (#359).
  • Always-on-top toggle with a T shortcut, hidden below ~900px width where a fifth button overflows the control panel (#360).
  • The Updates settings section is hidden on iOS, where its actions were no-ops (#340).

Linux Flatpak

The Linux player ships as a Flatpak on two OSTree channels: mydia for stable and mydia-beta for prereleases, both dev.mydia.player, distinguished by OSTree branch (#342).

flatpak remote-add --if-not-exists --from mydia https://flatpak.mydia.dev/mydia.flatpakrepo
flatpak install mydia dev.mydia.player
  • Built on org.gnome.Platform 50 with libass, libplacebo and mpv as manifest modules, so libmpv ships inside the app. The tarball links the system libmpv and dies at load time without it. Desktop entry and AppStream metadata are included, so the player appears in GNOME Software and KDE Discover (#342).
  • Signing runs in a separate job from building, keeping the GPG key out of the job that runs build code from the dependency tree (#342).
  • The signing public key is committed and embedded in both .flatpakrepo files, and rclone uploads against a bucket-scoped R2 token, which cannot call ListBuckets or CreateBucket (#353).
  • The staged OSTree skeleton is recreated with mkdir -p after the artifact round trip, which does not preserve empty directories (#354).
  • Flathub is added before the post-publish verification, so org.gnome.Platform//50 resolves there (#355).
  • The mydia-player-linux-*.tar.gz asset stays.

Technical

  • Release workflow pins a release to a commit SHA and refuses to publish partial builds (#299).
  • Dead code removal: Mydia.Library.FileParser (589 lines, no production callers, reached through an alias to the live parser), Mydia.Settings.QualityMatcher, the WebSocket relay stubs, the orphaned UserFavorite schema, the old home screen, and the min_quality_score config key whose only reader was QualityMatcher. New advisory mix mydia.dead_code task (#339).
  • The E2E stack runs the shipped production image. Dockerfile.e2e is deleted; it was a 351-line near-duplicate of the production Dockerfile. player/Dockerfile.test goes from 291 lines to 66. Player E2E coverage goes from 1 test file to 3 (#338).
  • flutter test runs at --concurrency=1 in CI, where the default silently drops files while exiting 0 (#338).
  • player/.fvmrc is the single source for the Flutter version (#260), and rust-toolchain.toml the single place naming a Rust version (#267).
  • NixOS module tests decoupled from the package build job (#363). Nix Tailwind binary hashes corrected, and stale ones no longer pass (#256).
  • macOS player artifacts packaged with ditto to preserve symlinks (#291).
  • A DownloadMonitorTest flake traced to a global adapter registry leak from two search test files (#362), and three tests that depended on real host state (#249, #226).
  • Credo runs in precommit (#253). Generated GraphQL Dart output is no longer tracked (#254). Dependency sweep (#245).
  • metadata-relay and iroh-relay metrics are scraped (#290).
  • Search, quality-profile skip and download-failure observability (#258).

Upgrade notes

Back up first.

  1. SQLite installs predating v0.10.0: a table rebuild shipped in v0.10.0 through v0.12.0 fired foreign-key deletes that wiped subtitles, media_hashes and transcode_jobs. Fixed in #292. This is the release to upgrade on.
  2. GraphQL denies unauthenticated requests to every root field except login and token refresh. Anything pointed at the API without credentials stops working. If your instance has ever been port-forwarded, assume the catalog was readable before this.
  3. The quality-profile migration cannot be rolled back. It backfills preferred_resolutions, then drops qualities, metadata_preferences and customizations. down/0 raises on purpose.
  4. Multi-resolution profiles now pick the highest resolution checked, where they picked the lowest. This changes what your instance grabs.
  5. Every quality profile without excluded_sources is backfilled with the cam tier: CAM, Telesync, Telecine, Screener, Workprint. This also changes what your instance grabs. It stays per-profile and editable in the Exclude tab, and a profile where the key was already cleared is left alone. Cam-tier files already in your library score zero, so automatic quality upgrades will replace them.
  6. Old unmatched download rows are deleted. Nothing referenced them and none had been imported. Anything still in a client reappears in the new tabs on the next scan.
  7. Downloads already stalled at zero bytes will not recover from the magnet tracker fix on their own. Re-grab them.
  8. Segment detection starts on its own after upgrade. It queues up to 20 seasons every five minutes and works them one at a time. The Docker image ships chromaprint; without fpcalc it queues nothing, logs at debug, and picks up the backlog once installed. Two additive migrations: media_segments, plus analysis state on media_files.
  9. Chapter-derived intro segments longer than 180 seconds are deleted and their files return to the detection backlog. Credits rows are untouched.
  10. media.scan_interval_hours is gone and every library starts manual-only, so nothing begins scanning on its own. A leftover env var or YAML key is ignored rather than rejected.
  11. Trashed media moves to a trash directory with a retention window, instead of being marked in place.
  12. min_quality_score is removed from config. Its only reader had no callers, so removing it changes no behavior. A leftover value is ignored.
  13. GraphQL additions are additive: TvShow.nextUp, playable files on ContinueWatchingItem, maxHeight on startStreamingSession, maxBitrate and maxHeight on StreamingSessionResult, three nullable fields on recentlyAdded, and contentRating, trailerUrl, cast and similar on Movie and TvShow. An older player build against this server keeps working, and a newer player against an older server retries a rejected document once with a frozen legacy version.
  14. Until the player app updates, it still asks for streaming candidates by media id. That previously returned an arbitrary file and now returns the highest-resolution one. The web player ships with the server and is unaffected.
  15. TV shows matched via TVDB rather than TMDB do not get contentRating or similar populated. The TVDB to TMDB shape transform does not carry those fields yet.

Full Changelog: v0.12.0...v0.13.0

Don't miss a new mydia release

NewReleases is sending notifications on new releases.