Privacy-first personalized recommendations for Jellyfin - local watch history analysis with zero tracking
C#
56
45 commits
updated Sep 8, 2026
Privacy-first personalized recommendations for Jellyfin based entirely on local watch history and metadata similarity. No cloud services, no tracking. Works on all Jellyfin clients (even TVs).
[!IMPORTANT] This project will not be updated for Jellyfin 12. If someone would like to maintain a Jellyfin 12-compatible fork, I would be happy to link to it here—please open an issue or discussion.
Please report any issues or feedback on GitHub Issues.
⚠️ Windows hosts: As of v0.6.0, this plugin creates filesystem symlinks to expose recommendations. On Windows, symlink creation requires either running Jellyfin as Administrator or enabling Windows Developer Mode (Settings → Privacy & security → For developers → Developer Mode). Without one of these, recommendation refreshes will log "Access denied creating symlink" and the virtual libraries will be empty. Docker-on-Linux and native Linux deployments are unaffected. See Troubleshooting below.
Add plugin repository:
Dashboard → Plugins → Repositories → Add
https://raw.githubusercontent.com/rdpharr/jellyfin-plugin-localrecs/main/manifest.json
Install plugin:
Dashboard → Plugins → Catalog → Install "Local Recommendations"
Restart Jellyfin server
Configure virtual libraries (see Setup below)
For each user, create two libraries:
Movies:
TV Shows:
For each user:
Access via: Dashboard → Plugins → Local Recommendations → Settings
Recommendation Counts
Filtering
Weighting Factors
Optional Features
Performance
Vocabulary size controls how many distinct actors, directors, and tags are included in the TF-IDF model. A higher value (e.g. 1000) captures more niche contributors and gives richer signals for large, varied libraries. A lower value (e.g. 200) is faster and uses less memory but may miss less-common cast members. The default of 500 is a good starting point for most libraries; raise it if recommendations feel too genre-driven and ignore specific actors you watch often.
Recency decay controls how much your recent watch history influences recommendations relative to older watches. It is expressed as a half-life in days: a value of 365 means a film watched a year ago contributes half as much to your taste profile as one watched today. Lower values (e.g. 90) make recommendations react quickly to recent binges; higher values (e.g. 730) give a more stable, long-term taste profile.
Update Schedule
Content-based filtering using TF-IDF embeddings and cosine similarity:
Weighting factors:
Recommendations appear as separate libraries for each user:
100% local processing:
If the refresh task completes without errors but the recommendation libraries are empty, check
the Jellyfin log for Access denied creating symlink. This means the Jellyfin process lacks
permission to create symbolic links, which is required on Windows.
Fix (pick one):
SeCreateSymbolicLinkPrivilege granted, or to a local administrator.After either change, run Dashboard → Scheduled Tasks → Refresh Local Recommendations.
Upgrade to v0.6.0 or later. Jellyfin 10.11.7 shipped a security fix
(GHSA-j2hf-x4q5-47j3)
that broke the old .strm-based approach. v0.6.0 switches to symlinks, which bypass the
restricted .strm parser entirely.
Prerequisites: .NET 9.0 SDK, Git
git clone https://github.com/rdpharr/jellyfin-plugin-localrecs.git
cd jellyfin-plugin-localrecs
# Build (uses dotnet-helper.sh wrapper)
bash dotnet-helper.sh build
# Run tests
bash dotnet-helper.sh test
# Output: Jellyfin.Plugin.LocalRecs/bin/Debug/net9.0/
Windows: Use Git Bash or WSL to run the helper script.
Contributions to the current Jellyfin version are welcome. See DESIGN.md for technical details and architecture. For Jellyfin 12 support, please create a fork; maintained forks can be submitted for inclusion in the notice above.
GNU General Public License v3.0 - see LICENSE.txt
18 followers · starred Apr 2026
C#
92.7%
HTML
7.2%
Privacy-first personalized recommendations for Jellyfin - local watch history analysis with zero tracking
C#
56
45 commits
updated Sep 8, 2026
Privacy-first personalized recommendations for Jellyfin based entirely on local watch history and metadata similarity. No cloud services, no tracking. Works on all Jellyfin clients (even TVs).
[!IMPORTANT] This project will not be updated for Jellyfin 12. If someone would like to maintain a Jellyfin 12-compatible fork, I would be happy to link to it here—please open an issue or discussion.
Please report any issues or feedback on GitHub Issues.
⚠️ Windows hosts: As of v0.6.0, this plugin creates filesystem symlinks to expose recommendations. On Windows, symlink creation requires either running Jellyfin as Administrator or enabling Windows Developer Mode (Settings → Privacy & security → For developers → Developer Mode). Without one of these, recommendation refreshes will log "Access denied creating symlink" and the virtual libraries will be empty. Docker-on-Linux and native Linux deployments are unaffected. See Troubleshooting below.
Add plugin repository:
Dashboard → Plugins → Repositories → Add
https://raw.githubusercontent.com/rdpharr/jellyfin-plugin-localrecs/main/manifest.json
Install plugin:
Dashboard → Plugins → Catalog → Install "Local Recommendations"
Restart Jellyfin server
Configure virtual libraries (see Setup below)
For each user, create two libraries:
Movies:
TV Shows:
For each user:
Access via: Dashboard → Plugins → Local Recommendations → Settings
Recommendation Counts
Filtering
Weighting Factors
Optional Features
Performance
Vocabulary size controls how many distinct actors, directors, and tags are included in the TF-IDF model. A higher value (e.g. 1000) captures more niche contributors and gives richer signals for large, varied libraries. A lower value (e.g. 200) is faster and uses less memory but may miss less-common cast members. The default of 500 is a good starting point for most libraries; raise it if recommendations feel too genre-driven and ignore specific actors you watch often.
Recency decay controls how much your recent watch history influences recommendations relative to older watches. It is expressed as a half-life in days: a value of 365 means a film watched a year ago contributes half as much to your taste profile as one watched today. Lower values (e.g. 90) make recommendations react quickly to recent binges; higher values (e.g. 730) give a more stable, long-term taste profile.
Update Schedule
Content-based filtering using TF-IDF embeddings and cosine similarity:
Weighting factors:
Recommendations appear as separate libraries for each user:
100% local processing:
If the refresh task completes without errors but the recommendation libraries are empty, check
the Jellyfin log for Access denied creating symlink. This means the Jellyfin process lacks
permission to create symbolic links, which is required on Windows.
Fix (pick one):
SeCreateSymbolicLinkPrivilege granted, or to a local administrator.After either change, run Dashboard → Scheduled Tasks → Refresh Local Recommendations.
Upgrade to v0.6.0 or later. Jellyfin 10.11.7 shipped a security fix
(GHSA-j2hf-x4q5-47j3)
that broke the old .strm-based approach. v0.6.0 switches to symlinks, which bypass the
restricted .strm parser entirely.
Prerequisites: .NET 9.0 SDK, Git
git clone https://github.com/rdpharr/jellyfin-plugin-localrecs.git
cd jellyfin-plugin-localrecs
# Build (uses dotnet-helper.sh wrapper)
bash dotnet-helper.sh build
# Run tests
bash dotnet-helper.sh test
# Output: Jellyfin.Plugin.LocalRecs/bin/Debug/net9.0/
Windows: Use Git Bash or WSL to run the helper script.
Contributions to the current Jellyfin version are welcome. See DESIGN.md for technical details and architecture. For Jellyfin 12 support, please create a fork; maintained forks can be submitted for inclusion in the notice above.
GNU General Public License v3.0 - see LICENSE.txt
18 followers · starred Apr 2026
C#
92.7%
HTML
7.2%