6 from datetime
import datetime
7 from xml
.dom
import minidom
14 from lrucache
import LRUCache
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',
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
)
53 element
= new_element
[0]
54 if not element
.firstChild
:
56 return element
.firstChild
.data
58 def _vtag_data(element
, tag
):
59 for name
in tag
.split('/'):
60 new_element
= element
.getElementsByTagName(name
)
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
)
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
]
82 mp4meta
= mutagen
.File(full_path
)
85 mp4_cache
[full_path
] = {}
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:
97 metadata
['isEpisode'] = ['false', 'true'][value
== 'TV Show']
99 metadata
[keys
[key
]] = value
100 # These keys begin with the copyright symbol \xA9
101 elif key
== '\xa9day':
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'):
109 metadata
[k
].append(value
)
111 metadata
[k
] = [value
]
112 elif key
== '\xa9nam':
113 if 'tvsh' in mp4meta
:
114 metadata
['episodeTitle'] = value
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
)
141 metadata
[items
[item
]] = [x
['name'] for x
in data
[item
]]
143 mp4_cache
[full_path
] = metadata
146 def from_dvrms(full_path
):
147 if full_path
in dvrms_cache
:
148 return dvrms_cache
[full_path
]
153 meta
= mutagen
.File(full_path
)
156 dvrms_cache
[full_path
] = {}
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']}
169 for tag
in keys
[tagname
]:
172 value
= str(meta
[tag
][0])
174 metadata
[tagname
] = value
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
199 def from_eyetv(full_path
):
200 keys
= {'TITLE': 'title', 'SUBTITLE': 'episodeTitle',
201 'DESCRIPTION': 'description', 'YEAR': 'movieYear',
202 'EPISODENUM': 'episodeNumber'}
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']
212 metadata
[keys
[key
]] = info
[key
]
214 metadata
['seriesTitle'] = info
['TITLE']
216 metadata
['vActor'] = [x
.strip() for x
in info
['ACTORS'].split(',')]
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
233 def from_text(full_path
):
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
:
247 key
, value
= [x
.strip() for x
in line
.split(sep
, 1)]
248 if not key
or not value
:
250 if key
.startswith('v'):
252 metadata
[key
].append(value
)
254 metadata
[key
] = [value
]
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()
263 metadata
[rating
] = ratings
[x
]
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
273 originalAirDate
= datetime
.fromtimestamp(mtime
)
275 metadata
= {'title': title
,
276 'originalAirDate': originalAirDate
.isoformat()}
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
))
288 def from_container(xmldoc
):
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]
299 data
= tag_data(details
, keys
[key
])
301 if key
== 'description':
302 data
= data
.replace(TRIBUNE_CR
, '')
303 elif key
== 'tvRating':
305 elif key
== 'displayMajorNumber':
307 data
, metadata
['displayMinorNumber'] = data
.split('-')
312 def from_details(xmldoc
):
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',
332 data
= tag_data(showing
, items
[item
])
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']
343 data
= _vtag_data(program
, item
)
345 metadata
[item
] = data
347 sb
= showing
.getElementsByTagName('showingBits')
349 metadata
['showingBits'] = sb
[0].attributes
['value'].value
351 for tag
in ['starRating', 'mpaaRating', 'colorCode']:
352 value
= _tag_value(program
, tag
)
354 metadata
[tag
] = value
356 rating
= _tag_value(showing
, 'tvRating')
358 metadata
['tvRating'] = 'x' + rating
[1]
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
379 if __name__
== '__main__':
380 if len(sys
.argv
) > 1:
382 ext
= os
.path
.splitext(sys
.argv
[1])[1].lower()
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]))
391 value
= metadata
[key
]
392 if type(value
) == list:
394 print '%s: %s' % (key
, item
.encode('utf8'))
396 if key
in HUMAN
and value
in HUMAN
[key
]:
397 print '%s: %s' % (key
, HUMAN
[key
][value
])
399 print '%s: %s' % (key
, value
.encode('utf8'))