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
):
129 tsn
= query
['tsn'][0]
130 for key
in config
.tivos
:
131 if config
.tivos
[key
]['name'] == tsn
:
134 tivo_name
= config
.tivos
[tsn
].get('name', tsn
)
136 container
= quote(query
['Container'][0].split('/')[0])
137 ip
= config
.get_ip(tsn
)
138 port
= config
.getPort()
140 baseurl
= 'http://%s:%s/%s' % (ip
, port
, container
)
141 if config
.getIsExternal(tsn
):
142 exturl
= config
.get_server('externalurl')
144 if not exturl
.endswith('/'):
146 baseurl
= exturl
+ container
149 baseurl
= 'http://%s:%s/%s' % (ip
, port
, container
)
151 path
= self
.get_local_base_path(handler
, query
)
153 files
= query
.get('File', [])
155 file_path
= os
.path
.normpath(path
+ '/' + f
)
156 queue
.append({'path': file_path
, 'name': f
, 'tsn': tsn
,
159 thread
.start_new_thread(Video
.process_queue
, (self
,))
161 logger
.info('[%s] Queued "%s" for Push to %s' %
162 (time
.strftime('%d/%b/%Y %H:%M:%S'),
163 unicode(file_path
, 'utf-8'), tivo_name
))
165 files
= [unicode(f
, 'utf-8') for f
in files
]
166 handler
.redir(PUSHED
% (tivo_name
, '<br>'.join(files
)), 5)
168 class BaseVideo(Plugin
):
170 CONTENT_TYPE
= 'x-container/tivo-videos'
172 tvbus_cache
= LRUCache(1)
174 def video_file_filter(self
, full_path
, type=None):
175 if os
.path
.isdir(unicode(full_path
, 'utf-8')):
178 return os
.path
.splitext(full_path
)[1].lower() in EXTENSIONS
180 return transcode
.supported_format(full_path
)
182 def send_file(self
, handler
, path
, query
):
183 mime
= 'video/x-tivo-mpeg'
184 tsn
= handler
.headers
.getheader('tsn', '')
185 tivo_name
= config
.tivos
[tsn
].get('name', tsn
)
187 is_tivo_file
= (path
[-5:].lower() == '.tivo')
189 if 'Format' in query
:
190 mime
= query
['Format'][0]
192 needs_tivodecode
= (is_tivo_file
and mime
== 'video/mpeg')
193 compatible
= (not needs_tivodecode
and
194 transcode
.tivo_compatible(path
, tsn
, mime
)[0])
197 offset
= int(handler
.headers
.getheader('Range')[6:-1])
202 valid
= bool(config
.get_bin('tivodecode') and
203 config
.get_server('tivo_mak'))
208 valid
= ((compatible
and offset
< os
.path
.getsize(path
)) or
209 (not compatible
and transcode
.is_resumable(path
, offset
)))
211 #faking = (mime in ['video/x-tivo-mpeg-ts', 'video/x-tivo-mpeg'] and
212 faking
= (mime
== 'video/x-tivo-mpeg' and
213 not (is_tivo_file
and compatible
))
214 fname
= unicode(path
, 'utf-8')
217 thead
= self
.tivo_header(tsn
, path
, mime
)
219 size
= os
.path
.getsize(fname
) + len(thead
)
220 handler
.send_response(200)
221 handler
.send_header('Content-Length', size
- offset
)
222 handler
.send_header('Content-Range', 'bytes %d-%d/%d' %
223 (offset
, size
- offset
- 1, size
))
225 handler
.send_response(206)
226 handler
.send_header('Transfer-Encoding', 'chunked')
227 handler
.send_header('Content-Type', mime
)
228 handler
.end_headers()
230 logger
.info('[%s] Start sending "%s" to %s' %
231 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
, tivo_name
))
237 if faking
and not offset
:
238 handler
.wfile
.write(thead
)
239 logger
.debug('"%s" is tivo compatible' % fname
)
240 f
= open(fname
, 'rb')
242 if mime
== 'video/mp4':
243 count
= qtfaststart
.process(f
, handler
.wfile
, offset
)
249 block
= f
.read(512 * 1024)
252 handler
.wfile
.write(block
)
254 except Exception, msg
:
258 logger
.debug('"%s" is not tivo compatible' % fname
)
260 count
= transcode
.resume_transfer(path
, handler
.wfile
,
263 count
= transcode
.transcode(False, path
, handler
.wfile
,
267 handler
.wfile
.write('0\r\n\r\n')
268 handler
.wfile
.flush()
269 except Exception, msg
:
272 mega_elapsed
= (time
.time() - start
) * 1024 * 1024
275 rate
= count
* 8.0 / mega_elapsed
276 logger
.info('[%s] Done sending "%s" to %s, %d bytes, %.2f Mb/s' %
277 (time
.strftime('%d/%b/%Y %H:%M:%S'), fname
,
278 tivo_name
, count
, rate
))
280 if fname
.endswith('.pyTivo-temp'):
283 def __duration(self
, full_path
):
284 return transcode
.video_info(full_path
)['millisecs']
286 def __total_items(self
, full_path
):
289 full_path
= unicode(full_path
, 'utf-8')
290 for f
in os
.listdir(full_path
):
291 if f
.startswith('.'):
293 f
= os
.path
.join(full_path
, f
)
294 f2
= f
.encode('utf-8')
298 if os
.path
.splitext(f2
)[1].lower() in EXTENSIONS
:
300 elif f2
in transcode
.info_cache
:
301 if transcode
.supported_format(f2
):
307 def __est_size(self
, full_path
, tsn
='', mime
=''):
308 # Size is estimated by taking audio and video bit rate adding 2%
310 if transcode
.tivo_compatible(full_path
, tsn
, mime
)[0]:
311 return os
.path
.getsize(unicode(full_path
, 'utf-8'))
314 audioBPS
= config
.getMaxAudioBR(tsn
) * 1000
315 #audioBPS = config.strtod(config.getAudioBR(tsn))
316 videoBPS
= transcode
.select_videostr(full_path
, tsn
)
317 bitrate
= audioBPS
+ videoBPS
318 return int((self
.__duration
(full_path
) / 1000) *
319 (bitrate
* 1.02 / 8))
321 def metadata_full(self
, full_path
, tsn
='', mime
='', mtime
=None):
323 vInfo
= transcode
.video_info(full_path
)
325 if ((int(vInfo
['vHeight']) >= 720 and
326 config
.getTivoHeight
>= 720) or
327 (int(vInfo
['vWidth']) >= 1280 and
328 config
.getTivoWidth
>= 1280)):
329 data
['showingBits'] = '4096'
331 data
.update(metadata
.basic(full_path
, mtime
))
332 if full_path
[-5:].lower() == '.tivo':
333 data
.update(metadata
.from_tivo(full_path
))
334 if full_path
[-4:].lower() == '.wtv':
335 data
.update(metadata
.from_mscore(vInfo
['rawmeta']))
337 if 'episodeNumber' in data
:
339 ep
= int(data
['episodeNumber'])
342 data
['episodeNumber'] = str(ep
)
344 if config
.getDebug() and 'vHost' not in data
:
345 compatible
, reason
= transcode
.tivo_compatible(full_path
, tsn
, mime
)
347 transcode_options
= {}
349 transcode_options
= transcode
.transcode(True, full_path
,
352 ['TRANSCODE=%s, %s' % (['YES', 'NO'][compatible
], reason
)] +
355 for k
, v
in sorted(vInfo
.items(), reverse
=True)] +
356 ['TRANSCODE OPTIONS: '] +
357 ["%s" % (v
) for k
, v
in transcode_options
.items()] +
358 ['SOURCE FILE: ', os
.path
.basename(full_path
)]
361 now
= datetime
.utcnow()
363 if data
['time'].lower() == 'file':
365 mtime
= os
.path
.getmtime(unicode(full_path
, 'utf-8'))
367 now
= datetime
.utcfromtimestamp(mtime
)
369 logger
.warning('Bad file time on ' + full_path
)
370 elif data
['time'].lower() == 'oad':
371 now
= isodt(data
['originalAirDate'])
374 now
= isodt(data
['time'])
376 logger
.warning('Bad time format: ' + data
['time'] +
377 ' , using current time')
379 duration
= self
.__duration
(full_path
)
380 duration_delta
= timedelta(milliseconds
= duration
)
381 min = duration_delta
.seconds
/ 60
382 sec
= duration_delta
.seconds
% 60
386 data
.update({'time': now
.isoformat(),
387 'startTime': now
.isoformat(),
388 'stopTime': (now
+ duration_delta
).isoformat(),
389 'size': self
.__est
_size
(full_path
, tsn
, mime
),
390 'duration': duration
,
391 'iso_duration': ('P%sDT%sH%sM%sS' %
392 (duration_delta
.days
, hours
, min, sec
))})
396 def QueryContainer(self
, handler
, query
):
397 tsn
= handler
.headers
.getheader('tsn', '')
398 subcname
= query
['Container'][0]
400 if not self
.get_local_path(handler
, query
):
401 handler
.send_error(404)
404 container
= handler
.container
405 force_alpha
= container
.getboolean('force_alpha')
406 use_html
= query
.get('Format', [''])[0].lower() == 'text/html'
408 files
, total
, start
= self
.get_files(handler
, query
,
409 self
.video_file_filter
,
413 local_base_path
= self
.get_local_base_path(handler
, query
)
415 video
= VideoDetails()
418 ltime
= time
.localtime(mtime
)
420 logger
.warning('Bad file time on ' + unicode(f
.name
, 'utf-8'))
422 ltime
= time
.localtime(mtime
)
423 video
['captureDate'] = hex(int(mtime
))
424 video
['textDate'] = time
.strftime('%b %d, %Y', ltime
)
425 video
['name'] = os
.path
.basename(f
.name
)
426 video
['path'] = f
.name
427 video
['part_path'] = f
.name
.replace(local_base_path
, '', 1)
428 if not video
['part_path'].startswith(os
.path
.sep
):
429 video
['part_path'] = os
.path
.sep
+ video
['part_path']
430 video
['title'] = os
.path
.basename(f
.name
)
431 video
['is_dir'] = f
.isdir
433 video
['small_path'] = subcname
+ '/' + video
['name']
434 video
['total_items'] = self
.__total
_items
(f
.name
)
436 if len(files
) == 1 or f
.name
in transcode
.info_cache
:
437 video
['valid'] = transcode
.supported_format(f
.name
)
439 video
.update(self
.metadata_full(f
.name
, tsn
,
442 video
['captureDate'] = hex(isogm(video
['time']))
444 video
['valid'] = True
445 video
.update(metadata
.basic(f
.name
, mtime
))
447 if self
.use_ts(tsn
, f
.name
):
448 video
['mime'] = 'video/x-tivo-mpeg-ts'
450 video
['mime'] = 'video/x-tivo-mpeg'
452 video
['textSize'] = metadata
.human_size(f
.size
)
457 t
= Template(HTML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
459 t
= Template(XML_CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
460 t
.container
= handler
.cname
468 t
.guid
= config
.getGUID()
469 t
.tivos
= config
.tivos
471 handler
.send_html(str(t
))
473 handler
.send_xml(str(t
))
475 def use_ts(self
, tsn
, file_path
):
476 if config
.is_ts_capable(tsn
):
477 if file_path
[-5:].lower() == '.tivo':
479 flag
= file(file_path
).read(8)
482 if ord(flag
[7]) & 0x20:
484 elif config
.has_ts_flag():
489 def get_details_xml(self
, tsn
, file_path
):
490 if (tsn
, file_path
) in self
.tvbus_cache
:
491 details
= self
.tvbus_cache
[(tsn
, file_path
)]
493 file_info
= VideoDetails()
494 file_info
['valid'] = transcode
.supported_format(file_path
)
495 if file_info
['valid']:
496 file_info
.update(self
.metadata_full(file_path
, tsn
))
498 t
= Template(TVBUS_TEMPLATE
, filter=EncodeUnicode
)
501 t
.get_tv
= metadata
.get_tv
502 t
.get_mpaa
= metadata
.get_mpaa
503 t
.get_stars
= metadata
.get_stars
505 self
.tvbus_cache
[(tsn
, file_path
)] = details
508 def tivo_header(self
, tsn
, path
, mime
):
509 def pad(length
, align
):
510 extra
= length
% align
512 extra
= align
- extra
515 if mime
== 'video/x-tivo-mpeg-ts':
519 details
= self
.get_details_xml(tsn
, path
)
521 chunk
= details
+ '\0' * (pad(ld
, 4) + 4)
523 blocklen
= lc
* 2 + 40
524 padding
= pad(blocklen
, 1024)
526 return ''.join(['TiVo', struct
.pack('>HHHLH', 4, flag
, 0,
527 padding
+ blocklen
, 2),
528 struct
.pack('>LLHH', lc
+ 12, ld
, 1, 0),
530 struct
.pack('>LLHH', lc
+ 12, ld
, 2, 0),
531 chunk
, '\0' * padding
])
533 def TVBusQuery(self
, handler
, query
):
534 tsn
= handler
.headers
.getheader('tsn', '')
536 path
= self
.get_local_path(handler
, query
)
537 file_path
= os
.path
.normpath(path
+ '/' + f
)
539 details
= self
.get_details_xml(tsn
, file_path
)
541 handler
.send_xml(details
)
543 class Video(BaseVideo
, Pushable
):
546 class VideoDetails(DictMixin
):
548 def __init__(self
, d
=None):
554 def __getitem__(self
, key
):
555 if key
not in self
.d
:
556 self
.d
[key
] = self
.default(key
)
559 def __contains__(self
, key
):
562 def __setitem__(self
, key
, value
):
565 def __delitem__(self
):
572 return self
.d
.__iter
__()
575 return self
.d
.iteritems()
577 def default(self
, key
):
580 'displayMajorNumber' : '0',
581 'displayMinorNumber' : '0',
582 'isEpisode' : 'true',
583 'colorCode' : ('COLOR', '4'),
584 'showType' : ('SERIES', '5')
588 elif key
.startswith('v'):