StashSync Groups Jellyfin Plugin

:placard: Summary Jellyfin Plugin to Read Groups from Stash.
:link: Repository https://github.com/Lurking987/jellyfin-plugins/tree/main/StashSync

StashSync Groups Jellyfin Plugin

A Jellyfin plugin that syncs your Stash App Groups to Jellyfin as Movies, with scenes mapped as chapter markers and full TheMovieDB metadata support.

Jellyfin
.NET
License


What It Does

Each Stash Group becomes a Movie entry in Jellyfin. The scenes inside the Group appear as chapter markers, letting you jump between them like chapters on a Blu-ray. If the Group has a TheMovieDB URL, the plugin automatically fetches the TMDB ID, poster, and backdrop during sync.

Stash Group "My Movie Title"
  β”œβ”€β”€ Scene 1 β€” "Opening"    β†’  Chapter 1   0:00:00
  β”œβ”€β”€ Scene 2 β€” "Middle"     β†’  Chapter 2   0:34:12
  └── Scene 3 β€” "Finale"     β†’  Chapter 3   1:02:45

Jellyfin Movie: "My Movie Title"
  β”œβ”€β”€ poster.jpg         (TMDB poster, falls back to Stash cover)
  β”œβ”€β”€ backdrop.jpg       (TMDB backdrop)
  β”œβ”€β”€ My Movie Title.strm     β†’ streams via Stash HTTP
  β”œβ”€β”€ My Movie Title.nfo      β†’ metadata + TMDB ID + chapters
  └── chapter-metadata.xml    β†’ scene chapter offsets

Why .strm files?
Jellyfin requires a physical file to anchor each library entry. A .strm is a single-line text file containing a stream URL β€” Jellyfin opens it and streams directly from Stash, while handling all the library UI itself.


Requirements

Jellyfin 10.9.0 or later
Stash App Any recent version with GraphQL API enabled
TMDB API Key Free β€” optional, needed for automatic poster/backdrop fetching
.NET SDK 8.0 (only needed to build from source)

Installation

Download the latest release

  1. Go to the Releases page and download Jellyfin.Plugin.StashSync.dll
  2. On your Jellyfin server, create the folder:
    <jellyfin-config>/plugins/StashSync_1.0.0.0/
    
  3. Copy the DLL into that folder
  4. Restart Jellyfin
  5. Confirm it loaded: Dashboard β†’ Plugins β†’ My Plugins β†’ StashSync should appear

TrueNAS Scale: The plugin folder is typically at
/mnt/.ix-apps/app_mounts/jellyfin/config/plugins/StashSync_1.0.0.0/

Build from source

git clone https://github.com/YOUR_USERNAME/jellyfin-plugin-stashsync.git
cd jellyfin-plugin-stashsync
dotnet build -c Release
# DLL output: bin/Release/net8.0/Jellyfin.Plugin.StashSync.dll

Setup

1. Configure the plugin

Dashboard β†’ Plugins β†’ My Plugins β†’ StashSync β†’ Settings

Setting Example Description
Stash URL http://192.168.1.50:9999 LAN IP of your Stash instance. No trailing slash.
Stash API Key eyJ... Only needed if Stash authentication is enabled.
TMDB API Key abc123... Free v3 key from themoviedb.org/settings/api. Used to fetch posters and backdrops automatically.
STRM Output Path /config/stash-groups Where .strm files will be written. Jellyfin must have write access.
Use Stream URLs :white_check_mark: Recommended. Stash serves video, Jellyfin handles the UI.
StashProxy URL http://192.168.1.50:5678 LAN IP of your StashProxy server
Min Scene Count 1 Groups with fewer scenes than this are skipped.

2. Add the output folder as a Jellyfin library

  1. Dashboard β†’ Libraries β†’ Add Media Library
  2. Type: Movies
  3. Folder: the same path set as STRM Output Path
  4. Make sure TheMovieDb is enabled under both Metadata downloaders and Image fetchers
  5. Click OK

3. Fix folder permissions (TrueNAS / Docker)

Jellyfin runs as uid 568 inside its container. The output folder must be owned by that user or new subfolders won’t be writable:

chown 568:568 /mnt/<pool>/path/to/stash-groups
chmod 777 /mnt/<pool>/path/to/stash-groups
chmod g+s /mnt/<pool>/path/to/stash-groups   # inherit group on new files

The g+s setgid flag is important β€” without it, newly created subfolders won’t inherit the correct ownership and Jellyfin will hit permission errors on subsequent syncs.

4. Deploy StashProxy

Multi-scene groups require StashProxy to stream correctly. Without it, only single-scene groups will play. See the StashProxy README for setup instructions.

Once StashProxy is running, set the Proxy URL in the plugin settings to http://<your-server-ip>:5678, then re-run the sync.


5. Run the sync

  1. Dashboard β†’ Scheduled Tasks β†’ StashSync β†’ Sync Stash Groups β†’ :play_button: Run
  2. Once complete, Scan Library Files on your stash-groups library
  3. Jellyfin will read each .nfo, match the TMDB ID, and fetch full metadata and images automatically

TMDB Integration

If a Stash Group has a TheMovieDB URL in its URLs field (e.g. https://www.themoviedb.org/movie/12345), the plugin will:

  • Parse the TMDB movie ID from the URL
  • Call the TMDB API to fetch the poster and backdrop image paths
  • Write the TMDB ID into the .nfo as the default uniqueid so Jellyfin uses it for all metadata lookups
  • Embed the poster and backdrop URLs directly in the .nfo so Jellyfin picks them up immediately on scan
  • Download poster.jpg and backdrop.jpg into the group folder as local copies

Groups without a TMDB URL will still sync β€” they’ll use the Stash cover image as the poster and whatever metadata Stash has.


Re-syncing After Changes

The sync is manual only. After adding/removing scenes from a Group or creating new Groups in Stash:

  1. Scheduled Tasks β†’ Sync Stash Groups β†’ :play_button: Run
  2. Refresh Metadata β†’ Replace all metadata + Replace existing images on the library

The sync task updates existing group folders in place and removes folders for Groups that no longer exist in Stash.


Troubleshooting

No groups appear after sync

  • Check the Jellyfin log for [StashSync] entries
  • Verify Stash is reachable: curl http://<stash-ip>:9999/graphql
  • Check that Jellyfin has write permission to the STRM Output Path

Movies appear but no images

  • Make sure your TMDB API Key is saved in the plugin settings
  • Confirm the Stash Group has a themoviedb.org/movie/... URL in its URLs field
  • Run Refresh Metadata β†’ Replace existing images on the library
  • Check that TheMovieDb is enabled under Library β†’ Image fetchers

Chapters don’t appear

  • Confirm chapter-metadata.xml exists in the group folder
  • Check Library Settings β†’ Metadata readers β€” StashSync should be listed and checked

Video won’t play

  • The .strm contains a URL like http://<stash-ip>:9999/scene/101/stream
  • Test that URL directly in a browser β€” if it loads, Jellyfin should be able to play it
  • If Stash has authentication enabled, make sure the Stash API Key is set in plugin settings

Permission errors on TrueNAS after sync

  • See the permissions fix in Step 3 of Setup above
  • The g+s setgid flag on the output folder is required for new subfolders to inherit the correct ownership

Project Structure

Plugin.cs                        Plugin entry point and ID
PluginServiceRegistrator.cs      Dependency injection registration
Configuration/
  PluginConfiguration.cs         Settings model
  configPage.html                Admin settings UI
GraphQL/
  StashModels.cs                 GraphQL DTOs and query definitions
  StashApiClient.cs              Stash API client with pagination
Tasks/
  SyncStashGroupsTask.cs         Scheduled task β€” orchestrates the sync
  StrmWriter.cs                  Writes .strm, .nfo, chapters, posters
Providers/
  StashGroupMetadataProvider.cs  Local metadata provider
  StashExternalId.cs             Registers Stash as an external ID type

Extending

Automatic sync on a schedule β€” add a trigger to GetDefaultTriggers() in SyncStashGroupsTask.cs:

yield return new TaskTriggerInfo
{
    Type = TaskTriggerInfo.TriggerInterval,
    IntervalTicks = TimeSpan.FromHours(6).Ticks
};

Performer/actor metadata β€” extend the GraphQL query in StashModels.cs to include performers { name } and write them into the .nfo as <actor> elements.

Studio collections β€” use the Stash studio name to group movies into Jellyfin collections.


License

MIT


Screenshots

Playback Example

Plaback StashProxy Logs

Jellyfin Movies and Movie Detail Page

Current Known Limitations

  • Full seeking seems to be limited to web browsers at the moment. Application version can seek within 30 seconds

Future Plan

  • HLS segmentation in the proxy β€” break the stream into HLS .ts segments with a playlist, so Jellyfin can fetch any segment on demand. This supposed to give full seeking capabilities on all clients.
  • Look for groups { scene { scene_index } } and if the scenes in the group have been designated a scene number, order the scenes in the chapter.xml by scene_index.