From 41c34b4bc5bd261a4c64ed305e7e5d00426d6263 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Mon, 11 Feb 2019 18:46:25 +0100 Subject: [PATCH] Attempt to support subtitle tracks in web player --- platypush/backend/http/__init__.py | 137 +++++++++++++-- .../backend/http/media/handlers/__init__.py | 14 +- platypush/backend/http/static/js/media.js | 160 ++++++++++-------- .../backend/http/templates/webplayer.html | 19 +++ 4 files changed, 241 insertions(+), 89 deletions(-) create mode 100644 platypush/backend/http/templates/webplayer.html diff --git a/platypush/backend/http/__init__.py b/platypush/backend/http/__init__.py index 4712ec30f..722282fe2 100644 --- a/platypush/backend/http/__init__.py +++ b/platypush/backend/http/__init__.py @@ -11,12 +11,12 @@ import time from multiprocessing import Process from flask import Flask, Response, abort, jsonify, request as http_request, \ - render_template, send_from_directory + render_template, send_from_directory, make_response from redis import Redis from platypush.config import Config -from platypush.context import get_backend, get_or_create_event_loop +from platypush.context import get_backend, get_plugin, get_or_create_event_loop from platypush.message import Message from platypush.message.event import Event, StopEvent from platypush.message.event.web.widget import WidgetUpdateEvent @@ -371,7 +371,7 @@ class HttpBackend(Backend): def get_media_id(source): return hashlib.sha1(source.encode()).hexdigest() - def register_media(source): + def register_media(source, subtitles=None): media_id = get_media_id(source) media_url = get_media_url(media_id) @@ -379,8 +379,10 @@ class HttpBackend(Backend): if media_id in media_map: return media_map[media_id] - media_hndl = MediaHandler.build(source, url=media_url) + media_hndl = MediaHandler.build(source, url=media_url, + subtitles=subtitles) media_map[media_id] = media_hndl + media_hndl.media_id = media_id self.logger.info('Streaming "{}" on {}'.format(source, media_url)) return media_hndl @@ -434,7 +436,6 @@ class HttpBackend(Backend): if not to_bytes: to_bytes = content_length-1 - # to_bytes = from_bytes + self._DEFAULT_STREAMING_BLOCK_SIZE content_length -= from_bytes else: to_bytes = int(to_bytes) @@ -450,11 +451,118 @@ class HttpBackend(Backend): headers['Content-Length'] = content_length - return Response(media_hndl.get_data( - from_bytes=from_bytes, to_bytes=to_bytes, - chunk_size=self._DEFAULT_STREAMING_CHUNK_SIZE), - status_code, headers=headers, mimetype=headers['Content-Type'], - direct_passthrough=True) + if 'webplayer' in request.args: + return render_template('webplayer.html', + media_url=media_hndl.url.replace( + self.remote_base_url, ''), + media_type=media_hndl.mime_type, + subtitles_url='/media/subtitles/{}.srt'. + format(media_id)) + else: + return Response(media_hndl.get_data( + from_bytes=from_bytes, to_bytes=to_bytes, + chunk_size=self._DEFAULT_STREAMING_CHUNK_SIZE), + status_code, headers=headers, mimetype=headers['Content-Type'], + direct_passthrough=True) + + + def add_subtitles(media_id, req): + """ + This route can be used to download and/or expose subtitles files + associated to a media file + """ + + media_hndl = media_map.get(media_id) + if not media_hndl: + raise FileNotFoundError('{} is not a registered media_id'. + format(media_id)) + + subfile = None + if req.data: + try: + subfile = json.loads(req.data.decode('utf-8')) \ + .get('filename') + if not subfile: + raise AttributeError + except Exception as e: + raise AttributeError(400, 'No filename in the request: {}' + .format(str(e))) + + if not subfile: + if not media_hndl.path: + raise NotImplementedError( + 'Subtitles are currently only supported for ' + + 'local media files') + + try: + subtitles = get_plugin('media.subtitles').get_subtitles( + media_hndl.path) + except Exception as e: + raise RuntimeError('Could not get subtitles: {}'. + format(str(e))) + + if not subtitles: + raise FileNotFoundError( + 'No subtitles found for resource {}'.format( + media_hndl.path)) + + subfile = get_plugin('media.subtitles').download( + link=subtitles[0].get('SubDownloadLink'), + media_resource=media_hndl.path).get('filename') + + media_hndl.set_subtitles(subfile) + return { + 'filename': subfile, + 'url': self.remote_base_url + '/media/subtitles/' + media_id + '.srt', + } + + def remove_subtitles(media_id): + media_hndl = media_map.get(media_id) + if not media_hndl: + raise FileNotFoundError('{} is not a registered media_id'. + format(media_id)) + + if not media_hndl.subtitles: + raise FileNotFoundError('{} has no subtitles attached'. + format(media_id)) + + media_hndl.remove_subtitles() + return {} + + + @app.route('/media/subtitles/.srt', methods=['GET', 'PUT', 'DELETE']) + def handle_subtitles(media_id): + """ + This route can be used to download and/or expose subtitle files + associated to a media file + """ + + if http_request.method == 'GET': + media_hndl = media_map.get(media_id) + if not media_hndl: + abort(404, 'No such media') + + if not media_hndl.subtitles: + abort(404, 'The media has no subtitles attached') + + return send_from_directory( + os.path.dirname(media_hndl.subtitles), + os.path.basename(media_hndl.subtitles), + mimetype='text/vtt') + + try: + if http_request.method == 'DELETE': + return jsonify(remove_subtitles(media_id)) + else: + return jsonify(add_subtitles(media_id, http_request)) + except FileNotFoundError as e: + abort(404, str(e)) + except AttributeError as e: + abort(400, str(e)) + except NotImplementedError as e: + abort(422, str(e)) + except Exception as e: + abort(500, str(e)) @app.route('/media', methods=['GET', 'PUT']) def add_or_get_media(): @@ -476,9 +584,14 @@ class HttpBackend(Backend): if not source: abort(400, 'The request does not contain any source') + subtitles = args.get('subtitles') try: - media_hndl = register_media(source) - return jsonify(dict(media_hndl)) + media_hndl = register_media(source, subtitles) + ret = dict(media_hndl) + if media_hndl.subtitles: + ret['subtitles_url'] = self.remote_base_url + \ + '/media/subtitles/' + media_hndl.media_id + '.srt' + return jsonify(ret) except FileNotFoundError as e: abort(404, str(e)) except AttributeError as e: diff --git a/platypush/backend/http/media/handlers/__init__.py b/platypush/backend/http/media/handlers/__init__.py index ae1d04a56..419e50e9d 100644 --- a/platypush/backend/http/media/handlers/__init__.py +++ b/platypush/backend/http/media/handlers/__init__.py @@ -7,7 +7,8 @@ class MediaHandler: prefix_handlers = [] def __init__(self, source, filename=None, - mime_type='application/octet-stream', name=None, url=None): + mime_type='application/octet-stream', name=None, url=None, + subtitles=None): matched_handlers = [hndl for hndl in self.prefix_handlers if source.startswith(hndl)] @@ -18,10 +19,12 @@ class MediaHandler: self.prefix_handlers)) self.name = name + self.path = None self.filename = name self.source = source self.url = url self.mime_type = mime_type + self.subtitles = subtitles self.content_length = 0 self._matched_handler = matched_handlers[0] @@ -42,8 +45,15 @@ class MediaHandler: def get_data(self, from_bytes=None, to_bytes=None, chunk_size=None): raise NotImplementedError() + def set_subtitles(self, subtitles_file): + self.subtitles = subtitles_file + + def remove_subtitles(self): + self.subtitles = None + def __iter__(self): - for attr in ['name', 'source', 'mime_type', 'url', 'prefix_handlers']: + for attr in ['name', 'source', 'mime_type', 'url', 'subtitles', + 'prefix_handlers']: yield (attr, getattr(self, attr)) diff --git a/platypush/backend/http/static/js/media.js b/platypush/backend/http/static/js/media.js index 7b0ce9bd3..225b7d662 100644 --- a/platypush/backend/http/static/js/media.js +++ b/platypush/backend/http/static/js/media.js @@ -50,6 +50,7 @@ $(document).ready(function() { 'icon': 'stop', 'html': 'Media playback stopped', }); + $mediaSearchSubtitles.hide(); break; } }; @@ -105,6 +106,7 @@ $(document).ready(function() { }; const startStreamingTorrent = function(torrent) { + // TODO support for subtitles download on torrent metadata received return new Promise((resolve, reject) => { execute( { @@ -127,7 +129,7 @@ $(document).ready(function() { }); }; - const startStreaming = function(media) { + const startStreaming = function(media, subtitles) { if (media.startsWith('magnet:?')) { return new Promise((resolve, reject) => { startStreamingTorrent(media) @@ -141,12 +143,20 @@ $(document).ready(function() { type: 'PUT', url: '/media', contentType: 'application/json', - data: JSON.stringify({ source: media }), + data: JSON.stringify({ + 'source': media, + 'subtitles': subtitles, + }), complete: (xhr, textStatus) => { if (xhr.status == 200) { var uri = xhr.responseJSON.url.match(/https?:\/\/[^\/]+(\/media\/.*)/)[1] - resolve(uri); + var subtitles; + if ('subtitles_url' in xhr.responseJSON) { + subtitles = xhr.responseJSON.subtitles_url.match(/https?:\/\/[^\/]+(\/media\/.*)/)[1] + } + + resolve(uri, subtitles); } else { reject(Error(xhr.responseText)); } @@ -168,7 +178,7 @@ $(document).ready(function() { const getSubtitles = function(resource) { return new Promise((resolve, reject) => { if (!window.config.media.subtitles) { - return; // media.subtitles plugin not configured + resolve(); // media.subtitles plugin not configured } run({ @@ -233,11 +243,9 @@ $(document).ready(function() { const playInBrowser = (resource, subtitles) => { return new Promise((resolve, reject) => { - // TODO support for subtitles in local player - - startStreaming(resource).then((url) => { - window.open(url, '_blank'); - resolve(url); + startStreaming(resource, subtitles).then((url, subtitles) => { + window.open(url + '?webplayer', '_blank'); + resolve(url, subtitles); }).catch((xhr, status, error) => { reject(xhr.responseText); }); @@ -299,16 +307,19 @@ $(document).ready(function() { var subtitlesConf = window.config.media.subtitles; - // TODO populate the subtitles panel and show the subtitles button - if (subtitlesConf && 'language' in subtitlesConf) { - tryFetchSubtitles(resource).then((subtitles) => { - if (!subtitles) { - showError('Cannot get subtitles: ' + error); - _play(resource).finally(onVideoReady); - } else { - _play(resource, subtitles).finally(onVideoReady); - } - }); + if (subtitlesConf) { + populateSubtitlesModal(resource); + + if ('language' in subtitlesConf) { + tryFetchSubtitles(resource).then((subtitles) => { + if (!subtitles) { + showError('Cannot get subtitles'); + _play(resource).finally(onVideoReady); + } else { + _play(resource, subtitles).finally(onVideoReady); + } + }); + } } else { _play(resource).finally(onVideoReady); } @@ -346,7 +357,7 @@ $(document).ready(function() { onVideoLoading(); startStreaming(resource) .then((url) => { - url = url + '?download=1' + url = url + '?download' window.open(url, '_blank'); resolve(url); }) @@ -359,6 +370,58 @@ $(document).ready(function() { }); }; + const populateSubtitlesModal = (resource) => { + $mediaSubtitlesMessage.text('Loading subtitles...'); + $mediaSubtitlesResults.text(''); + $mediaSubtitlesMessage.show(); + $mediaSubtitlesResultsContainer.hide(); + + getSubtitles(resource).then((subs) => { + if (!subs.length) { + $mediaSubtitlesMessage.text('No subtitles found'); + return; + } + + $mediaSearchSubtitles.show(); + for (var sub of subs) { + var flagCode; + if ('ISO639' in sub) { + switch(sub.ISO639) { + case 'en': flagCode = 'gb'; break; + default: flagCode = sub.ISO639; break; + } + } + + var $subContainer = $('
').addClass('row media-subtitle-container') + .data('download-link', sub.SubDownloadLink) + .data('resource', resource); + + var $subFlagIconContainer = $('
').addClass('one column'); + var $subFlagIcon = $('') + .addClass(flagCode ? 'flag-icon flag-icon-' + flagCode : ( + sub.IsLocal ? 'fa fa-download' : '')) + .text(!(flagCode || sub.IsLocal) ? '?' : ''); + + var $subMovieName = $('
').addClass('five columns') + .text(sub.MovieName); + + var $subFileName = $('
').addClass('six columns') + .text(sub.SubFileName); + + $subFlagIcon.appendTo($subFlagIconContainer); + $subFlagIconContainer.appendTo($subContainer); + $subMovieName.appendTo($subContainer); + $subFileName.appendTo($subContainer); + $subContainer.appendTo($mediaSubtitlesResults); + } + + $mediaSubtitlesMessage.hide(); + $mediaSubtitlesResultsContainer.show(); + }).catch((error) => { + $mediaSubtitlesMessage.text('Unable to load subtitles: ' + error.message); + }); + }; + const initBindings = function() { window.registerEventListener(onEvent); $searchForm.on('submit', function(event) { @@ -531,61 +594,8 @@ $(document).ready(function() { }); }); - $mediaSearchSubtitles.on('mouseup touchend', () => { - var resource = $mediaSearchSubtitles.data('resource'); - if (!resource) { - return; - } - - $mediaSubtitlesMessage.text('Loading subtitles...'); - $mediaSubtitlesResults.text(''); - $mediaSubtitlesMessage.show(); - $mediaSubtitlesResultsContainer.hide(); - - getSubtitles(resource).then((subs) => { - if (!subs.length) { - $mediaSubtitlesMessage.text('No subtitles found'); - return; - } - - for (var sub of subs) { - var flagCode; - if ('ISO639' in sub) { - switch(sub.ISO639) { - case 'en': flagCode = 'gb'; break; - default: flagCode = sub.ISO639; break; - } - } - - var $subContainer = $('
').addClass('row media-subtitle-container') - .data('download-link', sub.SubDownloadLink) - .data('resource', resource); - - var $subFlagIconContainer = $('
').addClass('one column'); - var $subFlagIcon = $('') - .addClass(flagCode ? 'flag-icon flag-icon-' + flagCode : ( - sub.IsLocal ? 'fa fa-download' : '')) - .text(!(flagCode || sub.IsLocal) ? '?' : ''); - - var $subMovieName = $('
').addClass('five columns') - .text(sub.MovieName); - - var $subFileName = $('
').addClass('six columns') - .text(sub.SubFileName); - - $subFlagIcon.appendTo($subFlagIconContainer); - $subFlagIconContainer.appendTo($subContainer); - $subMovieName.appendTo($subContainer); - $subFileName.appendTo($subContainer); - $subContainer.appendTo($mediaSubtitlesResults); - } - - $mediaSubtitlesMessage.hide(); - $mediaSubtitlesResultsContainer.show(); - }).catch((error) => { - $mediaSubtitlesMessage.text('Unable to load subtitles: ' + error.message); - }); - }); + // $mediaSearchSubtitles.on('mouseup touchend', () => { + // }); }; const initRemoteDevices = function() { diff --git a/platypush/backend/http/templates/webplayer.html b/platypush/backend/http/templates/webplayer.html new file mode 100644 index 000000000..62ad7ff67 --- /dev/null +++ b/platypush/backend/http/templates/webplayer.html @@ -0,0 +1,19 @@ + + + +