Keep JellyTag's data out of <config>/plugins (fixes #21) - #34
Open
calif94577 wants to merge 1 commit into
Open
Conversation
Jellyfin points BasePlugin.DataFolderPath at <config>/plugins/Jellyfin.Plugin.JellyTag, a sibling of <config>/plugins/configurations, and ImageCacheService creates it at startup. PluginManager.DiscoverPlugins() treats every folder under <config>/plugins as a plugin candidate and derives the name of a folder without a meta.json by cutting the full path at its last underscore. On a server whose data path contains an underscore (QNAP's /share/CACHEDEV1_DATA/..., or a bind such as /volume1/media_server/...) both folders collapse to the same generated identity and the duplicate is removed with Directory.Delete(path, recursive: true) - sometimes taking plugins/configurations, and with it every installed plugin's settings. Move the image cache to <cache>/jellytag and custom badges to <data>/jellytag so nothing of ours sits under <config>/plugins, and add a guarded one-time migration that relocates existing custom badges and deletes the stray folder left by earlier versions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
On some servers, installing JellyTag causes every installed plugin's settings to reset on restart (#21). The settings aren't "reset" —
<config>/plugins/configurations/is being recursively deleted by Jellyfin itself, and JellyTag is what triggers it.Chain of events
1. JellyTag creates an un-versioned folder directly inside
<config>/plugins/.CacheFolderPathis built fromBasePlugin.DataFolderPath, andImageCacheServicecreates it eagerly at startup:Jellyfin sets
DataFolderPathto<config>/plugins/Jellyfin.Plugin.JellyTag— with no version suffix, becauseBasePluginOfT.cstestsVersionbeforeSetAttributes()has assigned it:So we end up with an underscore-free folder sitting next to
<config>/plugins/configurations/.2. Jellyfin treats every folder under
plugins/as a plugin candidate, and for folders without ameta.jsonit derives the plugin name by cutting the entire path at its last underscore (PluginManager.LoadManifest):3. If the server's data path contains an underscore, both folders collapse to the same generated identity — same name, same MD5 id, same version — so
DiscoverPlugins()treats one as a stale duplicate and runsDirectory.Delete(path, recursive: true)on it.Running Jellyfin's own derivation code against real NAS paths:
Why the report looks the way it does
List.Sort, so it's roughly a coin flip per boot. IfJellyfin.Plugin.JellyTagloses, only the badge cache is lost and nobody notices.plugins/JellyTag_<version>/but leavesplugins/Jellyfin.Plugin.JellyTag/behind, so the collision keeps recurring on every boot until chance removes the stray folder instead ofconfigurations.This is a latent Jellyfin core bug (I'm filing it upstream separately), but any plugin that touches
DataFolderPathcan detonate it, and JellyTag does so unconditionally at startup. Fixing it plugin-side is both possible and, I'd argue, correct regardless.The fix
<cache>/jellytagviaIApplicationPaths.CachePath.<data>/jellytagviaIApplicationPaths.DataPath, behind a newBadgeFolderPathproperty replacing the fourDataFolderPathcall sites inJellyTagControllerand the one inImageOverlayService.<config>/pluginsanymore, so the name collision cannot form.MoveOutOfPluginsFolder()runs once at startup: it copies any existingcustom-badgesto the new location, then deletes the strayplugins/Jellyfin.Plugin.JellyTag/folder. This is what actually un-breaks servers that already hit this.Safety of the migration:
try/catchso it can never prevent the plugin from loading.PluginsPath, and refuses to touch the plugin's own install directory.Directory.Movethrows.Side benefit: the badge cache no longer grows inside the config volume, which on a NAS is frequently small.
Known limitation — please mention this in the release notes
Jellyfin runs
DiscoverPlugins()in thePluginManagerconstructor, before any plugin assembly is loaded. No plugin code can run earlier. So on the single boot where this update is first applied, the collision still exists and there is a last coin flip. From the boot after that, the stray folder is gone and it can never happen again.To skip that last coin flip, affected users should stop the server and remove the folder by hand before updating:
rm -rf "<config>/plugins/Jellyfin.Plugin.JellyTag"Settings already lost cannot be recovered — they were deleted recursively — so they'll need re-entering once.
Testing
Jellyfin.Controller/Jellyfin.Model10.11.0 on .NET 9 (0 warnings, 0 errors).LoadManifestname/version derivation was extracted and run under .NET 9 against the path layouts above to confirm exactly which configurations collide;DiscoverPlugins' deletion loop was simulated to confirm which folder gets deleted and that enumeration order decides it.I've left the version in
build.yaml/.csprojandmanifest.jsonalone — that's yours to bump on release.🤖 Generated with Claude Code