PLEX and Jellyfin Sort Script
Below is a script that I wrote a few years ago. I got tired of sorting television shows into the file hierarchy that
both PLEX and Jellyfin need in order to properly index and discover media. The script can be found below and the full
project files and usage can be found on my GitHub.
This script is written in Python3, but was originally written in Python2 and ported using a Python3 porting function. You'll see
very quickly that I am not a professional programmer. I'm am capable of hacking things to together to form a functional app that at the end of
the day still works and has been reliably used for years with very few edge case exceptions that I've had to adjust filenames.
Simply execute the Python3 script replacing your desired target and destination directories and the script will not only organize
but build-out the recommended file hierarchy that both PLEX and Jellyfin prefer for media discovery. It is very helpful if you are downloading or
ripping your files already in the stand TV show file format TVSHOW.S01E01.Episode.Name.mp4 and handles video containers MP4, MKV, AVI, or whatever
else you designate it to look for.
$ tvsorter3.py -d /staging_directory/unsorted/ -t /target_directory/tv/ -sv --auto
#!/usr/bin/python3
import sys
import getopt
import os
import shlex
import shutil
import re
import subprocess
import datetime
def prRed(prt):
print("\033[91m%s\033[00m" % prt)
def prBlue(prt):
print("\033[94m%s\033[00m" % prt)
def prGreen(prt):
print("\033[92m%s\033[00m" % prt)
def header():
#print("TV SHOW SORTER")
print("""
_______ _______ __
/_ __/ | / / ___/____ _____/ /____ _____
/ / | | / /\__ \/ __ \/ ___/ __/ _ \/ ___/
/ / | |/ /___/ / /_/ / / / /_/ __/ /
/_/ |___//____/\____/_/ \__/\___/_/
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
""")
def debug():
print("Number of arguments: ", len(sys.argv), 'arguments.')
print("Argument List:", str(sys.argv))
def logW(m):
now = datetime.datetime.now()
logdatetime = str(now.strftime("%Y-%m-%d--%H:%M:%S"))
filedate = str(now.strftime("%Y-%m-%d"))
LOGFILE = "/tmp/" + filedate + "-sort.log"
log = open(LOGFILE, 'a')
logmsg = logdatetime + " -- : " + m
log.write(logmsg)
log.write("\n")
log.close
def validate_init(directory,target):
print("\nChecking [%s] directory..." % directory)
if os.path.exists(directory):
print("\033[92m✓\033[00m OS Path specificed [%s] exists." % directory)
else:
print("\033[91mX\033[00m OS Path specified [%s] does not exist." % directory)
sys.exit(0)
print("\nChecking [%s] directory..." % target)
if os.path.exists(target):
print("\033[92m✓\033[00m OS Path specificed [%s] exists." % target)
else:
print("\033[91mX\033[00m OS Path specificed [%s] does not exist." % target)
file_list = os.listdir(directory)
print("")
print("\033[90mValidating file types of files found in:\033[00m %s" % (directory))
print("\nType:\tValid\tFilename:")
for f in file_list:
fextention = f.strip()[-3:].upper()
if f.endswith(('.avi','.mkv','.mp4')):
print("%s\t\033[92mVALID\033[00m\t%s%s" % (fextention,directory,f))
else:
print("%s\t\033[91mIGNORE\033[00m\t%s%s" % (fextention,directory,f))
print("")
def validate_dest(target):
if os.path.exists(target):
return True
else:
yes = {'yes','y', ''}
no = {'no','n'}
quit = {'quit','q'}
choice = input("\t\tCreate new directory? ([Y]es/[N]o/[Q]uit)\n\t\t# ").lower()
if choice in quit:
clean_exit()
if choice in yes:
try:
os.makedirs(target)
msg = "***CREATED \"" + target + "\" DIRECTORY***"
logW(msg)
print("\t\t\033[92mCreated directory: %s\033[00m\n" % (target))
except OSError:
if os.path.exists(target):
pass
else:
raise
if os.path.exists(target):
return True
else:
return False
def move_files(source_file,full_dest,f,dest,AUTO):
s = source_file
d = full_dest
QUIT = False
while QUIT == False:
yes = {'yes','y', ''}
no = {'no','n'}
quit = {'quit','q'}
if AUTO == False:
print("Suggested:\tMove \033[94m%s\033[00m to \033[94m%s\033[00m ?" % (f,dest))
print("")
choice = input("\t\tProceed with moving file into target directory? ([Y]es/[N]o/[Q]uit)\n\t\t# ").lower()
if choice in yes:
shutil.move(s,d)
msg = "moved %s to %s" % (s,d)
logW(msg)
print("\n\t\tMoved %s \n\t\t\tto\t\t\n \t\t%s" % (s,d))
return True
elif choice in no:
print("\t\tIgnoring file... moving on to next valid file")
return False
elif choice in quit:
QUIT = True
clean_exit()
else:
sys.stdout.write("Please respond with 'yes' or 'no' or 'quit'")
else:
shutil.move(s,d)
print("\n\t\tMoved %s \n\t\t\tto\t\t\n \t\t%s" % (s,d))
msg = "moved %s to %s" % (s,d)
logW(msg)
return True
def sort_files(directory,target,AUTO):
file_list = os.listdir(directory)
for f in file_list:
if f.endswith(('.avi','.mkv','.mp4', '.mpg')):
# Regex for TV Show Pattern Matching - this is the magic
tv = re.findall(r"(.*?)[ |.]S([\d+]{1,2})E([\d+]{1,2})[ |.]([\d+]{3,4}p|)", f, re.I)
for show in tv:
sname = show[0]
season = show[1]
episode = show[2]
source_file = directory + f
d = target + sname.replace("."," ").title() + "/" + "Season " + season + "/"
full_dest = d + f
print("")
print("Source File:\t%s" % (source_file))
print("Destination:\t%s" % (d))
DEST_EXIST = validate_dest(d)
if DEST_EXIST == True:
print("Dir Exists?:\t\033[92mTarget Folder Exists\033[00m")
move_files(source_file,full_dest,f,d,AUTO)
else:
print("Dir Exists?:\t\033[91mTarget Folder Doesn't Exist. Skipping...\033[00m")
print("")
def fail_out(err_msg):
msg = "Something went wrong: " + err_msg
prRed(msg)
sys.exit(0)
def clean_exit():
print("\n\n\tQuitting - Goodbye :)")
sys.exit(0)
def main(argv):
directory = ''
target = ''
logfile = ''
VALIDATE = False
SORT = False
DEBUG = False
AUTO = False
header()
try:
opts, args = getopt.getopt(argv,"hd:t:vsa",["directory=","target=", "validate","sort","auto"])
except getopt.GetoptError:
print(str(sys.argv))
print("%s -d -t " % sys.argv[0])
sys.exit(2)
for opt, arg in opts:
if opt == "-h":
print("%s -d -t " % sys.argv[0])
sys.exit()
if opt in ("-d", "--directory"):
if os.path.exists(arg):
if arg.strip()[-1] == "/":
directory = arg
else:
directory = arg + "/"
else:
err_msg = "OS Path " + arg + " specified does not exist."
fail_out(err_msg)
if opt in ("-t","--target"):
if os.path.exists(arg):
if arg.strip()[-1] == "/":
target = arg
else:
target = arg + "/"
else:
err_msg = "OS Path " + arg + " specified does not exist."
fail_out(err_msg)
if opt in ("-v","--validate"):
VALIDATE = True
if opt in ("-s","--sort"):
print("Sorting directory")
SORT = True
if opt in ("-a","--auto"):
print("Auto sort selected")
AUTO = True
if DEBUG == True:
debug()
if VALIDATE == True:
validate_init(directory,target)
if SORT == True:
sort_files(directory,target,AUTO)
print("\n\n")
prGreen("Complete")
print("\n\n")
if __name__ == '__main__':
main(sys.argv[1:])
