The Plex fix was one instance of a pattern every backend shares: build a
dict of remote users, then delete each local user whose key is absent from
it. A successful-but-empty fetch made that dict empty and deleted everyone,
and the sync runs automatically when the Users page loads.
Lift the decision into MediaClient._skip_prune_on_empty_remote and call it
from plex, jellyfin (emby by inheritance), audiobookshelf, komga, kavita,
romm, navidrome and drop. Plex now reuses its known_users list for the
prune loop rather than re-querying. Each backend gets a regression test
driving its real list_users with an empty remote set.
list_users() prunes local users that are missing from the remote set. It
guards against the request raising, but not against a successful response
that yields nothing, and it treats that as "the admin unshared everyone".
The sync isn't something you opt into. admin/users.html has a hidden
hx-get with hx-trigger="load delay:2s", and the server filter defaults to
empty, so simply opening the Users page syncs every configured server.
There are two ways the set goes empty without an exception. plex.tv can
return a successful response with no accounts, and
_filter_users_for_server can match nothing if the machineIdentifier
doesn't line up, which it does even when plex.tv returned a full list.
Reproduced against a copy of my own install: five local users, plex.tv
returning five accounts, and filtering on a machineIdentifier that
doesn't match yields zero, after which the prune deletes all five. With
this change the same run leaves all five in place.
Skip the prune when the remote set is empty but we still hold users
locally, and log it. Explicit deletion from the UI is unaffected, so an
admin who really did unshare everyone can still clear them out.