Symlinker watches a torrent download directory, identifies likely movies and TV episodes with TMDB, and creates Jellyfin-friendly symlinks in separate library folders. The source file is never moved, renamed, or modified.
- Polls a download directory for stable video files.
- Cleans noisy release names and guesses movie vs TV before TMDB lookup.
- Uses TMDB search to confirm the title and enrich naming.
- Creates symlinks in a movie or TV library tree.
- Sends unmatched or low-confidence items into a review queue visible in the web UI.
- Stores the top TMDB candidates for review items and lets you choose the correct one manually from the dashboard.
- Stores state and logs in SQLite so restarts do not duplicate work unnecessarily.
Movies:
Movies/Title (Year)/Title (Year).mkv
TV:
TV/Show Name (Year)/Season 01/Show Name (Year) - s01e01 - Episode Title.mkv
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
$env:APP_DATA_DIR = "./app_data"
uvicorn app.main:app --reloadOpen http://localhost:8000 and update the settings page with your real paths and TMDB key.
docker compose up --buildFor TrueNAS SCALE, mount real host paths instead of the sample directories in docker-compose.yml or your app manifest:
/downloads-> your torrent completed-downloads dataset/library/movies-> your Jellyfin movie dataset/library/tv-> your Jellyfin TV dataset/library/review-> optional holding or inspection dataset/app/app_data-> persistent config, DB, and logs
Yes, this can run as a TrueNAS SCALE Custom App.
The recommended deployment model is:
- Build the container image from this repo.
- Push it to a registry such as GitHub Container Registry.
- Create a Custom App in SCALE that points at that image.
- Mount your datasets into the container.
- Assign which mounted path is Downloads, Movies, TV, and Review from the app UI.
- Image:
ghcr.io/<your-user-or-org>/symlinker:latest - Port:
8000/TCP - Environment variable:
APP_DATA_DIR=/app/app_data
- You can mount datasets to the default paths below, or to any other in-container paths you prefer.
- After the app starts, go to
/settingsand choose which mounted path should be used for Downloads, Movies, TV, and Review.
Common defaults:
- Host path or dataset for completed downloads ->
/downloads - Host path or dataset for movies ->
/library/movies - Host path or dataset for TV ->
/library/tv - Host path or dataset for review ->
/library/review - Host path or dataset for app state ->
/app/app_data
- The container must be able to create directories and symlinks inside
/library/moviesand/library/tv. - Jellyfin must be allowed to traverse the symlinks created in those library datasets.
- The source files in
/downloadsare read-only from the app's perspective. They are never renamed, moved, or modified.
docker build -t ghcr.io/<your-user-or-org>/symlinker:latest .
echo <github-token> | docker login ghcr.io -u <github-user> --password-stdin
docker push ghcr.io/<your-user-or-org>/symlinker:latestSee docs/truenas-custom-app.md for a concrete Custom App configuration example.
This repo now includes .github/workflows/publish-image.yml to publish a container image to GitHub Container Registry.
The workflow:
- Builds the image from Dockerfile
- Publishes
latestfrom the default branch - Publishes tag versions when you push a Git tag such as
v0.1.0
To use it:
- Push this repo to GitHub.
- Make sure Actions are enabled.
- Push to your default branch to publish
ghcr.io/<owner>/symlinker:latest. - Optionally push a version tag such as
v0.1.0.
- This build assumes symlinks across datasets. That fits your stated requirement better than hardlinks.
- The app uses polling rather than inotify so it behaves predictably across container mounts and restarts.
- Files are only processed after they stop changing for the configured stability delay.
- If TMDB confidence is too low, the app does not link the file automatically.
/dashboard/settingsconfiguration page/healthzbasic health and config statusPOST /scanmanual scan triggerPOST /reprocessretry a review item
- Dataset roles are configured from the UI after your container mounts are in place.
- The settings page shows detected mounted paths so you can assign them without editing files in the container.
- The TMDB API key can be added or changed from the settings page.
- The activity log shows the source file, the linked library path, and the resulting Jellyfin-facing file name.
- Review items can display the top TMDB candidates with title, year, score, and media type, and you can select one manually.
- Best results come from conventional release names containing a year or
S01E01style episode markers. - Multi-episode files are not specially named yet.
- Review items are listed in the UI but not moved into the review directory.
- TMDB matching currently uses the top search result plus a simple confidence score.