Created
November 3, 2025 23:03
-
-
Save hortonew/da79fd9b1fb790eb10be597ad953a7d3 to your computer and use it in GitHub Desktop.
Interleave Shows for Jellyfin Playlist
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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