diff --git a/.gitmodules b/.gitmodules index 460cf2b5..f5dcbc0c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/media/controls.scss b/platypush/backend/http/static/css/source/webpanel/plugins/media/controls.scss index e8495f02..835bdf8c 100644 --- a/platypush/backend/http/static/css/source/webpanel/plugins/media/controls.scss +++ b/platypush/backend/http/static/css/source/webpanel/plugins/media/controls.scss @@ -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; + } } } diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/media/index.scss b/platypush/backend/http/static/css/source/webpanel/plugins/media/index.scss index 456e0743..58b64372 100644 --- a/platypush/backend/http/static/css/source/webpanel/plugins/media/index.scss +++ b/platypush/backend/http/static/css/source/webpanel/plugins/media/index.scss @@ -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; diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/media/results.scss b/platypush/backend/http/static/css/source/webpanel/plugins/media/results.scss index 9822b5fd..37c158ad 100644 --- a/platypush/backend/http/static/css/source/webpanel/plugins/media/results.scss +++ b/platypush/backend/http/static/css/source/webpanel/plugins/media/results.scss @@ -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; } } } } diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/media/subs.scss b/platypush/backend/http/static/css/source/webpanel/plugins/media/subs.scss new file mode 100644 index 00000000..cea6c893 --- /dev/null +++ b/platypush/backend/http/static/css/source/webpanel/plugins/media/subs.scss @@ -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; + } + } + } + } + } +} + diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/media/vars.scss b/platypush/backend/http/static/css/source/webpanel/plugins/media/vars.scss index ca594232..b585011b 100644 --- a/platypush/backend/http/static/css/source/webpanel/plugins/media/vars.scss +++ b/platypush/backend/http/static/css/source/webpanel/plugins/media/vars.scss @@ -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; + diff --git a/platypush/backend/http/static/flag-icons b/platypush/backend/http/static/flag-icons index c8031a67..c0465f4f 160000 --- a/platypush/backend/http/static/flag-icons +++ b/platypush/backend/http/static/flag-icons @@ -1 +1 @@ -Subproject commit c8031a673e8428b842199c0dcf66d12b6ed1d1d6 +Subproject commit c0465f4f9ddfee44ad601e906ab541ac0ca4c57d diff --git a/platypush/backend/http/static/js/plugins/media/handlers/file.js b/platypush/backend/http/static/js/plugins/media/handlers/file.js index e2a9defd..47262b8d 100644 --- a/platypush/backend/http/static/js/plugins/media/handlers/file.js +++ b/platypush/backend/http/static/js/plugins/media/handlers/file.js @@ -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); }, }); diff --git a/platypush/backend/http/static/js/plugins/media/index.js b/platypush/backend/http/static/js/plugins/media/index.js index 8adff6ba..6005820e 100644 --- a/platypush/backend/http/static/js/plugins/media/index.js +++ b/platypush/backend/http/static/js/plugins/media/index.js @@ -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); }, diff --git a/platypush/backend/http/static/js/plugins/media/players/browser.js b/platypush/backend/http/static/js/plugins/media/players/browser.js index dbe871da..31061c04 100644 --- a/platypush/backend/http/static/js/plugins/media/players/browser.js +++ b/platypush/backend/http/static/js/plugins/media/players/browser.js @@ -15,6 +15,13 @@ MediaPlayers.browser = Vue.extend({ }, }, + subFormats: { + type: Array, + default: () => { + return ['vtt']; + }, + }, + name: { type: String, default: 'Browser', diff --git a/platypush/backend/http/static/js/plugins/media/players/chromecast.js b/platypush/backend/http/static/js/plugins/media/players/chromecast.js index 2e7d24d5..5834cd33 100644 --- a/platypush/backend/http/static/js/plugins/media/players/chromecast.js +++ b/platypush/backend/http/static/js/plugins/media/players/chromecast.js @@ -15,6 +15,13 @@ MediaPlayers.chromecast = Vue.extend({ }, }, + subFormats: { + type: Array, + default: () => { + return ['vtt']; + }, + }, + device: { type: null, address: null, diff --git a/platypush/backend/http/static/js/plugins/media/players/local.js b/platypush/backend/http/static/js/plugins/media/players/local.js index 0e232524..8d972184 100644 --- a/platypush/backend/http/static/js/plugins/media/players/local.js +++ b/platypush/backend/http/static/js/plugins/media/players/local.js @@ -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} ); }, diff --git a/platypush/backend/http/static/js/plugins/media/subs.js b/platypush/backend/http/static/js/plugins/media/subs.js new file mode 100644 index 00000000..68c1e075 --- /dev/null +++ b/platypush/backend/http/static/js/plugins/media/subs.js @@ -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); + }, + }, +}); + diff --git a/platypush/backend/http/templates/plugins/media/controls.html b/platypush/backend/http/templates/plugins/media/controls.html index 1e5f76c7..268afb46 100644 --- a/platypush/backend/http/templates/plugins/media/controls.html +++ b/platypush/backend/http/templates/plugins/media/controls.html @@ -4,7 +4,9 @@
- +
diff --git a/platypush/backend/http/templates/plugins/media/index.html b/platypush/backend/http/templates/plugins/media/index.html index 8c32ec14..d5e51103 100644 --- a/platypush/backend/http/templates/plugins/media/index.html +++ b/platypush/backend/http/templates/plugins/media/index.html @@ -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) %} {% endfor %} + + diff --git a/platypush/backend/http/templates/plugins/media/subs.html b/platypush/backend/http/templates/plugins/media/subs.html new file mode 100644 index 00000000..c9e75f89 --- /dev/null +++ b/platypush/backend/http/templates/plugins/media/subs.html @@ -0,0 +1,34 @@ + + + + diff --git a/platypush/plugins/media/subtitles.py b/platypush/plugins/media/subtitles.py index c2ea4dc1..11906ad6 100644 --- a/platypush/plugins/media/subtitles.py +++ b/platypush/plugins/media/subtitles.py @@ -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' diff --git a/platypush/plugins/media/vlc.py b/platypush/plugins/media/vlc.py index 0372ba9c..eabcf1ad 100644 --- a/platypush/plugins/media/vlc.py +++ b/platypush/plugins/media/vlc.py @@ -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]