main.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. # Imports
  2. import re
  3. import os
  4. import pathlib
  5. import argparse
  6. from datetime import datetime
  7. import tvdb_v4_official
  8. # Default config
  9. default_apikey = os.path.expanduser('~/.tvdb_apikey')
  10. default_lang = 'eng'
  11. default_order = 'default'
  12. # Local helpers
  13. def readConfigLine(file, opt=False):
  14. value = None
  15. try:
  16. with open(file, 'r') as f:
  17. for line in f:
  18. if re.match('^\\s*#', line):
  19. continue
  20. else:
  21. value = line.strip()
  22. break
  23. except Exception as error:
  24. if not opt or cli.verbose:
  25. print('Unable to read config: {e}'.format(e=error))
  26. return value
  27. # CLI config
  28. parser = argparse.ArgumentParser('TVDB Renamer')
  29. parser.add_argument('--verbose', '-v', action='store_true', help='Display debug and episode data')
  30. parser.add_argument('--api', default=default_apikey, help='Path to a file containing the TVDB API key')
  31. parser.add_argument('--lang', '-l', help='Force TVDB language code')
  32. parser.add_argument('--order', '-o', help='Force TVDB order type')
  33. parser.add_argument('path', type=pathlib.Path, help='The directory containing files to be renamed')
  34. cli = parser.parse_args()
  35. if not os.path.isfile(cli.api):
  36. print('Invalid API key file path: {path}'.format(path=cli.api))
  37. exit(1)
  38. if not os.path.isdir(cli.path):
  39. print('Invalid path: {path}'.format(path=cli.path))
  40. exit(1)
  41. if cli.lang == 'None' or cli.lang == '':
  42. cli.lang = None
  43. if cli.verbose:
  44. print('Path: {p}'.format(p=cli.path))
  45. print('Forced Lang/Order: {l}/{o}'.format(l=cli.lang, o=cli.order))
  46. # Move to keep the file paths flat
  47. try:
  48. os.chdir(cli.path)
  49. except Exception as error:
  50. print('Unable to move to target directory: {e}'.format(e=error))
  51. exit(1)
  52. # Read the local directory
  53. seasons = {}
  54. try:
  55. dirfiles = os.listdir('.')
  56. except Exception as error:
  57. print('Unable to read to target directory: {e}'.format(e=error))
  58. exit(1)
  59. for file in dirfiles:
  60. f = {}
  61. f['file'] = file
  62. try:
  63. split = re.search(r'^(.+?)(\.(?:\w\w\.)?(?:sdh\.)?(?:synced\.)?\w{3,4}(?:-remux)?)$', file, re.IGNORECASE)
  64. if split is not None:
  65. f['base'] = split.group(1)
  66. f['extension'] = split.group(2)
  67. else:
  68. f['base'] = file
  69. f['extension'] = ''
  70. se = re.search(r'S(\d+)E(\d+)', file, flags=re.IGNORECASE)
  71. if se is None:
  72. # Does not contain a season/episode block
  73. continue
  74. f['season'] = int(se.group(1))
  75. f['number'] = int(se.group(2))
  76. if not f['season'] in seasons:
  77. seasons[ f['season'] ] = {}
  78. if not f['number'] in seasons[ f['season'] ]:
  79. seasons[ f['season'] ][ f['number'] ] = []
  80. seasons[ f['season'] ][ f['number'] ].append(f)
  81. except Exception as error:
  82. print('Unable to parse filename "{f}": {e}'.format(e=error, f=file))
  83. continue
  84. # Fetch the TVDB episode list
  85. try:
  86. apikey = readConfigLine(cli.api)
  87. id = readConfigLine('.tvdb', opt=True)
  88. if cli.lang is not None:
  89. lang = cli.lang
  90. else:
  91. lang = readConfigLine('.tvdb_lang', opt=True)
  92. if lang is None:
  93. lang = default_lang
  94. if cli.order is not None:
  95. order = cli.order
  96. else:
  97. order = readConfigLine('.tvdb_order', opt=True)
  98. if order is None:
  99. order = default_order
  100. if apikey is None or id is None:
  101. if cli.verbose:
  102. print('Invalid TVDB parameters')
  103. exit(1)
  104. if cli.verbose:
  105. print('Series: {id}'.format(id=id))
  106. print('Lang/Order: {l}/{o}'.format(l=lang, o=order))
  107. tvdb = tvdb_v4_official.TVDB(apikey)
  108. if lang is None:
  109. series = tvdb.get_series(id=id)
  110. else:
  111. series = tvdb.get_series_translation(id=id, lang=lang)
  112. data = tvdb.get_series_episodes(id=id, lang=lang, season_type=order)
  113. del tvdb
  114. del id
  115. del apikey
  116. except Exception as error:
  117. print(
  118. "Unable to fetch series {id}: {e}\nSeries:\n{s}\nEpisodes:\n{ep}".format(
  119. id=id, e=error, s=series, ep=data
  120. )
  121. )
  122. exit(1)
  123. # Parse and clean the data we need
  124. episodes = []
  125. try:
  126. series_name = str('_NoSeriesName' if series['name'] is None else series['name']).strip()
  127. if cli.verbose:
  128. print('Series: {name}'.format(name=series_name))
  129. for episode in data['episodes']:
  130. e = {}
  131. e['series'] = series_name
  132. e['season'] = int(episode['seasonNumber'])
  133. e['number'] = int(episode['number'])
  134. e['name'] = str('(Untitled)' if episode['name'] is None else episode['name']).strip()
  135. e['date'] = None if episode['aired'] is None else datetime.strptime(episode['aired'], '%Y-%m-%d')
  136. e['description'] = '' if episode['overview'] is None else episode['overview'].strip()
  137. e['filename'] = '{series} - S{season:02d}E{number:02d} - {name}'.format(
  138. series=e['series'], season=e['season'], number=e['number'], name=e['name']
  139. )
  140. e['filename'] = e['filename'].replace(':', ' -').replace('/', ' - ').replace(' ', ' ')
  141. episodes.append(e)
  142. except Exception as error:
  143. print('Unable to parse episode data: {e}'.format(e=error))
  144. exit(1)
  145. # Find unexpected episodes
  146. if cli.verbose:
  147. db_seasons = {}
  148. for e in episodes:
  149. if not e['season'] in db_seasons:
  150. db_seasons[ e['season'] ] = {}
  151. if not e['number'] in db_seasons[ e['season'] ]:
  152. db_seasons[ e['season'] ][ e['number'] ] = True
  153. for snum, s in seasons.items():
  154. for enum, elist in s.items():
  155. for e in elist:
  156. if (
  157. not e['season'] in db_seasons or
  158. not e['number'] in db_seasons[ e['season'] ]
  159. ):
  160. print('Unexpected: {f}'.format(f=e['file']))
  161. del db_seasons
  162. # Rename matched files
  163. for e in episodes:
  164. if (
  165. not e['season'] in seasons or
  166. not e['number'] in seasons[ e['season'] ]
  167. ):
  168. if cli.verbose:
  169. if e['date'] is None or e['date'] <= datetime.today():
  170. print('Missing: {m}'.format(m=e['filename']))
  171. continue
  172. files = seasons[ e['season'] ][ e['number'] ]
  173. for file in files:
  174. if file['base'] != e['filename']:
  175. src = file['file']
  176. dest = e['filename'] + file['extension']
  177. print('Rename\n\t{s}\n\t{d}'.format(s=src, d=dest))
  178. if (
  179. os.path.exists(dest) and
  180. file['base'].casefold() != e['filename'].casefold()
  181. ):
  182. print('Target exists: {d}'.format(d=dest))
  183. continue
  184. try:
  185. os.rename(src, dest)
  186. except Exception as error:
  187. print('Unable to rename {s}: {e}'.format(s=src, e=error))
  188. continue
  189. else:
  190. if cli.verbose:
  191. print('Found: {m}'.format(m=e['filename']))