Path matching adds a local GUID source named guid_path. WatchState derives a stable hash from backend-reported media file paths and stores it alongside the normal GUIDs.
Path matching became available in v1.8.5 and was disabled by default. It is enabled by default starting with the v1.10.6.
Path matching is useful when your backends have missing, weak, or inconsistent external IDs, but they still point at the same local media files.
Examples that still match by path:
/mnt/media/tv/Show Title/Season 01/S01E01.mkv
Z:\TV\Show Title\Season 01\S01E01.mkv
Examples that do not match by path:
/mnt/media/tv/Show Title/Season 01/S01E01.mkv
/mnt/media/tv/Show Title (2024)/Season 1/Episode 01.mkv
Path matching should remain enabled for most installations. If different media items share the same normalized path suffix, you can disable it:
- Go to Configuration > Environment.
- Click the Add button.
- Select
WS_GUID_PATH_ENABLED. - Toggle the switch to disable.
- Save the change.
Path matching only affects entities created or refreshed while the setting is enabled. Existing rows keep their current data until they are imported again. Existing rows without guid_path continue to work, but they cannot match by path until forced metadata refresh happens.
If all of your backends point to the same media files, it is usually enough to refresh just one backend. That one import seeds the local database with guid_path, and the other backends can continue operating in metadata-only mode while still matching against the stored path GUIDs.
For example:
- Go to Operations > Console.
- Run a full import for one backend that has the shared library:
state:import --user main --select-backend plex --force-fullIf your backends do not all share the same media, run a full refresh across all imported backends instead:
state:import -v --force-fullOnce the import is done, you can inspect the run log in Operations > Logs.
Important
Do not use --metadata-only for this refresh. metadata-only mode does not rewrite GUIDs for existing rows.
Backends with Import disabled are treated as metadata-only during state:import. That is usually fine when all backends share the same media, because one imported backend is enough to seed guid_path for the shared items. Only temporarily enable Import from the Backends page if that backend has items or media paths that are not covered by the backend you refreshed.
Note
Once the backfill is done, future imports and webhook events will store the new GUID.
Path matching does not try to understand your library root, movie folder, show folder, or season folder names. It normalizes the backend-reported file path by replacing backslashes with /, collapsing duplicate separators, and lowercasing path segments. It then hashes fixed suffixes.
| Stored field | Example backend path | Suffix used for matching | Rule |
|---|---|---|---|
| for movies | /foo/bar/movies/movie.mkv |
/movies/movie.mkv |
Final 2 path segments |
| for episodes | /foo/bar/tv/show/episode.mkv |
/tv/show/episode.mkv/1/1 |
Final 3 path segments plus logical season/episode |
| for episodes show | /foo/bar/tv/show/episode.mkv |
/tv/show |
The 2 directory segments immediately before the file |
The paths do not need to have the same leading root across backends. They only need the relevant trailing suffix to match.
Path matching is not meant to rescue every possible broken or non-standard library layout.
The important part is whether the suffix WatchState hashes is stable across backends.
| Case | Backend A suffix | Backend B suffix | Result |
|---|---|---|---|
| Same flat movie parent | /movies/movie.mkv |
/movies/movie.mkv |
Match |
| Different flat movie parent | /movies/movie.mkv |
/films/movie.mkv |
No match |
| Episode without season folder, same final 3 segments | /tv/anime1/ep1.mkv |
/tv/anime1/ep1.mkv |
Match |
| Episode without season folder, different library folder in final suffix | /tv/anime1/ep1.mkv |
/tvshows/anime1/ep1.mkv |
No match |
| Too few movie segments | /movie.mkv |
N/A | Skipped |
| Too few episode segments | /anime1/ep1.mkv |
N/A | Skipped |
Path GUID generation is skipped when there are not enough path segments to build the required suffix.
This is intentional. Path matching is an extra fallback for reasonably organized libraries, not a full library-repair system.
WatchState does not support per-backend root stripping, base-path mapping, or manual path replacement for path matching.
That choice is intentional for three reasons:
- The current implementation already ignores leading path segments by hashing only fixed trailing suffixes.
- Adding per-backend root detection or remapping would make the matching path more complex for a feature that is supposed to behave like a normal GUID source.
- Path matching is on a hot import path, so avoiding extra backend-specific lookup and rewrite logic keeps it simpler and cheaper to run.
Example:
| Backend | Full path | Movie suffix used |
|---|---|---|
| Plex | /media/movies/foo/foo.mkv |
/foo/foo.mkv |
| Jellyfin | /a/b/c/d/whatever/foo/foo.mkv |
/foo/foo.mkv |
These already match because WatchState only cares about /foo/foo.mkv, not the different leading roots.
Root stripping could help libraries where the configured library root appears inside the final suffix and differs between backends. For example, /mnt/tv/anime1/ep1.mkv and \\nas\tvshows\anime1\ep1.mkv would only match if WatchState knew to strip /mnt/tv and \\nas\tvshows first.
That tradeoff is acceptable. Path matching is intentionally optimized for the common case, not every uncommon or poorly structured layout. For more info, see this discussion.