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',