Support for subtitles in new media webplugin - WIP

This commit is contained in:
Fabio Manganiello 2019-06-25 00:48:00 +02:00
parent 17de2a194c
commit cf23e2fc72
18 changed files with 234 additions and 25 deletions

2
.gitmodules vendored
View file

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

View file

@ -14,6 +14,14 @@
.item-info { .item-info {
font-size: 1.15em; font-size: 1.15em;
letter-spacing: .02em; letter-spacing: .02em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
&:hover {
color: $default-hover-fg;
}
} }
} }

View file

@ -9,6 +9,7 @@
@import 'webpanel/plugins/media/results'; @import 'webpanel/plugins/media/results';
@import 'webpanel/plugins/media/controls'; @import 'webpanel/plugins/media/controls';
@import 'webpanel/plugins/media/info'; @import 'webpanel/plugins/media/info';
@import 'webpanel/plugins/media/subs';
.media-plugin { .media-plugin {
display: flex; display: flex;

View file

@ -34,8 +34,8 @@
&:nth-child(odd) { background: rgba(255, 255, 255, 0.0); } &:nth-child(odd) { background: rgba(255, 255, 255, 0.0); }
&:nth-child(even) { background: $default-bg-3; } &:nth-child(even) { background: $default-bg-3; }
&:hover { background: $hover-bg !important; } &:hover { background: $hover-bg; }
&.selected { background: $selected-bg !important; } &.selected { background: $selected-bg; }
} }
} }
} }

View file

@ -0,0 +1,60 @@
.media-plugin {
#media-subs {
.body {
padding: 0;
}
}
.subs-container {
.loading, .no-results {
display: flex;
align-items: center;
padding: 3rem;
font-size: 1.5em;
}
.subs {
.list {
max-height: 50vh;
overflow: auto;
}
.sub {
display: flex;
align-items: center;
padding: .75em .5em;
border-bottom: $default-border-2;
cursor: pointer;
&:nth-child(odd) { background: rgba(255, 255, 255, 0.0); }
&:nth-child(even) { background: $default-bg-3; }
&.selected { background: $selected-bg; }
&:hover {
background: $hover-bg;
border-radius: 1em;
}
}
.controls {
position: relative;
padding: 1.5rem 0;
height: $subs-control-height;
button {
position: absolute;
right: 0;
margin-right: 1rem;
border: $default-border-2;
box-shadow: $btn-default-shadow;
&:hover:not([disabled]) {
box-shadow: $btn-hover-default-shadow;
color: $default-hover-fg;
}
}
}
}
}
}

View file

@ -13,3 +13,7 @@ $result-item-icon: #444;
$devices-dropdown-z-index: 2; $devices-dropdown-z-index: 2;
$devices-dropdown-refresh-fg: #666; $devices-dropdown-refresh-fg: #666;
$subs-control-height: 4rem;
$btn-default-shadow: 2px 2px 2px #ddd;
$btn-hover-default-shadow: 3px 3px 3px #ddd;

@ -1 +1 @@
Subproject commit c8031a673e8428b842199c0dcf66d12b6ed1d1d6 Subproject commit c0465f4f9ddfee44ad601e906ab541ac0ca4c57d

View file

@ -19,7 +19,7 @@ MediaHandlers.file = Vue.extend({
{ {
text: 'Play with subtitles', text: 'Play with subtitles',
iconClass: 'fas fa-closed-captioning', iconClass: 'fas fa-closed-captioning',
action: this.searchSubtiles, action: this.searchSubtitles,
}, },
{ {
@ -84,8 +84,23 @@ MediaHandlers.file = Vue.extend({
this.bus.$emit('info', (await this.getMetadata(item))); this.bus.$emit('info', (await this.getMetadata(item)));
}, },
searchSubtitles: function(item) { infoLoad: function(url) {
if (!this.matchesUrl(url))
return;
this.info(url);
}, },
searchSubtitles: function(item) {
this.bus.$emit('search-subs', item);
},
},
created: function() {
const self = this;
setTimeout(() => {
self.infoLoadWatch = self.bus.$on('info-load', this.infoLoad);
}, 1000);
}, },
}); });

View file

@ -59,6 +59,10 @@ Vue.component('media', {
loading: false, loading: false,
item: {}, item: {},
}, },
subsModal: {
visible: false,
},
}; };
}, },
@ -104,8 +108,9 @@ Vue.component('media', {
item = await this.startStreaming(item.url); item = await this.startStreaming(item.url);
} }
let status = await this.selectedDevice.play(item.url); let status = await this.selectedDevice.play(item.url, item.subtitles);
this.subsModal.visible = false;
this.onStatusUpdate({ this.onStatusUpdate({
device: this.selectedDevice, device: this.selectedDevice,
status: status, status: status,
@ -172,6 +177,14 @@ Vue.component('media', {
return ret; return ret;
}, },
searchSubs: function(item) {
if (typeof item === 'string')
item = {url: item};
this.subsModal.visible = true;
this.$refs.subs.search(item);
},
selectDevice: async function(device) { selectDevice: async function(device) {
this.selectedDevice = device; this.selectedDevice = device;
let status = await this.selectedDevice.status(); let status = await this.selectedDevice.status();
@ -254,6 +267,7 @@ Vue.component('media', {
this.bus.$on('results-ready', this.onResultsReady); this.bus.$on('results-ready', this.onResultsReady);
this.bus.$on('status-update', this.onStatusUpdate); this.bus.$on('status-update', this.onStatusUpdate);
this.bus.$on('start-streaming', this.startStreaming); this.bus.$on('start-streaming', this.startStreaming);
this.bus.$on('search-subs', this.searchSubs);
setInterval(this.timerFunc, 1000); setInterval(this.timerFunc, 1000);
}, },

View file

@ -15,6 +15,13 @@ MediaPlayers.browser = Vue.extend({
}, },
}, },
subFormats: {
type: Array,
default: () => {
return ['vtt'];
},
},
name: { name: {
type: String, type: String,
default: 'Browser', default: 'Browser',

View file

@ -15,6 +15,13 @@ MediaPlayers.chromecast = Vue.extend({
}, },
}, },
subFormats: {
type: Array,
default: () => {
return ['vtt'];
},
},
device: { device: {
type: null, type: null,
address: null, address: null,

View file

@ -16,6 +16,13 @@ MediaPlayers.local = Vue.extend({
}, },
}, },
subFormats: {
type: Array,
default: () => {
return ['srt'];
},
},
device: { device: {
type: Object, type: Object,
default: () => { default: () => {
@ -46,10 +53,10 @@ MediaPlayers.local = Vue.extend({
return await request(this.pluginPrefix.concat('.status')); return await request(this.pluginPrefix.concat('.status'));
}, },
play: async function(resource) { play: async function(resource, subtitles=undefined) {
return await request( return await request(
this.pluginPrefix.concat('.play'), this.pluginPrefix.concat('.play'),
{resource: resource} {resource: resource, subtitles: subtitles}
); );
}, },

View file

@ -0,0 +1,45 @@
Vue.component('media-subs', {
template: '#tmpl-media-subs',
props: {
bus: { type: Object },
subFormats: {
type: Array,
default: () => [],
},
},
data: function() {
return {
loading: false,
media: {},
items: [],
selectedItem: undefined,
};
},
methods: {
search: async function(media) {
this.loading = true;
this.media = media;
this.selectedItem = undefined;
this.items = await request('media.subtitles.get_subtitles', {resource: this.media.url});
this.loading = false;
},
play: async function() {
let args = {link: this.selectedItem.SubDownloadLink};
if (this.media.url && this.media.url.startsWith('file://'))
args.media_resource = this.media.url;
if (this.subFormats.indexOf('srt') < 0)
args.convert_to_vtt = true;
this.media.subtitles = (await request('media.subtitles.download', args)).filename;
this.bus.$emit('play', this.media);
},
},
});

View file

@ -4,7 +4,9 @@
<div class="controls"> <div class="controls">
<div class="col-3 item-container"> <div class="col-3 item-container">
<div class="item-info"> <div class="item-info">
<span v-text="status.title" v-if="status.title"></span> <span v-text="status.title"
@click="bus.$emit('info-load', status.url)"
v-if="status.title"></span>
</div> </div>
</div> </div>

View file

@ -3,12 +3,14 @@
{% include 'plugins/media/results.html' %} {% include 'plugins/media/results.html' %}
{% include 'plugins/media/item.html' %} {% include 'plugins/media/item.html' %}
{% include 'plugins/media/info.html' %} {% include 'plugins/media/info.html' %}
{% include 'plugins/media/subs.html' %}
{% for script in utils.search_directory(static_folder + '/js/plugins/media/handlers', 'js', recursive=True) %} {% for script in utils.search_directory(static_folder + '/js/plugins/media/handlers', 'js', recursive=True) %}
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/handlers/' + script) }}"></script> <script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/handlers/' + script) }}"></script>
{% endfor %} {% endfor %}
<link rel="stylesheet" href="{{ url_for('static', filename='flag-icons/css/flag-icon.css') }}">
<script type="text/x-template" id="tmpl-media"> <script type="text/x-template" id="tmpl-media">
<div class="plugin media-plugin"> <div class="plugin media-plugin">
<div class="search"> <div class="search">
@ -38,10 +40,17 @@
v-if="selectedDevice && status[selectedDevice.type] && status[selectedDevice.type][selectedDevice.name] && (status[selectedDevice.type][selectedDevice.name].state === 'play' || status[selectedDevice.type][selectedDevice.name].state === 'pause')"> v-if="selectedDevice && status[selectedDevice.type] && status[selectedDevice.type][selectedDevice.name] && (status[selectedDevice.type][selectedDevice.name].state === 'play' || status[selectedDevice.type][selectedDevice.name].state === 'pause')">
</media-controls> </media-controls>
<modal id="media-info" title="Media info" v-model="infoModal.visible" ref="modalInfo"> <modal id="media-info" title="Media info" v-model="infoModal.visible">
<div class="loading" v-if="infoModal.loading">Loading</div> <div class="loading" v-if="infoModal.loading">Loading</div>
<media-info :bus="bus" :item="infoModal.item" v-else></media-info> <media-info :bus="bus" :item="infoModal.item" v-else></media-info>
</modal> </modal>
<modal id="media-subs" title="Subtitles" v-model="subsModal.visible">
<media-subs :bus="bus"
:subFormats="selectedDevice ? selectedDevice.subFormats : []"
ref="subs">
</media-subs>
</modal>
</div> </div>
</script> </script>

View file

@ -0,0 +1,34 @@
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/subs.js') }}"></script>
<script type="text/x-template" id="tmpl-media-subs">
<div class="subs-container">
<div class="loading" v-if="loading">Loading</div>
<div class="no-results" v-else-if="!Object.keys(items).length">No results</div>
<div class="subs" v-else>
<div class="list">
<div class="sub"
:class="{selected: selectedItem && selectedItem.SubDownloadLink === item.SubDownloadLink}"
@click="selectedItem = item"
v-for="item in items"
:key="item.SubDownloadLink">
<div class="col-1 icon">
<i class="fa fa-hdd" v-if="item.IsLocal"></i>
<i :class="'flag-icon flag-icon-' + item.ISO639" v-else></i>
</div>
<div class="col-11 title" v-text="item.SubFileName"></div>
</div>
</div>
<div class="controls">
<button type="button"
class="btn-default"
@click="play"
:disabled="!selectedItem">
Select and play
</button>
</div>
</div>
</div>
</script>

View file

@ -4,9 +4,8 @@ import requests
import tempfile import tempfile
import threading import threading
from platypush.message.response import Response
from platypush.plugins import Plugin, action from platypush.plugins import Plugin, action
from platypush.utils import find_files_by_ext, get_mime_type from platypush.utils import find_files_by_ext
class MediaSubtitlesPlugin(Plugin): class MediaSubtitlesPlugin(Plugin):
@ -20,7 +19,7 @@ class MediaSubtitlesPlugin(Plugin):
* **requests** (``pip install requests``) * **requests** (``pip install requests``)
""" """
def __init__(self, username, password, language=None, *args, **kwargs): def __init__(self, username, password, language=None, **kwargs):
""" """
:param username: Your OpenSubtitles username :param username: Your OpenSubtitles username
:type username: str :type username: str
@ -36,7 +35,7 @@ class MediaSubtitlesPlugin(Plugin):
from pythonopensubtitles.opensubtitles import OpenSubtitles from pythonopensubtitles.opensubtitles import OpenSubtitles
super().__init__(*args, **kwargs) super().__init__(**kwargs)
self._ost = OpenSubtitles() self._ost = OpenSubtitles()
self._token = self._ost.login(username, password) self._token = self._ost.login(username, password)
@ -52,7 +51,6 @@ class MediaSubtitlesPlugin(Plugin):
raise AttributeError('{} is neither a string nor a list'.format( raise AttributeError('{} is neither a string nor a list'.format(
language)) language))
@action @action
def get_subtitles(self, resource, language=None): def get_subtitles(self, resource, language=None):
""" """
@ -73,7 +71,7 @@ class MediaSubtitlesPlugin(Plugin):
resource = os.path.abspath(os.path.expanduser(resource)) resource = os.path.abspath(os.path.expanduser(resource))
if not os.path.isfile(resource): if not os.path.isfile(resource):
return (None, '{} is not a valid file'.format(resource)) return None, '{} is not a valid file'.format(resource)
file = resource file = resource
cwd = os.getcwd() cwd = os.getcwd()
@ -117,7 +115,6 @@ class MediaSubtitlesPlugin(Plugin):
finally: finally:
os.chdir(cwd) os.chdir(cwd)
@action @action
def download(self, link, media_resource=None, path=None, convert_to_vtt=False): def download(self, link, media_resource=None, path=None, convert_to_vtt=False):
""" """
@ -154,7 +151,6 @@ class MediaSubtitlesPlugin(Plugin):
return {'filename': link} return {'filename': link}
gzip_content = requests.get(link).content gzip_content = requests.get(link).content
f = None
if not path and media_resource: if not path and media_resource:
if media_resource.startswith('file://'): if media_resource.startswith('file://'):
@ -199,7 +195,7 @@ class MediaSubtitlesPlugin(Plugin):
try: try:
webvtt.read(filename) webvtt.read(filename)
return filename return filename
except Exception as e: except Exception:
webvtt.from_srt(filename).save() webvtt.from_srt(filename).save()
return '.'.join(filename.split('.')[:-1]) + '.vtt' return '.'.join(filename.split('.')[:-1]) + '.vtt'

View file

@ -384,7 +384,7 @@ class MediaVlcPlugin(MediaPlugin):
else: else:
status['state'] = PlayerState.STOP.value status['state'] = PlayerState.STOP.value
status['url'] = self._player.get_media().get_mrl() if self._player.get_media() else None status['url'] = urllib.parse.unquote(self._player.get_media().get_mrl()) if self._player.get_media() else None
status['position'] = float(self._player.get_time()/1000) if self._player.get_time() is not None else None status['position'] = float(self._player.get_time()/1000) if self._player.get_time() is not None else None
media = self._player.get_media() media = self._player.get_media()
@ -393,7 +393,7 @@ class MediaVlcPlugin(MediaPlugin):
status['seekable'] = status['duration'] is not None status['seekable'] = status['duration'] is not None
status['fullscreen'] = self._player.get_fullscreen() status['fullscreen'] = self._player.get_fullscreen()
status['mute'] = self._player.audio_get_mute() status['mute'] = self._player.audio_get_mute()
status['path'] = urllib.parse.unquote(status['url']) status['path'] = status['url']
status['pause'] = status['state'] == PlayerState.PAUSE.value status['pause'] = status['state'] == PlayerState.PAUSE.value
status['percent_pos'] = self._player.get_position()*100 status['percent_pos'] = self._player.get_position()*100
status['filename'] = urllib.parse.unquote(status['url']).split('/')[-1] status['filename'] = urllib.parse.unquote(status['url']).split('/')[-1]