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 tivo_cache
= LRUCache(50)
22 mp4_cache
= LRUCache(50)
24 def tag_data(element
, tag
):
25 for name
in tag
.split('/'):
26 new_element
= element
.getElementsByTagName(name
)
29 element
= new_element
[0]
30 if not element
.firstChild
:
32 return element
.firstChild
.data
34 def _vtag_data(element
, tag
):
35 for name
in tag
.split('/'):
36 new_element
= element
.getElementsByTagName(name
)
39 element
= new_element
[0]
40 elements
= element
.getElementsByTagName('element')
41 return [x
.firstChild
.data
for x
in elements
if x
.firstChild
]
43 def _tag_value(element
, tag
):
44 item
= element
.getElementsByTagName(tag
)
46 value
= item
[0].attributes
['value'].value
47 name
= item
[0].firstChild
.data
48 return name
[0] + value
[0]
50 def from_moov(full_path
):
51 if full_path
in mp4_cache
:
52 return mp4_cache
[full_path
]
58 mp4meta
= mutagen
.File(full_path
)
61 mp4_cache
[full_path
] = {}
64 for key
, value
in mp4meta
.items():
65 # The following 1-to-1 correspondence of atoms to pyTivo
66 # variables is TV-biased
67 keys
= {'tvnn': 'callsign', 'tven': 'episodeNumber',
68 'tvsh': 'seriesTitle'}
69 if type(value
) == list:
72 metadata
['isEpisode'] = ['false', 'true'][value
== 'TV Show']
74 metadata
[keys
[key
]] = value
75 # These keys begin with the copyright symbol \xA9
76 elif key
== '\xa9day':
78 value
+= '-01-01T16:00:00Z'
79 metadata
['originalAirDate'] = value
80 #metadata['time'] = value
81 elif key
in ['\xa9gen', 'gnre']:
82 for k
in ('vProgramGenre', 'vSeriesGenre'):
84 metadata
[k
].append(value
)
87 elif key
== '\xa9nam':
89 metadata
['episodeTitle'] = value
91 metadata
['title'] = value
93 # Description in desc, cmt, and/or ldes tags. Keep the longest.
94 elif key
in ['desc', '\xa9cmt', 'ldes'] and len(value
) > len_desc
:
95 metadata
['description'] = value
98 # A common custom "reverse DNS format" tag
99 # Possible TV values: TV-Y7 TV-Y TV-G TV-PG TV-14 TV-MA Unrated
100 # Possible MPAA values: G PG PG-13 R NC-17 Unrated
101 elif (key
== '----:com.apple.iTunes:iTunEXTC' and
102 ('us-tv' in value
or 'mpaa' in value
)):
103 ratings
= {'TV-Y7': 'x1', 'TV-Y': 'x2', 'TV-G': 'x3',
104 'TV-PG': 'x4', 'TV-14': 'x5', 'TV-MA': 'x6',
105 'Unrated': 'x7', 'G': 'G1', 'PG': 'P2',
106 'PG-13': 'P3', 'R': 'R4', 'NC-17': 'N6'}
107 rating
= value
.split("|")[1]
108 if rating
in ratings
:
110 metadata
['tvRating'] = ratings
[rating
]
111 elif 'mpaa' in value
:
112 metadata
['mpaaRating'] = ratings
[rating
]
114 # Actors, directors, producers, AND screenwriters may be in a long
115 # embedded XML plist, with key '----' and rDNS 'iTunMOVI'. Ughh!
117 mp4_cache
[full_path
] = metadata
120 def from_eyetv(full_path
):
121 ratings
= {'TVY7': 'x1', 'TVY': 'x2', 'TVG': 'x3',
122 'TVPG': 'x4', 'TV14': 'x5', 'TVMA': 'x6',
123 'G': 'G1', 'PG': 'P2', 'PG-13': 'P3',
124 'R': 'R4', 'NC-17': 'N6'}
125 keys
= {'TITLE': 'title', 'SUBTITLE': 'episodeTitle',
126 'DESCRIPTION': 'description', 'YEAR': 'movieYear',
127 'EPISODENUM': 'episodeNumber'}
129 path
, name
= os
.path
.split(full_path
)
130 eyetvp
= [x
for x
in os
.listdir(path
) if x
.endswith('.eyetvp')][0]
131 eyetvp
= os
.path
.join(path
, eyetvp
)
132 eyetv
= plistlib
.readPlist(eyetvp
)
133 if 'epg info' in eyetv
:
134 info
= eyetv
['epg info']
137 metadata
[keys
[key
]] = info
[key
]
139 metadata
['seriesTitle'] = info
['TITLE']
141 metadata
['vActor'] = [x
.strip() for x
in info
['ACTORS'].split(',')]
143 metadata
['vDirector'] = [info
['DIRECTOR']]
144 if info
['TV_RATING']:
145 metadata
['tvRating'] = ratings
[info
['TV_RATING']]
146 if info
['STAR_RATING']:
147 metadata
['starRating'] = 'x%d' % (len(info
['STAR_RATING']) * 2 - 1)
148 mpaa
= info
['MPAA_RATING']
149 if mpaa
and mpaa
!= 'NR':
150 metadata
['mpaaRating'] = ratings
[mpaa
]
151 # movieYear must be set for the mpaa/star ratings to work
152 if (('mpaaRating' in metadata
or 'starRating' in metadata
) and
153 'movieYear' not in metadata
):
154 metadata
['movieYear'] = eyetv
['info']['start'].year
157 def from_text(full_path
):
159 path
, name
= os
.path
.split(full_path
)
160 title
, ext
= os
.path
.splitext(name
)
161 for metafile
in [os
.path
.join(path
, title
) + '.properties',
162 os
.path
.join(path
, 'default.txt'), full_path
+ '.txt',
163 os
.path
.join(path
, '.meta', name
) + '.txt']:
164 if os
.path
.exists(metafile
):
165 sep
= ':='[metafile
.endswith('.properties')]
166 for line
in file(metafile
, 'U'):
167 if line
.strip().startswith('#') or not sep
in line
:
169 key
, value
= [x
.strip() for x
in line
.split(sep
, 1)]
170 if not key
or not value
:
172 if key
.startswith('v'):
174 metadata
[key
].append(value
)
176 metadata
[key
] = [value
]
178 metadata
[key
] = value
181 def basic(full_path
):
182 base_path
, name
= os
.path
.split(full_path
)
183 title
, ext
= os
.path
.splitext(name
)
184 mtime
= os
.stat(full_path
).st_mtime
187 originalAirDate
= datetime
.fromtimestamp(mtime
)
189 metadata
= {'title': title
,
190 'originalAirDate': originalAirDate
.isoformat()}
191 if ext
.lower() in ['.mp4', '.m4v', '.mov']:
192 metadata
.update(from_moov(full_path
))
193 if 'plistlib' in sys
.modules
and base_path
.endswith('.eyetv'):
194 metadata
.update(from_eyetv(full_path
))
195 metadata
.update(from_text(full_path
))
199 def from_container(xmldoc
):
202 keys
= {'title': 'Title', 'episodeTitle': 'EpisodeTitle',
203 'description': 'Description', 'seriesId': 'SeriesId',
204 'episodeNumber': 'EpisodeNumber', 'tvRating': 'TvRating',
205 'displayMajorNumber': 'SourceChannel', 'callsign': 'SourceStation'}
207 details
= xmldoc
.getElementsByTagName('Details')[0]
210 data
= tag_data(details
, keys
[key
])
212 if key
== 'description':
213 data
= data
.replace(TRIBUNE_CR
, '')
214 elif key
== 'tvRating':
216 elif key
== 'displayMajorNumber':
218 data
, metadata
['displayMinorNumber'] = data
.split('-')
223 def from_details(xmldoc
):
226 showing
= xmldoc
.getElementsByTagName('showing')[0]
227 program
= showing
.getElementsByTagName('program')[0]
229 items
= {'description': 'program/description',
230 'title': 'program/title',
231 'episodeTitle': 'program/episodeTitle',
232 'episodeNumber': 'program/episodeNumber',
233 'seriesId': 'program/series/uniqueId',
234 'seriesTitle': 'program/series/seriesTitle',
235 'originalAirDate': 'program/originalAirDate',
236 'isEpisode': 'program/isEpisode',
237 'movieYear': 'program/movieYear',
238 'partCount': 'partCount',
239 'partIndex': 'partIndex',
243 data
= tag_data(showing
, items
[item
])
245 if item
== 'description':
246 data
= data
.replace(TRIBUNE_CR
, '')
247 metadata
[item
] = data
249 vItems
= ['vActor', 'vChoreographer', 'vDirector',
250 'vExecProducer', 'vProgramGenre', 'vGuestStar',
251 'vHost', 'vProducer', 'vWriter']
254 data
= _vtag_data(program
, item
)
256 metadata
[item
] = data
258 sb
= showing
.getElementsByTagName('showingBits')
260 metadata
['showingBits'] = sb
[0].attributes
['value'].value
262 for tag
in ['starRating', 'mpaaRating', 'colorCode']:
263 value
= _tag_value(program
, tag
)
265 metadata
[tag
] = value
267 rating
= _tag_value(showing
, 'tvRating')
269 metadata
['tvRating'] = 'x' + rating
[1]
273 def from_tivo(full_path
):
274 if full_path
in tivo_cache
:
275 return tivo_cache
[full_path
]
277 tdcat_path
= config
.get_bin('tdcat')
278 tivo_mak
= config
.get_server('tivo_mak')
279 if tdcat_path
and tivo_mak
:
280 tcmd
= [tdcat_path
, '-m', tivo_mak
, '-2', full_path
]
281 tdcat
= subprocess
.Popen(tcmd
, stdout
=subprocess
.PIPE
)
282 xmldoc
= minidom
.parse(tdcat
.stdout
)
283 metadata
= from_details(xmldoc
)
284 tivo_cache
[full_path
] = metadata
290 if __name__
== '__main__':
291 if len(sys
.argv
) > 1:
293 ext
= os
.path
.splitext(sys
.argv
[1])[1].lower()
296 metadata
.update(from_tivo(sys
.argv
[1]))
297 elif ext
in ['.mp4', '.m4v', '.mov']:
298 metadata
.update(from_moov(sys
.argv
[1]))
300 value
= metadata
[key
]
301 if type(value
) == list:
303 print '%s: %s' % (key
, item
.encode('utf8'))
305 print '%s: %s' % (key
, value
.encode('utf8'))