PageRenderTime 305ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/tvdb_api/tvnamer.py

https://bitbucket.org/fyelles/sick-beard
Python | 340 lines | 292 code | 18 blank | 30 comment | 8 complexity | 9b24a81cffb74fb81acb7a87e1eaf412 MD5 | raw file
  1. #!/usr/bin/env python
  2. #encoding:utf-8
  3. #author:dbr/Ben
  4. #project:tvdb_api
  5. #repository:http://github.com/dbr/tvdb_api
  6. #license:Creative Commons GNU GPL v2
  7. # (http://creativecommons.org/licenses/GPL/2.0/)
  8. """
  9. tvnamer.py
  10. Automatic TV episode namer.
  11. Uses data from www.thetvdb.com via tvdb_api
  12. """
  13. __author__ = "dbr/Ben"
  14. __version__ = "1.1"
  15. import os, sys, re
  16. from optparse import OptionParser
  17. from tvdb_api import (tvdb_error, tvdb_shownotfound, tvdb_seasonnotfound,
  18. tvdb_episodenotfound, tvdb_episodenotfound, tvdb_attributenotfound, tvdb_userabort)
  19. from tvdb_api import Tvdb
  20. config = {}
  21. ### Start user config
  22. # The format of the renamed files (with and without episode names)
  23. config['with_ep_name'] = '%(seriesname)s - [%(seasno)02dx%(epno)02d] - %(epname)s.%(ext)s'
  24. config['without_ep_name'] = '%(seriesname)s - [%(seasno)02dx%(epno)02d].%(ext)s'
  25. # Whitelist of valid filename characters
  26. config['valid_filename_chars'] = """0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@£$%^&*()_+=-[]{}"'.,<>`~? """
  27. # Force the stripping of invalid Windows characters, even if the current
  28. # platform is not detected as Windows
  29. config['force_windows_compliant_filenames'] = False
  30. ### End user config
  31. if sys.platform == "win32" or config['force_windows_compliant_filenames']:
  32. # " * : < > ? | \ are all invalid on Windows
  33. config['valid_filename_chars'] = "".join([x for x in config['valid_filename_chars'] if x not in "\"*:<>?|\\"])
  34. # Regex's to parse filenames with. Each one is a tuple containing a filename parsing
  35. # regex and an episode splitting regex. The filename parsing regex must have 3 groups:
  36. # seriesname, season number and episode numbers. The episode numbers should be splittable
  37. # by the second regex. Use (?: optional) non-capturing groups if you need others.
  38. config['name_parse_multi_ep'] = [
  39. # foo_[s01]_[e01]_[e02]
  40. (re.compile('''^(.+?)[ \._\-]\[[Ss](\d+)\]((?:_\[[Ee]\d+\])+)[^\\/]*$'''),
  41. re.compile('''_\[[Ee](\d+)\]''')),
  42. # foo.1x09x10 or foo.1x09-10
  43. (re.compile('''^(.+?)[ \._\-]\[?([0-9]+)((?:[x-]\d+)+)[^\\/]*$'''),
  44. re.compile('''[x-](\d+)''')),
  45. # foo.s01.e01.e02, foo.s01e01e02, foo.s01_e01_e02, etc
  46. (re.compile('''^(.+?)[ \._\-][Ss]([0-9]+)((?:[\.\-_ ]?[Ee]\d+)+)[^\\/]*$'''),
  47. re.compile('''[\.\-_ ]?[Ee](\d+)''')),
  48. # foo.205 (single eps only)
  49. (re.compile('''^(.+)[ \._\-]([0-9]{1})([0-9]{2})[\._ -][^\\/]*$'''),
  50. re.compile("(\d{2})")),
  51. # foo.0205 (single eps only)
  52. (re.compile('''^(.+)[ \._\-]([0-9]{2})([0-9]{2,3})[\._ -][^\\/]*$'''),
  53. re.compile("(\d{2,3})"))
  54. ]
  55. def findFiles(args, recursive=False, verbose=False):
  56. """
  57. Takes a list of files/folders, grabs files inside them. Does not recurse
  58. more than one level (if a folder is supplied, it will list files within),
  59. unless recurse is True, in which case it will recursively find all files.
  60. """
  61. allfiles = []
  62. for cfile in args:
  63. if os.path.isdir(cfile):
  64. for sf in os.listdir(cfile):
  65. newpath = os.path.join(cfile, sf)
  66. if os.path.isfile(newpath):
  67. allfiles.append(newpath)
  68. else:
  69. if recursive:
  70. if verbose:
  71. print "Recursively scanning %s" % (newpath)
  72. allfiles.extend(
  73. findFiles([newpath], recursive=recursive, verbose=verbose)
  74. )
  75. #end if recursive
  76. #end if isfile
  77. #end for sf
  78. elif os.path.isfile(cfile):
  79. allfiles.append(cfile)
  80. #end if isdir
  81. #end for cfile
  82. return allfiles
  83. #end findFiles
  84. def processSingleName(name, verbose=False):
  85. filepath, filename = os.path.split(name)
  86. filename, ext = os.path.splitext(filename)
  87. # Remove leading . from extension
  88. ext = ext.replace(".", "", 1)
  89. for r in config['name_parse_multi_ep']:
  90. match = r[0].match(filename)
  91. if match:
  92. seriesname, seasno, eps = match.groups()
  93. #remove ._- characters from name (- removed only if next to end of line)
  94. seriesname = re.sub("[\._]|\-(?=$)", " ", seriesname).strip()
  95. allEps = re.findall(r[1], eps)
  96. return{'file_seriesname':seriesname,
  97. 'seasno':int(seasno),
  98. 'epno':[int(x) for x in allEps],
  99. 'filepath':filepath,
  100. 'filename':filename,
  101. 'ext':ext
  102. }
  103. else:
  104. #print "Invalid name: %s" % (name)
  105. return None
  106. #end for r
  107. def processNames(names, verbose=False):
  108. """
  109. Takes list of names, runs them though the config['name_parse'] regexs
  110. """
  111. allEps = []
  112. for f in names:
  113. cur = processSingleName(f, verbose=verbose)
  114. if cur is not None:
  115. allEps.append(cur)
  116. return allEps
  117. #end processNames
  118. def formatName(cfile):
  119. """
  120. Takes a file dict and renames files using the configured format
  121. """
  122. if cfile['epname']:
  123. n = config['with_ep_name'] % (cfile)
  124. else:
  125. n = config['without_ep_name'] % (cfile)
  126. #end if epname
  127. return n
  128. #end formatName
  129. def cleanName(name):
  130. """
  131. Cleans the supplied filename for renaming-to
  132. """
  133. name = name.encode('ascii', 'ignore') # convert unicode to ASCII
  134. return ''.join([c for c in name if c in config['valid_filename_chars']])
  135. #end cleanName
  136. def renameFile(oldfile, newfile, force=False):
  137. """
  138. Renames files, does not overwrite files unless forced
  139. """
  140. new_exists = os.access(newfile, os.F_OK)
  141. if new_exists:
  142. sys.stderr.write("New filename already exists.. ")
  143. if force:
  144. sys.stderr.write("overwriting\n")
  145. os.rename(oldfile, newfile)
  146. else:
  147. sys.stderr.write("skipping\n")
  148. return False
  149. #end if force
  150. else:
  151. os.rename(oldfile, newfile)
  152. return True
  153. #end if new_exists
  154. def processFile(t, opts, cfile):
  155. try:
  156. # Ask for episode name from tvdb_api
  157. epname = t[ cfile['file_seriesname'] ][ cfile['seasno'] ][ cfile['epno'] ]['episodename']
  158. except tvdb_shownotfound:
  159. # No such show found.
  160. # Use the show-name from the files name, and None as the ep name
  161. sys.stderr.write("! Warning: Show \"%s\" not found (for file %s.%s)\n" % (
  162. cfile['file_seriesname'],
  163. cfile['filename'],
  164. cfile['ext'])
  165. )
  166. cfile['seriesname'] = cfile['file_seriesname']
  167. cfile['epname'] = None
  168. except (tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_attributenotfound):
  169. # The season, episode or name wasn't found, but the show was.
  170. # Use the corrected show-name, but no episode name.
  171. sys.stderr.write("! Warning: Episode name not found for %s (in %s)\n" % (
  172. cfile['file_seriesname'],
  173. cfile['filepath'])
  174. )
  175. cfile['seriesname'] = t[ cfile['file_seriesname'] ]['seriesname']
  176. cfile['epname'] = None
  177. except tvdb_error, errormsg:
  178. # Error communicating with thetvdb.com
  179. sys.stderr.write(
  180. "! Warning: Error contacting www.thetvdb.com:\n%s\n" % (errormsg)
  181. )
  182. cfile['seriesname'] = cfile['file_seriesname']
  183. cfile['epname'] = None
  184. except tvdb_userabort, errormsg:
  185. # User aborted selection (q or ^c)
  186. print "\n", errormsg
  187. sys.exit(1)
  188. else:
  189. cfile['epname'] = epname
  190. cfile['seriesname'] = t[ cfile['file_seriesname'] ]['seriesname'] # get the corrected seriesname
  191. # Format new filename, strip unwanted characters
  192. newname = formatName(cfile)
  193. newname = cleanName(newname)
  194. # Append new filename (with extension) to path
  195. oldfile = os.path.join(
  196. cfile['filepath'],
  197. cfile['filename'] + "." + cfile['ext']
  198. )
  199. # Join path to new file name
  200. newfile = os.path.join(
  201. cfile['filepath'],
  202. newname
  203. )
  204. # Show new/old filename
  205. print "#" * 20
  206. print "Old name: %s" % (cfile['filename'] + "." + cfile['ext'])
  207. print "New name: %s" % (newname)
  208. # Either always rename, or prompt user
  209. if opts.always or (not opts.interactive):
  210. rename_result = renameFile(oldfile, newfile, force=opts.force)
  211. if rename_result:
  212. print "..auto-renaming"
  213. else:
  214. print "..not renamed"
  215. #end if rename_result
  216. return # next filename!
  217. #end if always
  218. ans = None
  219. while ans not in ['y', 'n', 'a', 'q', '']:
  220. print "Rename?"
  221. print "([y]/n/a/q)",
  222. try:
  223. ans = raw_input().strip()
  224. except KeyboardInterrupt, errormsg:
  225. print "\n", errormsg
  226. sys.exit(1)
  227. #end try
  228. #end while
  229. if len(ans) == 0:
  230. print "Renaming (default)"
  231. rename_result = renameFile(oldfile, newfile, force=opts.force)
  232. elif ans[0] == "a":
  233. opts.always = True
  234. rename_result = renameFile(oldfile, newfile, force=opts.force)
  235. elif ans[0] == "q":
  236. print "Aborting"
  237. sys.exit(1)
  238. elif ans[0] == "y":
  239. rename_result = renameFile(oldfile, newfile, force=opts.force)
  240. elif ans[0] == "n":
  241. print "Skipping"
  242. return
  243. else:
  244. print "Invalid input, skipping"
  245. #end if ans
  246. if rename_result:
  247. print "..renamed"
  248. else:
  249. print "..not renamed"
  250. #end if rename_result
  251. #end processFile
  252. def main():
  253. parser = OptionParser(usage="%prog [options] <file or directories>")
  254. parser.add_option("-d", "--debug", action="store_true", default=False, dest="debug",
  255. help="show debugging info")
  256. parser.add_option("-b", "--batch", action="store_false", dest="interactive",
  257. help="selects first search result, requires no human intervention once launched", default=False)
  258. parser.add_option("-i", "--interactive", action="store_true", dest="interactive", default=True,
  259. help="interactivly select correct show from search results [default]")
  260. parser.add_option("-s", "--selectfirst", action="store_true", dest="selectfirst", default=False,
  261. help="automatically select first series search result (instead of showing the select-series interface)")
  262. parser.add_option("-r", "--recursive", action="store_true", dest="recursive", default=True,
  263. help="recursivly search supplied directories for files to rename")
  264. parser.add_option("-a", "--always", action="store_true", default=False, dest="always",
  265. help="always renames files (but still lets user select correct show). Can be changed during runtime with the 'a' prompt-option")
  266. parser.add_option("-f", "--force", action="store_true", default=False, dest="force",
  267. help="forces file to be renamed, even if it will overwrite an existing file")
  268. opts, args = parser.parse_args()
  269. if len(args) == 0:
  270. parser.error("No filenames or directories supplied")
  271. #end if len(args)
  272. allFiles = findFiles(args, opts.recursive, verbose=opts.debug)
  273. validFiles = processNames(allFiles, verbose=opts.debug)
  274. if len(validFiles) == 0:
  275. sys.stderr.write("No valid files found\n")
  276. sys.exit(2)
  277. print "#" * 20
  278. print "# Starting tvnamer"
  279. print "# Processing %d files" % (len(validFiles))
  280. t = Tvdb(debug=opts.debug, interactive=opts.interactive, select_first=opts.selectfirst)
  281. print "# ..got tvdb mirrors"
  282. print "# Starting to process files"
  283. print "#" * 20
  284. for cfile in validFiles:
  285. print "# Processing %(file_seriesname)s (season: %(seasno)d, episode %(epno)d)" % (cfile)
  286. processFile(t, opts, cfile)
  287. print "# Done"
  288. #end main
  289. if __name__ == "__main__":
  290. main()