Merge branch 'wmcbrine/master'
[pyTivo/wmcbrine/lucasnz.git] / metadata.py
blobb14c061ba185f14fcb93e44eb0be412fef025dd4
1 #!/usr/bin/env python
3 import hashlib
4 import logging
5 import os
6 import struct
7 import subprocess
8 import sys
9 from datetime import datetime
10 from xml.dom import minidom
11 from xml.parsers import expat
12 try:
13 import plistlib
14 except:
15 pass
17 import mutagen
18 from lrucache import LRUCache
20 import config
21 import plugins.video.transcode
22 import turing
24 # Something to strip
25 TRIBUNE_CR = ' Copyright Tribune Media Services, Inc.'
27 TV_RATINGS = {'TV-Y7': 1, 'TV-Y': 2, 'TV-G': 3, 'TV-PG': 4, 'TV-14': 5,
28 'TV-MA': 6, 'TV-NR': 7, 'TVY7': 1, 'TVY': 2, 'TVG': 3,
29 'TVPG': 4, 'TV14': 5, 'TVMA': 6, 'TVNR': 7, 'Y7': 1,
30 'Y': 2, 'G': 3, 'PG': 4, '14': 5, 'MA': 6, 'NR': 7,
31 'UNRATED': 7, 'X1': 1, 'X2': 2, 'X3': 3, 'X4': 4, 'X5': 5,
32 'X6': 6, 'X7': 7}
34 MPAA_RATINGS = {'G': 1, 'PG': 2, 'PG-13': 3, 'PG13': 3, 'R': 4, 'X': 5,
35 'NC-17': 6, 'NC17': 6, 'NR': 8, 'UNRATED': 8, 'G1': 1,
36 'P2': 2, 'P3': 3, 'R4': 4, 'X5': 5, 'N6': 6, 'N8': 8}
38 STAR_RATINGS = {'1': 1, '1.5': 2, '2': 3, '2.5': 4, '3': 5, '3.5': 6,
39 '4': 7, '*': 1, '**': 3, '***': 5, '****': 7, 'X1': 1,
40 'X2': 2, 'X3': 3, 'X4': 4, 'X5': 5, 'X6': 6, 'X7': 7}
42 HUMAN = {'mpaaRating': {1: 'G', 2: 'PG', 3: 'PG-13', 4: 'R', 5: 'X',
43 6: 'NC-17', 8: 'NR'},
44 'tvRating': {1: 'Y7', 2: 'Y', 3: 'G', 4: 'PG', 5: '14',
45 6: 'MA', 7: 'NR'},
46 'starRating': {1: '1', 2: '1.5', 3: '2', 4: '2.5', 5: '3',
47 6: '3.5', 7: '4'},
48 'colorCode': {1: 'B & W', 2: 'COLOR AND B & W',
49 3: 'COLORIZED', 4: 'COLOR'}
52 BOM = '\xef\xbb\xbf'
54 GB = 1024 ** 3
55 MB = 1024 ** 2
56 KB = 1024
58 tivo_cache = LRUCache(50)
59 mp4_cache = LRUCache(50)
60 dvrms_cache = LRUCache(50)
61 nfo_cache = LRUCache(50)
63 mswindows = (sys.platform == "win32")
65 def get_mpaa(rating):
66 return HUMAN['mpaaRating'].get(rating, 'NR')
68 def get_tv(rating):
69 return HUMAN['tvRating'].get(rating, 'NR')
71 def get_stars(rating):
72 return HUMAN['starRating'].get(rating, '')
74 def get_color(value):
75 return HUMAN['colorCode'].get(value, 'COLOR')
77 def human_size(raw):
78 raw = float(raw)
79 if raw > GB:
80 tsize = '%.2f GB' % (raw / GB)
81 elif raw > MB:
82 tsize = '%.2f MB' % (raw / MB)
83 elif raw > KB:
84 tsize = '%.2f KB' % (raw / KB)
85 else:
86 tsize = '%d Bytes' % raw
87 return tsize
89 def tag_data(element, tag):
90 for name in tag.split('/'):
91 found = False
92 for new_element in element.childNodes:
93 if new_element.nodeName == name:
94 found = True
95 element = new_element
96 break
97 if not found:
98 return ''
99 if not element.firstChild:
100 return ''
101 return element.firstChild.data
103 def _vtag_data(element, tag):
104 for name in tag.split('/'):
105 new_element = element.getElementsByTagName(name)
106 if not new_element:
107 return []
108 element = new_element[0]
109 elements = element.getElementsByTagName('element')
110 return [x.firstChild.data for x in elements if x.firstChild]
112 def _vtag_data_alternate(element, tag):
113 elements = [element]
114 for name in tag.split('/'):
115 new_elements = []
116 for elmt in elements:
117 new_elements += elmt.getElementsByTagName(name)
118 elements = new_elements
119 return [x.firstChild.data for x in elements if x.firstChild]
121 def _tag_value(element, tag):
122 item = element.getElementsByTagName(tag)
123 if item:
124 value = item[0].attributes['value'].value
125 return int(value[0])
127 def from_moov(full_path):
128 if full_path in mp4_cache:
129 return mp4_cache[full_path]
131 metadata = {}
132 len_desc = 0
134 try:
135 mp4meta = mutagen.File(unicode(full_path, 'utf-8'))
136 assert(mp4meta)
137 except:
138 mp4_cache[full_path] = {}
139 return {}
141 # The following 1-to-1 correspondence of atoms to pyTivo
142 # variables is TV-biased
143 keys = {'tvnn': 'callsign',
144 'tvsh': 'seriesTitle'}
145 isTVShow = False
146 if 'stik' in mp4meta:
147 isTVShow = (mp4meta['stik'] == mutagen.mp4.MediaKind.TV_SHOW)
148 else:
149 isTVShow = 'tvsh' in mp4meta
150 for key, value in mp4meta.items():
151 if type(value) == list:
152 value = value[0]
153 if key in keys:
154 metadata[keys[key]] = value
155 elif key == 'tven':
156 #could be programId (EP, SH, or MV) or "SnEn"
157 if value.startswith('SH'):
158 metadata['isEpisode'] = 'false'
159 elif value.startswith('MV') or value.startswith('EP'):
160 metadata['isEpisode'] = 'true'
161 metadata['programId'] = value
162 elif key.startswith('S') and key.count('E') == 1:
163 epstart = key.find('E')
164 seasonstr = key[1:epstart]
165 episodestr = key[epstart+1:]
166 if (seasonstr.isdigit() and episodestr.isdigit()):
167 if len(episodestr) < 2:
168 episodestr = '0' + episodestr
169 metadata['episodeNumber'] = seasonstr+episodestr
170 elif key == 'tvsn':
171 #put together tvsn and tves to make episodeNumber
172 tvsn = str(value)
173 tves = '00'
174 if 'tves' in mp4meta:
175 tvesValue = mp4meta['tves']
176 if type(tvesValue) == list:
177 tvesValue = tvesValue[0]
178 tves = str(tvesValue)
179 if len(tves) < 2:
180 tves = '0' + tves
181 metadata['episodeNumber'] = tvsn+tves
182 # These keys begin with the copyright symbol \xA9
183 elif key == '\xa9day':
184 if isTVShow :
185 if len(value) == 4:
186 value += '-01-01T16:00:00Z'
187 metadata['originalAirDate'] = value
188 else:
189 if len(value) >= 4:
190 metadata['movieYear'] = value[:4]
191 #metadata['time'] = value
192 elif key in ['\xa9gen', 'gnre']:
193 for k in ('vProgramGenre', 'vSeriesGenre'):
194 if k in metadata:
195 metadata[k].append(value)
196 else:
197 metadata[k] = [value]
198 elif key == '\xa9nam':
199 if isTVShow:
200 metadata['episodeTitle'] = value
201 else:
202 metadata['title'] = value
204 # Description in desc, cmt, and/or ldes tags. Keep the longest.
205 elif key in ['desc', '\xa9cmt', 'ldes'] and len(value) > len_desc:
206 metadata['description'] = value
207 len_desc = len(value)
209 # A common custom "reverse DNS format" tag
210 elif (key == '----:com.apple.iTunes:iTunEXTC' and
211 ('us-tv' in value or 'mpaa' in value)):
212 rating = value.split("|")[1].upper()
213 if rating in TV_RATINGS and 'us-tv' in value:
214 metadata['tvRating'] = TV_RATINGS[rating]
215 elif rating in MPAA_RATINGS and 'mpaa' in value:
216 metadata['mpaaRating'] = MPAA_RATINGS[rating]
218 # Actors, directors, producers, AND screenwriters may be in a long
219 # embedded XML plist.
220 elif (key == '----:com.apple.iTunes:iTunMOVI' and
221 'plistlib' in sys.modules):
222 items = {'cast': 'vActor', 'directors': 'vDirector',
223 'producers': 'vProducer', 'screenwriters': 'vWriter'}
224 try:
225 data = plistlib.readPlistFromString(value)
226 except:
227 pass
228 else:
229 for item in items:
230 if item in data:
231 metadata[items[item]] = [x['name'] for x in data[item]]
232 elif (key == '----:com.pyTivo.pyTivo:tiVoINFO' and
233 'plistlib' in sys.modules):
234 try:
235 data = plistlib.readPlistFromString(value)
236 except:
237 pass
238 else:
239 for item in data:
240 metadata[item] = data[item]
242 mp4_cache[full_path] = metadata
243 return metadata
245 def from_mscore(rawmeta):
246 metadata = {}
247 keys = {'title': ['Title'],
248 'description': ['Description', 'WM/SubTitleDescription'],
249 'episodeTitle': ['WM/SubTitle'],
250 'callsign': ['WM/MediaStationCallSign'],
251 'displayMajorNumber': ['WM/MediaOriginalChannel'],
252 'originalAirDate': ['WM/MediaOriginalBroadcastDateTime'],
253 'rating': ['WM/ParentalRating'],
254 'credits': ['WM/MediaCredits'], 'genre': ['WM/Genre']}
256 for tagname in keys:
257 for tag in keys[tagname]:
258 try:
259 if tag in rawmeta:
260 value = rawmeta[tag][0]
261 if type(value) not in (str, unicode):
262 value = str(value)
263 if value:
264 metadata[tagname] = value
265 except:
266 pass
268 if 'episodeTitle' in metadata and 'title' in metadata:
269 metadata['seriesTitle'] = metadata['title']
270 if 'genre' in metadata:
271 value = metadata['genre'].split(',')
272 metadata['vProgramGenre'] = value
273 metadata['vSeriesGenre'] = value
274 del metadata['genre']
275 if 'credits' in metadata:
276 value = [x.split('/') for x in metadata['credits'].split(';')]
277 if len(value) > 3:
278 metadata['vActor'] = [x for x in (value[0] + value[3]) if x]
279 metadata['vDirector'] = [x for x in value[1] if x]
280 del metadata['credits']
281 if 'rating' in metadata:
282 rating = metadata['rating']
283 if rating in TV_RATINGS:
284 metadata['tvRating'] = TV_RATINGS[rating]
285 del metadata['rating']
287 return metadata
289 def from_dvrms(full_path):
290 if full_path in dvrms_cache:
291 return dvrms_cache[full_path]
293 try:
294 rawmeta = mutagen.File(unicode(full_path, 'utf-8'))
295 assert(rawmeta)
296 except:
297 dvrms_cache[full_path] = {}
298 return {}
300 metadata = from_mscore(rawmeta)
301 dvrms_cache[full_path] = metadata
302 return metadata
304 def from_eyetv(full_path):
305 keys = {'TITLE': 'title', 'SUBTITLE': 'episodeTitle',
306 'DESCRIPTION': 'description', 'YEAR': 'movieYear',
307 'EPISODENUM': 'episodeNumber'}
308 metadata = {}
309 path = os.path.dirname(unicode(full_path, 'utf-8'))
310 eyetvp = [x for x in os.listdir(path) if x.endswith('.eyetvp')][0]
311 eyetvp = os.path.join(path, eyetvp)
312 try:
313 eyetv = plistlib.readPlist(eyetvp)
314 except:
315 return metadata
316 if 'epg info' in eyetv:
317 info = eyetv['epg info']
318 for key in keys:
319 if info[key]:
320 metadata[keys[key]] = info[key]
321 if info['SUBTITLE']:
322 metadata['seriesTitle'] = info['TITLE']
323 if info['ACTORS']:
324 metadata['vActor'] = [x.strip() for x in info['ACTORS'].split(',')]
325 if info['DIRECTOR']:
326 metadata['vDirector'] = [info['DIRECTOR']]
328 for ptag, etag, ratings in [('tvRating', 'TV_RATING', TV_RATINGS),
329 ('mpaaRating', 'MPAA_RATING', MPAA_RATINGS),
330 ('starRating', 'STAR_RATING', STAR_RATINGS)]:
331 x = info[etag].upper()
332 if x and x in ratings:
333 metadata[ptag] = ratings[x]
335 # movieYear must be set for the mpaa/star ratings to work
336 if (('mpaaRating' in metadata or 'starRating' in metadata) and
337 'movieYear' not in metadata):
338 metadata['movieYear'] = eyetv['info']['start'].year
339 return metadata
341 def from_text(full_path):
342 metadata = {}
343 full_path = unicode(full_path, 'utf-8')
344 path, name = os.path.split(full_path)
345 title, ext = os.path.splitext(name)
347 search_paths = []
348 ptmp = full_path
349 while ptmp:
350 parent = os.path.dirname(ptmp)
351 if ptmp != parent:
352 ptmp = parent
353 else:
354 break
355 search_paths.append(os.path.join(ptmp, 'default.txt'))
357 search_paths.append(os.path.join(path, title) + '.properties')
358 search_paths.reverse()
360 search_paths += [full_path + '.txt',
361 os.path.join(path, '.meta', 'default.txt'),
362 os.path.join(path, '.meta', name) + '.txt']
364 for metafile in search_paths:
365 if os.path.exists(metafile):
366 sep = ':='[metafile.endswith('.properties')]
367 for line in file(metafile, 'U'):
368 if line.startswith(BOM):
369 line = line[3:]
370 if line.strip().startswith('#') or not sep in line:
371 continue
372 key, value = [x.strip() for x in line.split(sep, 1)]
373 if not key or not value:
374 continue
375 if key.startswith('v'):
376 if key in metadata:
377 metadata[key].append(value)
378 else:
379 metadata[key] = [value]
380 else:
381 metadata[key] = value
383 for rating, ratings in [('tvRating', TV_RATINGS),
384 ('mpaaRating', MPAA_RATINGS),
385 ('starRating', STAR_RATINGS)]:
386 x = metadata.get(rating, '').upper()
387 if x in ratings:
388 metadata[rating] = ratings[x]
389 else:
390 try:
391 x = int(x)
392 metadata[rating] = x
393 except:
394 pass
396 return metadata
398 def basic(full_path, mtime=None):
399 base_path, name = os.path.split(full_path)
400 title, ext = os.path.splitext(name)
401 if not mtime:
402 mtime = os.path.getmtime(unicode(full_path, 'utf-8'))
403 try:
404 originalAirDate = datetime.utcfromtimestamp(mtime)
405 except:
406 originalAirDate = datetime.utcnow()
408 metadata = {'title': title,
409 'originalAirDate': originalAirDate.isoformat()}
410 ext = ext.lower()
411 if ext in ['.mp4', '.m4v', '.mov']:
412 metadata.update(from_moov(full_path))
413 elif ext in ['.dvr-ms', '.asf', '.wmv']:
414 metadata.update(from_dvrms(full_path))
415 elif 'plistlib' in sys.modules and base_path.endswith('.eyetv'):
416 metadata.update(from_eyetv(full_path))
417 metadata.update(from_nfo(full_path))
418 metadata.update(from_text(full_path))
420 return metadata
422 def from_container(xmldoc):
423 metadata = {}
425 keys = {'title': 'Title', 'episodeTitle': 'EpisodeTitle',
426 'description': 'Description', 'programId': 'ProgramId',
427 'seriesId': 'SeriesId', 'episodeNumber': 'EpisodeNumber',
428 'tvRating': 'TvRating', 'displayMajorNumber': 'SourceChannel',
429 'callsign': 'SourceStation', 'showingBits': 'ShowingBits',
430 'mpaaRating': 'MpaaRating'}
432 details = xmldoc.getElementsByTagName('Details')[0]
434 for key in keys:
435 data = tag_data(details, keys[key])
436 if data:
437 if key == 'description':
438 data = data.replace(TRIBUNE_CR, '')
439 elif key == 'tvRating':
440 data = int(data)
441 elif key == 'displayMajorNumber':
442 if '-' in data:
443 data, metadata['displayMinorNumber'] = data.split('-')
444 metadata[key] = data
446 return metadata
448 def from_details(xml):
449 metadata = {}
451 xmldoc = minidom.parseString(xml)
452 showing = xmldoc.getElementsByTagName('showing')[0]
453 program = showing.getElementsByTagName('program')[0]
455 items = {'description': 'program/description',
456 'title': 'program/title',
457 'episodeTitle': 'program/episodeTitle',
458 'episodeNumber': 'program/episodeNumber',
459 'programId': 'program/uniqueId',
460 'seriesId': 'program/series/uniqueId',
461 'seriesTitle': 'program/series/seriesTitle',
462 'originalAirDate': 'program/originalAirDate',
463 'isEpisode': 'program/isEpisode',
464 'movieYear': 'program/movieYear',
465 'partCount': 'partCount',
466 'partIndex': 'partIndex',
467 'time': 'time'}
469 for item in items:
470 data = tag_data(showing, items[item])
471 if data:
472 if item == 'description':
473 data = data.replace(TRIBUNE_CR, '')
474 metadata[item] = data
476 vItems = ['vActor', 'vChoreographer', 'vDirector',
477 'vExecProducer', 'vProgramGenre', 'vGuestStar',
478 'vHost', 'vProducer', 'vWriter']
480 for item in vItems:
481 data = _vtag_data(program, item)
482 if data:
483 metadata[item] = data
485 sb = showing.getElementsByTagName('showingBits')
486 if sb:
487 metadata['showingBits'] = sb[0].attributes['value'].value
489 #for tag in ['starRating', 'mpaaRating', 'colorCode']:
490 for tag in ['starRating', 'mpaaRating']:
491 value = _tag_value(program, tag)
492 if value:
493 metadata[tag] = value
495 rating = _tag_value(showing, 'tvRating')
496 if rating:
497 metadata['tvRating'] = rating
499 return metadata
501 def _nfo_vitems(source, metadata):
503 vItems = {'vGenre': 'genre',
504 'vWriter': 'credits',
505 'vDirector': 'director',
506 'vActor': 'actor/name'}
508 for key in vItems:
509 data = _vtag_data_alternate(source, vItems[key])
510 if data:
511 metadata.setdefault(key, [])
512 for dat in data:
513 if not dat in metadata[key]:
514 metadata[key].append(dat)
516 if 'vGenre' in metadata:
517 metadata['vSeriesGenre'] = metadata['vProgramGenre'] = metadata['vGenre']
519 return metadata
521 def _parse_nfo(nfo_path, nfo_data=None):
522 # nfo files can contain XML or a URL to seed the XBMC metadata scrapers
523 # It's also possible to have both (a URL after the XML metadata)
524 # pyTivo only parses the XML metadata, but we'll try to stip the URL
525 # from mixed XML/URL files. Returns `None` when XML can't be parsed.
526 if nfo_data is None:
527 nfo_data = [line.strip() for line in file(nfo_path, 'rU')]
528 xmldoc = None
529 try:
530 xmldoc = minidom.parseString(os.linesep.join(nfo_data))
531 except expat.ExpatError, err:
532 if expat.ErrorString(err.code) == expat.errors.XML_ERROR_INVALID_TOKEN:
533 # might be a URL outside the xml
534 while len(nfo_data) > err.lineno:
535 if len(nfo_data[-1]) == 0:
536 nfo_data.pop()
537 else:
538 break
539 if len(nfo_data) == err.lineno:
540 # last non-blank line contains the error
541 nfo_data.pop()
542 return _parse_nfo(nfo_path, nfo_data)
543 return xmldoc
545 def _from_tvshow_nfo(tvshow_nfo_path):
546 if tvshow_nfo_path in nfo_cache:
547 return nfo_cache[tvshow_nfo_path]
549 items = {'description': 'plot',
550 'title': 'title',
551 'seriesTitle': 'showtitle',
552 'starRating': 'rating',
553 'tvRating': 'mpaa'}
555 nfo_cache[tvshow_nfo_path] = metadata = {}
557 xmldoc = _parse_nfo(tvshow_nfo_path)
558 if not xmldoc:
559 return metadata
561 tvshow = xmldoc.getElementsByTagName('tvshow')
562 if tvshow:
563 tvshow = tvshow[0]
564 else:
565 return metadata
567 for item in items:
568 data = tag_data(tvshow, items[item])
569 if data:
570 metadata[item] = data
572 metadata = _nfo_vitems(tvshow, metadata)
574 nfo_cache[tvshow_nfo_path] = metadata
575 return metadata
577 def _from_episode_nfo(nfo_path, xmldoc):
578 metadata = {}
580 items = {'description': 'plot',
581 'episodeTitle': 'title',
582 'seriesTitle': 'showtitle',
583 'originalAirDate': 'aired',
584 'starRating': 'rating',
585 'tvRating': 'mpaa'}
587 # find tvshow.nfo
588 path = nfo_path
589 while True:
590 basepath = os.path.dirname(path)
591 if path == basepath:
592 break
593 path = basepath
594 tv_nfo = os.path.join(path, 'tvshow.nfo')
595 if os.path.exists(tv_nfo):
596 metadata.update(_from_tvshow_nfo(tv_nfo))
597 break
599 episode = xmldoc.getElementsByTagName('episodedetails')
600 if episode:
601 episode = episode[0]
602 else:
603 return metadata
605 metadata['isEpisode'] = 'true'
606 for item in items:
607 data = tag_data(episode, items[item])
608 if data:
609 metadata[item] = data
611 season = tag_data(episode, 'displayseason')
612 if not season or season == "-1":
613 season = tag_data(episode, 'season')
614 if not season:
615 season = 1
617 ep_num = tag_data(episode, 'displayepisode')
618 if not ep_num or ep_num == "-1":
619 ep_num = tag_data(episode, 'episode')
620 if ep_num and ep_num != "-1":
621 metadata['episodeNumber'] = "%d%02d" % (int(season), int(ep_num))
623 if 'originalAirDate' in metadata:
624 metadata['originalAirDate'] += 'T00:00:00Z'
626 metadata = _nfo_vitems(episode, metadata)
628 return metadata
630 def _from_movie_nfo(xmldoc):
631 metadata = {}
633 movie = xmldoc.getElementsByTagName('movie')
634 if movie:
635 movie = movie[0]
636 else:
637 return metadata
639 items = {'description': 'plot',
640 'title': 'title',
641 'movieYear': 'year',
642 'starRating': 'rating',
643 'mpaaRating': 'mpaa'}
645 metadata['isEpisode'] = 'false'
647 for item in items:
648 data = tag_data(movie, items[item])
649 if data:
650 metadata[item] = data
652 metadata['movieYear'] = "%04d" % int(metadata.get('movieYear', 0))
654 metadata = _nfo_vitems(movie, metadata)
655 return metadata
657 def from_nfo(full_path):
658 if full_path in nfo_cache:
659 return nfo_cache[full_path]
661 metadata = nfo_cache[full_path] = {}
663 nfo_path = "%s.nfo" % os.path.splitext(full_path)[0]
664 if not os.path.exists(nfo_path):
665 return metadata
667 xmldoc = _parse_nfo(nfo_path)
668 if not xmldoc:
669 return metadata
671 if xmldoc.getElementsByTagName('episodedetails'):
672 # it's an episode
673 metadata.update(_from_episode_nfo(nfo_path, xmldoc))
674 elif xmldoc.getElementsByTagName('movie'):
675 # it's a movie
676 metadata.update(_from_movie_nfo(xmldoc))
678 # common nfo cleanup
679 if 'starRating' in metadata:
680 # .NFO 0-10 -> TiVo 1-7
681 rating = int(float(metadata['starRating']) * 6 / 10 + 1.5)
682 metadata['starRating'] = rating
684 for key, mapping in [('mpaaRating', MPAA_RATINGS),
685 ('tvRating', TV_RATINGS)]:
686 if key in metadata:
687 rating = mapping.get(metadata[key], None)
688 if rating:
689 metadata[key] = rating
690 else:
691 del metadata[key]
693 nfo_cache[full_path] = metadata
694 return metadata
696 def _tdcat_bin(tdcat_path, full_path, tivo_mak):
697 fname = unicode(full_path, 'utf-8')
698 if mswindows:
699 fname = fname.encode('cp1252')
700 tcmd = [tdcat_path, '-m', tivo_mak, '-2', fname]
701 tdcat = subprocess.Popen(tcmd, stdout=subprocess.PIPE)
702 return tdcat.stdout.read()
704 def _tdcat_py(full_path, tivo_mak):
705 xml_data = {}
707 tfile = open(full_path, 'rb')
708 header = tfile.read(16)
709 offset, chunks = struct.unpack('>LH', header[10:])
710 rawdata = tfile.read(offset - 16)
711 tfile.close()
713 count = 0
714 for i in xrange(chunks):
715 chunk_size, data_size, id, enc = struct.unpack('>LLHH',
716 rawdata[count:count + 12])
717 count += 12
718 data = rawdata[count:count + data_size]
719 xml_data[id] = {'enc': enc, 'data': data, 'start': count + 16}
720 count += chunk_size - 12
722 chunk = xml_data[2]
723 details = chunk['data']
724 if chunk['enc']:
725 xml_key = xml_data[3]['data']
727 hexmak = hashlib.md5('tivo:TiVo DVR:' + tivo_mak).hexdigest()
728 key = hashlib.sha1(hexmak + xml_key).digest()[:16] + '\0\0\0\0'
730 turkey = hashlib.sha1(key[:17]).digest()
731 turiv = hashlib.sha1(key).digest()
733 details = turing.Turing(turkey, turiv).crypt(details, chunk['start'])
735 return details
737 def from_tivo(full_path):
738 if full_path in tivo_cache:
739 return tivo_cache[full_path]
741 tdcat_path = config.get_bin('tdcat')
742 tivo_mak = config.get_server('tivo_mak')
743 try:
744 assert(tivo_mak)
745 if tdcat_path:
746 details = _tdcat_bin(tdcat_path, full_path, tivo_mak)
747 else:
748 details = _tdcat_py(full_path, tivo_mak)
749 metadata = from_details(details)
750 tivo_cache[full_path] = metadata
751 except:
752 metadata = {}
754 return metadata
756 def force_utf8(text):
757 if type(text) == str:
758 try:
759 text = text.decode('utf8')
760 except:
761 if sys.platform == 'darwin':
762 text = text.decode('macroman')
763 else:
764 text = text.decode('cp1252')
765 return text.encode('utf-8')
767 def dump(output, metadata):
768 for key in metadata:
769 value = metadata[key]
770 if type(value) == list:
771 for item in value:
772 output.write('%s: %s\n' % (key, item.encode('utf-8')))
773 else:
774 if key in HUMAN and value in HUMAN[key]:
775 output.write('%s: %s\n' % (key, HUMAN[key][value]))
776 else:
777 output.write('%s: %s\n' % (key, value.encode('utf-8')))
779 if __name__ == '__main__':
780 if len(sys.argv) > 1:
781 metadata = {}
782 config.init([])
783 logging.basicConfig()
784 fname = force_utf8(sys.argv[1])
785 ext = os.path.splitext(fname)[1].lower()
786 if ext == '.tivo':
787 metadata.update(from_tivo(fname))
788 elif ext in ['.mp4', '.m4v', '.mov']:
789 metadata.update(from_moov(fname))
790 elif ext in ['.dvr-ms', '.asf', '.wmv']:
791 metadata.update(from_dvrms(fname))
792 elif ext == '.wtv':
793 vInfo = plugins.video.transcode.video_info(fname)
794 metadata.update(from_mscore(vInfo['rawmeta']))
795 dump(sys.stdout, metadata)