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