Accomodate some broken installations of the Python Imaging Library.
[pyTivo/TheBayer.git] / metadata.py
blob17ce1b2c05aea26b3643ef79f2ec25350e33a511
1 #!/usr/bin/env python
3 import os
4 import subprocess
5 import sys
6 from datetime import datetime
7 from xml.dom import minidom
8 try:
9 import plistlib
10 except:
11 pass
13 import mutagen
14 from lrucache import LRUCache
16 import config
18 # Something to strip
19 TRIBUNE_CR = ' Copyright Tribune Media Services, Inc.'
21 TV_RATINGS = {'TV-Y7': 'x1', 'TV-Y': 'x2', 'TV-G': 'x3', 'TV-PG': 'x4',
22 'TV-14': 'x5', 'TV-MA': 'x6', 'TV-NR': 'x7',
23 'TVY7': 'x1', 'TVY': 'x2', 'TVG': 'x3', 'TVPG': 'x4',
24 'TV14': 'x5', 'TVMA': 'x6', 'TVNR': 'x7',
25 'Y7': 'x1', 'Y': 'x2', 'G': 'x3', 'PG': 'x4',
26 '14': 'x5', 'MA': 'x6', 'NR': 'x7', 'UNRATED': 'x7'}
28 MPAA_RATINGS = {'G': 'G1', 'PG': 'P2', 'PG-13': 'P3', 'PG13': 'P3',
29 'R': 'R4', 'X': 'X5', 'NC-17': 'N6', 'NC17': 'N6',
30 'NR': 'N8', 'UNRATED': 'N8'}
32 STAR_RATINGS = {'1': 'x1', '1.5': 'x2', '2': 'x3', '2.5': 'x4',
33 '3': 'x5', '3.5': 'x6', '4': 'x7',
34 '*': 'x1', '**': 'x3', '***': 'x5', '****': 'x7'}
36 HUMAN = {'mpaaRating': {'G1': 'G', 'P2': 'PG', 'P3': 'PG-13', 'R4': 'R',
37 'X5': 'X', 'N6': 'NC-17', 'N8': 'Unrated'},
38 'tvRating': {'x1': 'TV-Y7', 'x2': 'TV-Y', 'x3': 'TV-G',
39 'x4': 'TV-PG', 'x5': 'TV-14', 'x6': 'TV-MA',
40 'x7': 'Unrated'},
41 'starRating': {'x1': '1', 'x2': '1.5', 'x3': '2', 'x4': '2.5',
42 'x5': '3', 'x6': '3.5', 'x7': '4'}}
44 tivo_cache = LRUCache(50)
45 mp4_cache = LRUCache(50)
46 dvrms_cache = LRUCache(50)
48 def tag_data(element, tag):
49 for name in tag.split('/'):
50 new_element = element.getElementsByTagName(name)
51 if not new_element:
52 return ''
53 element = new_element[0]
54 if not element.firstChild:
55 return ''
56 return element.firstChild.data
58 def _vtag_data(element, tag):
59 for name in tag.split('/'):
60 new_element = element.getElementsByTagName(name)
61 if not new_element:
62 return []
63 element = new_element[0]
64 elements = element.getElementsByTagName('element')
65 return [x.firstChild.data for x in elements if x.firstChild]
67 def _tag_value(element, tag):
68 item = element.getElementsByTagName(tag)
69 if item:
70 value = item[0].attributes['value'].value
71 name = item[0].firstChild.data
72 return name[0] + value[0]
74 def from_moov(full_path):
75 if full_path in mp4_cache:
76 return mp4_cache[full_path]
78 metadata = {}
79 len_desc = 0
81 try:
82 mp4meta = mutagen.File(full_path)
83 assert(mp4meta)
84 except:
85 mp4_cache[full_path] = {}
86 return {}
88 # The following 1-to-1 correspondence of atoms to pyTivo
89 # variables is TV-biased
90 keys = {'tvnn': 'callsign', 'tven': 'episodeNumber',
91 'tvsh': 'seriesTitle'}
93 for key, value in mp4meta.items():
94 if type(value) == list:
95 value = value[0]
96 if key == 'stik':
97 metadata['isEpisode'] = ['false', 'true'][value == 'TV Show']
98 elif key in keys:
99 metadata[keys[key]] = value
100 # These keys begin with the copyright symbol \xA9
101 elif key == '\xa9day':
102 if len(value) == 4:
103 value += '-01-01T16:00:00Z'
104 metadata['originalAirDate'] = value
105 #metadata['time'] = value
106 elif key in ['\xa9gen', 'gnre']:
107 for k in ('vProgramGenre', 'vSeriesGenre'):
108 if k in metadata:
109 metadata[k].append(value)
110 else:
111 metadata[k] = [value]
112 elif key == '\xa9nam':
113 if 'tvsh' in mp4meta:
114 metadata['episodeTitle'] = value
115 else:
116 metadata['title'] = value
118 # Description in desc, cmt, and/or ldes tags. Keep the longest.
119 elif key in ['desc', '\xa9cmt', 'ldes'] and len(value) > len_desc:
120 metadata['description'] = value
121 len_desc = len(value)
123 # A common custom "reverse DNS format" tag
124 elif (key == '----:com.apple.iTunes:iTunEXTC' and
125 ('us-tv' in value or 'mpaa' in value)):
126 rating = value.split("|")[1].upper()
127 if rating in TV_RATINGS and 'us-tv' in value:
128 metadata['tvRating'] = TV_RATINGS[rating]
129 elif rating in MPAA_RATINGS and 'mpaa' in value:
130 metadata['mpaaRating'] = MPAA_RATINGS[rating]
132 # Actors, directors, producers, AND screenwriters may be in a long
133 # embedded XML plist.
134 elif (key == '----:com.apple.iTunes:iTunMOVI' and
135 'plistlib' in sys.modules):
136 items = {'cast': 'vActor', 'directors': 'vDirector',
137 'producers': 'vProducer', 'screenwriters': 'vWriter'}
138 data = plistlib.readPlistFromString(value)
139 for item in items:
140 if item in data:
141 metadata[items[item]] = [x['name'] for x in data[item]]
143 mp4_cache[full_path] = metadata
144 return metadata
146 def from_dvrms(full_path):
147 if full_path in dvrms_cache:
148 return dvrms_cache[full_path]
150 metadata = {}
152 try:
153 meta = mutagen.File(full_path)
154 assert(meta)
155 except:
156 dvrms_cache[full_path] = {}
157 return {}
159 keys = {'title': ['Title'],
160 'description': ['Description', 'WM/SubTitleDescription'],
161 'episodeTitle': ['WM/SubTitle'],
162 'callsign': ['WM/MediaStationCallSign'],
163 'displayMajorNumber': ['WM/MediaOriginalChannel'],
164 'originalAirDate': ['WM/MediaOriginalBroadcastDateTime'],
165 'rating': ['WM/ParentalRating'],
166 'credits': ['WM/MediaCredits'], 'genre': ['WM/Genre']}
168 for tagname in keys:
169 for tag in keys[tagname]:
170 try:
171 if tag in meta:
172 value = str(meta[tag][0])
173 if value:
174 metadata[tagname] = value
175 except:
176 pass
178 if 'episodeTitle' in metadata and 'title' in metadata:
179 metadata['seriesTitle'] = metadata['title']
180 if 'genre' in metadata:
181 value = metadata['genre'].split(',')
182 metadata['vProgramGenre'] = value
183 metadata['vSeriesGenre'] = value
184 del metadata['genre']
185 if 'credits' in metadata:
186 value = [x.split('/') for x in metadata['credits'].split(';')]
187 metadata['vActor'] = value[0] + value[3]
188 metadata['vDirector'] = value[1]
189 del metadata['credits']
190 if 'rating' in metadata:
191 rating = metadata['rating']
192 if rating in TV_RATINGS:
193 metadata['tvRating'] = TV_RATINGS[rating]
194 del metadata['rating']
196 dvrms_cache[full_path] = metadata
197 return metadata
199 def from_eyetv(full_path):
200 keys = {'TITLE': 'title', 'SUBTITLE': 'episodeTitle',
201 'DESCRIPTION': 'description', 'YEAR': 'movieYear',
202 'EPISODENUM': 'episodeNumber'}
203 metadata = {}
204 path, name = os.path.split(full_path)
205 eyetvp = [x for x in os.listdir(path) if x.endswith('.eyetvp')][0]
206 eyetvp = os.path.join(path, eyetvp)
207 eyetv = plistlib.readPlist(eyetvp)
208 if 'epg info' in eyetv:
209 info = eyetv['epg info']
210 for key in keys:
211 if info[key]:
212 metadata[keys[key]] = info[key]
213 if info['SUBTITLE']:
214 metadata['seriesTitle'] = info['TITLE']
215 if info['ACTORS']:
216 metadata['vActor'] = [x.strip() for x in info['ACTORS'].split(',')]
217 if info['DIRECTOR']:
218 metadata['vDirector'] = [info['DIRECTOR']]
220 for ptag, etag, ratings in [('tvRating', 'TV_RATING', TV_RATINGS),
221 ('mpaaRating', 'MPAA_RATING', MPAA_RATINGS),
222 ('starRating', 'STAR_RATING', STAR_RATINGS)]:
223 x = info[etag].upper()
224 if x and x in ratings:
225 metadata[ptag] = ratings[x]
227 # movieYear must be set for the mpaa/star ratings to work
228 if (('mpaaRating' in metadata or 'starRating' in metadata) and
229 'movieYear' not in metadata):
230 metadata['movieYear'] = eyetv['info']['start'].year
231 return metadata
233 def from_text(full_path):
234 metadata = {}
235 path, name = os.path.split(full_path)
236 title, ext = os.path.splitext(name)
238 for metafile in [os.path.join(path, title) + '.properties',
239 os.path.join(path, 'default.txt'), full_path + '.txt',
240 os.path.join(path, '.meta', 'default.txt'),
241 os.path.join(path, '.meta', name) + '.txt']:
242 if os.path.exists(metafile):
243 sep = ':='[metafile.endswith('.properties')]
244 for line in file(metafile, 'U'):
245 if line.strip().startswith('#') or not sep in line:
246 continue
247 key, value = [x.strip() for x in line.split(sep, 1)]
248 if not key or not value:
249 continue
250 if key.startswith('v'):
251 if key in metadata:
252 metadata[key].append(value)
253 else:
254 metadata[key] = [value]
255 else:
256 metadata[key] = value
258 for rating, ratings in [('tvRating', TV_RATINGS),
259 ('mpaaRating', MPAA_RATINGS),
260 ('starRating', STAR_RATINGS)]:
261 x = metadata.get(rating, '').upper()
262 if x in ratings:
263 metadata[rating] = ratings[x]
265 return metadata
267 def basic(full_path):
268 base_path, name = os.path.split(full_path)
269 title, ext = os.path.splitext(name)
270 mtime = os.stat(full_path).st_mtime
271 if (mtime < 0):
272 mtime = 0
273 originalAirDate = datetime.fromtimestamp(mtime)
275 metadata = {'title': title,
276 'originalAirDate': originalAirDate.isoformat()}
277 ext = ext.lower()
278 if ext in ['.mp4', '.m4v', '.mov']:
279 metadata.update(from_moov(full_path))
280 elif ext in ['.dvr-ms', '.asf', '.wmv']:
281 metadata.update(from_dvrms(full_path))
282 elif 'plistlib' in sys.modules and base_path.endswith('.eyetv'):
283 metadata.update(from_eyetv(full_path))
284 metadata.update(from_text(full_path))
286 return metadata
288 def from_container(xmldoc):
289 metadata = {}
291 keys = {'title': 'Title', 'episodeTitle': 'EpisodeTitle',
292 'description': 'Description', 'seriesId': 'SeriesId',
293 'episodeNumber': 'EpisodeNumber', 'tvRating': 'TvRating',
294 'displayMajorNumber': 'SourceChannel', 'callsign': 'SourceStation'}
296 details = xmldoc.getElementsByTagName('Details')[0]
298 for key in keys:
299 data = tag_data(details, keys[key])
300 if data:
301 if key == 'description':
302 data = data.replace(TRIBUNE_CR, '')
303 elif key == 'tvRating':
304 data = 'x' + data
305 elif key == 'displayMajorNumber':
306 if '-' in data:
307 data, metadata['displayMinorNumber'] = data.split('-')
308 metadata[key] = data
310 return metadata
312 def from_details(xmldoc):
313 metadata = {}
315 showing = xmldoc.getElementsByTagName('showing')[0]
316 program = showing.getElementsByTagName('program')[0]
318 items = {'description': 'program/description',
319 'title': 'program/title',
320 'episodeTitle': 'program/episodeTitle',
321 'episodeNumber': 'program/episodeNumber',
322 'seriesId': 'program/series/uniqueId',
323 'seriesTitle': 'program/series/seriesTitle',
324 'originalAirDate': 'program/originalAirDate',
325 'isEpisode': 'program/isEpisode',
326 'movieYear': 'program/movieYear',
327 'partCount': 'partCount',
328 'partIndex': 'partIndex',
329 'time': 'time'}
331 for item in items:
332 data = tag_data(showing, items[item])
333 if data:
334 if item == 'description':
335 data = data.replace(TRIBUNE_CR, '')
336 metadata[item] = data
338 vItems = ['vActor', 'vChoreographer', 'vDirector',
339 'vExecProducer', 'vProgramGenre', 'vGuestStar',
340 'vHost', 'vProducer', 'vWriter']
342 for item in vItems:
343 data = _vtag_data(program, item)
344 if data:
345 metadata[item] = data
347 sb = showing.getElementsByTagName('showingBits')
348 if sb:
349 metadata['showingBits'] = sb[0].attributes['value'].value
351 for tag in ['starRating', 'mpaaRating', 'colorCode']:
352 value = _tag_value(program, tag)
353 if value:
354 metadata[tag] = value
356 rating = _tag_value(showing, 'tvRating')
357 if rating:
358 metadata['tvRating'] = 'x' + rating[1]
360 return metadata
362 def from_tivo(full_path):
363 if full_path in tivo_cache:
364 return tivo_cache[full_path]
366 tdcat_path = config.get_bin('tdcat')
367 tivo_mak = config.get_server('tivo_mak')
368 if tdcat_path and tivo_mak:
369 tcmd = [tdcat_path, '-m', tivo_mak, '-2', full_path]
370 tdcat = subprocess.Popen(tcmd, stdout=subprocess.PIPE)
371 xmldoc = minidom.parse(tdcat.stdout)
372 metadata = from_details(xmldoc)
373 tivo_cache[full_path] = metadata
374 else:
375 metadata = {}
377 return metadata
379 if __name__ == '__main__':
380 if len(sys.argv) > 1:
381 metadata = {}
382 ext = os.path.splitext(sys.argv[1])[1].lower()
383 if ext == '.tivo':
384 config.init([])
385 metadata.update(from_tivo(sys.argv[1]))
386 elif ext in ['.mp4', '.m4v', '.mov']:
387 metadata.update(from_moov(sys.argv[1]))
388 elif ext in ['.dvr-ms', '.asf', '.wmv']:
389 metadata.update(from_dvrms(sys.argv[1]))
390 for key in metadata:
391 value = metadata[key]
392 if type(value) == list:
393 for item in value:
394 print '%s: %s' % (key, item.encode('utf8'))
395 else:
396 if key in HUMAN and value in HUMAN[key]:
397 print '%s: %s' % (key, HUMAN[key][value])
398 else:
399 print '%s: %s' % (key, value.encode('utf8'))