Skip to content

0.4.0

Choose a tag to compare

@jessielw jessielw released this 01 Sep 13:44
· 5 commits to main since this release

Note

This requires a Media Sync for some of the changes to take place. There is also a migration so startup will take a moment longer once as this takes place during this process.

Added

  • Support for multiple media servers
  • Per media server sync. Each media server card in Settings -> Media Servers has its own Sync button and reports its own last-sync time, and the dashboard shows each server's own time labelled main or linked. Syncing the main server runs the full media sync; syncing a linked server refreshes only that server's watch data and supplemental matches, so one linked server no longer waits on, or blocks, another.
  • Support for multiple Seerr instances, added from the Instance selector on the Seerr settings tab the same way Radarr and Sonarr instances are. A request frontend usually sits beside each media server, so an Overseerr and a Jellyseerr can now be configured side by side. Every instance is queried for the same question and the answers are combined: a title counts as requested if any instance holds a request, its request age is that of the newest live request anywhere, and deleting media clears the request and media entry on every instance. Candidate and media views link to each Seerr that has the item.
  • Media libraries now record the server they came from, and with more than one media server configured that server is named beside every library -- in a rule's Library Scope, on candidate and media views, and in the reason a rule gives for matching. Two servers can each hold a library called Movies, and the provider logo alone does not say whose. With a single media server the name is left off, since there is nothing to tell apart.
  • Shows which seasons a rule has matched in the candidates view without having to expand the media.
  • Rule fields Series fully watched and Series watched (%), for series rules. They answer for the whole show what Season fully watched answers for one season, against the same Sonarr episode inventory, so a series can be removed once it has ended and been watched: Sonarr series status = ended AND Series fully watched is true. Season 0 specials are excluded, the percentage is episode-weighted across seasons, and a single regular season Sonarr cannot answer for leaves the whole series unknown rather than judging it on the seasons that did report.
  • Rule field Fully watched by users, available to every scope, listing the people who have each individually finished the target. It takes the usual list operators, so matches all alice, bob is true only once both of them have finished it. This could not be expressed before: Season fully watched reads the media server's aggregate, which unions every viewer's progress together, so it is already true when one person finished the season and another watched a single episode of it -- and Playback users only asks who pressed play. Completion is measured against Sonarr's episode inventory, so a season still carrying unaired episodes cannot read as finished, and a target whose media server cannot report completion stays unknown rather than matching matches none.

Changed

  • Settings -> Media Servers is now a list of the servers you have, instead of a permanent card for every supported type. Pick a type from Server type, choose Add server, fill in the dialog, and the server joins the list once it saves -- so a server is either fully configured or not there at all. Each card shows whether that server is main or linked, whether it is enabled, and when it last synced, and carries its own Set as main, Sync, Edit, and Delete actions. The separate main-server dropdown and the single Save button that covered every server at once are gone: the first server added becomes the main server, and handing that role to another server is confirmed on the card that receives it, since it starts a full resync. Two servers of the same type can no longer be given the same name, which previously overwrote the first server instead of adding a second.
  • Behavior change: Seerr requester IDs are now written instanceId:userId -- for example 7:3 -- in rule conditions and in Settings -> User Signals -> Seerr Requester to Watch User Mapping. A Seerr user ID only identifies a person inside the Seerr that issued it, the same reason provider IDs never bridge across playback providers, so with two Seerrs configured a bare 3 names a different person on each. Existing rules and mappings are migrated automatically onto the Seerr that is configured today, so none of them change what they match. The requester picker writes the new form for you and groups its list by instance. A consequence worth knowing: the same person with an account on two Seerrs counts as two requesters, and neither one's watch progress or request dates count towards the other.
  • If any configured Seerr cannot be read, every Seerr-dependent rule is skipped for that run and the admin notice names the instance, rather than answering from the instances that did respond. A Seerr that is down still holds requests Reclaimerr cannot see, so a partial answer would report "not requested" for those titles -- and for a deletion rule that is the difference between keeping and deleting. Rule previews show the same warning instead of a normal empty result.
  • Deleting a Seerr instance now strips its requesters out of saved rules and requester watch mappings, and disables any rule whose requester condition is left with no values. An empty value list is not "matches nothing": is not any of against it matches everything, which would silently turn a protect rule into a delete rule.
  • Importing a rules file exported before this release now attaches your Seerr to its requester conditions instead of refusing them. Those files name requesters by a bare user ID, which is no longer a valid identity, so every rule using one failed to import. With a single Seerr configured the IDs are qualified onto it, exactly as the automatic migration does for rules already saved. With none or several configured it is still refused -- a bare ID names a different person on each instance and guessing is what the qualified form exists to prevent -- but the message now says so instead of only reporting a validation failure.
  • The Protected page now names the file a per-version protection covers, with its resolution, codec, HDR and Dolby Vision flags, and size -- the same details the requests pages already show. Two protections on the same movie previously both read "Specific version" with nothing to tell them apart, which is what made a leftover entry impossible to identify.
  • Rule import errors are now shown in full rather than counted. "Imported 0 rules with 1 error" left nothing to act on. A rule that imports but names a Seerr instance this install does not have is now called out separately, since its requester conditions match nobody until they are re-picked.

Fixed

  • The same title could appear on the Protected page twice after its file was replaced on disk. A replaced file is a new file as far as the media server is concerned -- it reports a new ID, and the rename-detection fallback deliberately does not cover it, since a re-encode changes the size, codec, and resolution it matches on. The sync deleted the old version row, but the protection pointing at it stayed: SQLite foreign keys are only enforced on request-scoped connections, so the ON DELETE SET NULL that was supposed to clean this up never fired during a background sync. The leftover entry was the worst of both worlds -- it protected nothing, because the cleanup scan skipped a version ID that no longer existed, while still occupying a row and still suppressing whole-movie candidates. Protecting the replacement then added a second entry beside it. A protection scoped to a file is now removed when that file leaves the library, along with its candidate and any request still awaiting a decision, and existing stale entries are cleaned up and de-duplicated on upgrade. Worth knowing: a protected movie file that gets upgraded is no longer protected afterwards, and has to be protected again -- the protection covered that file, and that file is gone.
  • Protecting one version of a movie that was already protected as a whole created a second entry, and so did the reverse. Movie protections compared the version ID for exact equality, so a whole-movie protection and a per-version protection never saw each other, even though the first already covers the second. Movie scopes now overlap the same way series scopes always have: a whole-movie protection blocks a per-version one, while protecting the whole movie is still allowed alongside an existing per-version protection, exactly as a series-wide protection is allowed alongside a season-scoped one. The rule is now shared by every route that creates a protection, rather than each one carrying its own copy.
  • An expired protection still reported "already protected" and refused a new one, and still appeared on the Protected page. It protected nothing -- every rule and cleanup query already ignored it -- so it now neither blocks nor is listed.
  • Two people requesting protection for the same title and both being approved created two protection entries. The pending-request check only looked at the person making the request, and the approval itself checked nothing at all. Approving now reuses the protection that already covers the target and widens it if the approval grants longer -- permanent wins, and an expiry only ever moves later. Approving still succeeds either way, since refusing the second approval would leave a legitimate request stuck as pending with no way to resolve it.
  • Deleting a movie version through a reclaim run left its protections, requests, and candidates behind pointing at the deleted row, for the same foreign-key reason. Every place a version row is removed now clears what was scoped to it first.
  • A Seerr Requester to Watch User Mapping left on Any instance while a specific instance's user was selected matched that user number on every Seerr -- a different person on each. "Any instance" means the named user anywhere, so an unscoped mapping now matches on the username, and falls back to the bare user ID only when it carries no username at all. Choosing Any instance also clears the user selection, since the two cannot both be true.
  • Promoting a different media server to main could silently retarget a rule's library scope instead of reporting it. Library rows were keyed by the library id alone, and Jellyfin and Emby derive that id from the library's path -- so two servers each holding a library at the same path report the same id, and the old row was renamed in place while every rule scoped to it kept matching, now against a different server's library. The stale-library notice stayed quiet because the id still existed. Library rows are now scoped to the server that reported them, so the leftover row is replaced and the notice fires, naming the current main server.
  • Playback conditions reported 0 plays for media watched on a second media server of the same type. A playback event carries a provider item id -- a Plex ratingKey, a Jellyfin or Emby item id -- that is only unique within the server that issued it, but events were resolved by service type alone. With two Plex servers, the main server's version rows shadowed the linked server's supplemental matches, so a play on the linked server was either credited to whichever title happened to share that ratingKey on the main server or dropped entirely -- and a title watched only on the linked server matched a Playback plays is 0 deletion rule while its view count plainly said otherwise. Events now record which server they were observed on and resolve against that server's own ids. Retained history is re-attributed and its aggregates rebuilt on the next playback refresh, so existing installs recover their history without re-importing it.
  • Episode plays reported by a linked server of the main server's own type were never attributed. There is one episode id column per service type, and a linked server may only write it when its type differs from main's -- otherwise it would overwrite the ids media-server deletes rely on -- so a second Plex server's episode ids were recorded nowhere and every episode play it reported resolved against the main server's ids or not at all. Supplemental matches now carry episode ids alongside the movie, series, and season ids they already held, matched on episode number within a season the folder or filename comparison has already paired. Run a sync to populate them.
  • Native watch state from a second Jellyfin or Emby server had the same collision: item snapshots were mapped to media by service type, so two servers of one type resolved each other's item ids. Requester-watch evidence and native playback aggregates now resolve each server's ids against that server's own matches.
  • The Tracearr server picker listed every Tracearr server once per configured media server of that type, so two Plex servers made each Tracearr Plex server appear twice.
  • Tracearr discovery confirmed the main server's Tracearr server for every media server of main's type. Only the main server writes the version rows the probe compares against, so a second Plex server inherited a recommendation that was never about it -- and, because a Tracearr server can only be bound once, the pair could not be saved. A Tracearr server already bound to another media server now moves rather than duplicating, and saved bindings are listed by media server name instead of by type.
  • Every media server reported the last completed Sync Media run as its own last sync, so two servers of the same type always showed the same time.
  • Promoting a second media server of the same type to main left the previous main flagged as main as well. Only servers of a different type were demoted, so two Plex servers could both carry the flag -- and since every lookup of "the main server" expects a single row, the next media sync and the next save from the media server settings both failed with MultipleResultsFound. Promotion now demotes every other media server by identity rather than by type.
  • A busy database during the session last_seen_at write rolled back the request's own database session, which expired the signed-in user and made the next permission check fail with greenlet_spawn has not been called. Under a running media sync this took out /api/info/ui-indicators repeatedly. The write now runs in its own session.

What's Changed

Full Changelog: 0.3.7...0.4.0