github getmydia/mydia v0.14.0-beta.5

pre-release4 hours ago

35 PRs since v0.14.0-beta.4, and most of them come from operators running beta.4's import rewrite against real libraries. What they found: re-scan physically moving files that were still on disk, duplicate media_files rows nothing had ever forbidden, monitoring switching itself back on, and the download matcher attaching season packs to titles that shared a first word and nothing else. Those are fixed. 0.14.0 holds in beta until the import path runs clean in the field for a while. Seven migrations, one irreversible.

Data loss and correctness

  • "Re-scan files" could delete your media. It decided a file was gone by diffing path strings against one inferred directory, then handed every unmatched row to the trash store, which found the file present and moved the bytes out of the library. The toast reported a clean sweep while it happened. Three separate conditions produced that false verdict, and a shared on-disk guard covers all three paths; a directory that will not resolve returns a failed scan rather than trashing every file for the item. Underneath it sat a second defect: two media_files rows could describe one physical file, which is the extra row re-scan was reading as missing. A unique index on library path plus relative path closes that, and a migration folds existing duplicates onto one winner, carrying dependents across where the winner does not already hold the key. An import racing another writer onto the same path adopts the existing file rather than failing, and the file details modal stops raising on every analyzed file (#658)
  • Re-enrichment had been overwriting monitored. One attrs map served both the create and the update path with monitored: true hardcoded, so any scan promoting a file for an item untouched for more than an hour reset the operator's choice. It stayed invisible because the change tracker recorded title, original title and year, nothing else. Monitored changes land in item history and the activity feed (#659)
  • Jaro-Winkler scored The Super Mario Galaxy Movie against a movie called Supergirl at 0.897. On a live instance, five season packs of one show attached to an unrelated title at 0.886, in a library that held no copy of the real show. The matcher rejects candidates whose words the release title does not cover, and the binding tiebreak stays out of the stored confidence. For the ones it still gets wrong, a file can be sent back to Review from its details in one transaction and with an activity event, and those details show where the file came from (#669)
  • A season pack could finish with its last episode marked missing while the content sat on disk, inside a combined S01E09E10 file. media_files.episode_id is a single foreign key, so the matcher kept the first episode number and dropped the rest. A join table carries the links, and a migration re-reads existing filenames to add back what was discarded (#679)

Import

  • Accept and re-match are durable background work again. An earlier refactor deleted both Oban workers and replaced neither. Since then the review page promoted candidates synchronously inside the LiveView process and re-matched in a task that died with the page, so "Import all" over a fresh library meant one sequential relay round trip per group inside a single handle_event, no retries, nothing surviving a closed tab. The intent lives on the candidate row, and a worker keyed on the library path finds its own work. Queued imports and their failures appear on the review page (#647)
  • ENABLE_IMPORT_LISTS gated a nav link and nothing else. The route, the 15-minute cron and the sync jobs all ran whatever it was set to. An audit turned up eight bugs behind it: a failing list stayed permanently due and re-enqueued every 15 minutes, roughly 96 relay hits a day; an Upcoming plus TV Shows list quietly filled with movies stored as TV shows; a custom URL was fetched with no scheme or destination check. The flag gates the feature for real, and with that fixed the default flips to on. Requests are limited to http and https against public addresses, validated on the resolved IP with every redirect hop rechecked, and IMPORT_LISTS_ALLOW_PRIVATE_DESTINATIONS=true opts a NAS or container network back in. Sync failures back off exponentially, capped at 24 hours (#665)

Trash and duplicates

  • Trash was fully built on the backend and completely invisible. Files moved to .mydia-trash and were purged after 30 days, with no way to see what was in there, restore something, or reclaim the space early. /admin/config/trash lists it newest first, with filter chips, per-row restore and permanent delete, and bulk actions running through an Oban worker so they survive a closed tab. media_files gains a trashed_reason stamped at all seven call sites. Three of those sit in the re-scan reconciliation paths and read nothing like producers, so missing them would have labelled every file an operator's own re-scan trashed as "Unknown". Retention moved into the layered config, and manual file deletion routes through the trash by default (#666)
  • On the duplicates page, every per-group control only changed what was marked. The one button that actually moved files ran across the whole library, so clearing a single item meant marking every other group Keep first. Each group carries a marking toggle and a button that trashes its marked files immediately, with an Undo toast holding the run's file ids. The library-wide button keeps its confirmation modal (#664)

Discover

  • Discover is the one place to find and add media, and the separate Add and Request pages are deleted. Two bugs surfaced while consolidating. Adding from Discover never queued an indexer search, and media.auto_search_on_add was declared, documented, loadable from the environment, and read by nothing in lib/. Mydia.Media.AddDefaults resolves library, quality profile, monitoring, season monitoring and search-on-add through an explicit choice, then the user's preference, then the instance setting, then a fallback. Every add path goes through it. Users can override the defaults in their profile (#649)
  • Adding a title took two dialogs, and the second was reachable only from Discover, leaving the dashboard and the media detail page with a library choice and nothing else. A single-library install never saw the caret at all. The picker is merged into Configure Before Adding. Two authorization gaps found along the way are closed, and the submit preamble the three hosts shared is extracted (#676, #678)
  • A production add failed with "Media not found: 280619". The relay routes every TV search to /tvdb/search, but the card shipped the resulting id in a param named tmdb_id. The 404 was the lucky outcome: a TVDB id that happens to exist on TMDB would have added the wrong show. TVDB-sourced results go to TVDB on add and preview (#672)
  • Clicking a poster in the detail page's recommendations rail or franchise strip opens the same trailer and detail dialog Discover uses. Those were the last two surfaces where a poster sat next to Add to Library and did nothing when clicked. The dialog carries its own recommendations rail, so you can hop between titles without closing it (#674)
  • The Add to Library button stops wrapping to two lines and spilling out of its pill (#673)

Collections and sections

  • Any smart collection can be pinned to the sidebar as a section. Sections render through the media index rather than the collection page, so they get the TV Shows toolbar: filters, sorting, selection, batch actions. Mark one exclusive and it claims its categories away from Movies and TV, which is what answers the long-standing request for an Anime destination without handing every instance a fixed Anime section. Pins are per user. Exclusivity is derived from the rules on every read, never stored (#644)
  • Four sidebar-section defects from a running instance: an undismissable excluded-items banner on every Movies and TV visit, an anime nudge that kept asking after you accepted it, icons that never emitted CSS, and a re-pin that wiped a customized icon (#654)
  • Preset collections. Sixteen common smart collections grouped into decades, genres, highlights, series and language, each showing a live count of matching items in your library, added in one click. Smart rules could already express most of this. Almost nobody knew, because the only way in was an empty condition builder. Nothing is auto-seeded, and adding produces an ordinary independent collection. The rules themselves gain a within_last operator for relative dates, and not_in works for language and status (#667)
  • Editing a smart collection showed Select... instead of the value the rule stores. Two causes, both reproduced against live data: multi-value operators joined their stored list into one string before comparing it against one option at a time, and the option list came from a library scan that omits any stored value the library no longer matches (#652)

Streaming

  • Far scrubs used to tear down the whole playback stack and rebuild it behind a full-screen spinner, so after resuming a movie at 45 minutes every backward seek restarted it. The playlist is computed up front from the media duration and served as a complete VOD playlist, with FFmpeg as a movable window underneath addressed by absolute segment index. A segment request resolves to one of three things: serve a file already on disk, park the caller until the encoder reaches it, or relocate the encoder. Requests are serialised through the session, so the several parallel requests a player fires on one seek trigger exactly one relocation (#638)
  • Every windowed HLS session crashed about 60ms in. The transcoder's segment callback was wired for both playlist modes, and a window session has no transcode window to mark ready, so the first report killed it. Window mode is the default for any client that does not ask for a full playlist, and the fallback whenever duration is unknown (#639)

Indexers

  • A monitored show searched, ranked 100 results, selected a season pack, and never downloaded it. No download row, no error, nothing in the application log. The download URL was fetched with redirects enabled, and the redirect pointed at a magnet: scheme Finch rejects. The magnet is returned rather than handed to Req, raised HTTP errors are contained at the Cardigann boundary, and Oban job failures are logged with their reason (#675)

Web UI

  • Poster cards clamp their title to two lines and reserved no height for it, so everything below sat at a different offset on each card and grid rows came out ragged. Every poster surface moved onto one shared card body (#657)
  • Poster grids hold their shape while they load. Discover sets its loading flag on every filter, genre, year, rating, sort and media-type change, so a grid already on screen at full height collapsed to a spinner and re-expanded each time (#662)
  • In dark mode the view-mode and density groups rendered as loose glyphs beside one blue chip. btn-ghost computes to a fully transparent background, which paints the page exactly, so an unselected segment measured 1.000 contrast against it. Theme, date and media type moved onto the same control (#668)
  • The media detail page's backdrop sat in two absolute layers inside the wrapper holding the entire page grid. A 1920x1080 image was upscaled about 3x in a box up to 4000px tall, the fade never landed, and the bottom edge cut off wherever the content happened to end. It is anchored to the viewport (#655)
  • The mobile dock sits above the sidebar in the stacking order and covered the user avatar row completely, putting Settings and Logout out of reach. It fades out while the drawer is open and drops out of the hit test and the tab order (#656)
  • The player CTA banner on the dashboard can be dismissed, and a profile setting removes the player's entry points from the web UI. The two preferences are independent: dismissing the banner never rearranges navigation (#650)

Player

  • Two players on the same account could not see each other, with nothing in the picker and no error. Discovery is server-mediated through a posted iroh node id, and a device without one is filtered out of every roster. iOS had never registered once. Registration is reconciled from watched inputs rather than fired once, revoked devices stay out of the roster, and settings shows the registration state (#648)
  • The home loading skeleton was visibly smaller than the content it stood in for. Every dimension disagreed with the real rail, so posters arrived wider than their placeholders and the rail resized as data landed. Rail geometry comes from one source (#661)
  • The calendar renders one cast button instead of two (#645)

CI and packaging

  • The NixOS module in beta.4 cannot boot. mime reads its types in its own module body, and the Nix build compiles each hex dependency in isolation, where config/config.exs is never seen, so it compiled against the default while the release's sys.config carried the subtitle types added in beta.4. The boot aborts on the mismatch. Mydia no longer configures mime, the release is booted once during the package build so this class of failure fails the build instead of the install, and every Alpine base running the BEAM is pinned (#640)
  • BUILD_COMMIT sat above mix deps.get in the Dockerfile, and three workflows pass a value that changes every commit, so both dependency layers had been uncacheable in all three since the line went in (#646)
  • The player E2E suite runs on pull requests again. Every BuildKit cache flag in that job had always been a silent no-op: the runtime token GitHub injects into actions never reaches a raw run: step (#643)
  • A promotion ownership test bound its two roles out of its own mailbox and then drove them through the Task structs, which is a coin flip written into the test rather than a flake (#651)
  • A migration added earlier in this cycle referenced a table the import split had already dropped, putting the production instance into a boot crash loop. Caught and fixed before release (#663)

Upgrade notes

  1. Seven migrations, one irreversible. The duplicate fold (#658) deletes the losing media_files rows rather than trashing them: a soft-trashed row with no trash metadata reads as legacy and would schedule your real files for deletion 30 days out. Rolling back lifts the unique index and nothing else. The folded rows are gone. Take a backup.
  2. Import lists default to on. ENABLE_IMPORT_LISTS used to gate a nav link while the cron and the sync jobs ran regardless. It gates the whole feature now, and a missing key defaults to on. An explicit false still wins. Lists you configured with auto-add will start being honoured, and auto-added items are monitored, which hands them to the search workers (#665).
  3. /add and /request are gone. Both are Discover now, with no redirect, so old bookmarks 404. The dashboard and sidebar links already point at the new location (#649, #676).
  4. Manual file deletion routes through the trash. Deleting a file from the UI moves it to .mydia-trash and holds it for TRASH_RETENTION_DAYS, 30 by default, instead of removing it immediately. Files trashed before this release carry no reason and show as Unknown. There is no backfill, because the information to reconstruct them does not exist (#666).
  5. A season pack may gain episodes on first boot. The relink migration re-reads existing filenames and links combined files such as S01E09E10 to every episode they contain, so episodes that read as missing can flip to present with nothing downloaded (#679).
  6. Check your monitored flags once. Re-enrichment had been resetting monitored to true since long before this release, so an item you deliberately unmonitored may have been switched back on. That stops here and future changes appear in item history, but nothing restores a flag already overwritten (#659).

Don't miss a new mydia release

NewReleases is sending notifications on new releases.