from glob import glob from threading import Thread import json import logging import mimetypes import os import time import asyncio 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 def load_srt(srt): if os.path.exists(srt): return ox.srt.load(srt) return [] 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 if isinstance(path, bytes): path = 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, *args): pos = args[0] if pos == 'time-pos' and len(args) > 1: pos = args[1] if pos is None: return if self.player and self.playlist and self.player.path: self.position = pos path = self.player.path if isinstance(path, bytes): path = 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): srt = self.path.replace('.mp4', '.srt') self.current = load_srt(srt) srt = self.next_path.replace('.mp4', '.srt') self.next = load_srt(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, main): self.sub = sub self.main = main 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 self.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) @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)) self.flush() 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)) 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 main = None def __init__(self, player, playlist): Thread.__init__(self) self.daemon = True self.player = player self.playlist = playlist self.start() def run(self): self.main = asyncio.new_event_loop() asyncio.set_event_loop(self.main) main(self.player, self.playlist, self) def join(self): if self.main: self.main.close() self.main.stop() 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 } main = IOLoop.instance() handlers = [ (r'/favicon.ico', NotFoundHandler), (r'/ws/', Socket, dict(sub=sub, main=main)), (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 def shutdown(): http_server.stop() #signal.signal(signal.SIGTERM, shutdown) http_server.listen(PORT, ADDRESS) try: main.start() except: pass shutdown()