Hardcode subtitles - supports srt and ass.
[pyTivo/wmcbrine/lucasnz.git] / httpserver.py
blob64f759007e8697dddffaa6ab51481d086853b2ed
1 import BaseHTTPServer
2 import SocketServer
3 import cgi
4 import gzip
5 import logging
6 import mimetypes
7 import os
8 import shutil
9 import socket
10 import time
11 from cStringIO import StringIO
12 from email.utils import formatdate
13 from urllib import unquote_plus, quote
14 from xml.sax.saxutils import escape
16 from Cheetah.Template import Template
17 import config
18 from plugin import GetPlugin, EncodeUnicode
20 SCRIPTDIR = os.path.dirname(__file__)
22 SERVER_INFO = """<?xml version="1.0" encoding="utf-8"?>
23 <TiVoServer>
24 <Version>1.6</Version>
25 <InternalName>pyTivo</InternalName>
26 <InternalVersion>1.0</InternalVersion>
27 <Organization>pyTivo Developers</Organization>
28 <Comment>http://pytivo.sf.net/</Comment>
29 </TiVoServer>"""
31 VIDEO_FORMATS = """<?xml version="1.0" encoding="utf-8"?>
32 <TiVoFormats>
33 <Format><ContentType>video/x-tivo-mpeg</ContentType><Description/></Format>
34 </TiVoFormats>"""
36 VIDEO_FORMATS_TS = """<?xml version="1.0" encoding="utf-8"?>
37 <TiVoFormats>
38 <Format><ContentType>video/x-tivo-mpeg</ContentType><Description/></Format>
39 <Format><ContentType>video/x-tivo-mpeg-ts</ContentType><Description/></Format>
40 </TiVoFormats>"""
42 BASE_HTML = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
43 "http://www.w3.org/TR/html4/strict.dtd">
44 <html> <head><title>pyTivo</title>
45 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
46 <link rel="stylesheet" type="text/css" href="/main.css">
47 </head> <body> %s </body> </html>"""
49 RELOAD = '<p>The <a href="%s">page</a> will reload in %d seconds.</p>'
50 UNSUP = '<h3>Unsupported Command</h3> <p>Query:</p> <ul>%s</ul>'
52 class TivoHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
53 def __init__(self, server_address, RequestHandlerClass):
54 self.containers = {}
55 self.stop = False
56 self.restart = False
57 self.logger = logging.getLogger('pyTivo')
58 BaseHTTPServer.HTTPServer.__init__(self, server_address,
59 RequestHandlerClass)
60 self.daemon_threads = True
62 def add_container(self, name, settings):
63 if name in self.containers or name == 'TiVoConnect':
64 raise "Container Name in use"
65 try:
66 self.containers[name] = settings
67 except KeyError:
68 self.logger.error('Unable to add container ' + name)
70 def reset(self):
71 self.containers.clear()
72 for section, settings in config.getShares():
73 self.add_container(section, settings)
75 def handle_error(self, request, client_address):
76 self.logger.exception('Exception during request from %s' %
77 (client_address,))
79 def set_beacon(self, beacon):
80 self.beacon = beacon
82 def set_service_status(self, status):
83 self.in_service = status
85 class TivoHTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
86 def __init__(self, request, client_address, server):
87 self.wbufsize = 0x10000
88 self.server_version = 'pyTivo/1.0'
89 self.protocol_version = 'HTTP/1.1'
90 self.sys_version = ''
91 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, request,
92 client_address, server)
94 def address_string(self):
95 host, port = self.client_address[:2]
96 return host
98 def version_string(self):
99 """ Override version_string() so it doesn't include the Python
100 version.
103 return self.server_version
105 def do_GET(self):
106 tsn = self.headers.getheader('TiVo_TCD_ID',
107 self.headers.getheader('tsn', ''))
108 if not self.authorize(tsn):
109 return
111 if tsn and (not config.tivos_found or tsn in config.tivos):
112 attr = config.tivos.get(tsn, {})
113 if 'address' not in attr:
114 attr['address'] = self.address_string()
115 if 'name' not in attr:
116 attr['name'] = self.server.beacon.get_name(attr['address'])
117 config.tivos[tsn] = attr
119 if '?' in self.path:
120 path, opts = self.path.split('?', 1)
121 query = cgi.parse_qs(opts)
122 else:
123 path = self.path
124 query = {}
126 if path == '/TiVoConnect':
127 self.handle_query(query, tsn)
128 else:
129 ## Get File
130 splitpath = [x for x in unquote_plus(path).split('/') if x]
131 if splitpath:
132 self.handle_file(query, splitpath)
133 else:
134 ## Not a file not a TiVo command
135 self.infopage()
137 def do_POST(self):
138 tsn = self.headers.getheader('TiVo_TCD_ID',
139 self.headers.getheader('tsn', ''))
140 if not self.authorize(tsn):
141 return
142 ctype, pdict = cgi.parse_header(self.headers.getheader('content-type'))
143 if ctype == 'multipart/form-data':
144 query = cgi.parse_multipart(self.rfile, pdict)
145 else:
146 length = int(self.headers.getheader('content-length'))
147 qs = self.rfile.read(length)
148 query = cgi.parse_qs(qs, keep_blank_values=1)
149 self.handle_query(query, tsn)
151 def do_command(self, query, command, target, tsn):
152 for name, container in config.getShares(tsn):
153 if target == name:
154 plugin = GetPlugin(container['type'])
155 if hasattr(plugin, command):
156 self.cname = name
157 self.container = container
158 method = getattr(plugin, command)
159 method(self, query)
160 return True
161 else:
162 break
163 return False
165 def handle_query(self, query, tsn):
166 mname = False
167 if 'Command' in query and len(query['Command']) >= 1:
169 command = query['Command'][0]
171 # If we are looking at the root container
172 if (command == 'QueryContainer' and
173 (not 'Container' in query or query['Container'][0] == '/')):
174 self.root_container()
175 return
177 if 'Container' in query:
178 # Dispatch to the container plugin
179 basepath = query['Container'][0].split('/')[0]
180 if self.do_command(query, command, basepath, tsn):
181 return
183 elif command == 'QueryItem':
184 path = query.get('Url', [''])[0]
185 splitpath = [x for x in unquote_plus(path).split('/') if x]
186 if splitpath and not '..' in splitpath:
187 if self.do_command(query, command, splitpath[0], tsn):
188 return
190 elif (command == 'QueryFormats' and 'SourceFormat' in query and
191 query['SourceFormat'][0].startswith('video')):
192 if config.is_ts_capable(tsn):
193 self.send_xml(VIDEO_FORMATS_TS)
194 else:
195 self.send_xml(VIDEO_FORMATS)
196 return
198 elif command == 'QueryServer':
199 self.send_xml(SERVER_INFO)
200 return
202 elif command in ('FlushServer', 'ResetServer'):
203 # Does nothing -- included for completeness
204 self.send_response(200)
205 self.send_header('Content-Length', '0')
206 self.end_headers()
207 self.wfile.flush()
208 return
210 # If we made it here it means we couldn't match the request to
211 # anything.
212 self.unsupported(query)
214 def send_content_file(self, path):
215 lmdate = os.path.getmtime(path)
216 try:
217 handle = open(path, 'rb')
218 except:
219 self.send_error(404)
220 return
222 # Send the header
223 mime = mimetypes.guess_type(path)[0]
224 self.send_response(200)
225 if mime:
226 self.send_header('Content-Type', mime)
227 self.send_header('Content-Length', os.path.getsize(path))
228 self.send_header('Last-Modified', formatdate(lmdate))
229 self.end_headers()
231 # Send the body of the file
232 try:
233 shutil.copyfileobj(handle, self.wfile)
234 except:
235 pass
236 handle.close()
237 self.wfile.flush()
239 def handle_file(self, query, splitpath):
240 if '..' not in splitpath: # Protect against path exploits
241 ## Pass it off to a plugin?
242 for name, container in self.server.containers.items():
243 if splitpath[0] == name:
244 self.cname = name
245 self.container = container
246 base = os.path.normpath(container['path'])
247 path = os.path.join(base, *splitpath[1:])
248 plugin = GetPlugin(container['type'])
249 plugin.send_file(self, path, query)
250 return
252 ## Serve it from a "content" directory?
253 base = os.path.join(SCRIPTDIR, *splitpath[:-1])
254 path = os.path.join(base, 'content', splitpath[-1])
256 if os.path.isfile(path):
257 self.send_content_file(path)
258 return
260 ## Give up
261 self.send_error(404)
263 def authorize(self, tsn=None):
264 # if allowed_clients is empty, we are completely open
265 allowed_clients = config.getAllowedClients()
266 if not allowed_clients or (tsn and config.isTsnInConfig(tsn)):
267 return True
268 client_ip = self.client_address[0]
269 for allowedip in allowed_clients:
270 if client_ip.startswith(allowedip):
271 return True
273 self.send_fixed('Unauthorized.', 'text/plain', 403)
274 return False
276 def log_message(self, format, *args):
277 self.server.logger.info("%s [%s] %s" % (self.address_string(),
278 self.log_date_time_string(), format%args))
280 def send_fixed(self, page, mime, code=200, refresh=''):
281 squeeze = (len(page) > 256 and mime.startswith('text') and
282 'gzip' in self.headers.getheader('Accept-Encoding', ''))
283 if squeeze:
284 out = StringIO()
285 gzip.GzipFile(mode='wb', fileobj=out).write(page)
286 page = out.getvalue()
287 out.close()
288 self.send_response(code)
289 self.send_header('Content-Type', mime)
290 self.send_header('Content-Length', len(page))
291 if squeeze:
292 self.send_header('Content-Encoding', 'gzip')
293 self.send_header('Expires', '0')
294 if refresh:
295 self.send_header('Refresh', refresh)
296 self.end_headers()
297 self.wfile.write(page)
298 self.wfile.flush()
300 def send_xml(self, page):
301 self.send_fixed(page, 'text/xml')
303 def send_html(self, page, code=200, refresh=''):
304 self.send_fixed(page, 'text/html; charset=utf-8', code, refresh)
306 def root_container(self):
307 tsn = self.headers.getheader('TiVo_TCD_ID', '')
308 tsnshares = config.getShares(tsn)
309 tsncontainers = []
310 for section, settings in tsnshares:
311 try:
312 mime = GetPlugin(settings['type']).CONTENT_TYPE
313 if mime.split('/')[1] in ('tivo-videos', 'tivo-music',
314 'tivo-photos'):
315 settings['content_type'] = mime
316 tsncontainers.append((section, settings))
317 except Exception, msg:
318 self.server.logger.error(section + ' - ' + str(msg))
319 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
320 'root_container.tmpl'),
321 filter=EncodeUnicode)
322 if self.server.beacon.bd:
323 t.renamed = self.server.beacon.bd.renamed
324 else:
325 t.renamed = {}
326 t.containers = tsncontainers
327 t.hostname = socket.gethostname()
328 t.escape = escape
329 t.quote = quote
330 self.send_xml(str(t))
332 def infopage(self):
333 t = Template(file=os.path.join(SCRIPTDIR, 'templates',
334 'info_page.tmpl'),
335 filter=EncodeUnicode)
336 t.admin = ''
338 if config.get_server('tivo_mak') and config.get_server('togo_path'):
339 t.togo = 'Pull from TiVos:<br>'
340 else:
341 t.togo = ''
343 if (config.get_server('tivo_username') and
344 config.get_server('tivo_password')):
345 t.shares = 'Push from video shares:<br>'
346 else:
347 t.shares = ''
349 for section, settings in config.getShares():
350 plugin_type = settings.get('type')
351 if plugin_type == 'settings':
352 t.admin += ('<a href="/TiVoConnect?Command=Settings&amp;' +
353 'Container=' + quote(section) +
354 '">Settings</a><br>')
355 elif plugin_type == 'togo' and t.togo:
356 for tsn in config.tivos:
357 if tsn and 'address' in config.tivos[tsn]:
358 t.togo += ('<a href="/TiVoConnect?' +
359 'Command=NPL&amp;Container=' + quote(section) +
360 '&amp;TiVo=' + config.tivos[tsn]['address'] +
361 '">' + config.tivos[tsn]['name'] +
362 '</a><br>')
363 elif plugin_type and t.shares:
364 plugin = GetPlugin(plugin_type)
365 if hasattr(plugin, 'Push'):
366 t.shares += ('<a href="/TiVoConnect?Command=' +
367 'QueryContainer&amp;Container=' +
368 quote(section) + '&Format=text/html">' +
369 section + '</a><br>')
371 self.send_html(str(t))
373 def unsupported(self, query):
374 message = UNSUP % '\n'.join(['<li>%s: %s</li>' % (key, repr(value))
375 for key, value in query.items()])
376 text = BASE_HTML % message
377 self.send_html(text, code=404)
379 def redir(self, message, seconds=2):
380 url = self.headers.getheader('Referer')
381 if url:
382 message += RELOAD % (url, seconds)
383 refresh = '%d; url=%s' % (seconds, url)
384 else:
385 refresh = ''
386 text = (BASE_HTML % message).encode('utf-8')
387 self.send_html(text, refresh=refresh)