2013-06-12, 07:18
Good luck! Also make sure to review the original post, there have been quite a few changes since you last used the script.
usage: episode_renamer.py [-h] [--debug] [--dry] [--skip]
[--num-matches NUM_MATCHES] [--copy]
[series] [csv_file] [input_dir] [output_dir]
Renames episodes in a directory according to a CSV file from the TVDB.
positional arguments:
series Name of series.
csv_file CSV file containing episode name and number.
input_dir Directory containing episodes.
output_dir Directory to place renamed episodes in.
optional arguments:
-h, --help show this help message and exit
--debug Print debugging output.
--dry Print what we would do, but do not do it.
--skip Skip non-matching episodes instead of interactively
renaming them.
--num-matches NUM_MATCHES
When interactively renaming, show this many close
matches.
--copy Copy files instead of renaming them.
#!/usr/bin/env python
# Episode Name - File Renamer
# Renames files without accurate episode order using the Episode Name only
# Originally coded by: Tim
# Cleaned and modified by: Kyle
# Import modules
from argparse import ArgumentParser, ArgumentTypeError
import os.path
import csv
import re
import errno
import unicodedata
import shutil
def getLevenshteinValue(a, b):
'''
The Levenshtein distance is defined as the minimal number of characters
you have to replace, insert or delete to transform one string into
another one. We use the same concept here to compare two lists of
sorted values to see how far off one is from the other.
See: http://en.wikipedia.org/wiki/Levenshtein_distance
'''
len_a = len(a)
len_b = len(b)
d = []
for i in xrange(len_a+1):
d.append([0] * (len_b+1))
for i in xrange(len_a+1):
d[i][0] = i
for j in xrange(len_b+1):
d[0][j] = j
for j in xrange(1, len_b+1):
for i in xrange(1, len_a+1):
if a[i-1] == b[j-1]:
d[i][j] = d[i-1][j-1]
else:
d[i][j] = min(d[i-1][j] + 1,
d[i][j-1] + 1,
d[i-1][j-1] + 1)
return d[len_a][len_b]
def cleanName(s):
"""
Strip the invalid filename characters from the string selected.
Feel free to add/remove additional .replace(X,X) as needed if you
want to remove other characters even if they are valid.
For example: , or [ or !
"""
s = s.replace("?","")
s = s.replace(":","")
s = s.replace("*","")
s = s.replace("<","")
s = s.replace(">","")
s = s.replace("|","")
s = s.replace("/","")
s = s.replace("\\","")
s = s.replace('"',"")
return s
def parseSeason(l):
"""
Takes the first cell of the CSV copied from the TVDB website
and strips out only the season.
"""
if l == "Special":
season = "00"
else:
season = l.split(" ")[0].zfill(2)
return season
def parseEpisode(l):
"""
Takes the first cell of the CSV copied from the TVDB website
and strips out only the episode. Pads a 0 before single digits.
"""
if l == "Special":
episode = "00"
else:
episode = l.split(" ")[-1].zfill(2)
return episode
def getEpisodeFiles(input_dir):
"""
Recursively iterate over the input directory, returning any files found.
"""
all_files = []
for root, _dirs, files in os.walk(input_dir):
for f in files:
all_files.append(os.path.join(root, f))
return all_files
def cleanEmptyDirectories(directory):
for root, dirs, files in os.walk(directory, topdown=False):
if not dirs and not files:
print 'Removing empty: %s' % root
os.rmdir(root)
def getEpisodeNames(csv_file):
"""
Returns dictionary of clean episode name -> (clean name, season, episode).
"""
episode_names = {}
first = True
with open(args.csv_file) as f:
reader = csv.reader(f)
for line in reader:
if first:
first = False
continue
line = [unicodedata.normalize('NFC', unicode(e, 'utf-8')) for e in line]
debug('Original: %s' % ','.join(line))
season = int(parseSeason(line[1]))
episode = int(parseEpisode(line[1]))
clean_name = cleanName(line[0])
debug('Parsed: %s season: %d episode: %d' % (clean_name, season, episode))
name_re = re.compile(clean_name, re.IGNORECASE | re.UNICODE)
episode_names[name_re] = (clean_name, season, episode)
return episode_names
def findMatch(episode_names, name):
while True:
for name_re in episode_names:
if name_re.search(name):
return episode_names[name_re]
if not args.skip:
# Get close matches
print
close_matches = [(getLevenshteinValue(n.pattern, name), n.pattern) for n in episode_names]
close_matches.sort()
print '"%s" not found; %d closest matches are:' % (name, args.num_matches)
for i, (value, close_match) in enumerate(close_matches[:args.num_matches]):
i += 1
print '%d. "%s" (%d)' % (i, close_match, value)
name = unicode(raw_input('Try (blank to skip): '), 'utf-8')
name = unicodedata.normalize('NFC', name)
if not name:
return None
try:
n = int(name)
_, name = close_matches[n-1]
except ValueError:
pass
else:
return None
def ensureExists(directory):
try:
os.makedirs(directory)
except OSError as e:
if e.errno != errno.EEXIST:
raise
def ensureArguments():
if args.series is None:
args.series = raw_input('Series name: ')
if args.csv_file is None:
while True:
try:
args.csv_file = existingFileType(raw_input('CSV file: '))
break
except ArgumentTypeError as e:
print str(e)
if args.input_dir is None:
while True:
try:
args.input_dir = existingDirectoryType(raw_input('Directory containing episodes: '))
break
except ArgumentTypeError as e:
print str(e)
if args.output_dir is None:
args.output_dir = raw_input('Destination directory: ')
def expandPath(path):
path = os.path.expanduser(path)
return os.path.expandvars(path)
def existingFileType(argument):
argument = expandPath(argument)
if not os.path.isfile(argument):
raise ArgumentTypeError('%s does not exist!' % argument)
return os.path.abspath(argument)
def existingDirectoryType(argument):
argument = expandPath(argument)
if not os.path.isdir(argument):
raise ArgumentTypeError('%s does not exist!' % argument)
return os.path.abspath(argument)
if __name__ == '__main__':
parser = ArgumentParser(description='Renames episodes in a directory according to a CSV file from the TVDB.')
parser.add_argument('series', nargs='?', help='Name of series.')
parser.add_argument('csv_file', nargs='?', type=existingFileType, help='CSV file containing episode name and number.')
parser.add_argument('input_dir', nargs='?', type=existingDirectoryType, help='Directory containing episodes.')
parser.add_argument('output_dir', nargs='?', help='Directory to place renamed episodes in.')
parser.add_argument('--debug', action='store_true', help='Print debugging output.')
parser.add_argument('--dry', action='store_true', help='Print what we would do, but do not do it.')
parser.add_argument('--skip', action='store_true', help='Skip non-matching episodes instead of interactively renaming them.')
parser.add_argument('--num-matches', default=5, type=int, help='When interactively renaming, show this many close matches.')
parser.add_argument('--copy', action='store_true', help='Copy files instead of renaming them.')
args = parser.parse_args()
if args.debug:
def debug(s):
print str(s)
else:
debug = lambda s: None
ensureArguments()
print 'Episode Renamer'
print '==============='
debug('Arguments: %s' % str(args))
print 'Renaming: %s' % args.series
print 'In: %s' % args.input_dir
print 'Using: %s' % args.csv_file
print 'To: %s' % args.output_dir
print
print 'Loading episode information ...'
episode_names = getEpisodeNames(args.csv_file)
num_renames = num_skipped = 0
for full_filename in getEpisodeFiles(args.input_dir):
base_filename = os.path.basename(full_filename)
base_name, extension = os.path.splitext(base_filename)
clean_base_name = cleanName(base_name)
name_match = findMatch(episode_names, clean_base_name)
if not name_match:
print 'skipping %s' % full_filename
num_skipped += 1
continue
episode_name, season, episode_number = name_match
full_episode_name = 'S%dE%d %s' % (season, episode_number, episode_name)
base_output_name = args.series + ' ' + full_episode_name + extension
full_output_name = os.path.join(args.output_dir, 'Season %d' % season, base_output_name)
num_renames += 1
print full_output_name
if not args.dry:
output_dir = os.path.dirname(full_output_name)
ensureExists(output_dir)
if args.copy:
shutil.copy2(full_filename, full_output_name)
else:
os.rename(full_filename, full_output_name)
print 'Renamed %d file(s). (Skipped %d)' % (num_renames, num_skipped)
if not args.dry:
cleanEmptyDirectories(args.input_dir)
# Episode Name - File Renamer
# Renames files without accurate episode order using the Episode Name only
# Coded by: Tim.
# Import modules
import os
import glob
import csv
# Assign inital values
repeat = "true"
edit = "true"
#Define custom functions
def invalid_char(s):
"""
Strip the invalid filename characters from the string selected.
Feel free to add/remove additional .replace(X,X) as needed if you
want to remove other characters even if they are valid.
For example: , or [ or !
"""
return s.replace("?","").replace(":","").replace("*","").replace("<","").replace(">","").replace("|","").replace("/","").replace("\\","").replace('"',"")
def season(l):
"""
Takes the first cell of the CSV copied from the TVDB website
and strips out only the season.
"""
if l == "Special":
season = "00"
else:
season = l.split(" ")[0].zfill(2)
return season
def episode(l):
"""
Takes the first cell of the CSV copied from the TVDB website
and strips out only the episode. Pads a 0 before single digits.
"""
if l == "Special":
episode = "00"
else:
episode = l.split(" ")[-1].zfill(2)
return episode
# Overall loop, allows user to re-run the entire script
while repeat == "true":
# Checks if the user defined variables need to be edited
if edit == "true":
# Prompt user to define static variables
series_name = input("Please enter your series name: ")
#series_name = "Charlie Brown"
print("\n")
data = input("Path to CSV: ")
#data = "C:\lt\cb.csv"
print("\n")
dir1 = input("Path to episodes (format C:\*): ")
#dir1 = "M:\TV Shows\CB\all\*"
print("\n")
move = input("Would you like to move renamed files? (Yes/No): ").lower()
if move in ("y", "ye", "yes"):
print("\n")
print("Enter path to root folder where files should be moved")
move_path = input("and season folders will be created (format C:\Show\): ")
edit = "false"
file_list = glob.glob(dir1)
print ("\n\n")
# Loop through file_list and look for matches in the CSV to the filename after the prefix assigned
for file in file_list:
fname = file
ext = fname[-4:]
with open(data, 'r') as file:
reader = csv.reader(file)
season_episode_name = ["S" + season(line[0]) + "E" + episode(line[0]) + " " + invalid_char(line[1]) for line in reader if invalid_char(line[1].lower()) in fname.lower() and line[1].lower() != ""]
season_dir = (''.join(season_episode_name)).split("E")[0][1:]
if season_episode_name:
season_episode_name = ''.join(season_episode_name)
fname2 = dir1[:-1] + series_name + " " + season_episode_name + ext
# If user chose to move files to another directory, then fname2 has the path replaced
if move in ("y", "ye", "yes"):
fname2 = move_path + "Season " + season_dir + "\\" + fname2[len(dir1[:-1]):]
# Generates the season directory if does not already exist
if not os.path.exists(move_path + "Season " + season_dir):
os.makedirs(move_path + "Season " + season_dir)
# Rename file and move if user requested
print(fname + "\n >>> " + fname2 + "\n")
os.rename(fname, fname2)
else:
print(fname + "\n >>> " "Unable to locate match, please rename manually.\n")
# Check if user wants to repeat, edit or exit
repeat = input ("\n\nType R to run again, Type E to edit the parameters, or Q to quit: ")
if repeat.lower() in ("r", "retry"):
repeat = "true"
print("\nRunning again with the same parameters..")
elif repeat.lower() in ("e", "edit"):
edit = "true"
repeat = "true"
print("\nEditing paramaters before running again..")
elif repeat.lower() in ("q", "quit"):
repeat = "false"
print("\nQuitting...")
else:
repeat = "false"
print("\nInvalid command.")
# When repeat no longer is "true" the script exiits with the below message
else:
input("\n\nPress enter to exit...")
(2013-05-22, 06:27)Tim. Wrote: What you need to do:old thread, still very useful,
- Make a copy of your original Looney Tunes folder. Always good to start by making a backup! I renamed my folder from ‘Looney Tunes’ to ‘lt’ to make it easier to type.
- Move all the episodes into one folder (the script does not scan multiple or recursive folders) for easier renaming.
- Double click on the python file you downloaded from this thread.
- You will be prompted for the series name. In this case: Looney Tunes
- Enter the path to the CSV you downloaded from this thread. For example: C:\download\lt.csv
- Enter the path to your COPY of the episodes (to be safe). For example: C:\lt\disc1\*
- If you want to move the files while renaming (useful for the 300+ episodes in the Looney Tunes Golden Collection!) type "y" or "yes"
- If you chose to rename your files, enter the path to your normal show directory For example C:\Looney Tunes\
- Done!