diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/media/info.scss b/platypush/backend/http/static/css/source/webpanel/plugins/media/info.scss
index 294e073e0..620d18418 100644
--- a/platypush/backend/http/static/css/source/webpanel/plugins/media/info.scss
+++ b/platypush/backend/http/static/css/source/webpanel/plugins/media/info.scss
@@ -1,4 +1,10 @@
.media-plugin {
+ #media-info {
+ .modal {
+ max-width: 90%;
+ }
+ }
+
.info-container {
.row {
display: flex;
diff --git a/platypush/backend/http/static/js/plugins/media/handlers/base.js b/platypush/backend/http/static/js/plugins/media/handlers/base.js
new file mode 100644
index 000000000..b6e505442
--- /dev/null
+++ b/platypush/backend/http/static/js/plugins/media/handlers/base.js
@@ -0,0 +1,64 @@
+MediaHandlers.base = Vue.extend({
+ props: {
+ bus: { type: Object },
+ iconClass: {
+ type: String,
+ },
+ },
+
+ computed: {
+ dropdownItems: function() {
+ return [
+ {
+ text: 'Play',
+ icon: 'play',
+ action: this.play,
+ },
+
+ {
+ text: 'View info',
+ icon: 'info',
+ action: this.info,
+ },
+ ];
+ },
+ },
+
+ methods: {
+ matchesUrl: function(url) {
+ return false;
+ },
+
+ getMetadata: async function(item, onlyBase=false) {
+ return {};
+ },
+
+ play: function(item) {
+ this.bus.$emit('play', item);
+ },
+
+ info: async function(item) {
+ this.bus.$emit('info-loading');
+ this.bus.$emit('info', {...item, ...(await this.getMetadata(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/handlers/file.js b/platypush/backend/http/static/js/plugins/media/handlers/file.js
index 47262b8d7..28aa18486 100644
--- a/platypush/backend/http/static/js/plugins/media/handlers/file.js
+++ b/platypush/backend/http/static/js/plugins/media/handlers/file.js
@@ -1,6 +1,5 @@
-MediaHandlers.file = Vue.extend({
+MediaHandlers.file = MediaHandlers.base.extend({
props: {
- bus: { type: Object },
iconClass: {
type: String,
default: 'fa fa-hdd',
@@ -64,10 +63,6 @@ MediaHandlers.file = Vue.extend({
return item;
},
- play: function(item) {
- this.bus.$emit('play', item);
- },
-
download: async function(item) {
this.bus.$on('streaming-started', (media) => {
if (media.resource === item.url) {
@@ -78,41 +73,35 @@ MediaHandlers.file = Vue.extend({
this.bus.$emit('start-streaming', item.url);
},
-
- info: async function(item) {
- this.bus.$emit('info-loading');
- this.bus.$emit('info', (await this.getMetadata(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);
},
});
MediaHandlers.generic = MediaHandlers.file.extend({
props: {
- bus: { type: Object },
iconClass: {
type: String,
default: 'fa fa-globe',
},
},
+ computed: {
+ dropdownItems: function() {
+ return [
+ {
+ text: 'Play',
+ icon: 'play',
+ action: this.play,
+ },
+
+ {
+ text: 'View info',
+ icon: 'info',
+ action: this.info,
+ },
+ ];
+ },
+ },
+
methods: {
getMetadata: async function(url) {
return {
@@ -120,10 +109,6 @@ MediaHandlers.generic = MediaHandlers.file.extend({
title: url,
};
},
-
- info: async function(item) {
- this.bus.$emit('info', (await this.getMetadata(item)));
- },
},
});
diff --git a/platypush/backend/http/static/js/plugins/media/handlers/youtube.js b/platypush/backend/http/static/js/plugins/media/handlers/youtube.js
index 08a880bba..6ea9e07c3 100644
--- a/platypush/backend/http/static/js/plugins/media/handlers/youtube.js
+++ b/platypush/backend/http/static/js/plugins/media/handlers/youtube.js
@@ -1,6 +1,5 @@
-MediaHandlers.youtube = Vue.extend({
+MediaHandlers.youtube = MediaHandlers.base.extend({
props: {
- bus: { type: Object },
iconClass: {
type: String,
default: 'fab fa-youtube',
@@ -19,7 +18,13 @@ MediaHandlers.youtube = Vue.extend({
{
text: 'Download (on server)',
icon: 'download',
- action: this.download,
+ action: this.downloadServer,
+ },
+
+ {
+ text: 'Download (on client)',
+ icon: 'download',
+ action: this.downloadClient,
},
{
@@ -37,17 +42,55 @@ MediaHandlers.youtube = Vue.extend({
},
getMetadata: function(url) {
- // TODO
return {};
},
- play: function(item) {
+ _getRawUrl: async function(url) {
+ if (url.indexOf('.googlevideo.com') < 0) {
+ url = await request('media.get_youtube_url', {url: url});
+ }
+
+ return url;
},
- download: function(item) {
+ play: async function(item) {
+ if (typeof item === 'string')
+ item = {url: item};
+
+ let url = await this._getRawUrl(item.url);
+ this.bus.$emit('play', {...item, url:url});
},
- info: function(item) {
+ downloadServer: async function(item) {
+ createNotification({
+ text: 'Downloading video',
+ image: {
+ icon: 'download',
+ },
+ });
+
+ let url = await this._getRawUrl(item.url);
+ let args = {
+ url: url,
+ }
+
+ if (item.title) {
+ args.filename = item.title + '.webm';
+ }
+
+ let path = await request('media.download', args);
+
+ createNotification({
+ text: 'Video downloaded to ' + path,
+ image: {
+ icon: 'check',
+ },
+ });
+ },
+
+ downloadClient: async function(item) {
+ let url = await this._getRawUrl(item.url);
+ window.open(url, '_blank');
},
},
});
diff --git a/platypush/backend/http/static/js/plugins/media/index.js b/platypush/backend/http/static/js/plugins/media/index.js
index 6005820e9..c1e4b5227 100644
--- a/platypush/backend/http/static/js/plugins/media/index.js
+++ b/platypush/backend/http/static/js/plugins/media/index.js
@@ -110,6 +110,9 @@ Vue.component('media', {
let status = await this.selectedDevice.play(item.url, item.subtitles);
+ if (item.title)
+ status.title = item.title;
+
this.subsModal.visible = false;
this.onStatusUpdate({
device: this.selectedDevice,
@@ -207,6 +210,10 @@ Vue.component('media', {
const status = event.status;
this.syncPosition(status);
+ if (status.state !== 'stop' && this.status[dev.type] && this.status[dev.type][dev.name]) {
+ status.title = status.title || this.status[dev.type][dev.name].title;
+ }
+
if (!this.status[dev.type])
Vue.set(this.status, dev.type, {});
Vue.set(this.status[dev.type], dev.name, status);
@@ -224,10 +231,20 @@ Vue.component('media', {
if (event.plugin.startsWith('media.'))
event.plugin = event.plugin.substr(6);
- if (this.status[event.player] && this.status[event.player][event.plugin])
- Vue.set(this.status[event.player], event.plugin, status);
- else if (this.status[event.plugin] && this.status[event.plugin][event.player])
- Vue.set(this.status[event.plugin], event.player, status);
+ var type, player;
+ if (this.status[event.player] && this.status[event.player][event.plugin]) {
+ type = event.player;
+ player = event.plugin;
+ } else if (this.status[event.plugin] && this.status[event.plugin][event.player]) {
+ type = event.plugin;
+ player = event.player;
+ }
+
+ if (status.state !== 'stop') {
+ status.title = status.title || this.status[type][player].title;
+ }
+
+ Vue.set(this.status[type], player, status);
},
timerFunc: function() {
diff --git a/platypush/backend/http/static/js/plugins/media/search.js b/platypush/backend/http/static/js/plugins/media/search.js
index 15520f40c..8b7f5a16f 100644
--- a/platypush/backend/http/static/js/plugins/media/search.js
+++ b/platypush/backend/http/static/js/plugins/media/search.js
@@ -15,6 +15,12 @@ Vue.component('media-search', {
obj[type] = true;
return obj;
}, {}),
+
+ searchTypes: Object.keys(this.supportedTypes).reduce((obj, type) => {
+ if (type !== 'generic' && type !== 'base')
+ obj[type] = true;
+ return obj;
+ }, {}),
};
},
@@ -31,7 +37,7 @@ Vue.component('media-search', {
},
search: async function(event) {
- const types = Object.entries(this.types).filter(t => t[0] !== 'generic' && t[1]).map(t => t[0]);
+ const types = Object.entries(this.searchTypes).filter(t => t[1]).map(t => t[0]);
const protocol = this.isUrl(this.query);
if (protocol) {
diff --git a/platypush/backend/http/templates/plugins/media/controls.html b/platypush/backend/http/templates/plugins/media/controls.html
index 268afb46f..90205177b 100644
--- a/platypush/backend/http/templates/plugins/media/controls.html
+++ b/platypush/backend/http/templates/plugins/media/controls.html
@@ -4,7 +4,7 @@
-
diff --git a/platypush/backend/http/templates/plugins/media/index.html b/platypush/backend/http/templates/plugins/media/index.html
index d5e511034..51c759cd3 100644
--- a/platypush/backend/http/templates/plugins/media/index.html
+++ b/platypush/backend/http/templates/plugins/media/index.html
@@ -5,8 +5,12 @@
{% 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) %}
+ {% if script != 'base.js' %}
+ {% endif %}
{% endfor %}
diff --git a/platypush/backend/http/templates/plugins/media/info.html b/platypush/backend/http/templates/plugins/media/info.html
index 8de91a038..f98b7a02a 100644
--- a/platypush/backend/http/templates/plugins/media/info.html
+++ b/platypush/backend/http/templates/plugins/media/info.html
@@ -4,7 +4,9 @@
+
+
+
+
+
+
Duration
diff --git a/platypush/backend/http/templates/plugins/media/search.html b/platypush/backend/http/templates/plugins/media/search.html
index bb1f44958..c759b8894 100644
--- a/platypush/backend/http/templates/plugins/media/search.html
+++ b/platypush/backend/http/templates/plugins/media/search.html
@@ -18,11 +18,11 @@
-
diff --git a/platypush/plugins/media/__init__.py b/platypush/plugins/media/__init__.py
index 2b6835cfa..11e9b3dc9 100644
--- a/platypush/plugins/media/__init__.py
+++ b/platypush/plugins/media/__init__.py
@@ -3,6 +3,7 @@ import functools
import os
import queue
import re
+import requests
import subprocess
import tempfile
import threading
@@ -80,7 +81,7 @@ class MediaPlugin(Plugin):
:type media_dirs: list
:param download_dir: Directory where external resources/torrents will be
- downloaded (default: none)
+ downloaded (default: ~/Downloads)
:type download_dir: str
:param env: Environment variables key-values to pass to the
@@ -110,7 +111,6 @@ class MediaPlugin(Plugin):
raise AttributeError('No media plugin configured')
media_dirs = media_dirs or player_config.get('media_dirs', [])
- download_dir = download_dir or player_config.get('download_dir')
if self.__class__.__name__ == 'MediaPlugin':
# Populate this plugin with the actions of the configured player
@@ -130,14 +130,14 @@ class MediaPlugin(Plugin):
)
)
- if download_dir:
- self.download_dir = os.path.abspath(os.path.expanduser(download_dir))
- if not os.path.isdir(self.download_dir):
- raise RuntimeError('download_dir [{}] is not a valid directory'
- .format(self.download_dir))
+ self.download_dir = os.path.abspath(os.path.expanduser(
+ download_dir or player_config.get('download_dir') or
+ os.path.join((os.environ['HOME'] or self._env.get('HOME') or '/'), 'Downloads')))
- self.media_dirs.add(self.download_dir)
+ if not os.path.isdir(self.download_dir):
+ os.makedirs(self.download_dir, exist_ok=True)
+ self.media_dirs.add(self.download_dir)
self._is_playing_torrent = False
self._videos_queue = []
@@ -494,6 +494,30 @@ class MediaPlugin(Plugin):
).group(1).split(':')[::-1])]
)
+ @action
+ def download(self, url, filename=None, directory=None):
+ """
+ Download a media URL
+
+ :param url: Media URL
+ :param filename: Media filename (default: URL filename)
+ :param directory: Destination directory (default: download_dir)
+ :return: The absolute path to the downloaded file
+ """
+
+ if not filename:
+ filename = url.split('/')[-1]
+ if not directory:
+ directory = self.download_dir
+
+ path = os.path.join(directory, filename)
+ content = requests.get(url).content
+
+ with open(path, 'wb') as f:
+ f.write(content)
+
+ return path
+
def is_local(self):
return self._is_local
@@ -507,7 +531,6 @@ class MediaPlugin(Plugin):
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)
diff --git a/platypush/plugins/media/omxplayer.py b/platypush/plugins/media/omxplayer.py
index d78a7f856..1c4df6415 100644
--- a/platypush/plugins/media/omxplayer.py
+++ b/platypush/plugins/media/omxplayer.py
@@ -278,7 +278,7 @@ class MediaOmxplayerPlugin(MediaPlugin):
return {
'duration': self._player.duration(),
- 'filename': urllib.parse.unquote(self._player.get_source()).split('/')[-1],
+ 'filename': urllib.parse.unquote(self._player.get_source()).split('/')[-1] if self._player.get_source().startswith('file://') else None,
'fullscreen': self._player.fullscreen(),
'mute': self._player._is_muted,
'path': self._player.get_source(),
@@ -286,7 +286,7 @@ class MediaOmxplayerPlugin(MediaPlugin):
'position': self._player.position(),
'seekable': self._player.can_seek(),
'state': state,
- 'title': urllib.parse.unquote(self._player.get_source()).split('/')[-1],
+ 'title': urllib.parse.unquote(self._player.get_source()).split('/')[-1] if self._player.get_source().startswith('file://') else None,
'url': self._player.get_source(),
'volume': self._player.volume(),
'volume_max': 100,
diff --git a/platypush/plugins/media/search/__init__.py b/platypush/plugins/media/search/__init__.py
index e72695c24..25ca14f2a 100644
--- a/platypush/plugins/media/search/__init__.py
+++ b/platypush/plugins/media/search/__init__.py
@@ -1,5 +1,6 @@
import logging
+
class MediaSearcher:
"""
Base class for media searchers
@@ -8,7 +9,6 @@ class MediaSearcher:
def __init__(self, *args, **kwargs):
self.logger = logging.getLogger(self.__class__.__name__)
-
def search(self, query, *args, **kwargs):
raise NotImplementedError('The search method should be implemented ' +
'by a derived class')
diff --git a/platypush/plugins/media/search/local.py b/platypush/plugins/media/search/local.py
index f3530a817..d79bbd7cd 100644
--- a/platypush/plugins/media/search/local.py
+++ b/platypush/plugins/media/search/local.py
@@ -215,11 +215,12 @@ class LocalMediaSearcher(MediaSearcher):
filter(MediaToken.token.in_(query_tokens)). \
group_by(MediaFile.path). \
having(func.count(MediaFileToken.token_id) >= len(query_tokens)):
- results[file_record.path] = {
- 'url': 'file://' + file_record.path,
- 'title': os.path.basename(file_record.path),
- 'size': os.path.getsize(file_record.path)
- }
+ if (os.path.isfile(file_record.path)):
+ results[file_record.path] = {
+ 'url': 'file://' + file_record.path,
+ 'title': os.path.basename(file_record.path),
+ 'size': os.path.getsize(file_record.path)
+ }
return results.values()
diff --git a/platypush/plugins/media/search/youtube.py b/platypush/plugins/media/search/youtube.py
index 2f406839c..18c5f9d29 100644
--- a/platypush/plugins/media/search/youtube.py
+++ b/platypush/plugins/media/search/youtube.py
@@ -1,11 +1,12 @@
import re
-import urllib
+import urllib.parse
+import urllib.request
from platypush.context import get_plugin
from platypush.plugins.media.search import MediaSearcher
class YoutubeMediaSearcher(MediaSearcher):
- def search(self, query):
+ def search(self, query, **kwargs):
"""
Performs a YouTube search either using the YouTube API (faster and
recommended, it requires the :mod:`platypush.plugins.google.youtube`
@@ -23,11 +24,12 @@ class YoutubeMediaSearcher(MediaSearcher):
return self._youtube_search_html_parse(query=query)
- def _youtube_search_api(self, query):
+ @staticmethod
+ def _youtube_search_api(query):
return [
{
'url': 'https://www.youtube.com/watch?v=' + item['id']['videoId'],
- 'title': item.get('snippet', {}).get('title', '
'),
+ **item.get('snippet', {}),
}
for item in get_plugin('google.youtube').search(query=query).output
if item.get('id', {}).get('kind') == 'youtube#video'
@@ -53,7 +55,7 @@ class YoutubeMediaSearcher(MediaSearcher):
html = ''
self.logger.info('{} YouTube video results for the search query "{}"'
- .format(len(results), query))
+ .format(len(results), query))
return results
diff --git a/platypush/plugins/media/vlc.py b/platypush/plugins/media/vlc.py
index eabcf1ad2..9043c2b76 100644
--- a/platypush/plugins/media/vlc.py
+++ b/platypush/plugins/media/vlc.py
@@ -45,6 +45,8 @@ class MediaVlcPlugin(MediaPlugin):
self._default_fullscreen = fullscreen
self._default_volume = volume
self._on_stop_callbacks = []
+ self._title = None
+ self._filename = None
@classmethod
def _watched_event_types(cls):
@@ -76,6 +78,9 @@ class MediaVlcPlugin(MediaPlugin):
def _reset_state(self):
self._latest_seek = None
+ self._title = None
+ self._filename = None
+
if self._player:
self._player.release()
self._player = None
@@ -105,8 +110,12 @@ class MediaVlcPlugin(MediaPlugin):
for cbk in self._on_stop_callbacks:
cbk()
elif event.type == EventType.MediaPlayerTitleChanged:
+ self._filename = event.u.filename
+ self._title = event.u.new_title
self._post_event(NewPlayingMediaEvent, resource=event.u.new_title)
elif event.type == EventType.MediaPlayerMediaChanged:
+ self._filename = event.u.filename
+ self._title = event.u.new_title
self._post_event(NewPlayingMediaEvent, resource=event.u.filename)
elif event.type == EventType.MediaPlayerLengthChanged:
self._post_event(NewPlayingMediaEvent, resource=self._get_current_resource())
@@ -396,8 +405,8 @@ class MediaVlcPlugin(MediaPlugin):
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]
- status['title'] = status['filename']
+ status['filename'] = self._filename
+ status['title'] = self._title
status['volume'] = self._player.audio_get_volume()
status['volume_max'] = 100