12 from UserDict
import DictMixin
13 from datetime
import datetime
, timedelta
14 from xml
.sax
.saxutils
import escape
16 from Cheetah
.Template
import Template
17 from lrucache
import LRUCache
24 from plugin
import EncodeUnicode
, Plugin
, quote
26 logger
= logging
.getLogger('pyTivo.video.video')
28 SCRIPTDIR
= os
.path
.dirname(__file__
)
32 PUSHED
= '<h3>Queued for Push to %s</h3> <p>%s</p>'
34 # Preload the templates
36 return file(os
.path
.join(SCRIPTDIR
, 'templates', name
), 'rb').read()
38 HTML_CONTAINER_TEMPLATE
= tmpl('container_html.tmpl')
39 XML_CONTAINER_TEMPLATE
= tmpl('container_xml.tmpl')
40 TVBUS_TEMPLATE
= tmpl('TvBus.tmpl')
42 EXTENSIONS
= """.tivo .mpg .avi .wmv .mov .flv .f4v .vob .mp4 .m4v .mkv
43 .ts .tp .trp .3g2 .3gp .3gp2 .3gpp .amv .asf .avs .bik .bix .box .bsf
44 .dat .dif .divx .dmb .dpg .dv .dvr-ms .evo .eye .flc .fli .flx .gvi .ivf
45 .m1v .m21 .m2t .m2ts .m2v .m2p .m4e .mjp .mjpeg .mod .moov .movie .mp21
46 .mpe .mpeg .mpv .mpv2 .mqv .mts .mvb .nsv .nuv .nut .ogm .qt .rm .rmvb
47 .rts .scm .smv .ssm .svi .vdo .vfw .vid .viv .vivo .vp6 .vp7 .vro .webm
48 .wm .wmd .wtv .yuv""".split()
52 assert(config
.get_bin('ffmpeg'))
54 use_extensions
= False
56 queue
= [] # Recordings to push
59 return time
.strptime(iso
[:19], '%Y-%m-%dT%H:%M:%S')
62 return datetime(*uniso(iso
)[:6])
65 return int(calendar
.timegm(uniso(iso
)))
67 class Pushable(object):
69 def push_one_file(self
, f
):
70 file_info
= VideoDetails()
71 file_info
['valid'] = transcode
.supported_format(f
['path'])
74 if config
.isHDtivo(f
['tsn']):
75 for m
in ['video/mp4', 'video/bif']:
76 if transcode
.tivo_compatible(f
['path'], f
['tsn'], m
)[0]:
80 if (mime
== 'video/mpeg' and
81 transcode
.mp4_remuxable(f
['path'], f
['tsn'])):
82 new_path
= transcode
.mp4_remux(f
['path'], f
['name'], f
['tsn'])
87 if file_info
['valid']:
88 file_info
.update(self
.metadata_full(f
['path'], f
['tsn'], mime
))
90 url
= f
['url'] + quote(f
['name'])
92 title
= file_info
['seriesTitle']
94 title
= file_info
['title']
96 source
= file_info
['seriesId']
100 subtitle
= file_info
['episodeTitle']
102 m
= mind
.getMind(f
['tsn'])
106 description
= file_info
['description'],
107 duration
= file_info
['duration'] / 1000,
108 size
= file_info
['size'],
113 tvrating
= file_info
['tvRating'])
114 except Exception, msg
:
117 def process_queue(self
):
121 self
.push_one_file(item
)
124 """ returns your external IP address by querying dyndns.org """
125 f
= urllib
.urlopen('http://checkip.dyndns.org/')
127 m
= re
.search('([\d]*\.[\d]*\.[\d]*\.[\d]*)', s
)
130 def Push(self
, handler
, query
):
131 tsn
= query
['tsn'][0]
132 for key
in config
.tivo_names
:
133 if config
.tivo_names
[key
] == tsn
:
136 tivo_name
= config
.tivo_names
.get(tsn
, tsn
)
138 container
= quote(query
['Container'][0].split('/')[0])
139 ip
= config
.get_ip(tsn
)
140 port
= config
.getPort()
142 baseurl
= 'http://%s:%s/%s' % (ip
, port
, container
)
143 if config
.getIsExternal(tsn
):
144 exturl
= config
.get_server('externalurl')
146 if not exturl
.endswith('/'):
148 baseurl
= exturl
+ container
151 baseurl
= 'http://%s:%s/%s' % (ip
, port
, container
)
153 path
= self
.get_local_base_path(handler
, query
)
155 files
= query
.get('File', [])
157 file_path
= path
+ os
.path
.normpath(f
)
158 queue
.append({'path': file_path
, 'name': f
, 'tsn': tsn
,
161 thread
.start_new_thread(Video
.process_queue
, (self
,))
163 logger
.info('[%s] Queued "%s" for Push to %s' %
164 (time
.strftime('%d/%b/%Y %H:%M:%S'),
165 unicode(file_path
, 'utf-8'), tivo_name
))
167 files
= [unicode(f
, 'utf-8') for f
in files
]
168 handler
.redir(PUSHED
% (tivo_name
, '<br>'.join(files
)), 5)
170 class BaseVideo(Plugin
):
172 CONTENT_TYPE
= 'x-container/tivo-videos'
174 tvbus_cache
= LRUCache(1)
176 def pre_cache(self
, full_path
):
177 if Video
.video_file_filter(self
, full_path
):
178 transcode
.supported_format(full_path
)
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', '')
191 tivo_name
= config
.tivo_names
.get(tsn
, tsn
)
193 is_tivo_file
= (path
[-5:].lower() == '.tivo')
195 if 'Format' in query
:
196 mime
= query
['Format'][0]
198 needs_tivodecode
= (is_tivo_file
and mime
== 'video/mpeg')
199 compatible
= (not needs_tivodecode
and
200 transcode
.tivo_compatible(path
, tsn
, mime
)[0])
203 offset
= int(handler
.headers
.getheader('Range')[6:-1])
208 valid
= bool(config
.get_bin('tivodecode') and
209 config
.get_server('tivo_mak'))
214 valid
= ((compatible
and offset
< os
.stat(path
).st_size
) or
215 (not compatible
and transcode
.is_resumable(path
, offset
)))
217 #faking = (mime in ['video/x-tivo-mpeg-ts', 'video/x-tivo-mpeg'] and
218 faking
= (mime
== 'video/x-tivo-mpeg' and
219 not (is_tivo_file
and compatible
))
220 fname
= unicode(path
, 'utf-8')
223 thead
= self
.tivo_header(tsn
, path
, mime
)
225 size
= os
.stat(fname
).st_size
+ len(thead
)
226 handler
.send_response(200)
227 handler
.send_header('Content-Length', size
- offset
)
228 handler
.send_header('Content-Range', 'bytes %d-%d/%d' %
229 (offset
, size
- offset
- 1, size
))
231 handler
.send_response(206)
232 handler
.send_header('Transfer-Encoding', 'chunked')
233 handler
.send_header('Content-Type', mime
)
234 handler
.send_header('Connection', 'close')
235 handler
.end_headers()
237 logger
.info('[%s] Start sending "%s" to %s' %
238 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
, tivo_name
))
244 if faking
and not offset
:
245 handler
.wfile
.write(thead
)
246 logger
.debug('"%s" is tivo compatible' % fname
)
247 f
= open(fname
, 'rb')
249 if mime
== 'video/mp4':
250 count
= qtfaststart
.process(f
, handler
.wfile
, offset
)
256 block
= f
.read(512 * 1024)
259 handler
.wfile
.write(block
)
261 except Exception, msg
:
265 logger
.debug('"%s" is not tivo compatible' % fname
)
267 count
= transcode
.resume_transfer(path
, handler
.wfile
,
270 count
= transcode
.transcode(False, path
, handler
.wfile
,
274 handler
.wfile
.write('0\r\n\r\n')
275 handler
.wfile
.flush()
276 except Exception, msg
:
279 mega_elapsed
= (time
.time() - start
) * 1024 * 1024
282 rate
= count
* 8.0 / mega_elapsed
283 logger
.info('[%s] Done sending "%s" to %s, %d bytes, %.2f Mb/s' %
284 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
,
285 tivo_name
, count
, rate
))
287 if fname
.endswith('.pyTivo-temp'):
290 def __duration(self
, full_path
):
291 return transcode
.video_info(full_path
)['millisecs']
293 def __total_items(self
, full_path
):
296 full_path
= unicode(full_path
, 'utf-8')
297 for f
in os
.listdir(full_path
):
298 if f
.startswith('.'):
300 f
= os
.path
.join(full_path
, f
)
301 f2
= f
.encode('utf-8')
305 if os
.path
.splitext(f2
)[1].lower() in EXTENSIONS
:
307 elif f2
in transcode
.info_cache
:
308 if transcode
.supported_format(f2
):
314 def __est_size(self
, full_path
, tsn
='', mime
=''):
315 # Size is estimated by taking audio and video bit rate adding 2%
317 if transcode
.tivo_compatible(full_path
, tsn
, mime
)[0]:
318 return int(os
.stat(unicode(full_path
, 'utf-8')).st_size
)
321 if config
.get_tsn('audio_codec', tsn
) == None:
322 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
=''):
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
))
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':
373 mtime
= os
.stat(unicode(full_path
, 'utf-8')).st_mtime
377 now
= datetime
.utcfromtimestamp(mtime
)
379 logger
.warning('Bad file time on ' + full_path
)
380 elif data
['time'].lower() == 'oad':
381 now
= isodt(data
['originalAirDate'])
384 now
= isodt(data
['time'])
386 logger
.warning('Bad time format: ' + data
['time'] +
387 ' , using current time')
389 duration
= self
.__duration
(full_path
)
390 duration_delta
= timedelta(milliseconds
= duration
)
391 min = duration_delta
.seconds
/ 60
392 sec
= duration_delta
.seconds
% 60
396 data
.update({'time': now
.isoformat(),
397 'startTime': now
.isoformat(),
398 'stopTime': (now
+ duration_delta
).isoformat(),
399 'size': self
.__est
_size
(full_path
, tsn
, mime
),
400 'duration': duration
,
401 'iso_duration': ('P%sDT%sH%sM%sS' %
402 (duration_delta
.days
, hours
, min, sec
))})
406 def QueryContainer(self
, handler
, query
):
407 tsn
= handler
.headers
.getheader('tsn', '')
408 subcname
= query
['Container'][0]
410 if not self
.get_local_path(handler
, query
):
411 handler
.send_error(404)
414 container
= handler
.container
415 force_alpha
= container
.get('force_alpha', 'False').lower() == 'true'
416 use_html
= query
.get('Format', [''])[0].lower() == 'text/html'
418 files
, total
, start
= self
.get_files(handler
, query
,
419 self
.video_file_filter
,
423 local_base_path
= self
.get_local_base_path(handler
, query
)
425 video
= VideoDetails()
428 ltime
= time
.localtime(mtime
)
430 logger
.warning('Bad file time on ' + unicode(f
.name
, 'utf-8'))
431 mtime
= int(time
.time())
432 ltime
= time
.localtime(mtime
)
433 video
['captureDate'] = hex(mtime
)
434 video
['textDate'] = time
.strftime('%b %d, %Y', ltime
)
435 video
['name'] = os
.path
.basename(f
.name
)
436 video
['path'] = f
.name
437 video
['part_path'] = f
.name
.replace(local_base_path
, '', 1)
438 if not video
['part_path'].startswith(os
.path
.sep
):
439 video
['part_path'] = os
.path
.sep
+ video
['part_path']
440 video
['title'] = os
.path
.basename(f
.name
)
441 video
['is_dir'] = f
.isdir
443 video
['small_path'] = subcname
+ '/' + video
['name']
444 video
['total_items'] = self
.__total
_items
(f
.name
)
446 if len(files
) == 1 or f
.name
in transcode
.info_cache
:
447 video
['valid'] = transcode
.supported_format(f
.name
)
449 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
))
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'] = ( '%.3f GB' %
462 (float(f
.size
) / (1024 ** 3)) )
467 t
= Template(HTML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
469 t
= Template(XML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
470 t
.container
= handler
.cname
478 t
.guid
= config
.getGUID()
479 t
.tivos
= config
.tivos
480 t
.tivo_names
= config
.tivo_names
482 handler
.send_html(str(t
))
484 handler
.send_xml(str(t
))
486 def use_ts(self
, tsn
, file_path
):
487 if config
.is_ts_capable(tsn
):
488 if file_path
[-5:].lower() == '.tivo':
490 flag
= file(file_path
).read(8)
493 if ord(flag
[7]) & 0x20:
495 elif config
.has_ts_flag():
500 def get_details_xml(self
, tsn
, file_path
):
501 if (tsn
, file_path
) in self
.tvbus_cache
:
502 details
= self
.tvbus_cache
[(tsn
, file_path
)]
504 file_info
= VideoDetails()
505 file_info
['valid'] = transcode
.supported_format(file_path
)
506 if file_info
['valid']:
507 file_info
.update(self
.metadata_full(file_path
, tsn
))
509 t
= Template(TVBUS_TEMPLATE
, filter=EncodeUnicode
)
512 t
.get_tv
= metadata
.get_tv
513 t
.get_mpaa
= metadata
.get_mpaa
514 t
.get_stars
= metadata
.get_stars
516 self
.tvbus_cache
[(tsn
, file_path
)] = details
519 def tivo_header(self
, tsn
, path
, mime
):
520 if mime
== 'video/x-tivo-mpeg-ts':
524 details
= self
.get_details_xml(tsn
, path
)
526 chunklen
= ld
* 2 + 44
527 padding
= 2048 - chunklen
% 1024
529 return ''.join(['TiVo', struct
.pack('>HHHLH', 4, flag
, 0,
530 padding
+ chunklen
, 2),
531 struct
.pack('>LLHH', ld
+ 16, ld
, 1, 0),
533 struct
.pack('>LLHH', ld
+ 19, ld
, 2, 0),
534 details
, '\0' * padding
])
536 def TVBusQuery(self
, handler
, query
):
537 tsn
= handler
.headers
.getheader('tsn', '')
539 path
= self
.get_local_path(handler
, query
)
540 file_path
= path
+ os
.path
.normpath(f
)
542 details
= self
.get_details_xml(tsn
, file_path
)
544 handler
.send_xml(details
)
546 class Video(BaseVideo
, Pushable
):
549 class VideoDetails(DictMixin
):
551 def __init__(self
, d
=None):
557 def __getitem__(self
, key
):
558 if key
not in self
.d
:
559 self
.d
[key
] = self
.default(key
)
562 def __contains__(self
, key
):
565 def __setitem__(self
, key
, value
):
568 def __delitem__(self
):
575 return self
.d
.__iter
__()
578 return self
.d
.iteritems()
580 def default(self
, key
):
583 'displayMajorNumber' : '0',
584 'displayMinorNumber' : '0',
585 'isEpisode' : 'true',
586 'colorCode' : ('COLOR', '4'),
587 'showType' : ('SERIES', '5')
591 elif key
.startswith('v'):