11 from UserDict
import DictMixin
12 from datetime
import datetime
, timedelta
13 from xml
.sax
.saxutils
import escape
15 from Cheetah
.Template
import Template
16 from lrucache
import LRUCache
21 from plugin
import EncodeUnicode
, Plugin
, quote
23 logger
= logging
.getLogger('pyTivo.video.video')
25 SCRIPTDIR
= os
.path
.dirname(__file__
)
29 # Preload the templates
31 return file(os
.path
.join(SCRIPTDIR
, 'templates', name
), 'rb').read()
33 CONTAINER_TEMPLATE
= tmpl('container.tmpl')
34 TVBUS_TEMPLATE
= tmpl('TvBus.tmpl')
35 XSL_TEMPLATE
= tmpl('container.xsl')
37 extfile
= os
.path
.join(SCRIPTDIR
, 'video.ext')
39 assert(config
.get_bin('ffmpeg'))
40 extensions
= file(extfile
).read().split()
46 CONTENT_TYPE
= 'x-container/tivo-videos'
48 def pre_cache(self
, full_path
):
49 if Video
.video_file_filter(self
, full_path
):
50 transcode
.supported_format(full_path
)
52 def video_file_filter(self
, full_path
, type=None):
53 if os
.path
.isdir(full_path
):
56 return os
.path
.splitext(full_path
)[1].lower() in extensions
58 return transcode
.supported_format(full_path
)
60 def send_file(self
, handler
, path
, query
):
62 tsn
= handler
.headers
.getheader('tsn', '')
64 is_tivo_file
= (path
[-5:].lower() == '.tivo')
66 if is_tivo_file
and transcode
.tivo_compatible(path
, tsn
, mime
)[0]:
67 mime
= 'video/x-tivo-mpeg'
70 mime
= query
['Format'][0]
72 is_tivo_push
= (mime
== 'video/mpeg' and is_tivo_file
)
74 compatible
= transcode
.tivo_compatible(path
, tsn
, mime
)[0]
76 offset
= handler
.headers
.getheader('Range')
78 offset
= int(offset
[6:-1]) # "bytes=XXX-"
80 handler
.send_response(206)
81 handler
.send_header('Content-Type', mime
)
84 if ((compatible
and (is_tivo_push
85 or offset
>= os
.stat(path
).st_size
)) or
86 (not compatible
and not transcode
.is_resumable(path
, offset
))):
87 handler
.send_header('Connection', 'close')
88 handler
.send_header('Transfer-Encoding', 'chunked')
90 handler
.wfile
.write('0\r\n')
96 logger
.debug('%s is tivo compatible' % path
)
99 if mime
== 'video/mp4':
100 qtfaststart
.fast_start(f
, handler
.wfile
, offset
)
103 tivodecode_path
= config
.get_bin('tivodecode')
104 tivo_mak
= config
.get_server('tivo_mak')
105 if tivodecode_path
and tivo_mak
:
107 tcmd
= [tivodecode_path
, '-m', tivo_mak
, path
]
108 tivodecode
= subprocess
.Popen(tcmd
,
109 stdout
=subprocess
.PIPE
, bufsize
=(512 * 1024))
110 f
= tivodecode
.stdout
113 shutil
.copyfileobj(f
, handler
.wfile
)
114 except Exception, msg
:
118 logger
.debug('%s is not tivo compatible' % path
)
120 transcode
.resume_transfer(path
, handler
.wfile
, offset
)
122 transcode
.transcode(False, path
, handler
.wfile
, tsn
)
123 logger
.debug("Finished outputing video")
125 def __duration(self
, full_path
):
126 return transcode
.video_info(full_path
)['millisecs']
128 def __total_items(self
, full_path
):
131 for f
in os
.listdir(full_path
):
132 if f
.startswith('.'):
134 f
= os
.path
.join(full_path
, f
)
138 if os
.path
.splitext(f
)[1].lower() in extensions
:
140 elif f
in transcode
.info_cache
:
141 if transcode
.supported_format(f
):
147 def __est_size(self
, full_path
, tsn
='', mime
=''):
148 # Size is estimated by taking audio and video bit rate adding 2%
150 if transcode
.tivo_compatible(full_path
, tsn
, mime
)[0]:
151 return int(os
.stat(full_path
).st_size
)
154 if config
.get_tsn('audio_codec', tsn
) == None:
155 audioBPS
= config
.getMaxAudioBR(tsn
) * 1000
157 audioBPS
= config
.strtod(config
.getAudioBR(tsn
))
158 videoBPS
= transcode
.select_videostr(full_path
, tsn
)
159 bitrate
= audioBPS
+ videoBPS
160 return int((self
.__duration
(full_path
) / 1000) *
161 (bitrate
* 1.02 / 8))
163 def getMetadataFromTxt(self
, full_path
):
165 path
, name
= os
.path
.split(full_path
)
166 for metafile
in [os
.path
.join(path
, 'default.txt'), full_path
+ '.txt',
167 os
.path
.join(path
, '.meta', name
) + '.txt']:
168 if os
.path
.exists(metafile
):
169 for line
in file(metafile
):
170 if line
.strip().startswith('#') or not ':' in line
:
172 key
, value
= [x
.strip() for x
in line
.split(':', 1)]
173 if key
.startswith('v'):
175 metadata
[key
].append(value
)
177 metadata
[key
] = [value
]
179 metadata
[key
] = value
182 def metadata_basic(self
, full_path
):
183 base_path
, title
= os
.path
.split(full_path
)
184 mtime
= os
.stat(full_path
).st_mtime
187 originalAirDate
= datetime
.fromtimestamp(mtime
)
189 metadata
= {'title': '.'.join(title
.split('.')[:-1]),
190 'originalAirDate': originalAirDate
.isoformat()}
192 metadata
.update(self
.getMetadataFromTxt(full_path
))
196 def metadata_full(self
, full_path
, tsn
='', mime
=''):
198 vInfo
= transcode
.video_info(full_path
)
199 compat
= transcode
.tivo_compatible(full_path
, tsn
, mime
)
201 transcode_options
= transcode
.transcode(True, full_path
, '', tsn
)
203 transcode_options
= {}
205 if config
.getDebug():
206 metadata
['vHost'] = (
207 ['TRANSCODE=%s, %s' % (['YES', 'NO'][compat
[0]], compat
[1])] +
210 for k
, v
in sorted(vInfo
.items(), reverse
=True)] +
211 ['TRANSCODE OPTIONS: '] +
212 ["%s" % (v
) for k
, v
in transcode_options
.items()] +
213 ['SOURCE FILE: ', os
.path
.split(full_path
)[1]]
216 if ((int(vInfo
['vHeight']) >= 720 and
217 config
.getTivoHeight
>= 720) or
218 (int(vInfo
['vWidth']) >= 1280 and
219 config
.getTivoWidth
>= 1280)):
220 metadata
['showingBits'] = '4096'
222 metadata
.update(self
.metadata_basic(full_path
))
224 now
= datetime
.utcnow()
225 duration
= self
.__duration
(full_path
)
226 duration_delta
= timedelta(milliseconds
= duration
)
227 min = duration_delta
.seconds
/ 60
228 sec
= duration_delta
.seconds
% 60
232 metadata
.update({'time': now
.isoformat(),
233 'startTime': now
.isoformat(),
234 'stopTime': (now
+ duration_delta
).isoformat(),
235 'size': self
.__est
_size
(full_path
, tsn
, mime
),
236 'duration': duration
,
237 'iso_duration': ('P%sDT%sH%sM%sS' %
238 (duration_delta
.days
, hours
, min, sec
))})
242 def QueryContainer(self
, handler
, query
):
243 tsn
= handler
.headers
.getheader('tsn', '')
244 subcname
= query
['Container'][0]
245 cname
= subcname
.split('/')[0]
247 if (not cname
in handler
.server
.containers
or
248 not self
.get_local_path(handler
, query
)):
249 handler
.send_error(404)
252 container
= handler
.server
.containers
[cname
]
253 precache
= container
.get('precache', 'False').lower() == 'true'
254 force_alpha
= container
.get('force_alpha', 'False').lower() == 'true'
256 files
, total
, start
= self
.get_files(handler
, query
,
257 self
.video_file_filter
,
261 local_base_path
= self
.get_local_base_path(handler
, query
)
263 mtime
= datetime
.fromtimestamp(f
.mdate
)
264 video
= VideoDetails()
265 video
['captureDate'] = hex(int(time
.mktime(mtime
.timetuple())))
266 video
['name'] = os
.path
.split(f
.name
)[1]
267 video
['path'] = f
.name
268 video
['part_path'] = f
.name
.replace(local_base_path
, '', 1)
269 if not video
['part_path'].startswith(os
.path
.sep
):
270 video
['part_path'] = os
.path
.sep
+ video
['part_path']
271 video
['title'] = os
.path
.split(f
.name
)[1]
272 video
['is_dir'] = f
.isdir
274 video
['small_path'] = subcname
+ '/' + video
['name']
275 video
['total_items'] = self
.__total
_items
(f
.name
)
277 if precache
or len(files
) == 1 or f
.name
in transcode
.info_cache
:
278 video
['valid'] = transcode
.supported_format(f
.name
)
280 video
.update(self
.metadata_full(f
.name
, tsn
))
282 video
['valid'] = True
283 video
.update(self
.metadata_basic(f
.name
))
287 t
= Template(CONTAINER_TEMPLATE
, filter=EncodeUnicode
)
296 t
.guid
= config
.getGUID()
297 t
.tivos
= config
.tivos
298 t
.tivo_names
= config
.tivo_names
299 handler
.send_response(200)
300 handler
.send_header('Content-Type', 'text/xml')
301 handler
.end_headers()
302 handler
.wfile
.write(t
)
304 def TVBusQuery(self
, handler
, query
):
305 tsn
= handler
.headers
.getheader('tsn', '')
307 path
= self
.get_local_path(handler
, query
)
308 file_path
= path
+ os
.path
.normpath(f
)
310 file_info
= VideoDetails()
311 file_info
['valid'] = transcode
.supported_format(file_path
)
312 if file_info
['valid']:
313 file_info
.update(self
.metadata_full(file_path
, tsn
))
315 t
= Template(TVBUS_TEMPLATE
, filter=EncodeUnicode
)
318 handler
.send_response(200)
319 handler
.send_header('Content-Type', 'text/xml')
320 handler
.end_headers()
321 handler
.wfile
.write(t
)
323 def XSL(self
, handler
, query
):
324 handler
.send_response(200)
325 handler
.send_header('Content-Type', 'text/xml')
326 handler
.end_headers()
327 handler
.wfile
.write(XSL_TEMPLATE
)
329 def Push(self
, handler
, query
):
330 tsn
= query
['tsn'][0]
331 for key
in config
.tivo_names
:
332 if config
.tivo_names
[key
] == tsn
:
336 container
= quote(query
['Container'][0].split('/')[0])
338 port
= config
.getPort()
340 baseurl
= 'http://%s:%s' % (ip
, port
)
341 if config
.getIsExternal(tsn
):
342 exturl
= config
.get_server('externalurl')
347 baseurl
= 'http://%s:%s' % (ip
, port
)
349 path
= self
.get_local_base_path(handler
, query
)
351 for f
in query
.get('File', []):
352 file_path
= path
+ os
.path
.normpath(f
)
354 file_info
= VideoDetails()
355 file_info
['valid'] = transcode
.supported_format(file_path
)
358 if config
.isHDtivo(tsn
):
359 for m
in ['video/mp4', 'video/bif']:
360 if transcode
.tivo_compatible(file_path
, tsn
, m
)[0]:
364 if file_info
['valid']:
365 file_info
.update(self
.metadata_full(file_path
, tsn
, mime
))
367 url
= baseurl
+ '/%s%s' % (container
, quote(f
))
369 title
= file_info
['seriesTitle']
371 title
= file_info
['title']
373 source
= file_info
['seriesId']
377 subtitle
= file_info
['episodeTitle']
378 if (not subtitle
and file_info
['isEpisode'] != 'false' and
379 file_info
['seriesTitle']):
380 subtitle
= file_info
['title']
381 logger
.debug('Pushing ' + url
)
383 m
= mind
.getMind(tsn
)
387 description
= file_info
['description'],
388 duration
= file_info
['duration'] / 1000,
389 size
= file_info
['size'],
395 handler
.send_response(500)
396 handler
.end_headers()
397 handler
.wfile
.write('%s\n\n%s' % (e
, traceback
.format_exc() ))
400 referer
= handler
.headers
.getheader('Referer')
401 handler
.send_response(302)
402 handler
.send_header('Location', referer
)
403 handler
.end_headers()
406 """ returns your external IP address by querying dyndns.org """
407 f
= urllib
.urlopen('http://checkip.dyndns.org/')
409 m
= re
.search('([\d]*\.[\d]*\.[\d]*\.[\d]*)', s
)
412 class VideoDetails(DictMixin
):
414 def __init__(self
, d
=None):
420 def __getitem__(self
, key
):
421 if key
not in self
.d
:
422 self
.d
[key
] = self
.default(key
)
425 def __contains__(self
, key
):
428 def __setitem__(self
, key
, value
):
431 def __delitem__(self
):
438 return self
.d
.__iter
__()
441 return self
.d
.iteritems()
443 def default(self
, key
):
446 'episodeNumber' : '0',
447 'displayMajorNumber' : '0',
448 'displayMinorNumber' : '0',
449 'isEpisode' : 'true',
450 'colorCode' : ('COLOR', '4'),
451 'showType' : ('SERIES', '5'),
452 'tvRating' : ('NR', '7')
456 elif key
.startswith('v'):