cdosea-play/cdoseaplay/subtitleserver.py

277 lines
7.7 KiB
Python

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()