add subtitle preview server
This commit is contained in:
parent
651607e354
commit
2ce50f2971
7 changed files with 322 additions and 4 deletions
1
MANIFEST.in
Normal file
1
MANIFEST.in
Normal file
|
@ -0,0 +1 @@
|
||||||
|
recursive-include cdoseaplay/static *
|
|
@ -5,6 +5,7 @@ import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from .utils import update_playlist, get_player
|
from .utils import update_playlist, get_player
|
||||||
|
from .subtitleserver import SubtitleServer
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger('cdosea')
|
logger = logging.getLogger('cdosea')
|
||||||
|
@ -40,6 +41,7 @@ def main():
|
||||||
player.register_key_binding('q', q_binding)
|
player.register_key_binding('q', q_binding)
|
||||||
|
|
||||||
update_playlist(args.playlist, args.prefix)
|
update_playlist(args.playlist, args.prefix)
|
||||||
|
sub = SubtitleServer(player, args.playlist)
|
||||||
player.loadlist(args.playlist)
|
player.loadlist(args.playlist)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
@ -48,6 +50,7 @@ def main():
|
||||||
except:
|
except:
|
||||||
sys.exit()
|
sys.exit()
|
||||||
del player
|
del player
|
||||||
|
sub.join()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
1
cdoseaplay/static/index.html
Normal file
1
cdoseaplay/static/index.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<script src="subtitles.js"></script>
|
77
cdoseaplay/static/subtitles.js
Normal file
77
cdoseaplay/static/subtitles.js
Normal file
|
@ -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 + '<br>'
|
||||||
|
|
||||||
|
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 += '<br><br><b>' + sub['in'] + ' ' + sub.out + ': ' + sub.value + '</b>'
|
||||||
|
} else {
|
||||||
|
html += '<br><br>' + sub['in'] + ' ' + sub.out + ': ' + sub.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (app.status.subtitles[app.status.next]) {
|
||||||
|
html += '<br><br>Next: '
|
||||||
|
app.status.subtitles[app.status.next].forEach(function(sub) {
|
||||||
|
html += '<br><br>' + 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)
|
235
cdoseaplay/subtitleserver.py
Normal file
235
cdoseaplay/subtitleserver.py
Normal file
|
@ -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()
|
|
@ -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.observe_property('time-pos', lambda pos: print('Now playing at {:.2f}s'.format(pos)))
|
||||||
player.fullscreen = fullscreen
|
player.fullscreen = fullscreen
|
||||||
player.loop = 'inf'
|
player.loop = 'inf'
|
||||||
|
player.loop_file = 'no'
|
||||||
|
|
||||||
|
|
||||||
return player
|
return player
|
||||||
|
|
||||||
|
|
7
setup.py
7
setup.py
|
@ -1,5 +1,5 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from setuptools import setup
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="cdoseaplay",
|
name="cdoseaplay",
|
||||||
|
@ -12,9 +12,8 @@ setup(
|
||||||
scripts=[
|
scripts=[
|
||||||
'cdosea-play',
|
'cdosea-play',
|
||||||
],
|
],
|
||||||
packages=[
|
packages=find_packages(exclude=['tests', 'tests.*']),
|
||||||
'cdoseaplay'
|
include_package_data=True,
|
||||||
],
|
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'ox >= 2.1.541,<3',
|
'ox >= 2.1.541,<3',
|
||||||
'requests >= 1.1.0',
|
'requests >= 1.1.0',
|
||||||
|
|
Loading…
Reference in a new issue