A terminal UI for building and maintaining a Jellyfin-compatible symlink library — without touching your original files.
Jellyfin identifies media by strict naming conventions. Raw downloads rarely follow them. jellylink solves this non-destructively: it keeps your originals exactly where they are and builds a parallel directory tree of symlinks that Jellyfin can read cleanly.
/tank/media/ /tank/jellyfin/
Movies/ Movies/
Inception.2010.1080p.BluRay.mkv → Inception (2010)/
Inception (2010) - 1080p.mkv ⇢ ...
Serije/ Serije/
Breaking.Bad/ Breaking Bad (2008) [imdbid-tt0903747]/
S01/ Season 01/
Breaking.Bad.S01E01.mkv → Breaking Bad S01E01.mkv ⇢ ...
- Non-destructive — originals are never moved, renamed, or modified
- Lazy file tree — only reads directories as you expand them
- Smart name parser — strips codec junk, extracts year and episode codes, recognises multi-episode and special-episode patterns
- Live rule validation — highlights symlinks that violate Jellyfin naming rules in yellow so you can fix them immediately
- Incremental cache — link creation and deletion update the UI state in place; no full re-scan on every keystroke
- Undo — every create, rename, and delete is reversible within the session
- Bulk auto-link — link an entire directory tree in one keypress
- ISO extraction — call
7zto unpack an ISO in-place without leaving the TUI
- Python 3.10+
- Linux / macOS (uses POSIX symlinks)
- A colour terminal (xterm-256color or similar)
7z(p7zip) — only needed for the ISO extraction feature
No third-party Python packages required.
git clone https://github.com/youruser/jellylink.git
cd jellylinkEdit the three constants at the top of jellylink.py:
MEDIA_ROOT = "/tank/media" # root of your raw media library
JELLY_ROOT = "/tank/jellyfin" # root of the symlink tree that Jellyfin reads
SERIES_CATS = ["Serije", "AnimiraneSerije", "Telenovele"]SERIES_CATS lists the first-level subdirectory names that contain TV series. Every other category is treated as movies. The same category names must exist (or will be created) under both roots.
In Jellyfin → Dashboard → Libraries, add each category folder inside JELLY_ROOT as a library (e.g. /tank/jellyfin/Movies as a Movie library, /tank/jellyfin/Serije as a TV Shows library).
python3 jellylink.pyA terminal width of at least 100 columns is recommended.
MEDIA TREE [FILTER] SIZE JELLYFIN LINKS [2 broken]
▶ Movies ← cyan (no links inside)
▼ Serije ← green (has links inside)
▼ Breaking Bad (2008) [imdbid-tt0903747] ← green
▼ Season 01
• Breaking.Bad.S01E01.mkv 2.1 GB -> Breaking Bad S01E01.mkv
• Breaking.Bad.S01E02.mkv 1.9 GB -> Breaking Bad S01E02.mkv
• Breaking.Bad.S01E03.mkv 2.0 GB ← unlinked
▼ Season 02
• Breaking.Bad.S02E01.mkv 2.3 GB -> Breaking Bad S01E01.mkv ← yellow (rule violation)
▶ Dark (2017) [imdbid-tt5753856] ← magenta (marked complete)
─────────────────────────────────────────────────────────────────────────────────────────────────
| l:Link | a:Auto | e:Edit | f:Fix | d:Del | b:Bulk | z:Undo | c:Done | u:Undone | x:Iso |
The left pane is a lazy-loading tree rooted at MEDIA_ROOT. The right pane shows the symlink filename for every file that has one. The header shows active modifiers ([FILTER]) and the broken-link count when non-zero.
Left pane — directories
| Colour | Meaning |
|---|---|
| Cyan | Directory with no linked descendants |
| Green | Directory with at least one linked descendant |
| Magenta | Directory marked complete (.is_complete file present) |
Right pane — symlink names
| Colour | Meaning |
|---|---|
| Green | Symlink exists and passes all naming rules |
| Yellow | Symlink exists but violates at least one naming rule — use f to fix |
Broken symlinks (target file has been moved or deleted) are shown in the header count. Use r to refresh the cache after resolving them externally.
| Key | Action |
|---|---|
↑ / ↓ |
Move cursor |
Enter |
Expand / collapse directory |
Backspace |
Collapse current entry and jump to its parent |
q |
Quit |
| Key | Action |
|---|---|
l |
Link — create a symlink for the selected file. Opens a prompt pre-filled with the auto-generated name; edit freely, Enter to confirm, Esc to cancel. |
a |
Auto — silently accept the auto-generated name without prompting. |
e |
Edit — rename an existing symlink. Prompt is pre-filled with the current link name. |
f |
Fix — discard the current link name and re-generate it from the source path. Opens a prompt so you can review before confirming. Useful after source folders are renamed. |
d |
Delete — remove the symlink for the selected file. |
b |
Bulk auto — recursively run a on every unlinked video file under the selected directory. |
z |
Undo — reverse the last create, edit, or delete. Each z steps back one action. |
| Key | Action |
|---|---|
F (Shift+f) |
Toggle unlinked filter — hide files that already have a symlink, showing only what still needs to be processed. |
c |
Mark selected directory as complete — creates an empty .is_complete file, turning the folder magenta. |
u |
Unmark complete — removes the .is_complete file. |
x |
Extract ISO — runs 7z x on the selected .iso file in-place, then reloads the tree. |
r |
Refresh — re-scan JELLY_ROOT and rebuild the symlink cache from disk. |
Every symlink is validated on creation. A yellow link in the right pane means one or more rules are broken. Press f on the file to regenerate the name.
JELLY_ROOT/
<Category>/
<Title> (<Year>) [imdbid-ttXXXXXXX]/
<Title> (<Year>) [imdbid-ttXXXXXXX] - 1080p.mkv ← symlink
| Rule | Detail |
|---|---|
| No reserved characters | Filename must not contain `` < > : " / \ |
| Filename matches folder | When the folder carries an [imdbid-...] tag, the filename must start with the full folder name |
JELLY_ROOT/
<Category>/
<Show> (<Year>) [imdbid-ttXXXXXXX]/
Season 01/
<Show> S01E01 - 1080p.mkv ← symlink
Season 00/
OVA 1 - Recap.mkv ← specials (relaxed rules)
| Rule | Detail |
|---|---|
| Minimum depth | Path must be Category/Show/Season/File — at least 4 levels |
SxxExx required |
Regular episode filenames must contain a season/episode code |
| Zero-padded season | Season folder must be Season 01, not Season 1 |
| Filename starts with show name | Regular episodes must start with the show folder name (IMDB tag stripped) |
Season 00 — Specials: Files inside
Season 00are exempt from theSxxExxand show-name-prefix rules, matching Jellyfin's own handling of specials folders.
When creating or fixing a link, jellylink derives a clean name from the source path. Metadata is gathered in priority order:
| Priority | Source | Notes |
|---|---|---|
| 1 | Existing link's parent folder (fix mode only) | Reuses the name already confirmed for this show/movie |
| 2 | Source path ancestor carrying [imdbid-...] |
Folder name used verbatim — most reliable |
| 3 | Nearest meaningful parent folder | Codec/release junk stripped; . and _ converted to spaces |
| 4 | Year fallback | Scanned from any path component; (2010) preferred over bare 2010 |
Junk tokens stripped from folder names: 1080p 720p 2160p 4k uhd bluray dvdrip x264 x265 hevc aac dts web-dl hdtv webrip brrip xvid h264 h265 and common container extensions.
| Pattern | Example source filename | Parsed output |
|---|---|---|
Standard SxxExx |
Show.S01E05.mkv |
S01E05 |
Alternate NNxNN |
Show.01x05.mkv |
S01E05 |
| Multi-episode concatenated | Show.S01E01E02.mkv |
S01E01-E02 |
| Multi-episode range (dash) | Show.S01E01-E03.mkv or S01E01-03 |
S01E01-E03 |
| Numbered special | OVA2.mkv, SP03.mkv, NCED1.mkv |
S00E02, S00E03, S00E01 |
| Bare special keyword | Special.mkv, OVA.mkv |
S00E01 (placeholder) |
If any component cannot be determined, a placeholder is inserted (<name>, <year>, <SxxExx>) and the prompt opens so you can fill it in manually.
Starting from scratch on a new import:
- Select the top-level category folder (e.g.
Serije) - Press
b— Bulk auto-links every video file recursively - Press
F(Shift+f) to enable the unlinked filter — any filesbcouldn't handle appear immediately - Work down the remaining files with
l(Link)
Most reliable setup:
Name your source folders as Show Name (2020) [imdbid-tt1234567] before importing. The IMDB tag in the folder name is the highest-priority metadata source; auto-generation will always produce a correct, unambiguous result.
Completion tracking:
c / u toggle a .is_complete marker on any directory. Use this to distinguish fully-processed sections of the library (magenta) from in-progress ones (green) at a glance.
Undo scope:
The undo stack is in-memory only and is cleared on quit. If you need to undo a bulk operation, press z once per file.
External changes:
r (Refresh) re-scans JELLY_ROOT from disk. This is only needed if you modify symlinks outside jellylink — all in-app operations update the cache immediately without a re-scan.
MEDIA_ROOT/ JELLY_ROOT/
Movies/ Movies/
Inception (2010)/ Inception (2010) [imdbid-tt1375666]/
Inception.2010.1080p.mkv → Inception (2010) [imdbid-tt1375666] - 1080p.mkv
└─ symlink → MEDIA_ROOT/Movies/...
Serije/ Serije/
Breaking Bad (2008)/ Breaking Bad (2008) [imdbid-tt0903747]/
S01/ Season 01/
Breaking.Bad.S01E01.mkv → Breaking Bad S01E01.mkv
Specials/ Season 00/
OVA.mkv → Breaking Bad S00E01.mkv
Bug reports and pull requests are welcome. When reporting an issue, please include:
- The filename or folder name that was parsed incorrectly
- What
jellylinkgenerated vs. what Jellyfin expects - Your
SERIES_CATSconfiguration if it differs from the default
MIT