diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..d216a99 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +recursive-include cdoseaplay/static * diff --git a/cdoseaplay/play.py b/cdoseaplay/play.py index aea4dc6..c0585a3 100755 --- a/cdoseaplay/play.py +++ b/cdoseaplay/play.py @@ -5,6 +5,7 @@ import os import sys from .utils import update_playlist, get_player +from .subtitleserver import SubtitleServer import logging logger = logging.getLogger('cdosea') @@ -40,6 +41,7 @@ def main(): player.register_key_binding('q', q_binding) update_playlist(args.playlist, args.prefix) + sub = SubtitleServer(player, args.playlist) player.loadlist(args.playlist) while True: @@ -48,6 +50,7 @@ def main(): except: sys.exit() del player + sub.join() if __name__ == '__main__': diff --git a/cdoseaplay/static/index.html b/cdoseaplay/static/index.html new file mode 100644 index 0000000..50d4bfb --- /dev/null +++ b/cdoseaplay/static/index.html @@ -0,0 +1 @@ + diff --git a/cdoseaplay/static/subtitles.js b/cdoseaplay/static/subtitles.js new file mode 100644 index 0000000..db3ef3c --- /dev/null +++ b/cdoseaplay/static/subtitles.js @@ -0,0 +1,77 @@ +'use strict' + +var app = {} + +app.status = { + currentTime: 0, + path: null, + subtitles: {} +} + +app.render = function() { + + var html = '' + + html += 'Current Time: ' + app.status.currentTime + '
' + + if (app.status.subtitles[app.status.current]) { + app.status.subtitles[app.status.current].forEach(function(sub) { + if (app.status.currentTime >= sub['in'] && app.status.currentTime < sub.out) { + html += '

' + sub['in'] + ' ' + sub.out + ': ' + sub.value + '' + } else { + html += '

' + sub['in'] + ' ' + sub.out + ': ' + sub.value + } + }) + } + if (app.status.subtitles[app.status.next]) { + html += '

Next: ' + app.status.subtitles[app.status.next].forEach(function(sub) { + html += '

' + sub['in'] + ' ' + sub.out + ': ' + sub.value + }) + } + document.body.innerHTML = html +} + +app.connectWS = function() { + app.ws = new WebSocket('ws://' + document.location.host + '/ws/') + app.ws.onopen = function() { + //console.log('open') + } + app.ws.onerror = function(event) { + //console.log('error') + ws.close() + } + app.ws.onclose = function(event) { + //console.log('closed') + setTimeout(app.connectWS, 1000) + } + app.ws.onmessage = function(event) { + var request + try { + request = JSON.parse(event.data) + } catch(e) { + request = { + 'error': {}, + 'debug': event.data + } + } + //console.log('message', request) + app.status.currentTime = request.currentTime + + if (request.current) { + app.status.current = request.current + } + if (request.next) { + app.status.next = request.next + } + if (request.subtitles) { + Object.keys(request.subtitles).forEach(function(name) { + app.status.subtitles[name] = request.subtitles[name] + }) + } + app.render() + } +} + +app.connectWS() +window.addEventListener('load', app.render, false) diff --git a/cdoseaplay/subtitleserver.py b/cdoseaplay/subtitleserver.py new file mode 100644 index 0000000..2eb38d2 --- /dev/null +++ b/cdoseaplay/subtitleserver.py @@ -0,0 +1,235 @@ +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 + with open(playlist, 'r') as fd: + self.playlist = fd.read().strip().split('\n') + self.player.observe_property('time-pos', self.update) + + 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): + srt = self.path.replace('.mp4', '.srt') + self.current = ox.srt.load(srt) + 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): + + 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) + + def join(self): + IOLoop.instance().stop() + return Thread.join(self) + + +def main(player, playlist): + sub = Subtitles(player, playlist) + + 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)) + main = IOLoop.instance() + + def shutdown(): + http_server.stop() + + #signal.signal(signal.SIGTERM, shutdown) + http_server.listen(PORT, ADDRESS) + try: + main.start() + except: + pass + shutdown() diff --git a/cdoseaplay/utils.py b/cdoseaplay/utils.py index 6b79555..f6d96ea 100644 --- a/cdoseaplay/utils.py +++ b/cdoseaplay/utils.py @@ -25,6 +25,8 @@ def get_player(fullscreen=True): #player.observe_property('time-pos', lambda pos: print('Now playing at {:.2f}s'.format(pos))) player.fullscreen = fullscreen player.loop = 'inf' + player.loop_file = 'no' + return player diff --git a/setup.py b/setup.py index 4322a5e..4268f4e 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -from setuptools import setup +from setuptools import setup, find_packages setup( name="cdoseaplay", @@ -12,9 +12,8 @@ setup( scripts=[ 'cdosea-play', ], - packages=[ - 'cdoseaplay' - ], + packages=find_packages(exclude=['tests', 'tests.*']), + include_package_data=True, install_requires=[ 'ox >= 2.1.541,<3', 'requests >= 1.1.0',