10 from UserDict
import DictMixin
11 from datetime
import datetime
, timedelta
12 from xml
.sax
.saxutils
import escape
14 from Cheetah
.Template
import Template
15 from lrucache
import LRUCache
22 from plugin
import EncodeUnicode
, Plugin
, quote
24 logger
= logging
.getLogger('pyTivo.video.video')
26 SCRIPTDIR
= os
.path
.dirname(__file__
)
30 PUSHED
= '<h3>Queued for Push to %s</h3> <p>%s</p>'
32 # Preload the templates
34 return file(os
.path
.join(SCRIPTDIR
, 'templates', name
), 'rb').read()
36 HTML_CONTAINER_TEMPLATE_MOBILE
= tmpl('container_mob.tmpl')
37 HTML_CONTAINER_TEMPLATE
= tmpl('container_html.tmpl')
38 XML_CONTAINER_TEMPLATE
= tmpl('container_xml.tmpl')
39 TVBUS_TEMPLATE
= tmpl('TvBus.tmpl')
41 EXTENSIONS
= """.tivo .mpg .avi .wmv .mov .flv .f4v .vob .mp4 .m4v .mkv
42 .ts .tp .trp .3g2 .3gp .3gp2 .3gpp .amv .asf .avs .bik .bix .box .bsf
43 .dat .dif .divx .dmb .dpg .dv .dvr-ms .evo .eye .flc .fli .flx .gvi .ivf
44 .m1v .m21 .m2t .m2ts .m2v .m2p .m4e .mjp .mjpeg .mod .moov .movie .mp21
45 .mpe .mpeg .mpv .mpv2 .mqv .mts .mvb .nsv .nuv .nut .ogm .qt .rm .rmvb
46 .rts .scm .smv .ssm .svi .vdo .vfw .vid .viv .vivo .vp6 .vp7 .vro .webm
47 .wm .wmd .wtv .yuv""".split()
51 assert(config
.get_bin('ffmpeg'))
53 use_extensions
= False
55 queue
= [] # Recordings to push
58 return time
.strptime(iso
[:19], '%Y-%m-%dT%H:%M:%S')
61 return datetime(*uniso(iso
)[:6])
64 return int(calendar
.timegm(uniso(iso
)))
66 class Pushable(object):
68 def push_one_file(self
, f
):
69 file_info
= VideoDetails()
70 file_info
['valid'] = transcode
.supported_format(f
['path'])
72 temp_share
= config
.get_server('temp_share', '')
75 for name
, data
in config
.getShares():
76 if temp_share
== name
:
77 temp_share_path
= data
.get('path')
80 if config
.isHDtivo(f
['tsn']):
81 for m
in ['video/mp4', 'video/bif']:
82 if transcode
.tivo_compatible(f
['path'], f
['tsn'], m
)[0]:
86 if (mime
== 'video/mpeg' and
87 transcode
.mp4_remuxable(f
['path'], f
['tsn'])):
88 new_path
= transcode
.mp4_remux(f
['path'], f
['name'], f
['tsn'], temp_share_path
)
94 port
= config
.getPort()
95 container
= quote(temp_share
) + '/'
96 f
['url'] = 'http://%s:%s/%s' % (ip
, port
, container
)
98 if file_info
['valid']:
99 file_info
.update(self
.metadata_full(f
['path'], f
['tsn'], mime
))
101 url
= f
['url'] + quote(f
['name'])
103 title
= file_info
['seriesTitle']
105 title
= file_info
['title']
107 source
= file_info
['seriesId']
111 subtitle
= file_info
['episodeTitle']
113 m
= mind
.getMind(f
['tsn'])
117 description
= file_info
['description'],
118 duration
= file_info
['duration'] / 1000,
119 size
= file_info
['size'],
124 tvrating
= file_info
['tvRating'])
125 except Exception, msg
:
128 def process_queue(self
):
132 self
.push_one_file(item
)
135 """ returns your external IP address by querying dyndns.org """
136 f
= urllib
.urlopen('http://checkip.dyndns.org/')
138 m
= re
.search('([\d]*\.[\d]*\.[\d]*\.[\d]*)', s
)
141 def Push(self
, handler
, query
):
142 tsn
= query
['tsn'][0]
143 for key
in config
.tivos
:
144 if config
.tivos
[key
]['name'] == tsn
:
147 tivo_name
= config
.tivos
[tsn
].get('name', tsn
)
149 container
= quote(query
['Container'][0].split('/')[0])
150 ip
= config
.get_ip(tsn
)
151 port
= config
.getPort()
153 baseurl
= 'http://%s:%s/%s' % (ip
, port
, container
)
154 if config
.getIsExternal(tsn
):
155 exturl
= config
.get_server('externalurl')
157 if not exturl
.endswith('/'):
159 baseurl
= exturl
+ container
162 baseurl
= 'http://%s:%s/%s' % (ip
, port
, container
)
164 path
= self
.get_local_base_path(handler
, query
)
166 files
= query
.get('File', [])
168 file_path
= os
.path
.normpath(path
+ '/' + f
)
169 queue
.append({'path': file_path
, 'name': f
, 'tsn': tsn
,
172 thread
.start_new_thread(Video
.process_queue
, (self
,))
174 logger
.info('[%s] Queued "%s" for Push to %s' %
175 (time
.strftime('%d/%b/%Y %H:%M:%S'),
176 unicode(file_path
, 'utf-8'), tivo_name
))
178 files
= [unicode(f
, 'utf-8') for f
in files
]
179 handler
.redir(PUSHED
% (tivo_name
, '<br>'.join(files
)), 5)
181 class BaseVideo(Plugin
):
183 CONTENT_TYPE
= 'x-container/tivo-videos'
185 tvbus_cache
= LRUCache(1)
187 def video_file_filter(self
, full_path
, type=None):
188 if os
.path
.isdir(unicode(full_path
, 'utf-8')):
191 return os
.path
.splitext(full_path
)[1].lower() in EXTENSIONS
193 return transcode
.supported_format(full_path
)
195 def send_file(self
, handler
, path
, query
):
196 mime
= 'video/x-tivo-mpeg'
197 tsn
= handler
.headers
.getheader('tsn', '')
198 tivo_name
= config
.tivos
[tsn
].get('name', tsn
)
200 is_tivo_file
= (path
[-5:].lower() == '.tivo')
202 if 'Format' in query
:
203 mime
= query
['Format'][0]
205 needs_tivodecode
= (is_tivo_file
and mime
== 'video/mpeg')
206 compatible
= (not needs_tivodecode
and
207 transcode
.tivo_compatible(path
, tsn
, mime
)[0])
210 offset
= int(handler
.headers
.getheader('Range')[6:-1])
215 valid
= bool(config
.get_bin('tivodecode') and
216 config
.get_server('tivo_mak'))
221 valid
= ((compatible
and offset
< os
.path
.getsize(path
)) or
222 (not compatible
and transcode
.is_resumable(path
, offset
)))
224 #faking = (mime in ['video/x-tivo-mpeg-ts', 'video/x-tivo-mpeg'] and
225 faking
= (mime
== 'video/x-tivo-mpeg' and
226 not (is_tivo_file
and compatible
))
227 fname
= unicode(path
, 'utf-8')
230 thead
= self
.tivo_header(tsn
, path
, mime
)
232 size
= os
.path
.getsize(fname
) + len(thead
)
233 handler
.send_response(200)
234 handler
.send_header('Content-Length', size
- offset
)
235 handler
.send_header('Content-Range', 'bytes %d-%d/%d' %
236 (offset
, size
- offset
- 1, size
))
238 handler
.send_response(206)
239 handler
.send_header('Transfer-Encoding', 'chunked')
240 handler
.send_header('Content-Type', mime
)
241 handler
.end_headers()
243 logger
.info('[%s] Start sending "%s" to %s' %
244 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
, tivo_name
))
250 if faking
and not offset
:
251 handler
.wfile
.write(thead
)
252 logger
.debug('"%s" is tivo compatible' % fname
)
253 f
= open(fname
, 'rb')
255 if mime
== 'video/mp4':
256 count
= qtfaststart
.process(f
, handler
.wfile
, offset
)
262 block
= f
.read(512 * 1024)
265 handler
.wfile
.write(block
)
267 except Exception, msg
:
271 logger
.debug('"%s" is not tivo compatible' % fname
)
273 count
= transcode
.resume_transfer(path
, handler
.wfile
,
276 count
= transcode
.transcode(False, path
, handler
.wfile
,
280 handler
.wfile
.write('0\r\n\r\n')
281 handler
.wfile
.flush()
282 except Exception, msg
:
285 mega_elapsed
= (time
.time() - start
) * 1024 * 1024
288 rate
= count
* 8.0 / mega_elapsed
289 logger
.info('[%s] Done sending "%s" to %s, %d bytes, %.2f Mb/s' %
290 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
,
291 tivo_name
, count
, rate
))
293 if fname
.endswith('.pyTivo-temp'):
296 def __duration(self
, full_path
):
297 return transcode
.video_info(full_path
)['millisecs']
299 def __total_items(self
, full_path
):
302 full_path
= unicode(full_path
, 'utf-8')
303 for f
in os
.listdir(full_path
):
304 if f
.startswith('.'):
306 f
= os
.path
.join(full_path
, f
)
307 f2
= f
.encode('utf-8')
311 if os
.path
.splitext(f2
)[1].lower() in EXTENSIONS
:
313 elif f2
in transcode
.info_cache
:
314 if transcode
.supported_format(f2
):
320 def __est_size(self
, full_path
, tsn
='', mime
=''):
321 # Size is estimated by taking audio and video bit rate adding 2%
323 if transcode
.tivo_compatible(full_path
, tsn
, mime
)[0]:
324 return os
.path
.getsize(unicode(full_path
, 'utf-8'))
327 audioBPS
= config
.getMaxAudioBR(tsn
) * 1000
328 #audioBPS = config.strtod(config.getAudioBR(tsn))
329 videoBPS
= transcode
.select_videostr(full_path
, tsn
)
330 bitrate
= audioBPS
+ videoBPS
331 return int((self
.__duration
(full_path
) / 1000) *
332 (bitrate
* 1.02 / 8))
334 def metadata_full(self
, full_path
, tsn
='', mime
='', mtime
=None):
336 vInfo
= transcode
.video_info(full_path
)
338 if ((int(vInfo
['vHeight']) >= 720 and
339 config
.getTivoHeight
>= 720) or
340 (int(vInfo
['vWidth']) >= 1280 and
341 config
.getTivoWidth
>= 1280)):
342 data
['showingBits'] = '4096'
344 data
.update(metadata
.basic(full_path
, mtime
))
345 if full_path
[-5:].lower() == '.tivo':
346 data
.update(metadata
.from_tivo(full_path
))
347 if full_path
[-4:].lower() == '.wtv':
348 data
.update(metadata
.from_mscore(vInfo
['rawmeta']))
350 if 'episodeNumber' in data
:
352 ep
= int(data
['episodeNumber'])
355 data
['episodeNumber'] = str(ep
)
357 if config
.getDebug() and 'vHost' not in data
:
358 compatible
, reason
= transcode
.tivo_compatible(full_path
, tsn
, mime
)
360 transcode_options
= {}
362 transcode_options
= transcode
.transcode(True, full_path
,
365 ['TRANSCODE=%s, %s' % (['YES', 'NO'][compatible
], reason
)] +
368 for k
, v
in sorted(vInfo
.items(), reverse
=True)] +
369 ['TRANSCODE OPTIONS: '] +
370 ["%s" % (v
) for k
, v
in transcode_options
.items()] +
371 ['SOURCE FILE: ', os
.path
.basename(full_path
)]
374 now
= datetime
.utcnow()
376 if data
['time'].lower() == 'file':
378 mtime
= os
.path
.getmtime(unicode(full_path
, 'utf-8'))
380 now
= datetime
.utcfromtimestamp(mtime
)
382 logger
.warning('Bad file time on ' + full_path
)
383 elif data
['time'].lower() == 'oad':
384 now
= isodt(data
['originalAirDate'])
387 now
= isodt(data
['time'])
389 logger
.warning('Bad time format: ' + data
['time'] +
390 ' , using current time')
392 duration
= self
.__duration
(full_path
)
393 duration_delta
= timedelta(milliseconds
= duration
)
394 min = duration_delta
.seconds
/ 60
395 sec
= duration_delta
.seconds
% 60
399 data
.update({'time': now
.isoformat(),
400 'startTime': now
.isoformat(),
401 'stopTime': (now
+ duration_delta
).isoformat(),
402 'size': self
.__est
_size
(full_path
, tsn
, mime
),
403 'duration': duration
,
404 'iso_duration': ('P%sDT%sH%sM%sS' %
405 (duration_delta
.days
, hours
, min, sec
))})
409 def QueryContainer(self
, handler
, query
):
410 tsn
= handler
.headers
.getheader('tsn', '')
411 subcname
= query
['Container'][0]
412 useragent
= handler
.headers
.getheader('User-Agent', '')
414 if not self
.get_local_path(handler
, query
):
415 handler
.send_error(404)
418 container
= handler
.container
419 force_alpha
= container
.getboolean('force_alpha')
420 use_html
= query
.get('Format', [''])[0].lower() == 'text/html'
422 files
, total
, start
= self
.get_files(handler
, query
,
423 self
.video_file_filter
,
427 local_base_path
= self
.get_local_base_path(handler
, query
)
429 video
= VideoDetails()
432 ltime
= time
.localtime(mtime
)
434 logger
.warning('Bad file time on ' + unicode(f
.name
, 'utf-8'))
436 ltime
= time
.localtime(mtime
)
437 video
['captureDate'] = hex(int(mtime
))
438 video
['textDate'] = time
.strftime('%b %d, %Y', ltime
)
439 video
['name'] = os
.path
.basename(f
.name
)
440 video
['path'] = f
.name
441 video
['part_path'] = f
.name
.replace(local_base_path
, '', 1)
442 if not video
['part_path'].startswith(os
.path
.sep
):
443 video
['part_path'] = os
.path
.sep
+ video
['part_path']
444 video
['title'] = os
.path
.basename(f
.name
)
445 video
['is_dir'] = f
.isdir
447 video
['small_path'] = subcname
+ '/' + video
['name']
448 video
['total_items'] = self
.__total
_items
(f
.name
)
450 if len(files
) == 1 or f
.name
in transcode
.info_cache
:
451 video
['valid'] = transcode
.supported_format(f
.name
)
453 video
.update(self
.metadata_full(f
.name
, tsn
,
456 video
['captureDate'] = hex(isogm(video
['time']))
458 video
['valid'] = True
459 video
.update(metadata
.basic(f
.name
, mtime
))
461 if self
.use_ts(tsn
, f
.name
):
462 video
['mime'] = 'video/x-tivo-mpeg-ts'
464 video
['mime'] = 'video/x-tivo-mpeg'
466 video
['textSize'] = metadata
.human_size(f
.size
)
470 logger
.debug('mobileagent: %d useragent: %s' % (useragent
.lower().find('mobile'), useragent
.lower()))
471 use_mobile
= useragent
.lower().find('mobile') > 0
474 t
= Template(HTML_CONTAINER_TEMPLATE_MOBILE
, filter=EncodeUnicode
)
476 t
= Template(HTML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
478 t
= Template(XML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
480 t
.container
= handler
.cname
488 t
.guid
= config
.getGUID()
489 t
.tivos
= config
.tivos
491 handler
.send_html(str(t
))
493 handler
.send_xml(str(t
))
495 def use_ts(self
, tsn
, file_path
):
496 if config
.is_ts_capable(tsn
):
497 if file_path
[-5:].lower() == '.tivo':
499 flag
= file(file_path
).read(8)
502 if ord(flag
[7]) & 0x20:
504 elif config
.has_ts_flag():
509 def get_details_xml(self
, tsn
, file_path
):
510 if (tsn
, file_path
) in self
.tvbus_cache
:
511 details
= self
.tvbus_cache
[(tsn
, file_path
)]
513 file_info
= VideoDetails()
514 file_info
['valid'] = transcode
.supported_format(file_path
)
515 if file_info
['valid']:
516 file_info
.update(self
.metadata_full(file_path
, tsn
))
518 t
= Template(TVBUS_TEMPLATE
, filter=EncodeUnicode
)
521 t
.get_tv
= metadata
.get_tv
522 t
.get_mpaa
= metadata
.get_mpaa
523 t
.get_stars
= metadata
.get_stars
525 self
.tvbus_cache
[(tsn
, file_path
)] = details
528 def tivo_header(self
, tsn
, path
, mime
):
529 def pad(length
, align
):
530 extra
= length
% align
532 extra
= align
- extra
535 if mime
== 'video/x-tivo-mpeg-ts':
539 details
= self
.get_details_xml(tsn
, path
)
541 chunk
= details
+ '\0' * (pad(ld
, 4) + 4)
543 blocklen
= lc
* 2 + 40
544 padding
= pad(blocklen
, 1024)
546 return ''.join(['TiVo', struct
.pack('>HHHLH', 4, flag
, 0,
547 padding
+ blocklen
, 2),
548 struct
.pack('>LLHH', lc
+ 12, ld
, 1, 0),
550 struct
.pack('>LLHH', lc
+ 12, ld
, 2, 0),
551 chunk
, '\0' * padding
])
553 def TVBusQuery(self
, handler
, query
):
554 tsn
= handler
.headers
.getheader('tsn', '')
556 path
= self
.get_local_path(handler
, query
)
557 file_path
= os
.path
.normpath(path
+ '/' + f
)
559 details
= self
.get_details_xml(tsn
, file_path
)
561 handler
.send_xml(details
)
563 class Video(BaseVideo
, Pushable
):
566 class VideoDetails(DictMixin
):
568 def __init__(self
, d
=None):
574 def __getitem__(self
, key
):
575 if key
not in self
.d
:
576 self
.d
[key
] = self
.default(key
)
579 def __contains__(self
, key
):
582 def __setitem__(self
, key
, value
):
585 def __delitem__(self
):
592 return self
.d
.__iter
__()
595 return self
.d
.iteritems()
597 def default(self
, key
):
600 'displayMajorNumber' : '0',
601 'displayMinorNumber' : '0',
602 'isEpisode' : 'true',
603 'colorCode' : ('COLOR', '4'),
604 'showType' : ('SERIES', '5')
608 elif key
.startswith('v'):