Skip to content

v0.13.0-beta.3

Pre-release
Pre-release

Choose a tag to compare

@arsfeld arsfeld released this 05 Aug 01:00
Immutable release. Only release title and notes can be modified.
8d05858

Third beta for v0.13.0, cut from master. 10 PRs since beta.2.

Most of this came out of watching a production instance for a day. Four of the fixes below were found by reading its logs rather than by anyone reporting them.

Mydia was grabbing camcorder rips

There was no source-quality floor anywhere in the automatic search path. A TELESYNC or a CAM release passed every gate and got downloaded whenever it was the best candidate, which is the normal situation for a film still in cinemas.

Two of the four automatic grabs in one log window were telesyncs, and one of them had already been imported into the library:

Spider Man.Brand.New.Day.2026.1080p.TELESYNC.x264-DKS   score 83.4  grabbed
The Odyssey (2026) 1080p HQ HDTS - x264 ... 3.3GB.mkv   score 75.4  grabbed + imported
Jane.Eyre.1996.1080p.BluRay.x264-VETO[rarbg]            score 62.3

Both outscored the legitimate releases beside them. The profile already declared preferred_sources: ["BluRay", "WEB-DL"], and that declaration was worth about 9 points out of 100, so it never decided anything. There is also no minimum score before grabbing, so a release scoring zero still gets downloaded when it is the only survivor.

Quality profiles now carry an excluded_sources list, with an Exclude tab in the editor. Excluded sources are dropped before ranking rather than penalized. Every existing profile is backfilled with the cam tier, along with the eight shipped defaults and 23 presets. Manual search ignores the list, since clicking download on a specific release is explicit intent.

The source detection was rewritten in the same change, and had to be. The old patterns were unanchored two-letter alternations, so Ghosts.of.Mars classified as Telesync, Watchmen as Telecine, Scream as Screener and Cameron as CAM. Shipping the filter on top of that would have converted a grabbing-garbage bug into a refusing-good-releases bug. There is now one delimiter-anchored vocabulary shared by the indexer parser and the library scorer, pinned by a false-positive corpus.

Cam-tier files already in your library now count as a hard violation and score zero, so the upgrade path will replace them.

Closes #80.

Trackerless magnets

House of the Dragon S03E07 and 22 other downloads sat at exactly zero bytes until the stall detector gave up on them at 180 minutes, then got re-grabbed onto another dead magnet and repeated. The releases were fine. The magnets Mydia handed the download client had no trackers on them at all.

Bitmagnet always emits a bare magnet:?xt=urn:btih:HASH&dn=NAME, and Prowlarr does whenever its /download redirects to a magnet instead of serving a .torrent. Mydia forwarded those verbatim, which leaves the client nothing but DHT to find peers with.

This was invisible while rqbit was the download client, because its DHT reached the swarm. It surfaced the moment a client took over whose DHT does not.

client link type completed
rqbit magnet 26 / 26
transmission magnet 0 / 23
transmission .torrent URL 32 / 48

Magnets now get a public tracker list appended at the one point every magnet-producing path funnels back through. The stored URL keeps the indexer's original string and the info hash is never altered, so history, duplicate detection and client-side dedupe all still match. A magnet that already advertises a tracker passes through untouched.

This fixes new grabs. Anything already sitting stalled at zero bytes in your client needs re-grabbing to recover.

Imports you can fix by hand

An import failed with "No importable files found for TV show". Accurate, and useless. The message reported that nothing was importable while hiding the one fact that resolves it, which is what the download actually contained. Here it was a single 891.9 MB Windows executable, a fake release the filter was right to refuse.

The import code computed the file list, filtered it, found nothing, and then discarded the list. So the UI had nothing to show, and the failure was terminal, so there was nothing to hang a retry on.

Any import failure that reaches the file-listing stage now keeps what it saw: name, size, why each file was skipped, and the parser's guess at season and episode. Files rejected purely on extension get an ffprobe header read, so a fake release says "Not media (invalid data)" and a genuinely obfuscated one names its container and codec and tells you to force the import.

The Downloads Issues tab gets a match-files modal listing all of that, with a per-file target prefilled from the parser. Import any subset. The cramped inline picker it replaces is retired.

There is also a reject action, which blacklists the release, removes it from the download client, deletes the row, and queues a fresh search. Worth knowing why that matters: the import path never called Blacklists.add/5, so the blacklist table was empty in production and the same fake release could be re-grabbed on every subsequent search, indefinitely.

Partially-complete seasons could never be filled

One log line, once an hour, forever:

Found release for: All Creatures Great & Small (2020): All.Creatures...S03...KONTRAST
Download failed for: ... (Download already exists)

Nothing was downloading. Season 3 has 7 episodes, 2 of them had files, and the search correctly picked a season pack for the other 5. The existing-files guard then rejected the grab, because it blocked a pack if any episode in the season had a file. One stray episode file poisoned a season permanently.

A second defect compounded it: two different checks returned the same atom, one meaning "something is in flight" and the other "files are on disk". The search callers read that atom as the former and skip the individual-episode fallback, so the 5 missing episodes were never searched by any route either.

The guard now blocks only when every episode the pack targets is already on disk, and the two reasons are separate atoms. Import skips pack files for episodes that already have a live media file, so relaxing the guard does not trade a stuck download for duplicate rows.

The player always opens the right episode

The show detail hero Play button was dead in production. Two copies of the same query existed and disagreed. The one in queries/tv_show_detail.graphql asked for the files it needed and was imported by nothing, while the inline string that actually executed omitted them. File selection therefore always returned null and the button rendered permanently greyed. Reading the .graphql file led you to conclude the feature worked. Both dead documents are deleted.

Every show-level Play affordance now opens the correct next unplayed episode, labelled with the reason it was chosen, and resumes silently when you are partway through. Up Next and Continue Watching start playback on tap instead of opening a detail page.

Behind that sat a real backend bug. determine_next_episode/2 tested only completion percentage and ignored watched. Marking an episode watched writes position_seconds: 0, which computes to 0%, so a watched episode matched "in progress" forever and Up Next never advanced. That affected the web UI too.

Skip Intro skipped the episode

Pressing Skip Intro on a Bluey episode jumped to 7:00 of a 7:18 episode.

Container chapters partition the whole timeline, so a chapter's end_time is where the next chapter begins, not where the named thing stops. Bluey ships two chapters per episode, Intro and Credits, so the stored intro covered 96% of the runtime at confidence 1.0 and the player seeks to a segment's end.

Measured across a production library, 128 of 402 chapter-derived intros ran over 180 seconds. The fingerprint engine produced 739 and not one exceeded 121 seconds. Chapter-derived intros are now bounded at 180s or 25% of runtime, whichever is tighter. Rejection is per chapter rather than per file, so a release whose first title match is implausible can still resolve on a later one. Jujutsu Kaisen's cold open, labelled "Intro", now loses to the "Opening" chapter behind it.

Credits are deliberately left unbounded. That chapter runs to the next marker or to end of file, and both are where the credits genuinely end. Zero of 389 chapter-derived credits rows were implausible.

Movies remember they were watched

The movie detail screen in the player had no watched controls, and its progress bar was hidden once a movie was watched with nothing put in its place, so a watched movie and one you had never opened rendered identically. There is a watched toggle in the app bar now, and a Watched · Aug 2 badge in the slot the progress bar occupies.

macOS window chrome

The player window is frameless on macOS, and Flutter reports no safe-area inset for the strip AppKit floats the traffic lights in. The only compensation lived in the app shell's desktop branch, and every detail page and both player routes render outside the shell, so the back button on movie, show, episode and collection pages sat directly underneath the close and minimize buttons.

The strip is now reserved once at the app root by injecting it into MediaQuery.padding.top, which SafeArea, Scaffold and AppBar already consume, so the fix reaches every screen without those screens changing. It drops to zero in native fullscreen, where macOS hides the buttons. Linux and Windows are untouched. Three widgets that were leaning on that padding being zero are fixed alongside it, including the player letterboxing every video.

Dev environment

Mostly invisible if you only run Mydia, but it accounts for a chunk of the diff. Entering a devenv shell used to run ecto.create && backup && ecto.migrate, and the backup task booted the whole supervision tree, which died on a fresh database before migrations ever ran. 26 of 58 worktrees on one machine held an empty database that could never be migrated. Shell entry no longer touches the database, ./dev db.setup does that job, and ./dev up -d no longer collides with a second Postgres.

One piece of it does reach production. A database with no applied migrations now reports that it has no schema instead of attempting a backup, so a brand-new instance stops snapshotting an empty file on first boot and a fresh Postgres instance stops warning that no backup was taken.

Upgrade notes

Back up first. If you are coming from v0.12.0 rather than from a beta, read the beta.2 and beta.1 notes below as well, there are migrations down there that delete rows.

  1. Two migrations, both irreversible by design, both operating on derived or default data rather than anything you entered.
  2. Every quality profile that does not already carry excluded_sources gets the cam tier added: CAM, Telesync, Telecine, Screener, Workprint. This changes what your instance grabs. It stays per-profile and editable in the Exclude tab, and a profile where you have already cleared the key is left alone.
  3. Cam-tier files already in your library now count as a hard violation and score zero. If you have automatic quality upgrades enabled, they will be replaced.
  4. Chapter-derived intro segments longer than 180 seconds are deleted and their files return to the detection backlog, the same reset the per-season Re-analyze button performs. On the library this was measured against, that is 128 rows. Credits rows are untouched, and the detector rebuilds what it can on the scheduler's next tick.
  5. Magnets now leave Mydia with trackers attached. Downloads already stalled at zero bytes in your client will not recover on their own, so re-grab them.
  6. GraphQL gains TvShow.nextUp and playable files on ContinueWatchingItem. Both are additive and nextEpisode is unchanged, so a player build older than this server keeps working.

Docker is ghcr.io/getmydia/mydia:0.13.0-beta.3, or :beta. Add -pg for the Postgres image.

Full Changelog: v0.13.0-beta.2...v0.13.0-beta.3


Also in v0.13.0-beta.2

Second beta for v0.13.0, cut from master. 23 PRs since beta.1.

Read the auth section first. Part of what's fixed in it has been broken in every release Mydia has ever shipped.

The GraphQL API was readable without a login

Someone reported that paired players stop working after 30 days. They do. Pairing mints an access token with a 30-day TTL and nothing ever renewed it, because the player's refresh call was a stub and there was no endpoint behind it anyway. Once it expired, every request arrived unauthenticated. Browsing still rendered, playback died with "Authentication required", and re-pairing by hand was the only way out. The player now swaps its pairing token for a fresh access token and retries once, over HTTP and p2p both. HLS is gated on the same token, so playback comes back with it.

Digging into that turned up the bigger problem. Authorization was opt-in per resolver and 19 resolvers never opted in. 14 of those were browse, so anyone who could reach the endpoint could read your entire catalog. Four were downloads, which also let them start transcode jobs.

Every root field now goes through middleware that denies by default, with login and the two refresh mutations allowlisted. The old per-resolver checks sit underneath it. A test walks every root field and breaks the build if a new one turns up ungated.

If your instance only listens on your LAN there wasn't much exposure here. If you've ever port-forwarded it, assume the catalog was readable.

Skip intro and credits

Mydia finds the intro and the credits in TV episodes now, and the player gives you a button.

Detection runs over a whole season at once, which is the only way the audio approach works at all. Files that already carry chapter markers get read straight off them and nothing else runs. Failing that, every episode in the season gets fingerprinted with Chromaprint and correlated, hunting for the stretch of audio they all have in common. blackdetect then drags the credits boundary onto the actual fade.

It waits until enough files in a season are ready before it'll commit to an answer, so a season you're halfway through downloading gets left alone.

Every season shows its status in the admin UI with a re-analyze button that queues on the spot. In the player a skip button turns up when you hit a segment. Auto-skip is its own toggle in settings and it's off by default.

Release notes in the app

Notes ship inside the image now. There's a /changelog page, and after an upgrade a banner lists the versions you haven't read yet. v0.2.0 through v0.12.0 are backfilled so it isn't empty on first boot.

Betas don't bundle a notes file, so that page stops at v0.12.0 until 0.13.0 goes stable.

Casting

Chromecast never worked on macOS, for the same reason it never worked on iOS: no Bonjour declaration, so the picker came up empty. Both declare it now.

Picking a device connects to it, no need to hit play first, and the cast bar owns up to connecting and not-connected states. Casting resumes where you left off and reports real progress. Subtitles survive a seek that restarts the stream. Seeking past the streamed window restarts the session, where it used to stall. On Apple platforms a denied local network permission says so and hands you a button to the right settings pane. It used to just look like an empty device list.

Desktop windows

The player remembers where its window was and how big, and puts it back, even when the monitor it lived on is gone. It sizes itself to the video's aspect when playback starts, then leaves off once you've resized it yourself.

Downloaded playback

Progress on downloaded media gets recorded locally, so resume works offline and syncs back when the server's reachable again. A record only counts as synced once the server confirms it took it.

Docs

docs.mydia.dev is reorganized around Diátaxis. The deployment guide is five how-tos now, the two PostgreSQL guides are one, and the OIDC, Cardigann and env var material stopped being duplicated across three places. mkdocs builds with --strict in CI, so a dead link fails the build.

Fixes

The Issues tab library picker took the tab down for any item that had metadata, and guest request search crashed the moment it had a result to show. Both were structs read with bracket syntax (#282).

Also: a plugin whose new manifest asks for more than you granted it now says so, the admin status page shows upgrades that got stuck, and quality profile preset ids are pinned unique.

Upgrade notes

Back up first. If you're coming from v0.12.0 and not from beta.1, read beta.1's notes below too, there are migrations down there that delete rows.

  1. Two additive migrations: media_segments, plus segment analysis state on media_files.
  2. Segment detection starts on its own after you upgrade. It queues up to 20 seasons every five minutes and works them one at a time, so a big TV library gets chewed through slowly in the background. The Docker image ships chromaprint. If fpcalc isn't on your install it queues nothing and logs at debug, and it'll pick the backlog up whenever you do install it.
  3. GraphQL denies unauthenticated requests to every root field except login and token refresh. Anything you've pointed at the API without credentials stops working.
  4. The database actually gets backed up before migrations now. The code was there and tested, but its only caller was a dev mix task, so the unattended upgrade path, the one nearly all of you use, ran migrations with nothing to restore from. SQLite snapshots with VACUUM INTO. On Postgres it logs the pg_dump command for you, since pg_dump isn't guaranteed to be installed next to the server and a backup you can't trust is worse than none. A failed backup logs an error and boots anyway, because a full disk shouldn't lock you out of your own instance. SKIP_BACKUPS works now, having been documented for ages and read nowhere.

Docker is ghcr.io/getmydia/mydia:0.13.0-beta.2, or :beta. Add -pg for the Postgres image.

Full Changelog: v0.13.0-beta.1...v0.13.0-beta.2


Also in v0.13.0-beta.1

Back from vacation. 52 PRs have landed since v0.12.0, most of them small fixes, plus three things worth calling out properly. Read the upgrade notes at the bottom before you pull this, there's a data-loss fix in there and two migrations that delete rows.

Automatic quality upgrades

The "Allow automatic quality upgrades" checkbox has been sitting in the admin UI for months doing nothing at all. It works now. A daily sweep looks for library files scoring below their profile's cutoff and goes looking for something better.

The budget is counted in indexer searches rather than items, since searches are what gets you banned from a private tracker. Episodes group into season packs where they can. After import the new file gets re-scored on real ffprobe output, and if it turns out worse than what it replaced, it goes to trash and the release gets blacklisted.

upgrade_until_quality is gone. There's a score cutoff and a minimum margin in its place, so a better codec or audio track can now trigger an upgrade on its own.

External downloads

Plenty of people run a torrent client for things besides Mydia. Every one of those torrents used to get a database row, show up in your Queue, count toward the sidebar badge, and sit in Issues waiting to be fixed. Dismissing them didn't help, the next monitor tick put them straight back.

Unmanaged torrents are now read off the client on each scan and never persisted. Anything that parses as a movie or show goes to Issues → Needs Matching. The rest lands in a new External tab. Match from either one and the download gets created at that point and imports normally.

Player

Chromecast and DLNA both work, on desktop and mobile. The old Chromecast path never could have: iOS was missing its Bonjour declaration, so the picker came back empty every time. If the receiver can reach your server it gets a direct URL and playback survives you quitting the app. Over p2p the player bridges the stream across your LAN.

Two caveats I'd rather say up front than have you find: cast sessions don't survive an app restart, and DLNA subtitles are best-effort because sidecar signalling varies too much between TVs to promise anything.

The playback chrome got rebuilt. The scrubber was 3px at 20% opacity with a thumb that only appeared on hover, four different Material icon families had drifted into one control row, and the timecodes sat 1450px apart in opposite corners. It's one component now. Also new: a neutral palette with a gold accent, hold anywhere to drag the window on desktop, and season episodes in a horizontal rail.

Smaller things

Library search across movies, shows, episodes and collections. It was unreachable on desktop before this, the magnifier only rendered in the mobile app bar.

Scheduled library scanning, per library, off by default. Manual-search grabs come back immediately and survive closing the modal. Bulk auto-search from the selection toolbar. Movie franchises on the detail page, TV trailers, and a progress filter on library pages from @aescolastico.

Quality profiles collapsed down to one model and one struct with the dead config stripped out.

Fixes

Multi-resolution quality profiles were grabbing the lowest resolution you'd checked. Tick 720p and 1080p, get 720p. Fixed, though see note 4 below, because it changes what your instance picks.

Most of the rest was downloads. qBittorrent 5.2+, TorBox uploads (thanks @ppoloskov), two bugs found on a live instance where 47 of 101 downloads never imported, provider failure reasons reaching debrid blacklists, rqbit shared-directory contamination. Removing a download client no longer leaves its downloads pointing at a name that doesn't resolve.

On the player: stale data reaching the UI, cast controls you couldn't get to from most screens, a failed startup step leaving a blank window, cold HLS streams restarting from zero. The web UI got infinite scroll, favicon and date sorting fixes.

Upgrade notes

Back up first.

  1. SQLite table rebuilds were firing foreign-key deletes on child tables. One of them shipped in v0.10.0 through v0.12.0 and wiped subtitles, media_hashes and transcode_jobs for anyone coming from a pre-v0.10 install. The fix lives in the migration helper, so every rebuild inherits it. If you're on an old install, this is the release to upgrade on.
  2. The quality-profile migration can't be rolled back. It backfills preferred_resolutions, then drops qualities, metadata_preferences and customizations. down/0 raises on purpose.
  3. Old unmatched download rows get deleted. Nothing referenced them and none had ever been imported. Anything still sitting in a client reappears in the new tabs on the next scan.
  4. Multi-resolution profiles now pick the highest resolution you've checked, where they used to pick the lowest. It's the bug fix from above, but it will change your grabs.
  5. media.scan_interval_hours is gone and every library starts out manual-only, so nothing begins scanning on its own after you upgrade. A leftover env var or YAML key gets ignored rather than rejected, so your instance still boots.
  6. Trashed media moves to a trash directory with a retention window. It used to just get marked in place.