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
|
||||
|
||||
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__':
|
||||
|
|
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.fullscreen = fullscreen
|
||||
player.loop = 'inf'
|
||||
player.loop_file = 'no'
|
||||
|
||||
|
||||
return player
|
||||
|
||||
|
|
7
setup.py
7
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',
|
||||
|
|
Loading…
Reference in a new issue