Skip to content

Instantly share code, notes, and snippets.

@hortonew
Created November 3, 2025 23:03
Show Gist options
  • Select an option

  • Save hortonew/da79fd9b1fb790eb10be597ad953a7d3 to your computer and use it in GitHub Desktop.

Select an option

Save hortonew/da79fd9b1fb790eb10be597ad953a7d3 to your computer and use it in GitHub Desktop.
Interleave Shows for Jellyfin Playlist
#!/usr/bin/env python3
import os
import xml.etree.ElementTree as ET
from datetime import datetime
# Configuration — adjust as needed
MEDIA_ROOT = "/volume1/docker/tv"
SHOWS = [
"Show 1",
"Show 2",
"..."
]
OUTPUT_FILE = "interleaved_playlist.xml"
LOCAL_TITLE = "Good TV" # title of your playlist
CONTENT_RATING = "TV-PG" # optional / custom if desired
OWNER_USERID = "" # set to the actual Jellyfin user ID if known
def list_episodes(show_path, season_number):
"""
Return a sorted list of episode file paths for a given season of a show.
"""
season_dir = os.path.join(show_path, f"Season {season_number}")
if not os.path.isdir(season_dir):
return []
files = [f for f in os.listdir(season_dir)
if os.path.isfile(os.path.join(season_dir, f))
and f.lower().endswith((".mkv", ".mp4", ".avi"))]
files.sort()
return [os.path.join(season_dir, f) for f in files]
def build_interleaved_playlist(media_root, shows):
"""
Build interleaved episode list:
- iterate season by season (1, 2, …)
- for each season, iterate each show in `shows` list, and for each show list its episodes for that season in order
- continue until no show has any episodes for that season
"""
show_paths = { show: os.path.join(media_root, show) for show in shows }
season = 1
playlist_paths = []
while True:
# gather episodes for this season for each show
ep_lists = {}
max_eps = 0
for show, spath in show_paths.items():
eps = list_episodes(spath, season)
if eps:
ep_lists[show] = eps
if len(eps) > max_eps:
max_eps = len(eps)
if not ep_lists:
# no shows had any episodes this season — finish
break
# interleave episode ordering: ep1 of show1, ep1 of show2, … then ep2 of show1, ep2 of show2, …
for ep_index in range(max_eps):
for show in shows:
eps = ep_lists.get(show)
if eps and ep_index < len(eps):
# convert the full path into the Jellyfin playlist Path prefix /tv
full_path = eps[ep_index]
# full_path is like "/volume1/docker/tv/Show/Season X/…"
# We want to transform it into "/tv/Show/Season X/…"
rel_path = full_path
prefix = media_root.rstrip(os.sep)
if rel_path.startswith(prefix):
# strip off the media_root part and replace with /tv
rel_sub = rel_path[len(prefix):]
# ensure leading slash
if not rel_sub.startswith(os.sep):
rel_sub = os.sep + rel_sub
jellyfin_path = "/tv" + rel_sub
else:
# fallback: use the path as is
jellyfin_path = full_path
playlist_paths.append(jellyfin_path)
season += 1
return playlist_paths
def write_jellyfin_playlist_xml(paths, output_file, local_title, content_rating, owner_userid=""):
"""
Write a Jellyfin‐style playlist XML with the given file paths.
"""
item = ET.Element("Item")
ET.SubElement(item, "ContentRating").text = content_rating
ET.SubElement(item, "Added").text = datetime.utcnow().strftime("%m/%d/%Y %H:%M:%S")
ET.SubElement(item, "LockData").text = "false"
ET.SubElement(item, "LocalTitle").text = local_title
ET.SubElement(item, "RunningTime").text = "0"
ET.SubElement(item, "OwnerUserId").text = owner_userid
playlist_items = ET.SubElement(item, "PlaylistItems")
for p in paths:
pi = ET.SubElement(playlist_items, "PlaylistItem")
ET.SubElement(pi, "Path").text = p
ET.SubElement(item, "Shares")
ET.SubElement(item, "PlaylistMediaType").text = "Video"
tree = ET.ElementTree(item)
with open(output_file, "wb") as f:
f.write(b"<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>\n")
tree.write(f, encoding="utf-8", xml_declaration=False)
def main():
paths = build_interleaved_playlist(MEDIA_ROOT, SHOWS)
write_jellyfin_playlist_xml(paths, OUTPUT_FILE, LOCAL_TITLE, CONTENT_RATING, OWNER_USERID)
print(f"Wrote playlist with {len(paths)} items to {OUTPUT_FILE}")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment