forked from platypush/platypush
Support for subtitles in new media webplugin - WIP
This commit is contained in:
parent
17de2a194c
commit
cf23e2fc72
18 changed files with 234 additions and 25 deletions
2
.gitmodules
vendored
2
.gitmodules
vendored
|
@ -6,4 +6,4 @@
|
|||
url = https://github.com/BlackLight/platypush.wiki.git
|
||||
[submodule "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
|
||||
|
|
|
@ -14,6 +14,14 @@
|
|||
.item-info {
|
||||
font-size: 1.15em;
|
||||
letter-spacing: .02em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $default-hover-fg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
@import 'webpanel/plugins/media/results';
|
||||
@import 'webpanel/plugins/media/controls';
|
||||
@import 'webpanel/plugins/media/info';
|
||||
@import 'webpanel/plugins/media/subs';
|
||||
|
||||
.media-plugin {
|
||||
display: flex;
|
||||
|
|
|
@ -34,8 +34,8 @@
|
|||
|
||||
&:nth-child(odd) { background: rgba(255, 255, 255, 0.0); }
|
||||
&:nth-child(even) { background: $default-bg-3; }
|
||||
&:hover { background: $hover-bg !important; }
|
||||
&.selected { background: $selected-bg !important; }
|
||||
&:hover { background: $hover-bg; }
|
||||
&.selected { background: $selected-bg; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,3 +13,7 @@ $result-item-icon: #444;
|
|||
$devices-dropdown-z-index: 2;
|
||||
$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
|
|
@ -19,7 +19,7 @@ MediaHandlers.file = Vue.extend({
|
|||
{
|
||||
text: 'Play with subtitles',
|
||||
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)));
|
||||
},
|
||||
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -59,6 +59,10 @@ Vue.component('media', {
|
|||
loading: false,
|
||||
item: {},
|
||||
},
|
||||
|
||||
subsModal: {
|
||||
visible: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -104,8 +108,9 @@ Vue.component('media', {
|
|||
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({
|
||||
device: this.selectedDevice,
|
||||
status: status,
|
||||
|
@ -172,6 +177,14 @@ Vue.component('media', {
|
|||
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) {
|
||||
this.selectedDevice = device;
|
||||
let status = await this.selectedDevice.status();
|
||||
|
@ -254,6 +267,7 @@ Vue.component('media', {
|
|||
this.bus.$on('results-ready', this.onResultsReady);
|
||||
this.bus.$on('status-update', this.onStatusUpdate);
|
||||
this.bus.$on('start-streaming', this.startStreaming);
|
||||
this.bus.$on('search-subs', this.searchSubs);
|
||||
|
||||
setInterval(this.timerFunc, 1000);
|
||||
},
|
||||
|
|
|
@ -15,6 +15,13 @@ MediaPlayers.browser = Vue.extend({
|
|||
},
|
||||
},
|
||||
|
||||
subFormats: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return ['vtt'];
|
||||
},
|
||||
},
|
||||
|
||||
name: {
|
||||
type: String,
|
||||
default: 'Browser',
|
||||
|
|
|
@ -15,6 +15,13 @@ MediaPlayers.chromecast = Vue.extend({
|
|||
},
|
||||
},
|
||||
|
||||
subFormats: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return ['vtt'];
|
||||
},
|
||||
},
|
||||
|
||||
device: {
|
||||
type: null,
|
||||
address: null,
|
||||
|
|
|
@ -16,6 +16,13 @@ MediaPlayers.local = Vue.extend({
|
|||
},
|
||||
},
|
||||
|
||||
subFormats: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return ['srt'];
|
||||
},
|
||||
},
|
||||
|
||||
device: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
|
@ -46,10 +53,10 @@ MediaPlayers.local = Vue.extend({
|
|||
return await request(this.pluginPrefix.concat('.status'));
|
||||
},
|
||||
|
||||
play: async function(resource) {
|
||||
play: async function(resource, subtitles=undefined) {
|
||||
return await request(
|
||||
this.pluginPrefix.concat('.play'),
|
||||
{resource: resource}
|
||||
{resource: resource, subtitles: subtitles}
|
||||
);
|
||||
},
|
||||
|
||||
|
|
45
platypush/backend/http/static/js/plugins/media/subs.js
Normal file
45
platypush/backend/http/static/js/plugins/media/subs.js
Normal 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);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -4,7 +4,9 @@
|
|||
<div class="controls">
|
||||
<div class="col-3 item-container">
|
||||
<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>
|
||||
|
||||
|
|
|
@ -3,12 +3,14 @@
|
|||
{% include 'plugins/media/results.html' %}
|
||||
{% include 'plugins/media/item.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) %}
|
||||
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/handlers/' + script) }}"></script>
|
||||
{% endfor %}
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='flag-icons/css/flag-icon.css') }}">
|
||||
|
||||
<script type="text/x-template" id="tmpl-media">
|
||||
<div class="plugin media-plugin">
|
||||
<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')">
|
||||
</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>
|
||||
<media-info :bus="bus" :item="infoModal.item" v-else></media-info>
|
||||
</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>
|
||||
</script>
|
||||
|
||||
|
|
34
platypush/backend/http/templates/plugins/media/subs.html
Normal file
34
platypush/backend/http/templates/plugins/media/subs.html
Normal 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>
|
||||
|
|
@ -4,9 +4,8 @@ import requests
|
|||
import tempfile
|
||||
import threading
|
||||
|
||||
from platypush.message.response import Response
|
||||
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):
|
||||
|
@ -20,7 +19,7 @@ class MediaSubtitlesPlugin(Plugin):
|
|||
* **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
|
||||
:type username: str
|
||||
|
@ -36,7 +35,7 @@ class MediaSubtitlesPlugin(Plugin):
|
|||
|
||||
from pythonopensubtitles.opensubtitles import OpenSubtitles
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._ost = OpenSubtitles()
|
||||
self._token = self._ost.login(username, password)
|
||||
|
@ -52,7 +51,6 @@ class MediaSubtitlesPlugin(Plugin):
|
|||
raise AttributeError('{} is neither a string nor a list'.format(
|
||||
language))
|
||||
|
||||
|
||||
@action
|
||||
def get_subtitles(self, resource, language=None):
|
||||
"""
|
||||
|
@ -73,7 +71,7 @@ class MediaSubtitlesPlugin(Plugin):
|
|||
|
||||
resource = os.path.abspath(os.path.expanduser(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
|
||||
cwd = os.getcwd()
|
||||
|
@ -117,7 +115,6 @@ class MediaSubtitlesPlugin(Plugin):
|
|||
finally:
|
||||
os.chdir(cwd)
|
||||
|
||||
|
||||
@action
|
||||
def download(self, link, media_resource=None, path=None, convert_to_vtt=False):
|
||||
"""
|
||||
|
@ -151,10 +148,9 @@ class MediaSubtitlesPlugin(Plugin):
|
|||
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
|
||||
f = None
|
||||
|
||||
if not path and media_resource:
|
||||
if media_resource.startswith('file://'):
|
||||
|
@ -181,7 +177,7 @@ class MediaSubtitlesPlugin(Plugin):
|
|||
os.unlink(path)
|
||||
raise e
|
||||
|
||||
return { 'filename': path }
|
||||
return {'filename': path}
|
||||
|
||||
@action
|
||||
def to_vtt(self, filename):
|
||||
|
@ -199,7 +195,7 @@ class MediaSubtitlesPlugin(Plugin):
|
|||
try:
|
||||
webvtt.read(filename)
|
||||
return filename
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
webvtt.from_srt(filename).save()
|
||||
return '.'.join(filename.split('.')[:-1]) + '.vtt'
|
||||
|
||||
|
|
|
@ -384,7 +384,7 @@ class MediaVlcPlugin(MediaPlugin):
|
|||
else:
|
||||
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
|
||||
|
||||
media = self._player.get_media()
|
||||
|
@ -393,7 +393,7 @@ class MediaVlcPlugin(MediaPlugin):
|
|||
status['seekable'] = status['duration'] is not None
|
||||
status['fullscreen'] = self._player.get_fullscreen()
|
||||
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['percent_pos'] = self._player.get_position()*100
|
||||
status['filename'] = urllib.parse.unquote(status['url']).split('/')[-1]
|
||||
|
|
Loading…
Reference in a new issue