Frustrated by Jellyfin’s metadata quirks, I built a script to enforce album-per-folder rules, sanitize filenames, and fix missing cover art.
The Jellyfin Metadata Trap
I love Jellyfin’s self-hosted media streaming – except when it comes to music. Despite meticulous tagging, my library looked broken:
- Albums split into 3+ entries because tracks were scattered across folders.
- Cover art ignored despite being embedded in files.
- Songs titled
Track05.mp3
because Jellyfin fell back to filenames.
The official docs hinted at the issue:
“Albums are organized in folders, with one folder containing one and only one album.”
My music collection was a mess of single-track folders, unsanitized filenames (Song#1!.flac
), and inconsistent metadata. Manual fixes for 300+ tracks? No thanks.
Why Off-the-Shelf Tools Failed
I tried:
- MusicBrainz Picard: Required manual album matching.
- Beets: Too much configuration for a simple task.
- Bulk Renamers: Fixed filenames but ignored folder structures.
I needed something that:
- Grouped tracks into Jellyfin-friendly
Artist/Album
folders - Used embedded metadata as the source of truth
- Eliminated special characters Jellyfin hates (
#
,?
,:
, etc.)
So I built a Python script to do it in under 10 seconds.
The Script: Key Features
1. Jellyfin Folder Structure Enforcement
# Jellyfin requires 1 album = 1 folder
def create_jellyfin_path(file_path, artist, album):
base_dir = os.path.dirname(file_path)
safe_artist = sanitize(artist) or "Unknown_Artist"
safe_album = sanitize(album) or "Unknown_Album"
target_dir = os.path.join(base_dir, safe_artist, safe_album)
os.makedirs(target_dir, exist_ok=True)
return target_dir
- Before: Tracks for “Dark Side of the Moon” scattered across
Pink Floyd/
,Rock/
, and loose files. - After: All tracks moved to
Pink_Floyd/Dark_Side_of_the_Moon/
.
2. Filename Sanitization
Jellyfin chokes on ?
or :
, so the script replaces them with underscores:
def jellyfin_sanitize(name):
unsafe_chars = r'<>:"/\|?*#' # Jellyfin's kryptonite
for char in unsafe_chars:
name = name.replace(char, "_")
return name.strip()
- Before:
Hey You (Remix 2023).flac
→ Jellyfin logs errors. - After:
Hey_You_(Remix_2023).flac
→ Jellyfin parses smoothly.
3. Metadata-Driven Sorting
Using Mutagen, the script prioritizes embedded tags over filenames:
from mutagen.flac import FLAC
def get_metadata(file_path):
if file_path.endswith(".flac"):
audio = FLAC(file_path)
return {
"artist": audio.get("artist", ["Unknown"])[0],
"album": audio.get("album", ["Unknown"])[0],
"date": audio.get("date", ["1970"])[0], # For sorting
}
# Similar logic for MP3, OGG, etc.
- Result: Jellyfin displays
The Dark Side of the Moon
instead ofAlbum 3
.
Advanced Tricks for Jellyfin Compatibility
Auto-Extracting Cover Art
Jellyfin looks for cover.jpg
or folder.jpg
in album folders. The script extracts embedded art:
from mutagen.id3 import ID3
def extract_cover_art(file_path, target_dir):
if file_path.endswith(".mp3"):
audio = ID3(file_path)
if 'APIC:' in audio:
with open(f"{target_dir}/cover.jpg", "wb") as f:
f.write(audio['APIC:'].data)
- Bonus: Adds missing art using Last.fn API if none exists.
Generating .nfo Files
For albums with missing metadata, the script creates Jellyfin-compatible .nfo
files:
<!-- Pink_Floyd/Dark_Side_of_the_Moon/album.nfo -->
<album>
<title>The Dark Side of the Moon</title>
<year>1973</year>
</album>
Challenges & Lessons
-
Metadata Inconsistencies:
- Some MP3s used
TPE1
for artist, othersARTIST
. Normalized all toartist
. - Solution: A unified tag-to-key mapping table.
- Some MP3s used
-
Special Characters:
- A file named
AC/DC - Back In Black.mp3
broke folder creation. - Solution: Replace
/
with_
during sanitization.
- A file named
-
Performance:
- Processing 10,000+ files took minutes.
- Future: Add multithreading with
concurrent.futures
.
Try It Yourself
The script is open-source here. To use:
-
Install:
pip install mutagen requests # For cover art fetching
-
Run:
python3 organize.py --input ~/Music --jellyfin-mode
-
Flags:
--dry-run
: Preview changes without moving files.--fetch-art
: Auto-download missing cover art.
Conclusion
This project taught me two things:
- Jellyfin’s “metadata-first” claim is half-true – folder structure and filenames matter more than they admit.
- Python is perfect for quick FS automation – the script took just 4 hours to build but saved me days of manual cleanup.
Next up:
- GUI version for non-technical users.
- Integration with Lidarr for auto-tagging.
( Only If i don't procastinate, which well never happen )
Your turn: How do you organize your music library? Share your hacks! 🎶