-
Notifications
You must be signed in to change notification settings - Fork 0
/
rename.py
310 lines (288 loc) · 11.3 KB
/
rename.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
from pytvdbapi import api;
import os.path;
import re;
import shutil;
import pickle;
import sys;
#Cleans a string of chars which cannot be in a windows file path
def cleanString( inp ):
inp = str(inp).replace('\\',', ').replace('/',', ').replace(':',', ').replace('*','').replace('?','').replace('"','').replace('<','(').replace('>',')').replace('|','');
return inp;
#Cleans removes non standards symbols from show names before db search
def cleanShowName( inp ):
inp = str(inp).replace('-',' ').replace('.',' ');
return inp;
#Reads a plaintext dictionary from the file specified by arg
def readDictionary ( inp ):
rtn = {};
try:
for tln in open(inp):
tln = tln.strip('\n').strip('\r');
ln = tln.split('=');
if len(ln)==2:
rtn[ln[0]]=ln[1];
else:
print("Err unable to parse replacement: %s" % (tln));
except FileNotFoundError:
print("Unable to load dictionary: %s"%(inp))
pass;
return rtn;
#def writeDictionary ( outDictionary, filePath ):
# with open(filePath, 'wb') as outFile:
# for key in outDictionary:
# outFile.write("%s=%s\n" % (key, outDictionary[value]));
#Reads a plaintext array from the file specified by arg
def readList ( inp ):
rtn = [];
try:
for tln in open(inp):
rtn.append(tln.strip('\n').strip('\r'));
except FileNotFoundError:
pass;
return rtn;
def writeList ( outList, filePath ):
with open(filePath, 'w') as outFile:
for i in outList:
outFile.write("%s\n" % i);
def tvdbsearch (showName):
#Basic loop to give 5 attempts to TVDB api before crashing
showResults = []
retries = 0;
hasResult = False;
while not(hasResult):
try :
showResults = tvdb.search(showName, 'en')
except :
retries+=1;
print("\rAttempt %d/5 failed!" %(retries));
if retries<5:
continue
else:
print("\rtvdb.search() failed after 5 retries!");
raise
print("\r");
hasResult = True;
return showResults;
#TVDB returns strings in unicode, this causes crashes where certain foreign characters are found
def stripUnicode (inp):
return (b"".join([x.encode('ascii', 'replace') for x in inp])).decode()
#Attempts to turn arg showName into a valid show name recognised by tvdb
#Returns emptystring on failure
def findShow ( showName, isSilent ):
showResults = tvdbsearch(showName)
if len(showResults)!=1:
while len(showResults)!=1:
#If silent, exit with failure
if isSilent or showName == "":
return "";
#If no results, ask for new term and rerun search
elif len(showResults) == 0:
print("'%s' does not resolve to any results, please enter an alternate search.\n Blank search will skip file." %(showName))
showName = input("Search term: ")
showResults = tvdbsearch(showName)
#If multiple results, prompt the user to select the desired
elif len(showResults) > 1:#Could also check result 1 isn't identical to cleaned show name
print("'%s' has multiple results, please select desired result." %(showName))
for i in range(0,len(showResults)):
print("%d: %s" % (i, stripUnicode(showResults[i].SeriesName)))
print("x: Perform new search")
print("c: Cancel and skip file")
while len(showResults) > 1:
option = input("Enter desired option: ").strip().lower();
if option=='x':
showResults=[]
elif option=='c':
showName = ""
showResults=[]
elif option.isdigit() and int(option) < len(showResults):
showResults=[showResults[int(option)]]
return showResults[0];
#Config
runSilent = False;
runRecursive = False;
runInplace = False;
videoRoot = ".";
videoDest = "";
videoTypes = [".mp4", ".avi", ".mkv", ".wmv"];
files = [];
dropShows = [];
def printHelp():
print("Usage:");
print("rename.py [args] <sourcePath>");
print("Args:");
print("-silent, -s: Suppress output");
print("-recurse, -r: Recursively find files in source path");
print("-inplace, -i: Rename files inplace, not compatible with -dest or -d");
print("-dest <destPath>, -d <destPath>: Specify a destination path (else source will be used)");
print("-drop <showName>: Specify a showname to be dropped from namePairs");
exit();
#Process Args
if len(sys.argv)>1:
i = 0;
while i < len(sys.argv)-2:
i=i+1;
if sys.argv[i]=="-help" or sys.argv[i]=="-?":
printHelp();
elif sys.argv[i]=="-silent" or sys.argv[i]=="-s":
runSilent = True;
elif sys.argv[i]=="-recurse" or sys.argv[i]=="-r":
runRecursive=True;
elif sys.argv[i]=="-inplace" or sys.argv[i]=="-i":
runInplace=True;
elif sys.argv[i]=="-dest" or sys.argv[i]=="-d":
i=i+1;
if i < (len(sys.argv)-1):
videoDest=sys.argv[i];
else:
print("-dest, -d args require a trailing arg");
printHelp();
elif sys.argv[i]=="-drop":
i=i+1;
if i < (len(sys.argv)-1):
dropShows.append(sys.argv[i]);
else:
print("-drop args require a trailing arg");
printHelp();
else:
print("Arg '%s' was not recognised."%(sys.argv[i]));
printHelp();
else:
printHelp();
videoRoot = sys.argv[len(sys.argv)-1];
#Block bad args
if runInplace and videoDest!="":
print("-inplace,-i and -dest,-d args are incompatible");
printHelp();
if videoDest=="":
videoDest=videoRoot;
#Regexes
showRegex = re.compile('^(.+?([. ][0-9]{4})?)[ .]-?[ .]?(s(eason[. ]?)?)?([0-9]{1,2})[ x.]?(e(pisode[. ]?)?)?([0-9]{2})[^p]', re.IGNORECASE);
typeRegex = re.compile('\.([a-zA-Z0-9]+)$', re.IGNORECASE);
#Build list 'files' of all video files to be handled
for root, directories, filenames in os.walk(videoRoot):
for filename in filenames:
for type in videoTypes:
if filename.lower().endswith(type):
files.append(os.path.join(root,filename));
continue;
#If not recursive, skip others
if not runRecursive:
break;
if not(runSilent):
print("%d video files found for processing." % (len(files)));
tvdb = api.TVDB('A0857036BEACEE1A');
# tvdb = api.TVDB('9E8E50DD-AF0C-45D5-BDC4-2A4BFEBEA36D');
namePairs = {}
if os.path.exists('namePairs.cfg'):
namePairsF = open('namePairs.cfg', 'rb');
namePairs = pickle.load(namePairsF);
# Drop requested shows
for show in dropShows:
showName = cleanShowName(show);
if showName in namePairs:
print("Confirm that show '%s' should be dropped."%(show));
option = input("Confirm [Y/N]: ").strip().lower();
if option=='y':
del namePairs[showName];
elif showName.lower() in namePairs:
print("Confirm that show '%s' should be dropped."%(show));
option = input("Confirm [Y/N]: ").strip().lower();
if option=='y':
del namePairs[showName.lower()];
else:
print("Show '%s' was not found to be dropped."%(show));
replacements = readDictionary('replacements.cfg');
skip = readList('skip.cfg');
for file in files:
filename = os.path.basename(file);
#print(filename)
#Skip file if in skiplist
if any(filename in s for s in skip):
continue;
type = typeRegex.search(filename).group(0);
show = showRegex.match(filename);
if not(show):
if not(runSilent):
print("Cannot detect show details in file: %s" % (filename));
#Show skipped, add to skiplist
skip.append(filename);
else:
showName = cleanShowName(show.group(1)).strip().lower();
showSeasonNo = int(show.group(5));
showEpisodeNo = int(show.group(8));
#print("%s - s%se%s - null" % (showName, showSeasonNo, showEpisodeNo))
if not(showName in namePairs):
showNameConfirmed = findShow(showName,runSilent);
if showNameConfirmed=="":
#Show skipped, add to skiplist
skip.append(filename);
continue;
namePairs[showName]=showNameConfirmed;
else:
#Update show stored in namePairs (could optionally only run this if episode missing)
try:
namePairs[showName].update();
except:
print("Failed to update show: %s with DB." %(showName));
#Show is now the desired show, so we can pull episode info
show = namePairs[showName];
showName = stripUnicode(show.SeriesName);
#If show name is a list (seems to occur when showname contains '|')
#Concat the elements
if isinstance(showName, (list,)):
showName = ', '.join(showName);
#Replace show name if desired
if showName in replacements:
showName = replacements[showName];
try:
showSeason = show[showSeasonNo];
except:
print("'%s' season %d was not found, skipping file." %(showName,showSeasonNo));
continue;
try:
showEpisode = showSeason[showEpisodeNo];
except:
print("'%s' s%02de%02d has invalid episode no, skipping file." %(showName,showSeasonNo, showEpisodeNo));
continue;
#Create new file name ~/Show Name/Show Name - sXXeYY - Episode Name.type
#runInplace?videoRoot:videoDest;
newFilePath = videoRoot if runInplace else videoDest;
#Ensure the root ends with a slash
if not(newFilePath.endswith(os.path.sep)):
newFilePath += os.path.sep;
if not runInplace:
#Append the show name as a directory
newFilePath += cleanString(showName);
newFilePath += os.path.sep;
#Attempt to create that directory
if not os.path.exists(newFilePath):
os.mkdir(newFilePath);
#Append the show name as part of the file name
newFilePath += cleanString(showName);
newFilePath += ' - ';
#Append the season number
newFilePath += 's';
newFilePath += format(showEpisode.SeasonNumber, '02');
#Append the episode number
newFilePath += 'e'
newFilePath += format(showEpisode.EpisodeNumber, '02');
newFilePath += ' - ';
#Append episode name
newFilePath += cleanString(stripUnicode(showEpisode.EpisodeName));
#Append file type
newFilePath += type;
if file != newFilePath and not(runSilent):
print(file.encode("utf-8"));
print(newFilePath.encode("utf-8"));
#Move file
try:
shutil.move(file, newFilePath);
except PermissionError:
print("Cannot update file %s, access denied." % (file.encode("utf-8")));
#End loop - Next file
#Cleanup any empty directories, nfos
#Save skiplist
writeList(skip,'skip.cfg');
#Save namePairs
namePairsF = open('namePairs.cfg', 'wb');
pickle.dump(namePairs, namePairsF);