Subtitles support

- Added support for local and OpenSubtitles media subs
- Added management of media events in web panel
This commit is contained in:
Fabio Manganiello 2019-02-11 00:55:20 +01:00
parent 630850ee9a
commit 34f0264d5e
16 changed files with 713 additions and 96 deletions

3
.gitmodules vendored
View file

@ -4,3 +4,6 @@
[submodule "docs/wiki"]
path = docs/wiki
url = https://github.com/BlackLight/platypush.wiki.git
[submodule "platypush/backend/http/static/flag-icons"]
path = platypush/backend/http/static/flag-icons
url = git@github.com:lipis/flag-icon-css.git

View file

@ -377,8 +377,7 @@ class HttpBackend(Backend):
with self._media_map_lock:
if media_id in media_map:
raise FileExistsError('"{}" is already registered on "{}"'.
format(source, media_map[media_id].url))
return media_map[media_id]
media_hndl = MediaHandler.build(source, url=media_url)
media_map[media_id] = media_hndl
@ -480,8 +479,6 @@ class HttpBackend(Backend):
try:
media_hndl = register_media(source)
return jsonify(dict(media_hndl))
except FileExistsError as e:
abort(409, str(e))
except FileNotFoundError as e:
abort(404, str(e))
except AttributeError as e:

View file

@ -57,6 +57,19 @@ header {
padding: 2.5rem 2rem 1.5rem 2rem;
}
.form-footer {
text-align: right;
margin-top: 2rem;
border-top: 1px solid #ddd;
}
.form-footer * > input[type=button],
.form-footer * > button {
margin-top: 2rem;
text-transform: uppercase;
font-size: 1.3rem;
}
#date-time {
text-align: right;
padding-right: 30px;

View file

@ -10,6 +10,9 @@
form#video-ctrl {
text-align: center;
}
form#video-ctrl * > button[data-modal="#media-subtitles-modal"] {
display: none;
}
#video-seeker-container {
margin-top: 0.5em;
@ -139,3 +142,39 @@ form#video-ctrl {
color: #666;
}
#media-subtitles-modal * > .media-subtitles-results-container {
display: none;
padding: .75rem;
}
#media-subtitles-modal * > .media-subtitles-results-header {
background: #eee;
margin-bottom: 1rem;
padding: 1rem .25rem;
border: 1px solid #ccc;
}
#media-subtitles-modal * > .media-subtitles-results {
padding: .75rem;
}
#media-subtitles-modal * > .media-subtitles-message {
display: none;
}
#media-subtitles-modal * > .media-subtitle-container {
cursor: pointer;
}
#media-subtitles-modal * > .media-subtitle-container:nth-child(odd) {
background-color: #f2f2f2;
}
#media-subtitles-modal * > .media-subtitle-container.selected {
background-color: #c8ffd0 !important;
}
#media-subtitles-modal * > .media-subtitle-container:hover {
background-color: #daf8e2 !important;
}

@ -0,0 +1 @@
Subproject commit c8031a673e8428b842199c0dcf66d12b6ed1d1d6

View file

@ -149,7 +149,7 @@ $(document).ready(function() {
var initModalOpenBindings = function() {
$('body').on('mouseup touchend', '[data-modal]', function(event) {
var $source = $(event.target);
var $source = $(this);
var $modal = $($source.data('modal'));
$modal.height($(document).height() + 2);
@ -167,13 +167,13 @@ $(document).ready(function() {
var initModalCloseBindings = function() {
$('body').on('mouseup touchend', '[data-dismiss-modal]', function(event) {
var $source = $(event.target);
var $source = $(this);
var $modal = $($source.data('dismiss-modal'));
$modal.fadeOut();
});
$('body').on('mouseup touchend', function(event) {
var $source = $(event.target);
var $source = $(this);
if (!$source.parents('.modal').length
&& !$source.data('modal')
&& !$source.data('dismiss-modal')) {
@ -186,7 +186,7 @@ $(document).ready(function() {
var initPanelOpenBindings = function() {
$('body').on('mouseup touchend', '[data-panel]', function(event) {
var $source = $(event.target);
var $source = $(this);
var $panel = $($source.data('panel'));
setTimeout(() => {
$panel.show();
@ -196,7 +196,7 @@ $(document).ready(function() {
var initPanelCloseBindings = function() {
$('body').on('mouseup touchend', function(event) {
var $source = $(event.target);
var $source = $(this);
if ($source.data('panel') || $source.parents('[data-panel]').length) {
var $panel = $source.data('panel') ? $($source.data('panel')) :
$($source.parents('[data-panel]').data('panel'));
@ -270,6 +270,21 @@ function execute(request, onSuccess, onError, onComplete) {
});
}
function run(request) {
request['type'] = 'request';
return new Promise((resolve, reject) => {
execute(request,
onSuccess = (response) => {
resolve(response);
},
onError = (xhr, status, error) => {
reject(xhr, status, error);
}
);
});
}
function createNotification(options) {
var $notificationContainer = $('#notification-container');
var $notification = $('<div></div>').addClass('notification');
@ -348,3 +363,10 @@ function createNotification(options) {
$notification.fadeIn();
}
function showError(errorMessage) {
createNotification({
'icon': 'exclamation',
'text': errorMessage,
});
}

View file

@ -12,10 +12,49 @@ $(document).ready(function() {
$searchBarContainer = $('#media-search-bar-container'),
$mediaBtnsContainer = $('#media-btns-container'),
$mediaItemPanel = $('#media-item-panel'),
$mediaSubtitlesModal = $('#media-subtitles-modal'),
$mediaSubtitlesResultsContainer = $mediaSubtitlesModal.find('.media-subtitles-results-container'),
$mediaSubtitlesResults = $mediaSubtitlesModal.find('.media-subtitles-results'),
$mediaSubtitlesMessage = $mediaSubtitlesModal.find('.media-subtitles-message'),
$mediaSearchSubtitles = $ctrlForm.find('[data-modal="#media-subtitles-modal"]'),
prevVolume = undefined,
selectedResource = undefined;
var updateVideoResults = function(videos) {
const onEvent = (event) => {
switch (event.args.type) {
case 'platypush.message.event.media.MediaPlayRequestEvent':
createNotification({
'icon': 'stream',
'html': 'Processing media' + ('resource' in event.args
? ' ' + event.args.resource : ''),
});
break;
case 'platypush.message.event.media.MediaPlayEvent':
createNotification({
'icon': 'play',
'html': 'Starting media playback' + ('resource' in event.args
? ' for ' + event.args.resource : ''),
});
break;
case 'platypush.message.event.media.MediaPauseEvent':
createNotification({
'icon': 'pause',
'html': 'Media playback paused',
});
break;
case 'platypush.message.event.media.MediaStopEvent':
createNotification({
'icon': 'stop',
'html': 'Media playback stopped',
});
break;
}
};
const updateVideoResults = function(videos) {
$videoResults.html('');
for (var video of videos) {
var $videoResult = $('<div></div>')
@ -30,7 +69,7 @@ $(document).ready(function() {
}
};
var getVideoIconByUrl = function(url) {
const getVideoIconByUrl = function(url) {
var $icon = $('<i class="fa"></i>');
if (url.startsWith('file://')) {
@ -48,7 +87,7 @@ $(document).ready(function() {
return $iconContainer;
};
var getSelectedDevice = function() {
const getSelectedDevice = function() {
var device = { isBrowser: false, isRemote: false, name: undefined };
var $remoteDevice = $devsPanel.find('.cast-device.selected')
.filter((i, dev) => !$(dev).data('local') && !$(dev).data('browser') && $(dev).data('name'));
@ -65,7 +104,7 @@ $(document).ready(function() {
return device;
};
var startStreamingTorrent = function(torrent) {
const startStreamingTorrent = function(torrent) {
return new Promise((resolve, reject) => {
execute(
{
@ -88,7 +127,7 @@ $(document).ready(function() {
});
};
var startStreaming = function(media) {
const startStreaming = function(media) {
if (media.startsWith('magnet:?')) {
return new Promise((resolve, reject) => {
startStreamingTorrent(media)
@ -105,17 +144,8 @@ $(document).ready(function() {
data: JSON.stringify({ source: media }),
complete: (xhr, textStatus) => {
var url;
if (xhr.status == 200) {
url = xhr.responseJSON.url;
} else if (xhr.status == 409) {
// Media mount point already registered
url = xhr.responseText.match(
/.*is already registered on ("|&quot;)(https?:\/\/[^\/]+\/media\/[0-9a-f]+\.[0-9a-z]+)("|&quot;).*/)[2]
}
if (url) {
var uri = url.match(/https?:\/\/[^\/]+(\/media\/.*)/)[1]
var uri = xhr.responseJSON.url.match(/https?:\/\/[^\/]+(\/media\/.*)/)[1]
resolve(uri);
} else {
reject(Error(xhr.responseText));
@ -125,7 +155,7 @@ $(document).ready(function() {
});
};
var stopStreaming = function(media_id) {
const stopStreaming = function(media_id) {
return new Promise((resolve, reject) => {
$.ajax({
type: 'DELETE',
@ -135,7 +165,174 @@ $(document).ready(function() {
});
};
var play = function(resource) {
const getSubtitles = function(resource) {
return new Promise((resolve, reject) => {
if (!window.config.media.subtitles) {
return; // media.subtitles plugin not configured
}
run({
action: 'media.subtitles.get_subtitles',
args: { 'resource': resource }
}).then((response) => {
resolve(response.response.output);
}).catch((error) => {
reject(error.message);
});
});
};
const downloadSubtitles = function(link, mediaResource) {
return new Promise((resolve, reject) => {
run({
action: 'media.subtitles.download',
args: {
'link': link,
'media_resource': mediaResource,
}
}).then((response) => {
resolve(response.response.output.filename);
}).catch((error) => {
reject(error.message);
});
});
};
const setSubtitles = (filename) => {
return new Promise((resolve, reject) => {
run({
action: 'media.set_subtitles',
args: { 'filename': filename }
}).then((response) => {
resolve(response.response.output);
}).catch((error) => {
reject(error.message);
});
});
};
const playOnChromecast = (resource, device, subtitles) => {
return new Promise((resolve, reject) => {
var requestArgs = {
action: 'media.chromecast.play',
args: {
'resource': resource,
'chromecast': device,
},
};
// TODO support for subtitles on Chromecast through internal streaming server
run(requestArgs).then((response) => {
resolve(response);
}).catch((error) => {
reject(error.message);
});
});
};
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);
}).catch((xhr, status, error) => {
reject(xhr.responseText);
});
});
};
const playOnServer = (resource, subtitles) => {
return new Promise((resolve, reject) => {
var requestArgs = {
action: 'media.play',
args: { 'resource': resource },
};
if (subtitles) {
requestArgs.args.subtitles = subtitles;
}
run(requestArgs).then((response) => {
resolve(resource);
}).catch((error) => {
reject(error.message);
});
});
};
const _play = (resource, subtitles) => {
return new Promise((resolve, reject) => {
var playHndl;
var selectedDevice = getSelectedDevice();
if (selectedDevice.isBrowser) {
playHndl = playInBrowser(resource, subtitles);
} else if (selectedDevice.isRemote) {
playHndl = playOnChromecast(resource, selectedDevice.name, subtitles);
} else {
playHndl = playOnServer(resource, subtitles);
}
playHndl.then((response) => {
resolve(resource);
}).catch((error) => {
showError('Playback error: ' + error.message);
reject(error.message);
});
});
};
const play = (resource) => {
return new Promise((resolve, reject) => {
var results = $videoResults.html();
var onVideoLoading = () => { $videoResults.text('Loading video...'); };
var onVideoReady = () => {
$videoResults.html(results);
resolve(resource);
};
$mediaSearchSubtitles.data('resource', resource);
onVideoLoading();
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);
}
});
} else {
_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();
});
});
});
};
const download = function(resource) {
return new Promise((resolve, reject) => {
var results = $videoResults.html();
var onVideoLoading = function() {
@ -146,57 +343,24 @@ $(document).ready(function() {
$videoResults.html(results);
};
var requestArgs = {
type: 'request',
action: 'media.play',
args: { resource: resource },
};
var selectedDevice = getSelectedDevice();
if (selectedDevice.isBrowser) {
onVideoLoading();
startStreaming(resource)
.then((url) => {
window.open(url, '_blank');
resolve(url);
})
.catch((xhr, status, error) => {
reject(xhr.responseText);
})
.finally(() => {
onVideoReady();
});
return;
}
if (selectedDevice.isRemote) {
requestArgs.action = 'media.chromecast.play';
requestArgs.args.chromecast = selectedDevice.name;
}
onVideoLoading();
execute(
requestArgs,
function(response) {
$videoResults.html(results);
resolve(resource);
},
function(xhr, status, error) {
onVideoReady();
startStreaming(resource)
.then((url) => {
url = url + '?download=1'
window.open(url, '_blank');
resolve(url);
})
.catch((xhr, status, error) => {
reject(xhr.responseText);
}
);
})
.finally(() => {
onVideoReady();
});
});
};
var download = function(resource) {
// TODO
};
var initBindings = function() {
const initBindings = function() {
window.registerEventListener(onEvent);
$searchForm.on('submit', function(event) {
var $input = $(this).find('input[name=video-search-text]');
var resource = $input.val();
@ -347,11 +511,12 @@ $(document).ready(function() {
});
$mediaItemPanel.on('click', '[data-action]', function() {
if ($(this).hasClass('disabled')) {
var $action = $(this);
if ($action.hasClass('disabled')) {
return;
}
var action = $(this).data('action');
var action = $action.data('action');
var resource = $mediaItemPanel.data('resource');
if (!resource) {
return;
@ -365,9 +530,65 @@ $(document).ready(function() {
$mediaItemPanel.find('[data-action]').removeClass('disabled');
});
});
$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 = $('<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();
}).catch((error) => {
$mediaSubtitlesMessage.text('Unable to load subtitles: ' + error.message);
});
});
};
var initRemoteDevices = function() {
const initRemoteDevices = function() {
$devsList.find('.cast-device[data-remote]').addClass('disabled');
execute(
@ -414,7 +635,7 @@ $(document).ready(function() {
);
};
var init = function() {
const init = function() {
initRemoteDevices();
initBindings();
};

View file

@ -1,5 +1,12 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/media.js') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/media.css') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='flag-icons/css/flag-icon.min.css') }}"></script>
<script type="text/javascript">
window.config = window.config || {};
window.config.media = window.config.media || {};
window.config.media.subtitles = JSON.parse('{{ utils.to_json(utils.get_config("media.subtitles")) | safe }}');
</script>
<div class="row" id="video-container">
<form action="#" id="video-search">
@ -86,6 +93,10 @@
<button data-action="next">
<i class="fa fa-step-forward"></i>
</button>
<button data-action="subtitles" data-modal="#media-subtitles-modal">
<i class="fa fa-comment"></i>
</button>
</div>
</div>
@ -114,6 +125,28 @@
</div>
</div>
<div id="media-subtitles-modal" class="modal">
<div class="modal-container">
<div class="modal-header">
Subtitles results
</div>
<div class="modal-body">
<div class="row media-subtitles-results-container">
<div class="row media-subtitles-results-header">
<div class="one column">Lang</div>
<div class="five columns">Movie</div>
<div class="six columns">Subtitles</div>
</div>
<div class="row media-subtitles-results">
</div>
</div>
<div class="row media-subtitles-message">No media selected or playing</div>
</div>
</div>
</div>
<div class="row" id="video-results-container">
<div id="video-results"></div>
</div>

View file

@ -8,6 +8,20 @@ class MediaEvent(Event):
super().__init__(*args, **kwargs)
class MediaPlayRequestEvent(MediaEvent):
"""
Event triggered when a new media playback request is received
"""
def __init__(self, resource=None, *args, **kwargs):
"""
:param resource: File name or URI of the played video
:type resource: str
"""
super().__init__(*args, resource=resource, **kwargs)
class MediaPlayEvent(MediaEvent):
"""
Event triggered when a new media content is played

View file

@ -213,6 +213,10 @@ class MediaPlugin(Plugin):
def toggle_subtitles(self, *args, **kwargs):
raise self._NOT_IMPLEMENTED_ERR
@action
def set_subtitles(self, filename, *args, **kwargs):
raise self._NOT_IMPLEMENTED_ERR
@action
def is_playing(self, *args, **kwargs):
raise self._NOT_IMPLEMENTED_ERR
@ -339,7 +343,7 @@ class MediaPlugin(Plugin):
@action
def start_streaming(self, media):
def start_streaming(self, media, download=False):
"""
Starts streaming local media over the specified HTTP port.
The stream will be available to HTTP clients on
@ -348,6 +352,10 @@ class MediaPlugin(Plugin):
:param media: Media to stream
:type media: str
:param download: Set to True if you prefer to download the file from
the streaming link instead of streaming it
:type download: bool
:returns: dict containing the streaming URL.Example::
{
@ -366,8 +374,9 @@ class MediaPlugin(Plugin):
return
self.logger.info('Starting streaming {}'.format(media))
response = requests.put('{url}/media'.format(url=http.local_base_url),
json = { 'source': media })
response = requests.put('{url}/media{download}'.format(
url=http.local_base_url, download='?download' if download else ''),
json = { 'source': media })
if not response.ok:
self.logger.warning('Unable to start streaming: {}'.

View file

@ -4,10 +4,12 @@ import pychromecast
from pychromecast.controllers.youtube import YouTubeController
from platypush.context import get_plugin
from platypush.context import get_plugin, get_bus
from platypush.plugins import Plugin, action
from platypush.plugins.media import MediaPlugin
from platypush.utils import get_mime_type
from platypush.message.event.media import MediaPlayEvent, MediaPlayRequestEvent, \
MediaStopEvent, MediaPauseEvent, NewPlayingMediaEvent
class MediaChromecastPlugin(MediaPlugin):
@ -157,6 +159,9 @@ class MediaChromecastPlugin(MediaPlugin):
if not chromecast:
chromecast = self.chromecast
get_bus().post(MediaPlayRequestEvent(resource=resource,
device=chromecast))
cast = self.get_chromecast(chromecast)
cast.wait()
@ -197,10 +202,13 @@ class MediaChromecastPlugin(MediaPlugin):
mc.play_media(resource, content_type, title=title, thumb=image_url,
current_time=current_time, autoplay=autoplay,
stream_type=stream_type, subtitles=subtitles,
subtitles_lang=subtitles_lang, subtitles_mime=subtitles_mime,
subtitle_id=subtitle_id)
subtitles_lang=subtitles_lang,
subtitles_mime=subtitles_mime, subtitle_id=subtitle_id)
mc.block_until_active()
get_bus().post(MediaPlayEvent(resource=resource,
device=chromecast))
@classmethod
@ -223,14 +231,20 @@ class MediaChromecastPlugin(MediaPlugin):
def pause(self, chromecast=None):
cast = self.get_chromecast(chromecast or self.chromecast)
if cast.media_controller.is_paused:
return cast.media_controller.play()
ret = cast.media_controller.play()
get_bus().post(MediaPlayEvent(device=chromecast or self.chromecast))
return ret
elif cast.media_controller.is_playing:
return cast.media_controller.pause()
ret = cast.media_controller.pause()
get_bus().post(MediaPauseEvent(device=chromecast or self.chromecast))
return ret
@action
def stop(self, chromecast=None):
return self.get_chromecast(chromecast or self.chromecast).media_controller.stop()
ret = self.get_chromecast(chromecast or self.chromecast).media_controller.stop()
get_bus().post(MediaStopEvent(device=chromecast or self.chromecast))
return ret
@action
@ -276,13 +290,38 @@ class MediaChromecastPlugin(MediaPlugin):
@action
def enable_subtitle(self, chromecast=None):
return self.get_chromecast(chromecast or self.chromecast).media_controller.enable_subtitle()
def list_subtitles(self, chromecast=None):
return self.get_chromecast(chromecast or self.chromecast) \
.media_controller.subtitle_tracks
@action
def disable_subtitle(self, chromecast=None):
return self.get_chromecast(chromecast or self.chromecast).media_controller.disable_subtitle()
def enable_subtitles(self, chromecast=None, track_id=None):
mc = self.get_chromecast(chromecast or self.chromecast).media_controller
if track_id is not None:
return mc.enable_subtitle(track_id)
elif mc.subtitle_tracks:
return mc.enable_subtitle(mc.subtitle_tracks[0].get('trackId'))
@action
def disable_subtitles(self, chromecast=None, track_id=None):
mc = self.get_chromecast(chromecast or self.chromecast).media_controller
if track_name:
return mc.disable_subtitle(track_name)
elif mc.current_subtitle_tracks:
return mc.disable_subtitle(mc.current_subtitle_tracks[0])
@action
def toggle_subtitles(self, chromecast=None):
mc = self.get_chromecast(chromecast or self.chromecast).media_controller
all_subs = mc.status.subtitle_tracks
cur_subs = mc.status.status.current_subtitle_tracks
if cur_subs:
return self.disable_subtitle(chromecast, cur_subs[0])
else:
return self.enable_subtitle(chromecast, all_subs[0].get('trackId'))
@action

View file

@ -1,14 +1,15 @@
import os
import select
import subprocess
import tempfile
import threading
import time
from platypush.context import get_bus, get_plugin
from platypush.message.response import Response
from platypush.plugins.media import PlayerState, MediaPlugin
from platypush.message.event.media import MediaPlayEvent, MediaPauseEvent, \
MediaStopEvent, NewPlayingMediaEvent
from platypush.message.event.media import MediaPlayEvent, MediaPlayRequestEvent, \
MediaPauseEvent, MediaStopEvent, NewPlayingMediaEvent
from platypush.plugins import action
from platypush.utils import find_bins_in_path
@ -62,6 +63,9 @@ class MediaMplayerPlugin(MediaPlugin):
from MPlayer before considering a response ready (default: 0.5 seconds)
:type mplayer_timeout: float
:param subtitles: Path to the subtitles file
:type subtitles: str
:param args: Default arguments that will be passed to the MPlayer
executable
:type args: list
@ -237,19 +241,46 @@ class MediaMplayerPlugin(MediaPlugin):
return _thread
def _get_subtitles_file(self, subtitles):
if not subtitles:
return
if subtitles.startswith('file://'):
subtitles = subtitles[len('file://'):]
if os.path.isfile(subtitles):
return os.path.abspath(subtitles)
else:
import requests
content = requests.get(subtitles).content
f = tempfile.NamedTemporaryFile(prefix='media_subs_',
suffix='.srt', delete=False)
with f:
f.write(content)
return f.name
@action
def play(self, resource, mplayer_args=None):
def play(self, resource, subtitles=None, mplayer_args=None):
"""
Play a resource.
:param resource: Resource to play - can be a local file or a remote URL
:type resource: str
:param subtitles: Path to optional subtitle file
:type subtitles: str
:param mplayer_args: Extra runtime arguments that will be passed to the
MPlayer executable
:type mplayer_args: list[str]
"""
get_bus().post(MediaPlayRequestEvent(resource=resource))
if subtitles:
mplayer_args = mplayer_args or []
mplayer_args += ['-sub', self._get_subtitles_file(subtitles)]
resource = self._get_resource(resource)
if resource.startswith('file://'):
resource = resource[7:]
@ -258,12 +289,16 @@ class MediaMplayerPlugin(MediaPlugin):
return get_plugin('media.webtorrent').play(resource)
self._is_playing_torrent = False
return self._exec('loadfile', resource, mplayer_args=mplayer_args)
ret = self._exec('loadfile', resource, mplayer_args=mplayer_args)
get_bus().post(MediaPlayEvent(resource=resource))
return ret
@action
def pause(self):
""" Toggle the paused state """
return self._exec('pause')
ret = self._exec('pause')
get_bus().post(MediaPauseEvent())
return ret
def _stop_torrent(self):
if self._is_playing_torrent:
@ -276,13 +311,15 @@ class MediaMplayerPlugin(MediaPlugin):
@action
def stop(self):
""" Stop the playback """
return self._exec('stop')
# return self._exec('stop')
return self.quit()
@action
def quit(self):
""" Quit the player """
self._stop_torrent()
self._exec('quit')
get_bus().post(MediaStopEvent())
@action
def voldown(self, step=10.0):
@ -310,6 +347,12 @@ class MediaMplayerPlugin(MediaPlugin):
subs = self.get_property('sub_visibility').output.get('sub_visibility')
return self._exec('sub_visibility', int(not subs))
@action
def set_subtitles(self, filename):
""" Sets media subtitles from filename """
self._exec('sub_visibility', 1)
return self._exec('sub_load', filename)
@action
def is_playing(self):
"""

View file

@ -46,7 +46,7 @@ class MediaOmxplayerPlugin(MediaPlugin):
self._handlers = { e.value: [] for e in PlayerEvent }
@action
def play(self, resource):
def play(self, resource, subtitles=None, *args, **kwargs):
"""
Play a resource.
@ -58,6 +58,9 @@ class MediaOmxplayerPlugin(MediaPlugin):
* Torrents (format: Magnet links, Torrent URLs or local Torrent files)
"""
if subtitles:
args.append('--subtitles', subtitles)
resource = self._get_resource(resource)
if self._player:
try:

View file

@ -0,0 +1,176 @@
import gzip
import os
import requests
import tempfile
from platypush.message.response import Response
from platypush.plugins import Plugin, action
from platypush.utils import find_files_by_ext, get_mime_type
class MediaSubtitlesPlugin(Plugin):
"""
Plugin to get video subtitles from OpenSubtitles
Requires:
* **python-opensubtitles** (``pip install -e 'git+https://github.com/agonzalezro/python-opensubtitles#egg=python-opensubtitles'``)
* **requests** (``pip install requests``)
"""
def __init__(self, username, password, language=None, *args, **kwargs):
"""
:param username: Your OpenSubtitles username
:type username: str
:param password: Your OpenSubtitles password
:type password: str
:param language: Preferred language name, ISO639 code or OpenSubtitles
language ID to be used for the subtitles. Also supports an (ordered)
list of preferred languages
:type language: str or list[str]
"""
from pythonopensubtitles.opensubtitles import OpenSubtitles
super().__init__(*args, **kwargs)
self._ost = OpenSubtitles()
self._token = self._ost.login(username, password)
self.languages = []
if language:
if isinstance(language, str):
self.languages.append(language.lower())
elif isinstance(language, list):
self.languages.extend([l.lower() for l in language])
else:
raise AttributeError('{} is neither a string nor a list'.format(
language))
@action
def get_subtitles(self, resource, language=None):
"""
Get the subtitles data for a video resource
:param resource: Media file, torrent or URL to the media resource
:type resource: str
:param language: Language name or code (default: configured preferred
language). Choose 'all' for all the languages
:type language: str
"""
from pythonopensubtitles.utils import File
if resource.startswith('file://'):
resource = resource[len('file://'):]
resource = os.path.abspath(os.path.expanduser(resource))
if not os.path.isfile(resource):
return (None, '{} is not a valid file'.format(resource))
file = resource
cwd = os.getcwd()
media_dir = os.path.dirname(resource)
os.chdir(media_dir)
file = file.split(os.sep)[-1]
local_subs = [{
'IsLocal': True,
'MovieName': '[Local subtitle]',
'SubFileName': sub.split(os.sep)[-1],
'SubDownloadLink': 'file://' + os.path.join(media_dir, sub),
} for sub in find_files_by_ext(media_dir, '.srt', '.vtt')]
self.logger.info('Found {} local subtitles for {}'.format(
len(local_subs), file))
languages = [language.lower()] if language else self.languages
try:
file_hash = File(file).get_hash()
subs = self._ost.search_subtitles([{
'sublanguageid': 'all',
'moviehash': file_hash,
}])
subs = [
sub for sub in subs if not languages or languages[0] == 'all' or
sub.get('LanguageName', '').lower() in languages or
sub.get('SubLanguageID', '').lower() in languages or
sub.get('ISO639', '').lower() in languages
]
for sub in subs:
sub['IsLocal'] = False
self.logger.info('Found {} OpenSubtitles items for {}'.format(
len(subs), file))
return local_subs + subs
finally:
os.chdir(cwd)
@action
def download(self, link, media_resource=None, path=None):
"""
Downloads a subtitle link (.srt/.vtt file or gzip/zip OpenSubtitles
archive link) to the specified directory
:param link: Local subtitles file or OpenSubtitles gzip download link
:type link: str
:param path: Path where the subtitle file will be downloaded (default:
temporary file under /tmp)
:type path: str
:param media_resource: Name of the media resource. If set and if it's a
media local file then the subtitles will be saved in the same folder
:type media_resource: str
:returns: dict. Format::
{
"filename": "/path/to/subtitle/file.srt"
}
"""
if link.startswith('file://'):
link = link[len('file://'):]
if os.path.isfile(link):
return { 'filename': link }
gzip_content = requests.get(link).content
f = None
if not path and media_resource:
if media_resource.startswith('file://'):
media_resource = media_resource[len('file://'):]
if os.path.isfile(media_resource):
media_resource = os.path.abspath(media_resource)
path = os.path.join(
os.path.dirname(media_resource),
'.'.join(os.path.basename(media_resource).split('.')[:-1])) + '.srt'
if path:
f = open(path, 'wb')
else:
f = tempfile.NamedTemporaryFile(prefix='media_subs_',
suffix='.srt', delete=False)
path = f.name
try:
with f:
f.write(gzip.decompress(gzip_content))
except Exception as e:
os.unlink(path)
raise e
return { 'filename': path }
# vim:sw=4:ts=4:et:

View file

@ -124,3 +124,6 @@ inputs
# soundfile
# numpy
# Support for media subtitles
# git+https://github.com/agonzalezro/python-opensubtitles#egg=python-opensubtitles

View file

@ -97,6 +97,7 @@ setup(
'Support for sound devices': ['sounddevice', 'soundfile', 'numpy'],
# '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 media subtitles: ['git+https://github.com/agonzalezro/python-opensubtitles#egg=python-opensubtitles']
},
)