from glob import glob from threading import Thread import json import logging import mimetypes import os import time from tornado.httpserver import HTTPServer from tornado import gen from tornado.ioloop import IOLoop from tornado.web import Application, RequestHandler, HTTPError from tornado.websocket import WebSocketHandler import requests import tornado.web import ox.srt logger = logging.getLogger('cdosea.subtitles') sockets = [] STATIC_ROOT = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'static') ADDRESS = '0.0.0.0' PORT = 8080 DEBUG = False class Subtitles(): path = None next_path = None playlist = None player = None last = 0 def __init__(self, player, playlist): self.player = player self.load_playlist(playlist) self.player.observe_property('time-pos', self.update) def load_playlist(self, playlist): with open(playlist, 'r') as fd: self.playlist = fd.read().strip().split('\n') self.path = None self.next_path = None if self.player.path: path = self.player.path.decode() self.path = path self.update_next() self.load_subtitles() data = {} data['subtitles'] = {} data['subtitles'][os.path.basename(self.path)] = self.current data['subtitles'][os.path.basename(self.next_path)] = self.next self.trigger(data) def update(self, pos): if pos is None: return if self.player and self.playlist and self.player.path: self.position = pos path = self.player.path.decode() trigger_path = path != self.path self.path = path self.update_next() self.load_subtitles() data = { 'current': os.path.basename(self.path), 'next': os.path.basename(self.next_path), 'currentTime': pos } if trigger_path: data['subtitles'] = {} data['subtitles'][os.path.basename(self.next_path)] = self.next self.last = 0 if abs(pos - self.last) > 0.5 or trigger_path: self.trigger(data) self.last = pos def update_next(self): index = self.playlist.index(self.path) + 1 if index == len(self.playlist): index = 0 self.next_path = self.playlist[index] def load_subtitles(self): if 'png' in self.path: self.current = [] else: srt = self.path.replace('.mp4', '.srt') self.current = ox.srt.load(srt) if 'png' in self.next_path: self.next = [] else: srt = self.next_path.replace('.mp4', '.srt') self.next = ox.srt.load(srt) def trigger(self, data): logger.debug('trigger %s', data) for socket in sockets: socket.post(data) def status(self): data = { 'curent': os.path.basename(self.path) if self.path else '', 'next': os.path.basename(self.next_path) if self.next_path else '', 'currentTime': self.position } data['subtitles'] = {} data['subtitles'][os.path.basename(self.path)] = self.current data['subtitles'][os.path.basename(self.next_path)] = self.next return data class Socket(WebSocketHandler): def initialize(self, sub): self.sub = sub def check_origin(self, origin): # bypass same origin check return True def open(self): if self not in sockets: sockets.append(self) self.post(self.sub.status()) # websocket calls def on_close(self): if self in sockets: sockets.remove(self) def on_message(self, message): pass #logger.debug('got message %s', message) def post(self, data): try: message = json.dumps(data) except: logger.debug('failed to serialize data %s', data) return main = IOLoop.instance() main.add_callback(lambda: self.write_message(message)) class StaticFileHandler(RequestHandler): def initialize(self, root): self.root = root def head(self): self.get(include_body=False) @tornado.web.asynchronous @gen.coroutine def get(self, include_body=True): path = self.root + self.request.path if self.request.path == '/': path += 'index.html' mimetype = mimetypes.guess_type(path)[0] if mimetype is None: mimetype = 'text/html' self.set_header('Content-Type', mimetype) size = os.stat(path).st_size self.set_header('Accept-Ranges', 'bytes') chunk_size = 4096 if include_body: if 'Range' in self.request.headers: self.set_status(206) r = self.request.headers.get('Range').split('=')[-1].split('-') start = int(r[0]) end = int(r[1]) if r[1] else (size - 1) length = end - start + 1 self.set_header('Content-Length', str(length)) self.set_header('Content-Range', 'bytes %s-%s/%s' % (start, end, size)) with open(path, 'rb') as fd: fd.seek(start) p = 0 while p < length: chunk = max(chunk_size, length-p) self.write(fd.read(chunk)) yield gen.Task(self.flush, include_footers=False) p += chunk else: self.set_header('Content-Length', str(size)) with open(path, 'rb') as fd: p = 0 length = size while p < length: chunk = max(chunk_size, length-p) self.write(fd.read(chunk)) yield gen.Task(self.flush, include_footers=False) self.flush() p += chunk self.finish() else: self.set_header('Content-Length', str(size)) class NotFoundHandler(RequestHandler): def get(self): raise HTTPError(404) class SubtitleServer(Thread): sub = None def __init__(self, player, playlist): Thread.__init__(self) self.daemon = True self.player = player self.playlist = playlist self.start() def run(self): main(self.player, self.playlist, self) def join(self): IOLoop.instance().stop() return Thread.join(self) def load_playlist(self, playlist): if self.sub: self.sub.load_playlist(playlist) def main(player, playlist, parent=None): sub = Subtitles(player, playlist) if parent: parent.sub = sub options = { 'debug': DEBUG, 'gzip': False } handlers = [ (r'/favicon.ico', NotFoundHandler), (r'/ws/', Socket, dict(sub=sub)), (r'/.*', StaticFileHandler, dict(root=STATIC_ROOT)) ] if DEBUG: log_format = '%(asctime)s:%(levelname)s:%(name)s:%(message)s' logging.basicConfig(level=logging.DEBUG, format=log_format) http_server = HTTPServer(Application(handlers, **options)) if parent: parent.server = http_server main = IOLoop.instance() def shutdown(): http_server.stop() #signal.signal(signal.SIGTERM, shutdown) http_server.listen(PORT, ADDRESS) try: main.start() except: pass shutdown()