forked from platypush/platypush
Added support for VTT subtitles and subtitles toggling both in local and browser media players
This commit is contained in:
parent
41c34b4bc5
commit
5cbd0fdfe7
10 changed files with 257 additions and 107 deletions
|
@ -11,7 +11,7 @@ import time
|
||||||
|
|
||||||
from multiprocessing import Process
|
from multiprocessing import Process
|
||||||
from flask import Flask, Response, abort, jsonify, request as http_request, \
|
from flask import Flask, Response, abort, jsonify, request as http_request, \
|
||||||
render_template, send_from_directory, make_response
|
render_template, send_from_directory
|
||||||
|
|
||||||
from redis import Redis
|
from redis import Redis
|
||||||
|
|
||||||
|
@ -379,8 +379,20 @@ class HttpBackend(Backend):
|
||||||
if media_id in media_map:
|
if media_id in media_map:
|
||||||
return media_map[media_id]
|
return media_map[media_id]
|
||||||
|
|
||||||
|
subfile = None
|
||||||
|
|
||||||
|
if subtitles:
|
||||||
|
try:
|
||||||
|
subfile = get_plugin('media.subtitles').download(
|
||||||
|
link=subtitles, convert_to_vtt=True
|
||||||
|
).output.get('filename')
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning('Unable to load subtitle {}: {}'
|
||||||
|
.format(subtitles, str(e)))
|
||||||
|
|
||||||
media_hndl = MediaHandler.build(source, url=media_url,
|
media_hndl = MediaHandler.build(source, url=media_url,
|
||||||
subtitles=subtitles)
|
subtitles=subfile)
|
||||||
|
|
||||||
media_map[media_id] = media_hndl
|
media_map[media_id] = media_hndl
|
||||||
media_hndl.media_id = media_id
|
media_hndl.media_id = media_id
|
||||||
|
|
||||||
|
@ -456,8 +468,9 @@ class HttpBackend(Backend):
|
||||||
media_url=media_hndl.url.replace(
|
media_url=media_hndl.url.replace(
|
||||||
self.remote_base_url, ''),
|
self.remote_base_url, ''),
|
||||||
media_type=media_hndl.mime_type,
|
media_type=media_hndl.mime_type,
|
||||||
subtitles_url='/media/subtitles/{}.srt'.
|
subtitles_url='/media/subtitles/{}.vtt'.
|
||||||
format(media_id))
|
format(media_id) if media_hndl.subtitles
|
||||||
|
else None)
|
||||||
else:
|
else:
|
||||||
return Response(media_hndl.get_data(
|
return Response(media_hndl.get_data(
|
||||||
from_bytes=from_bytes, to_bytes=to_bytes,
|
from_bytes=from_bytes, to_bytes=to_bytes,
|
||||||
|
@ -508,12 +521,13 @@ class HttpBackend(Backend):
|
||||||
|
|
||||||
subfile = get_plugin('media.subtitles').download(
|
subfile = get_plugin('media.subtitles').download(
|
||||||
link=subtitles[0].get('SubDownloadLink'),
|
link=subtitles[0].get('SubDownloadLink'),
|
||||||
media_resource=media_hndl.path).get('filename')
|
media_resource=media_hndl.path,
|
||||||
|
convert_to_vtt=True).get('filename')
|
||||||
|
|
||||||
media_hndl.set_subtitles(subfile)
|
media_hndl.set_subtitles(subfile)
|
||||||
return {
|
return {
|
||||||
'filename': subfile,
|
'filename': subfile,
|
||||||
'url': self.remote_base_url + '/media/subtitles/' + media_id + '.srt',
|
'url': self.remote_base_url + '/media/subtitles/' + media_id + '.vtt',
|
||||||
}
|
}
|
||||||
|
|
||||||
def remove_subtitles(media_id):
|
def remove_subtitles(media_id):
|
||||||
|
@ -530,7 +544,7 @@ class HttpBackend(Backend):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
@app.route('/media/subtitles/<media_id>.srt', methods=['GET', 'PUT', 'DELETE'])
|
@app.route('/media/subtitles/<media_id>.vtt', methods=['GET', 'POST', 'DELETE'])
|
||||||
def handle_subtitles(media_id):
|
def handle_subtitles(media_id):
|
||||||
"""
|
"""
|
||||||
This route can be used to download and/or expose subtitle files
|
This route can be used to download and/or expose subtitle files
|
||||||
|
@ -590,7 +604,7 @@ class HttpBackend(Backend):
|
||||||
ret = dict(media_hndl)
|
ret = dict(media_hndl)
|
||||||
if media_hndl.subtitles:
|
if media_hndl.subtitles:
|
||||||
ret['subtitles_url'] = self.remote_base_url + \
|
ret['subtitles_url'] = self.remote_base_url + \
|
||||||
'/media/subtitles/' + media_hndl.media_id + '.srt'
|
'/media/subtitles/' + media_hndl.media_id + '.vtt'
|
||||||
return jsonify(ret)
|
return jsonify(ret)
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
abort(404, str(e))
|
abort(404, str(e))
|
||||||
|
@ -754,6 +768,7 @@ class HttpBackend(Backend):
|
||||||
self.logger.info('Initialized HTTP backend on port {}'.format(self.port))
|
self.logger.info('Initialized HTTP backend on port {}'.format(self.port))
|
||||||
|
|
||||||
self.app = self.webserver()
|
self.app = self.webserver()
|
||||||
|
self.app.debug = False
|
||||||
self.server_proc = Process(target=self.app.run,
|
self.server_proc = Process(target=self.app.run,
|
||||||
name='WebServer',
|
name='WebServer',
|
||||||
kwargs=kwargs)
|
kwargs=kwargs)
|
||||||
|
|
|
@ -53,8 +53,9 @@ class MediaHandler:
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
for attr in ['name', 'source', 'mime_type', 'url', 'subtitles',
|
for attr in ['name', 'source', 'mime_type', 'url', 'subtitles',
|
||||||
'prefix_handlers']:
|
'prefix_handlers', 'media_id']:
|
||||||
yield (attr, getattr(self, attr))
|
if hasattr(self, attr):
|
||||||
|
yield (attr, getattr(self, attr))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,9 @@ $(document).ready(function() {
|
||||||
$mediaSubtitlesMessage = $mediaSubtitlesModal.find('.media-subtitles-message'),
|
$mediaSubtitlesMessage = $mediaSubtitlesModal.find('.media-subtitles-message'),
|
||||||
$mediaSearchSubtitles = $ctrlForm.find('[data-modal="#media-subtitles-modal"]'),
|
$mediaSearchSubtitles = $ctrlForm.find('[data-modal="#media-subtitles-modal"]'),
|
||||||
prevVolume = undefined,
|
prevVolume = undefined,
|
||||||
selectedResource = undefined;
|
selectedResource = undefined,
|
||||||
|
browserVideoWindow = undefined,
|
||||||
|
browserVideoElement = undefined;
|
||||||
|
|
||||||
const onEvent = (event) => {
|
const onEvent = (event) => {
|
||||||
switch (event.args.type) {
|
switch (event.args.type) {
|
||||||
|
@ -158,7 +160,7 @@ $(document).ready(function() {
|
||||||
|
|
||||||
resolve(uri, subtitles);
|
resolve(uri, subtitles);
|
||||||
} else {
|
} else {
|
||||||
reject(Error(xhr.responseText));
|
reject(xhr.responseText);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -192,13 +194,14 @@ $(document).ready(function() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadSubtitles = function(link, mediaResource) {
|
const downloadSubtitles = function(link, mediaResource, vtt=false) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
run({
|
run({
|
||||||
action: 'media.subtitles.download',
|
action: 'media.subtitles.download',
|
||||||
args: {
|
args: {
|
||||||
'link': link,
|
'link': link,
|
||||||
'media_resource': mediaResource,
|
'media_resource': mediaResource,
|
||||||
|
'convert_to_vtt': vtt,
|
||||||
}
|
}
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
resolve(response.response.output.filename);
|
resolve(response.response.output.filename);
|
||||||
|
@ -244,10 +247,17 @@ $(document).ready(function() {
|
||||||
const playInBrowser = (resource, subtitles) => {
|
const playInBrowser = (resource, subtitles) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
startStreaming(resource, subtitles).then((url, subtitles) => {
|
startStreaming(resource, subtitles).then((url, subtitles) => {
|
||||||
window.open(url + '?webplayer', '_blank');
|
browserVideoWindow = window.open(
|
||||||
|
url + '?webplayer', '_blank');
|
||||||
|
|
||||||
|
browserVideoWindow.addEventListener('load', () => {
|
||||||
|
browserVideoElement = browserVideoWindow.document
|
||||||
|
.querySelector('#video-player');
|
||||||
|
});
|
||||||
|
|
||||||
resolve(url, subtitles);
|
resolve(url, subtitles);
|
||||||
}).catch((xhr, status, error) => {
|
}).catch((error) => {
|
||||||
reject(xhr.responseText);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -287,8 +297,8 @@ $(document).ready(function() {
|
||||||
playHndl.then((response) => {
|
playHndl.then((response) => {
|
||||||
resolve(resource);
|
resolve(resource);
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
showError('Playback error: ' + error.message);
|
showError('Playback error: ' + (error ? error : 'undefined'));
|
||||||
reject(error.message);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -302,44 +312,36 @@ $(document).ready(function() {
|
||||||
resolve(resource);
|
resolve(resource);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var defaultPlay = () => { _play(resource).finally(onVideoReady); };
|
||||||
$mediaSearchSubtitles.data('resource', resource);
|
$mediaSearchSubtitles.data('resource', resource);
|
||||||
onVideoLoading();
|
onVideoLoading();
|
||||||
|
|
||||||
var subtitlesConf = window.config.media.subtitles;
|
var subtitlesConf = window.config.media.subtitles;
|
||||||
|
|
||||||
if (subtitlesConf) {
|
if (subtitlesConf) {
|
||||||
populateSubtitlesModal(resource);
|
populateSubtitlesModal(resource).then((subs) => {
|
||||||
|
if ('language' in subtitlesConf) {
|
||||||
if ('language' in subtitlesConf) {
|
if (subs) {
|
||||||
tryFetchSubtitles(resource).then((subtitles) => {
|
downloadSubtitles(subs[0].SubDownloadLink, resource).then((subtitles) => {
|
||||||
if (!subtitles) {
|
_play(resource, subtitles).finally(onVideoReady);
|
||||||
showError('Cannot get subtitles');
|
resolve(resource, subtitles);
|
||||||
_play(resource).finally(onVideoReady);
|
}).catch((error) => {
|
||||||
|
defaultPlay();
|
||||||
|
resolve(resource);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
_play(resource, subtitles).finally(onVideoReady);
|
defaultPlay();
|
||||||
|
resolve(resource);
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
}
|
defaultPlay();
|
||||||
} else {
|
resolve(resource);
|
||||||
_play(resource).finally(onVideoReady);
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const tryFetchSubtitles = (resource) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
getSubtitles(resource).then((subs) => {
|
|
||||||
if (!subs) {
|
|
||||||
resolve();
|
|
||||||
return; // No subtitles found
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadSubtitles(subs[0].SubDownloadLink, resource).then((filename) => {
|
|
||||||
resolve(filename);
|
|
||||||
}).catch((error) => {
|
|
||||||
resolve();
|
|
||||||
});
|
});
|
||||||
});
|
} else {
|
||||||
|
defaultPlay();
|
||||||
|
resolve(resource);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -361,8 +363,8 @@ $(document).ready(function() {
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
resolve(url);
|
resolve(url);
|
||||||
})
|
})
|
||||||
.catch((xhr, status, error) => {
|
.catch((error) => {
|
||||||
reject(xhr.responseText);
|
reject(error);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
onVideoReady();
|
onVideoReady();
|
||||||
|
@ -371,54 +373,114 @@ $(document).ready(function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const populateSubtitlesModal = (resource) => {
|
const populateSubtitlesModal = (resource) => {
|
||||||
$mediaSubtitlesMessage.text('Loading subtitles...');
|
return new Promise((resolve, reject) => {
|
||||||
$mediaSubtitlesResults.text('');
|
$mediaSubtitlesMessage.text('Loading subtitles...');
|
||||||
$mediaSubtitlesMessage.show();
|
$mediaSubtitlesResults.text('');
|
||||||
$mediaSubtitlesResultsContainer.hide();
|
$mediaSubtitlesMessage.show();
|
||||||
|
$mediaSubtitlesResultsContainer.hide();
|
||||||
|
|
||||||
getSubtitles(resource).then((subs) => {
|
getSubtitles(resource).then((subs) => {
|
||||||
if (!subs.length) {
|
if (!subs) {
|
||||||
$mediaSubtitlesMessage.text('No subtitles found');
|
$mediaSubtitlesMessage.text('No subtitles found');
|
||||||
return;
|
resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
$mediaSearchSubtitles.show();
|
$mediaSearchSubtitles.show();
|
||||||
for (var sub of subs) {
|
for (var sub of subs) {
|
||||||
var flagCode;
|
var flagCode;
|
||||||
if ('ISO639' in sub) {
|
if ('ISO639' in sub) {
|
||||||
switch(sub.ISO639) {
|
switch(sub.ISO639) {
|
||||||
case 'en': flagCode = 'gb'; break;
|
case 'en': flagCode = 'gb'; break;
|
||||||
default: flagCode = sub.ISO639; break;
|
default: flagCode = sub.ISO639; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var $subContainer = $('<div></div>').addClass('row media-subtitle-container')
|
||||||
|
.data('download-link', sub.SubDownloadLink)
|
||||||
|
.data('resource', resource);
|
||||||
|
|
||||||
|
var $subFlagIconContainer = $('<div></div>').addClass('one column');
|
||||||
|
var $subFlagIcon = $('<span></span>')
|
||||||
|
.addClass(flagCode ? 'flag-icon flag-icon-' + flagCode : (
|
||||||
|
sub.IsLocal ? 'fa fa-download' : ''))
|
||||||
|
.text(!(flagCode || sub.IsLocal) ? '?' : '');
|
||||||
|
|
||||||
|
var $subMovieName = $('<div></div>').addClass('five columns')
|
||||||
|
.text(sub.MovieName);
|
||||||
|
|
||||||
|
var $subFileName = $('<div></div>').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();
|
||||||
|
resolve(subs);
|
||||||
|
}).catch((error) => {
|
||||||
|
$mediaSubtitlesMessage.text('Unable to load subtitles: ' + error.message);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setBrowserPlayerSubs = (resource, subtitles) => {
|
||||||
|
var mediaId;
|
||||||
|
if (!browserVideoElement) {
|
||||||
|
showError('No video is currently playing in the browser');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
$.get('/media').then((media) => {
|
||||||
|
for (var m of media) {
|
||||||
|
if (m.source === resource) {
|
||||||
|
mediaId = m.media_id;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var $subContainer = $('<div></div>').addClass('row media-subtitle-container')
|
if (!mediaId) {
|
||||||
.data('download-link', sub.SubDownloadLink)
|
reject(resource + ' is not a registered media');
|
||||||
.data('resource', resource);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var $subFlagIconContainer = $('<div></div>').addClass('one column');
|
return $.ajax({
|
||||||
var $subFlagIcon = $('<span></span>')
|
type: 'POST',
|
||||||
.addClass(flagCode ? 'flag-icon flag-icon-' + flagCode : (
|
url: '/media/subtitles/' + mediaId + '.vtt',
|
||||||
sub.IsLocal ? 'fa fa-download' : ''))
|
contentType: 'application/json',
|
||||||
.text(!(flagCode || sub.IsLocal) ? '?' : '');
|
data: JSON.stringify({
|
||||||
|
'filename': subtitles,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}).then(() => {
|
||||||
|
resolve(resource, subtitles);
|
||||||
|
}).catch((error) => {
|
||||||
|
reject('Cannot set subtitles for ' + resource + ': ' + error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
var $subMovieName = $('<div></div>').addClass('five columns')
|
const setLocalPlayerSubs = (resource, subtitles) => {
|
||||||
.text(sub.MovieName);
|
return new Promise((resolve, reject) => {
|
||||||
|
run({
|
||||||
var $subFileName = $('<div></div>').addClass('six columns')
|
action: 'media.remove_subtitles'
|
||||||
.text(sub.SubFileName);
|
}).then((response) => {
|
||||||
|
return run({
|
||||||
$subFlagIcon.appendTo($subFlagIconContainer);
|
action: 'media.set_subtitles',
|
||||||
$subFlagIconContainer.appendTo($subContainer);
|
args: {
|
||||||
$subMovieName.appendTo($subContainer);
|
'filename': subtitles,
|
||||||
$subFileName.appendTo($subContainer);
|
}
|
||||||
$subContainer.appendTo($mediaSubtitlesResults);
|
})
|
||||||
}
|
}).then((response) => {
|
||||||
|
resolve(response);
|
||||||
$mediaSubtitlesMessage.hide();
|
}).catch((error) => {
|
||||||
$mediaSubtitlesResultsContainer.show();
|
reject(error);
|
||||||
}).catch((error) => {
|
});
|
||||||
$mediaSubtitlesMessage.text('Unable to load subtitles: ' + error.message);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -435,6 +497,7 @@ $(document).ready(function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
$input.prop('disabled', true);
|
$input.prop('disabled', true);
|
||||||
|
$mediaItemPanel.find('[data-action]').removeClass('disabled');
|
||||||
$videoResults.text('Searching...');
|
$videoResults.text('Searching...');
|
||||||
|
|
||||||
if (resource.match(new RegExp('^https?://')) ||
|
if (resource.match(new RegExp('^https?://')) ||
|
||||||
|
@ -594,8 +657,29 @@ $(document).ready(function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// $mediaSearchSubtitles.on('mouseup touchend', () => {
|
$mediaSubtitlesModal.on('mouseup touchend', '.media-subtitle-container', (event) => {
|
||||||
// });
|
var resource = $(event.currentTarget).data('resource');
|
||||||
|
var link = $(event.currentTarget).data('downloadLink');
|
||||||
|
var selectedDevice = getSelectedDevice();
|
||||||
|
|
||||||
|
if (selectedDevice.isRemote) {
|
||||||
|
showError('Changing subtitles at runtime on Chromecast is not yet supported');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var convertToVTT = selectedDevice.isBrowser;
|
||||||
|
|
||||||
|
downloadSubtitles(link, resource, convertToVTT).then((subtitles) => {
|
||||||
|
if (selectedDevice.isBrowser) {
|
||||||
|
return setBrowserPlayerSubs(resource, subtitles);
|
||||||
|
} else {
|
||||||
|
return setLocalPlayerSubs(resource, subtitles);
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
console.warning('Could not load subtitles ' + link +
|
||||||
|
' to the player: ' + (error || 'undefined error'))
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const initRemoteDevices = function() {
|
const initRemoteDevices = function() {
|
||||||
|
@ -607,8 +691,7 @@ $(document).ready(function() {
|
||||||
action: 'media.chromecast.get_chromecasts',
|
action: 'media.chromecast.get_chromecasts',
|
||||||
},
|
},
|
||||||
|
|
||||||
function(results) {
|
onSuccess = function(results) {
|
||||||
$devsRefreshBtn.removeClass('disabled');
|
|
||||||
$devsList.find('.cast-device[data-remote]').remove();
|
$devsList.find('.cast-device[data-remote]').remove();
|
||||||
|
|
||||||
if (!results || results.response.errors.length) {
|
if (!results || results.response.errors.length) {
|
||||||
|
@ -641,6 +724,10 @@ $(document).ready(function() {
|
||||||
$nameContainer.appendTo($cast);
|
$nameContainer.appendTo($cast);
|
||||||
$cast.appendTo($devsList);
|
$cast.appendTo($devsList);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onComplete = function() {
|
||||||
|
$devsRefreshBtn.removeClass('disabled');
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
<script>
|
<script>
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
var video = document.querySelector('#video');
|
var video = document.querySelector('#video-player');
|
||||||
video.addEventListener('load', () => {
|
video.addEventListener('loadedmetadata', () => {
|
||||||
var tracks = video.textTracks;
|
video.play();
|
||||||
if (tracks.length) {
|
|
||||||
tracks[0].mode = 'showing';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var tracks = video.textTracks;
|
||||||
|
if (tracks.length) {
|
||||||
|
tracks[0].mode = 'showing';
|
||||||
|
}
|
||||||
}, false);
|
}, false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<video id="video" controls autoplay preload="metadata">
|
<video id="video-player" controls autoplay preload="metadata" width="100%" height="100%">
|
||||||
<source src="{{ media_url }}" type="{{ media_type }}">
|
<source src="{{ media_url }}" type="{{ media_type }}">
|
||||||
{% if subtitles_url %}
|
{% if subtitles_url %}
|
||||||
<track label="Subtitles" kind="subtitles" src="{{ subtitles_url }}" default>
|
<track label="Subtitles" kind="subtitles" src="{{ subtitles_url }}" default>
|
||||||
|
|
|
@ -208,11 +208,11 @@ class Request(Message):
|
||||||
|
|
||||||
if response and response.is_error():
|
if response and response.is_error():
|
||||||
logger.warning(('Response processed with errors from ' +
|
logger.warning(('Response processed with errors from ' +
|
||||||
'action {}.{}: {}').format(
|
'action {}: {}').format(
|
||||||
plugin, self.action, str(response)))
|
self.action, str(response)))
|
||||||
else:
|
else:
|
||||||
logger.info('Processed response from action {}.{}: {}'.
|
logger.info('Processed response from action {}: {}'.
|
||||||
format(plugin, self.action, str(response)))
|
format(self.action, str(response)))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Retry mechanism
|
# Retry mechanism
|
||||||
plugin.logger.exception(e)
|
plugin.logger.exception(e)
|
||||||
|
|
|
@ -217,6 +217,10 @@ class MediaPlugin(Plugin):
|
||||||
def set_subtitles(self, filename, *args, **kwargs):
|
def set_subtitles(self, filename, *args, **kwargs):
|
||||||
raise self._NOT_IMPLEMENTED_ERR
|
raise self._NOT_IMPLEMENTED_ERR
|
||||||
|
|
||||||
|
@action
|
||||||
|
def remove_subtitles(self, *args, **kwargs):
|
||||||
|
raise self._NOT_IMPLEMENTED_ERR
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def is_playing(self, *args, **kwargs):
|
def is_playing(self, *args, **kwargs):
|
||||||
raise self._NOT_IMPLEMENTED_ERR
|
raise self._NOT_IMPLEMENTED_ERR
|
||||||
|
|
|
@ -353,6 +353,14 @@ class MediaMplayerPlugin(MediaPlugin):
|
||||||
self._exec('sub_visibility', 1)
|
self._exec('sub_visibility', 1)
|
||||||
return self._exec('sub_load', filename)
|
return self._exec('sub_load', filename)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def remove_subtitles(self, index=None):
|
||||||
|
""" Removes the subtitle specified by the index (default: all) """
|
||||||
|
if index is None:
|
||||||
|
return self._exec('sub_remove')
|
||||||
|
else:
|
||||||
|
return self._exec('sub_remove', index)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def is_playing(self):
|
def is_playing(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -2,6 +2,7 @@ import gzip
|
||||||
import os
|
import os
|
||||||
import requests
|
import requests
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import threading
|
||||||
|
|
||||||
from platypush.message.response import Response
|
from platypush.message.response import Response
|
||||||
from platypush.plugins import Plugin, action
|
from platypush.plugins import Plugin, action
|
||||||
|
@ -15,6 +16,7 @@ class MediaSubtitlesPlugin(Plugin):
|
||||||
Requires:
|
Requires:
|
||||||
|
|
||||||
* **python-opensubtitles** (``pip install -e 'git+https://github.com/agonzalezro/python-opensubtitles#egg=python-opensubtitles'``)
|
* **python-opensubtitles** (``pip install -e 'git+https://github.com/agonzalezro/python-opensubtitles#egg=python-opensubtitles'``)
|
||||||
|
* **webvtt** (``pip install webvtt-py``), optional, to convert srt subtitles into vtt format ready for web streaming
|
||||||
* **requests** (``pip install requests``)
|
* **requests** (``pip install requests``)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -39,6 +41,7 @@ class MediaSubtitlesPlugin(Plugin):
|
||||||
self._ost = OpenSubtitles()
|
self._ost = OpenSubtitles()
|
||||||
self._token = self._ost.login(username, password)
|
self._token = self._ost.login(username, password)
|
||||||
self.languages = []
|
self.languages = []
|
||||||
|
self._file_lock = threading.RLock()
|
||||||
|
|
||||||
if language:
|
if language:
|
||||||
if isinstance(language, str):
|
if isinstance(language, str):
|
||||||
|
@ -116,7 +119,7 @@ class MediaSubtitlesPlugin(Plugin):
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def download(self, link, media_resource=None, path=None):
|
def download(self, link, media_resource=None, path=None, convert_to_vtt=False):
|
||||||
"""
|
"""
|
||||||
Downloads a subtitle link (.srt/.vtt file or gzip/zip OpenSubtitles
|
Downloads a subtitle link (.srt/.vtt file or gzip/zip OpenSubtitles
|
||||||
archive link) to the specified directory
|
archive link) to the specified directory
|
||||||
|
@ -132,6 +135,10 @@ class MediaSubtitlesPlugin(Plugin):
|
||||||
media local file then the subtitles will be saved in the same folder
|
media local file then the subtitles will be saved in the same folder
|
||||||
:type media_resource: str
|
:type media_resource: str
|
||||||
|
|
||||||
|
:param convert_to_vtt: If set to True, then the downloaded subtitles
|
||||||
|
will be converted to VTT format (default: no conversion)
|
||||||
|
:type convert_to_vtt: bool
|
||||||
|
|
||||||
:returns: dict. Format::
|
:returns: dict. Format::
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -142,6 +149,8 @@ class MediaSubtitlesPlugin(Plugin):
|
||||||
if link.startswith('file://'):
|
if link.startswith('file://'):
|
||||||
link = link[len('file://'):]
|
link = link[len('file://'):]
|
||||||
if os.path.isfile(link):
|
if os.path.isfile(link):
|
||||||
|
if convert_to_vtt:
|
||||||
|
link = self.to_vtt(link).output
|
||||||
return { 'filename': link }
|
return { 'filename': link }
|
||||||
|
|
||||||
gzip_content = requests.get(link).content
|
gzip_content = requests.get(link).content
|
||||||
|
@ -166,11 +175,33 @@ class MediaSubtitlesPlugin(Plugin):
|
||||||
try:
|
try:
|
||||||
with f:
|
with f:
|
||||||
f.write(gzip.decompress(gzip_content))
|
f.write(gzip.decompress(gzip_content))
|
||||||
|
if convert_to_vtt:
|
||||||
|
path = self.to_vtt(path).output
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
os.unlink(path)
|
os.unlink(path)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
return { 'filename': path }
|
return { 'filename': path }
|
||||||
|
|
||||||
|
@action
|
||||||
|
def to_vtt(self, filename):
|
||||||
|
"""
|
||||||
|
Get the VTT content given an SRT file. Will return the original content if
|
||||||
|
the file is already in VTT format.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if filename.lower().endswith('.vtt'):
|
||||||
|
return filename
|
||||||
|
|
||||||
|
import webvtt
|
||||||
|
|
||||||
|
with self._file_lock:
|
||||||
|
try:
|
||||||
|
webvtt.read(filename)
|
||||||
|
return filename
|
||||||
|
except Exception as e:
|
||||||
|
webvtt.from_srt(filename).save()
|
||||||
|
return '.'.join(filename.split('.')[:-1]) + '.vtt'
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -126,4 +126,5 @@ inputs
|
||||||
|
|
||||||
# Support for media subtitles
|
# Support for media subtitles
|
||||||
# git+https://github.com/agonzalezro/python-opensubtitles#egg=python-opensubtitles
|
# git+https://github.com/agonzalezro/python-opensubtitles#egg=python-opensubtitles
|
||||||
|
# webvtt-py
|
||||||
|
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -95,9 +95,10 @@ setup(
|
||||||
'Support for Plex plugin': ['plexapi'],
|
'Support for Plex plugin': ['plexapi'],
|
||||||
'Support for Chromecast plugin': ['pychromecast'],
|
'Support for Chromecast plugin': ['pychromecast'],
|
||||||
'Support for sound devices': ['sounddevice', 'soundfile', 'numpy'],
|
'Support for sound devices': ['sounddevice', 'soundfile', 'numpy'],
|
||||||
|
'Support for web media subtitles': ['webvtt-py']
|
||||||
# 'Support for Leap Motion backend': ['git+ssh://git@github.com:BlackLight/leap-sdk-python3.git'],
|
# 'Support for Leap Motion backend': ['git+ssh://git@github.com:BlackLight/leap-sdk-python3.git'],
|
||||||
# 'Support for Flic buttons': ['git+https://@github.com/50ButtonsEach/fliclib-linux-hci.git']
|
# 'Support for Flic buttons': ['git+https://@github.com/50ButtonsEach/fliclib-linux-hci.git']
|
||||||
# 'Support for media subtitles: ['git+https://github.com/agonzalezro/python-opensubtitles#egg=python-opensubtitles']
|
# 'Support for media subtitles': ['git+https://github.com/agonzalezro/python-opensubtitles#egg=python-opensubtitles']
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue