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
= tmpl('container_html.tmpl')
37 XML_CONTAINER_TEMPLATE
= tmpl('container_xml.tmpl')
38 TVBUS_TEMPLATE
= tmpl('TvBus.tmpl')
40 EXTENSIONS
= """.tivo .mpg .avi .wmv .mov .flv .f4v .vob .mp4 .m4v .mkv
41 .ts .tp .trp .3g2 .3gp .3gp2 .3gpp .amv .asf .avs .bik .bix .box .bsf
42 .dat .dif .divx .dmb .dpg .dv .dvr-ms .evo .eye .flc .fli .flx .gvi .ivf
43 .m1v .m21 .m2t .m2ts .m2v .m2p .m4e .mjp .mjpeg .mod .moov .movie .mp21
44 .mpe .mpeg .mpv .mpv2 .mqv .mts .mvb .nsv .nuv .nut .ogm .qt .rm .rmvb
45 .rts .scm .smv .ssm .svi .vdo .vfw .vid .viv .vivo .vp6 .vp7 .vro .webm
46 .wm .wmd .wtv .yuv""".split()
50 assert(config
.get_bin('ffmpeg'))
52 use_extensions
= False
54 queue
= [] # Recordings to push
57 return time
.strptime(iso
[:19], '%Y-%m-%dT%H:%M:%S')
60 return datetime(*uniso(iso
)[:6])
63 return int(calendar
.timegm(uniso(iso
)))
65 class Pushable(object):
67 def push_one_file(self
, f
):
68 file_info
= VideoDetails()
69 file_info
['valid'] = transcode
.supported_format(f
['path'])
72 if config
.isHDtivo(f
['tsn']):
73 for m
in ['video/mp4', 'video/bif']:
74 if transcode
.tivo_compatible(f
['path'], f
['tsn'], m
)[0]:
78 if (mime
== 'video/mpeg' and
79 transcode
.mp4_remuxable(f
['path'], f
['tsn'])):
80 new_path
= transcode
.mp4_remux(f
['path'], f
['name'], f
['tsn'])
85 if file_info
['valid']:
86 file_info
.update(self
.metadata_full(f
['path'], f
['tsn'], mime
))
88 url
= f
['url'] + quote(f
['name'])
90 title
= file_info
['seriesTitle']
92 title
= file_info
['title']
94 source
= file_info
['seriesId']
98 subtitle
= file_info
['episodeTitle']
100 m
= mind
.getMind(f
['tsn'])
104 description
= file_info
['description'],
105 duration
= file_info
['duration'] / 1000,
106 size
= file_info
['size'],
111 tvrating
= file_info
['tvRating'])
112 except Exception, msg
:
115 def process_queue(self
):
119 self
.push_one_file(item
)
122 """ returns your external IP address by querying dyndns.org """
123 f
= urllib
.urlopen('http://checkip.dyndns.org/')
125 m
= re
.search('([\d]*\.[\d]*\.[\d]*\.[\d]*)', s
)
128 def Push(self
, handler
, query
):
130 tsn
= query
['tsn'][0]
132 logger
.error('Push requires a TiVo Service Number')
133 handler
.send_error(404)
136 for key
in config
.tivos
:
137 if config
.tivos
[key
]['name'] == tsn
:
140 tivo_name
= config
.tivos
[tsn
].get('name', tsn
)
142 container
= quote(query
['Container'][0].split('/')[0])
143 ip
= config
.get_ip(tsn
)
144 port
= config
.getPort()
146 baseurl
= 'http://%s:%s/%s' % (ip
, port
, container
)
147 if config
.getIsExternal(tsn
):
148 exturl
= config
.get_server('externalurl')
150 if not exturl
.endswith('/'):
152 baseurl
= exturl
+ container
155 baseurl
= 'http://%s:%s/%s' % (ip
, port
, container
)
157 path
= self
.get_local_base_path(handler
, query
)
159 files
= query
.get('File', [])
161 file_path
= os
.path
.normpath(path
+ '/' + f
)
162 queue
.append({'path': file_path
, 'name': f
, 'tsn': tsn
,
165 thread
.start_new_thread(Video
.process_queue
, (self
,))
167 logger
.info('[%s] Queued "%s" for Push to %s' %
168 (time
.strftime('%d/%b/%Y %H:%M:%S'),
169 unicode(file_path
, 'utf-8'), tivo_name
))
171 files
= [unicode(f
, 'utf-8') for f
in files
]
172 handler
.redir(PUSHED
% (tivo_name
, '<br>'.join(files
)), 5)
174 class BaseVideo(Plugin
):
176 CONTENT_TYPE
= 'x-container/tivo-videos'
178 tvbus_cache
= LRUCache(1)
180 def video_file_filter(self
, full_path
, type=None):
181 if os
.path
.isdir(unicode(full_path
, 'utf-8')):
184 return os
.path
.splitext(full_path
)[1].lower() in EXTENSIONS
186 return transcode
.supported_format(full_path
)
188 def send_file(self
, handler
, path
, query
):
189 mime
= 'video/x-tivo-mpeg'
190 tsn
= handler
.headers
.getheader('tsn', '')
192 tivo_name
= config
.tivos
[tsn
].get('name', tsn
)
194 tivo_name
= handler
.address_string()
196 is_tivo_file
= (path
[-5:].lower() == '.tivo')
198 if 'Format' in query
:
199 mime
= query
['Format'][0]
201 needs_tivodecode
= (is_tivo_file
and mime
== 'video/mpeg')
202 compatible
= (not needs_tivodecode
and
203 transcode
.tivo_compatible(path
, tsn
, mime
)[0])
206 offset
= int(handler
.headers
.getheader('Range')[6:-1])
211 valid
= bool(config
.get_bin('tivodecode') and
212 config
.get_server('tivo_mak'))
217 valid
= ((compatible
and offset
< os
.path
.getsize(path
)) or
218 (not compatible
and transcode
.is_resumable(path
, offset
)))
220 #faking = (mime in ['video/x-tivo-mpeg-ts', 'video/x-tivo-mpeg'] and
221 faking
= (mime
== 'video/x-tivo-mpeg' and
222 not (is_tivo_file
and compatible
))
223 fname
= unicode(path
, 'utf-8')
226 thead
= self
.tivo_header(tsn
, path
, mime
)
228 size
= os
.path
.getsize(fname
) + len(thead
)
229 handler
.send_response(200)
230 handler
.send_header('Content-Length', size
- offset
)
231 handler
.send_header('Content-Range', 'bytes %d-%d/%d' %
232 (offset
, size
- offset
- 1, size
))
234 handler
.send_response(206)
235 handler
.send_header('Transfer-Encoding', 'chunked')
236 handler
.send_header('Content-Type', mime
)
237 handler
.end_headers()
239 logger
.info('[%s] Start sending "%s" to %s' %
240 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
, tivo_name
))
246 if faking
and not offset
:
247 handler
.wfile
.write(thead
)
248 logger
.debug('"%s" is tivo compatible' % fname
)
249 f
= open(fname
, 'rb')
251 if mime
== 'video/mp4':
252 count
= qtfaststart
.process(f
, handler
.wfile
, offset
)
258 block
= f
.read(512 * 1024)
261 handler
.wfile
.write(block
)
263 except Exception, msg
:
267 logger
.debug('"%s" is not tivo compatible' % fname
)
269 count
= transcode
.resume_transfer(path
, handler
.wfile
,
272 count
= transcode
.transcode(False, path
, handler
.wfile
,
276 handler
.wfile
.write('0\r\n\r\n')
277 handler
.wfile
.flush()
278 except Exception, msg
:
281 mega_elapsed
= (time
.time() - start
) * 1024 * 1024
284 rate
= count
* 8.0 / mega_elapsed
285 logger
.info('[%s] Done sending "%s" to %s, %d bytes, %.2f Mb/s' %
286 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
,
287 tivo_name
, count
, rate
))
289 if fname
.endswith('.pyTivo-temp'):
292 def __duration(self
, full_path
):
293 return transcode
.video_info(full_path
)['millisecs']
295 def __total_items(self
, full_path
):
298 full_path
= unicode(full_path
, 'utf-8')
299 for f
in os
.listdir(full_path
):
300 if f
.startswith('.'):
302 f
= os
.path
.join(full_path
, f
)
303 f2
= f
.encode('utf-8')
307 if os
.path
.splitext(f2
)[1].lower() in EXTENSIONS
:
309 elif f2
in transcode
.info_cache
:
310 if transcode
.supported_format(f2
):
316 def __est_size(self
, full_path
, tsn
='', mime
=''):
317 # Size is estimated by taking audio and video bit rate adding 2%
319 if transcode
.tivo_compatible(full_path
, tsn
, mime
)[0]:
320 return os
.path
.getsize(unicode(full_path
, 'utf-8'))
323 audioBPS
= config
.getMaxAudioBR(tsn
) * 1000
324 #audioBPS = config.strtod(config.getAudioBR(tsn))
325 videoBPS
= transcode
.select_videostr(full_path
, tsn
)
326 bitrate
= audioBPS
+ videoBPS
327 return int((self
.__duration
(full_path
) / 1000) *
328 (bitrate
* 1.02 / 8))
330 def metadata_full(self
, full_path
, tsn
='', mime
='', mtime
=None):
332 vInfo
= transcode
.video_info(full_path
)
334 if ((int(vInfo
['vHeight']) >= 720 and
335 config
.getTivoHeight
>= 720) or
336 (int(vInfo
['vWidth']) >= 1280 and
337 config
.getTivoWidth
>= 1280)):
338 data
['showingBits'] = '4096'
340 data
.update(metadata
.basic(full_path
, mtime
))
341 if full_path
[-5:].lower() == '.tivo':
342 data
.update(metadata
.from_tivo(full_path
))
343 if full_path
[-4:].lower() == '.wtv':
344 data
.update(metadata
.from_mscore(vInfo
['rawmeta']))
346 if 'episodeNumber' in data
:
348 ep
= int(data
['episodeNumber'])
351 data
['episodeNumber'] = str(ep
)
353 if config
.getDebug() and 'vHost' not in data
:
354 compatible
, reason
= transcode
.tivo_compatible(full_path
, tsn
, mime
)
356 transcode_options
= {}
358 transcode_options
= transcode
.transcode(True, full_path
,
361 ['TRANSCODE=%s, %s' % (['YES', 'NO'][compatible
], reason
)] +
364 for k
, v
in sorted(vInfo
.items(), reverse
=True)] +
365 ['TRANSCODE OPTIONS: '] +
366 ["%s" % (v
) for k
, v
in transcode_options
.items()] +
367 ['SOURCE FILE: ', os
.path
.basename(full_path
)]
370 now
= datetime
.utcnow()
372 if data
['time'].lower() == 'file':
374 mtime
= os
.path
.getmtime(unicode(full_path
, 'utf-8'))
376 now
= datetime
.utcfromtimestamp(mtime
)
378 logger
.warning('Bad file time on ' + full_path
)
379 elif data
['time'].lower() == 'oad':
380 now
= isodt(data
['originalAirDate'])
383 now
= isodt(data
['time'])
385 logger
.warning('Bad time format: ' + data
['time'] +
386 ' , using current time')
388 duration
= self
.__duration
(full_path
)
389 duration_delta
= timedelta(milliseconds
= duration
)
390 min = duration_delta
.seconds
/ 60
391 sec
= duration_delta
.seconds
% 60
395 data
.update({'time': now
.isoformat(),
396 'startTime': now
.isoformat(),
397 'stopTime': (now
+ duration_delta
).isoformat(),
398 'size': self
.__est
_size
(full_path
, tsn
, mime
),
399 'duration': duration
,
400 'iso_duration': ('P%sDT%sH%sM%sS' %
401 (duration_delta
.days
, hours
, min, sec
))})
405 def QueryContainer(self
, handler
, query
):
406 tsn
= handler
.headers
.getheader('tsn', '')
407 subcname
= query
['Container'][0]
409 if not self
.get_local_path(handler
, query
):
410 handler
.send_error(404)
413 container
= handler
.container
414 force_alpha
= container
.getboolean('force_alpha')
415 use_html
= query
.get('Format', [''])[0].lower() == 'text/html'
417 files
, total
, start
= self
.get_files(handler
, query
,
418 self
.video_file_filter
,
422 local_base_path
= self
.get_local_base_path(handler
, query
)
424 video
= VideoDetails()
427 ltime
= time
.localtime(mtime
)
429 logger
.warning('Bad file time on ' + unicode(f
.name
, 'utf-8'))
431 ltime
= time
.localtime(mtime
)
432 video
['captureDate'] = hex(int(mtime
))
433 video
['textDate'] = time
.strftime('%b %d, %Y', ltime
)
434 video
['name'] = os
.path
.basename(f
.name
)
435 video
['path'] = f
.name
436 video
['part_path'] = f
.name
.replace(local_base_path
, '', 1)
437 if not video
['part_path'].startswith(os
.path
.sep
):
438 video
['part_path'] = os
.path
.sep
+ video
['part_path']
439 video
['title'] = os
.path
.basename(f
.name
)
440 video
['is_dir'] = f
.isdir
442 video
['small_path'] = subcname
+ '/' + video
['name']
443 video
['total_items'] = self
.__total
_items
(f
.name
)
445 if len(files
) == 1 or f
.name
in transcode
.info_cache
:
446 video
['valid'] = transcode
.supported_format(f
.name
)
448 video
.update(self
.metadata_full(f
.name
, tsn
,
451 video
['captureDate'] = hex(isogm(video
['time']))
453 video
['valid'] = True
454 video
.update(metadata
.basic(f
.name
, mtime
))
456 if self
.use_ts(tsn
, f
.name
):
457 video
['mime'] = 'video/x-tivo-mpeg-ts'
459 video
['mime'] = 'video/x-tivo-mpeg'
461 video
['textSize'] = metadata
.human_size(f
.size
)
466 t
= Template(HTML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
468 t
= Template(XML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
469 t
.container
= handler
.cname
477 t
.guid
= config
.getGUID()
478 t
.tivos
= config
.tivos
480 handler
.send_html(str(t
))
482 handler
.send_xml(str(t
))
484 def use_ts(self
, tsn
, file_path
):
485 if config
.is_ts_capable(tsn
):
486 if file_path
[-5:].lower() == '.tivo':
488 flag
= file(file_path
).read(8)
491 if ord(flag
[7]) & 0x20:
493 elif config
.has_ts_flag():
498 def get_details_xml(self
, tsn
, file_path
):
499 if (tsn
, file_path
) in self
.tvbus_cache
:
500 details
= self
.tvbus_cache
[(tsn
, file_path
)]
502 file_info
= VideoDetails()
503 file_info
['valid'] = transcode
.supported_format(file_path
)
504 if file_info
['valid']:
505 file_info
.update(self
.metadata_full(file_path
, tsn
))
507 t
= Template(TVBUS_TEMPLATE
, filter=EncodeUnicode
)
510 t
.get_tv
= metadata
.get_tv
511 t
.get_mpaa
= metadata
.get_mpaa
512 t
.get_stars
= metadata
.get_stars
514 self
.tvbus_cache
[(tsn
, file_path
)] = details
517 def tivo_header(self
, tsn
, path
, mime
):
518 def pad(length
, align
):
519 extra
= length
% align
521 extra
= align
- extra
524 if mime
== 'video/x-tivo-mpeg-ts':
528 details
= self
.get_details_xml(tsn
, path
)
530 chunk
= details
+ '\0' * (pad(ld
, 4) + 4)
532 blocklen
= lc
* 2 + 40
533 padding
= pad(blocklen
, 1024)
535 return ''.join(['TiVo', struct
.pack('>HHHLH', 4, flag
, 0,
536 padding
+ blocklen
, 2),
537 struct
.pack('>LLHH', lc
+ 12, ld
, 1, 0),
539 struct
.pack('>LLHH', lc
+ 12, ld
, 2, 0),
540 chunk
, '\0' * padding
])
542 def TVBusQuery(self
, handler
, query
):
543 tsn
= handler
.headers
.getheader('tsn', '')
545 path
= self
.get_local_path(handler
, query
)
546 file_path
= os
.path
.normpath(path
+ '/' + f
)
548 details
= self
.get_details_xml(tsn
, file_path
)
550 handler
.send_xml(details
)
552 class Video(BaseVideo
, Pushable
):
555 class VideoDetails(DictMixin
):
557 def __init__(self
, d
=None):
563 def __getitem__(self
, key
):
564 if key
not in self
.d
:
565 self
.d
[key
] = self
.default(key
)
568 def __contains__(self
, key
):
571 def __setitem__(self
, key
, value
):
574 def __delitem__(self
):
581 return self
.d
.__iter
__()
584 return self
.d
.iteritems()
586 def default(self
, key
):
589 'displayMajorNumber' : '0',
590 'displayMinorNumber' : '0',
591 'isEpisode' : 'true',
592 'colorCode' : ('COLOR', '4'),
593 'showType' : ('SERIES', '5')
597 elif key
.startswith('v'):