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 ab9ab7864..456e07436 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
@@ -8,6 +8,7 @@
@import 'webpanel/plugins/media/devices';
@import 'webpanel/plugins/media/results';
@import 'webpanel/plugins/media/controls';
+@import 'webpanel/plugins/media/info';
.media-plugin {
display: flex;
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
new file mode 100644
index 000000000..294e073e0
--- /dev/null
+++ b/platypush/backend/http/static/css/source/webpanel/plugins/media/info.scss
@@ -0,0 +1,24 @@
+.media-plugin {
+ .info-container {
+ .row {
+ display: flex;
+ align-items: center;
+ padding: .75em .5em;
+ border-bottom: $default-border-2;
+
+ &:hover {
+ background: $hover-bg;
+ border-radius: 1em;
+ }
+
+ .attr {
+ font-size: 1.1em;
+ }
+
+ .value {
+ text-align: right;
+ }
+ }
+ }
+}
+
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 6d71ce231..e2a9defdd 100644
--- a/platypush/backend/http/static/js/plugins/media/handlers/file.js
+++ b/platypush/backend/http/static/js/plugins/media/handlers/file.js
@@ -23,7 +23,7 @@ MediaHandlers.file = Vue.extend({
},
{
- text: 'Download',
+ text: 'Download (on client)',
icon: 'download',
action: this.download,
},
@@ -42,21 +42,46 @@ MediaHandlers.file = Vue.extend({
return !!url.match('^(file://)?/');
},
- getMetadata: function(url) {
- return {
- url: url,
- title: url.split('/').pop(),
- };
+ getMetadata: async function(item, onlyBase=false) {
+ if (typeof item === 'string') {
+ item = {
+ url: item,
+ };
+ }
+
+ if (!item.path)
+ item.path = item.url.startsWith('file://') ? item.url.substr(7) : item.url;
+
+ if (!item.title)
+ item.title = item.path.split('/').pop();
+
+ if (!item.size && !onlyBase)
+ item.size = await request('file.getsize', {filename: item.path});
+
+ if (!item.duration && !onlyBase)
+ item.duration = await request('media.get_media_file_duration', {filename: item.path});
+
+ return item;
},
play: function(item) {
this.bus.$emit('play', item);
},
- download: function(item) {
+ download: async function(item) {
+ this.bus.$on('streaming-started', (media) => {
+ if (media.resource === item.url) {
+ this.bus.$off('streaming-started');
+ window.open(media.url + '?download', '_blank');
+ }
+ });
+
+ this.bus.$emit('start-streaming', item.url);
},
- info: function(item) {
+ info: async function(item) {
+ this.bus.$emit('info-loading');
+ this.bus.$emit('info', (await this.getMetadata(item)));
},
searchSubtitles: function(item) {
@@ -74,12 +99,16 @@ MediaHandlers.generic = MediaHandlers.file.extend({
},
methods: {
- getMetadata: function(url) {
+ getMetadata: async function(url) {
return {
url: url,
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/torrent.js b/platypush/backend/http/static/js/plugins/media/handlers/torrent.js
index 490fe0299..2df004098 100644
--- a/platypush/backend/http/static/js/plugins/media/handlers/torrent.js
+++ b/platypush/backend/http/static/js/plugins/media/handlers/torrent.js
@@ -17,7 +17,7 @@ MediaHandlers.torrent = Vue.extend({
},
{
- text: 'Download',
+ text: 'Download (on server)',
icon: 'download',
action: this.download,
},
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 99925ba8f..08a880bba 100644
--- a/platypush/backend/http/static/js/plugins/media/handlers/youtube.js
+++ b/platypush/backend/http/static/js/plugins/media/handlers/youtube.js
@@ -17,7 +17,7 @@ MediaHandlers.youtube = Vue.extend({
},
{
- text: 'Download',
+ text: 'Download (on server)',
icon: 'download',
action: this.download,
},
diff --git a/platypush/backend/http/static/js/plugins/media/index.js b/platypush/backend/http/static/js/plugins/media/index.js
index 7e50da60c..8adff6ba7 100644
--- a/platypush/backend/http/static/js/plugins/media/index.js
+++ b/platypush/backend/http/static/js/plugins/media/index.js
@@ -24,6 +24,16 @@ const mediaUtils = {
ret.push(t.m, t.s);
return ret.join(':');
},
+
+ convertSize: function(size) {
+ size = parseInt(size); // Normalize strings
+
+ const units = ['B', 'KB', 'MB', 'GB'];
+ let s=size, i=0;
+
+ for (; s > 1024 && i < units.length; i++, s = parseInt(s/1024));
+ return (size / Math.pow(2, 10*i)).toFixed(2) + ' ' + units[i];
+ },
},
};
@@ -38,10 +48,17 @@ Vue.component('media', {
results: [],
status: {},
selectedDevice: {},
+
loading: {
results: false,
media: false,
},
+
+ infoModal: {
+ visible: false,
+ loading: false,
+ item: {},
+ },
};
},
@@ -52,14 +69,11 @@ Vue.component('media', {
},
methods: {
- refresh: async function() {
- },
-
onResultsLoading: function() {
this.loading.results = true;
},
- onResultsReady: function(results) {
+ onResultsReady: async function(results) {
for (const result of results) {
if (result.type && MediaHandlers[result.type]) {
result.handler = MediaHandlers[result.type];
@@ -76,7 +90,7 @@ Vue.component('media', {
}
}
- Object.entries(result.handler.getMetadata(result.url)).forEach(entry => {
+ Object.entries(await result.handler.getMetadata(result, onlyBase=true)).forEach(entry => {
Vue.set(result, entry[0], entry[1]);
});
}
@@ -131,14 +145,31 @@ Vue.component('media', {
},
info: function(item) {
- // TODO
- console.log(item);
+ for (const [attr, value] of Object.entries(item)) {
+ Vue.set(this.infoModal.item, attr, value);
+ }
+
+ this.infoModal.loading = false;
+ this.infoModal.visible = true;
+ },
+
+ infoLoading: function() {
+ this.infoModal.loading = true;
+ this.infoModal.visible = true;
},
startStreaming: async function(item) {
- return await request('media.start_streaming', {
- media: item.url,
+ const resource = item instanceof Object ? item.url : item;
+ const ret = await request('media.start_streaming', {
+ media: resource,
});
+
+ this.bus.$emit('streaming-started', {
+ url: ret.url,
+ resource: resource,
+ });
+
+ return ret;
},
selectDevice: async function(device) {
@@ -199,8 +230,6 @@ Vue.component('media', {
},
created: function() {
- this.refresh();
-
for (const [type, Handler] of Object.entries(MediaHandlers)) {
MediaHandlers[type] = new Handler();
MediaHandlers[type].bus = this.bus;
@@ -219,10 +248,12 @@ Vue.component('media', {
this.bus.$on('seek', this.seek);
this.bus.$on('volume', this.setVolume);
this.bus.$on('info', this.info);
+ this.bus.$on('info-loading', this.infoLoading);
this.bus.$on('selected-device', this.selectDevice);
this.bus.$on('results-loading', this.onResultsLoading);
this.bus.$on('results-ready', this.onResultsReady);
this.bus.$on('status-update', this.onStatusUpdate);
+ this.bus.$on('start-streaming', this.startStreaming);
setInterval(this.timerFunc, 1000);
},
diff --git a/platypush/backend/http/static/js/plugins/media/info.js b/platypush/backend/http/static/js/plugins/media/info.js
new file mode 100644
index 000000000..8360d53d5
--- /dev/null
+++ b/platypush/backend/http/static/js/plugins/media/info.js
@@ -0,0 +1,13 @@
+Vue.component('media-info', {
+ template: '#tmpl-media-info',
+ mixins: [mediaUtils],
+ props: {
+ bus: { type: Object },
+
+ item: {
+ type: Object,
+ default: () => {},
+ }
+ },
+});
+
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 0ceb18b04..dbe871dae 100644
--- a/platypush/backend/http/static/js/plugins/media/players/browser.js
+++ b/platypush/backend/http/static/js/plugins/media/players/browser.js
@@ -10,6 +10,7 @@ MediaPlayers.browser = Vue.extend({
default: () => {
return {
youtube: true,
+ generic: true,
};
},
},
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 499972324..2e7d24d5a 100644
--- a/platypush/backend/http/static/js/plugins/media/players/chromecast.js
+++ b/platypush/backend/http/static/js/plugins/media/players/chromecast.js
@@ -10,6 +10,7 @@ MediaPlayers.chromecast = Vue.extend({
default: () => {
return {
youtube: true,
+ generic: true,
};
},
},
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 fa4940795..0e232524b 100644
--- a/platypush/backend/http/static/js/plugins/media/players/local.js
+++ b/platypush/backend/http/static/js/plugins/media/players/local.js
@@ -11,6 +11,7 @@ MediaPlayers.local = Vue.extend({
return {
file: true,
youtube: true,
+ generic: true,
};
},
},
diff --git a/platypush/backend/http/static/js/plugins/media/results.js b/platypush/backend/http/static/js/plugins/media/results.js
index 75f993d4c..2fb1128d1 100644
--- a/platypush/backend/http/static/js/plugins/media/results.js
+++ b/platypush/backend/http/static/js/plugins/media/results.js
@@ -60,14 +60,6 @@ Vue.component('media-results', {
this.selectedItem = item;
openDropdown(this.$refs.mediaItemDropdown);
},
-
- play: function(item) {
- this.bus.$emit('play', item);
- },
-
- info: function(item) {
- this.bus.$emit('info', item);
- },
},
created: function() {
diff --git a/platypush/backend/http/templates/plugins/media/index.html b/platypush/backend/http/templates/plugins/media/index.html
index 0e1232c3e..8c32ec14c 100644
--- a/platypush/backend/http/templates/plugins/media/index.html
+++ b/platypush/backend/http/templates/plugins/media/index.html
@@ -2,6 +2,8 @@
{% include 'plugins/media/controls.html' %}
{% include 'plugins/media/results.html' %}
{% include 'plugins/media/item.html' %}
+{% include 'plugins/media/info.html' %}
+
{% for script in utils.search_directory(static_folder + '/js/plugins/media/handlers', 'js', recursive=True) %}
@@ -35,6 +37,11 @@
:status="selectedDevice && status[selectedDevice.type] && status[selectedDevice.type][selectedDevice.name] ? status[selectedDevice.type][selectedDevice.name] : {}"
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')">
+
+
+ Loading
+
+
diff --git a/platypush/backend/http/templates/plugins/media/info.html b/platypush/backend/http/templates/plugins/media/info.html
new file mode 100644
index 000000000..8de91a038
--- /dev/null
+++ b/platypush/backend/http/templates/plugins/media/info.html
@@ -0,0 +1,26 @@
+
+
+
+
diff --git a/platypush/plugins/file.py b/platypush/plugins/file.py
index 49950c8ca..28b57e93d 100644
--- a/platypush/plugins/file.py
+++ b/platypush/plugins/file.py
@@ -54,5 +54,12 @@ class FilePlugin(Plugin):
with open(self._get_path(filename), 'a') as f:
f.write(content)
+ @action
+ def getsize(self, filename):
+ """
+ Get the size of the specified filename in bytes
+ """
+ return os.path.getsize(filename)
+
# vim:sw=4:ts=4:et:
diff --git a/platypush/plugins/google/youtube.py b/platypush/plugins/google/youtube.py
index 4912969e1..1b447a317 100644
--- a/platypush/plugins/google/youtube.py
+++ b/platypush/plugins/google/youtube.py
@@ -2,16 +2,11 @@
.. moduleauthor:: Fabio Manganiello
"""
-import base64
-import datetime
-import os
-
from platypush.plugins import action
from platypush.plugins.google import GooglePlugin
-from platypush.plugins.calendar import CalendarInterface
-class GoogleYoutubePlugin(GooglePlugin, CalendarInterface):
+class GoogleYoutubePlugin(GooglePlugin):
"""
YouTube plugin
"""
@@ -24,31 +19,33 @@ class GoogleYoutubePlugin(GooglePlugin, CalendarInterface):
# See https://developers.google.com/youtube/v3/getting-started#resources
_default_types = ['video']
-
def __init__(self, *args, **kwargs):
super().__init__(scopes=self.scopes, *args, **kwargs)
-
@action
def search(self, parts=None, query='', types=None, max_results=25, **kwargs):
"""
Search for YouTube content.
- :param parts: List of parts to get (default: snippet). See the `YouTube API documentation `_.
+ :param parts: List of parts to get (default: snippet). See the `YouTube API documentation
+ `_.
:type parts: list[str] or str
:param query: Query string (default: empty string)
:type query: str
- :param types: List of types to retrieve (default: video). See the `YouTube API documentation `_.
+ :param types: List of types to retrieve (default: video). See the `YouTube API documentation
+ `_.
:type types: list[str] or str
:param max_results: Maximum number of items that will be returned (default: 25).
:type max_results: int
- :param kwargs: Any extra arguments that will be transparently passed to the YouTube API, see the `YouTube API documentation `_.
+ :param kwargs: Any extra arguments that will be transparently passed to the YouTube API, see the
+ `YouTube API documentation `_.
- :return: A list of YouTube resources, see the `YouTube API documentation `_.
+ :return: A list of YouTube resources, see the `YouTube API documentation
+ `_.
"""
parts = parts or self._default_parts[:]
diff --git a/platypush/plugins/media/__init__.py b/platypush/plugins/media/__init__.py
index 78ac8a08f..2b6835cfa 100644
--- a/platypush/plugins/media/__init__.py
+++ b/platypush/plugins/media/__init__.py
@@ -1,4 +1,5 @@
import enum
+import functools
import os
import queue
import re
@@ -28,10 +29,12 @@ class MediaPlugin(Plugin):
Requires:
* A media player installed (supported so far: mplayer, vlc, mpv, omxplayer, chromecast)
- * The :class:`platypush.plugins.media.webtorrent` plugin for optional torrent support through webtorrent (recommended)
+ * The :class:`platypush.plugins.media.webtorrent` plugin for optional torrent support through webtorrent
+ (recommended)
* **python-libtorrent** (``pip install python-libtorrent``), optional, for torrent support over native library
* **youtube-dl** installed on your system (see your distro instructions), optional for YouTube support
* **requests** (``pip install requests``), optional, for local files over HTTP streaming supporting
+ * **ffmpeg**,optional, to get media files metadata
To start the local media stream service over HTTP you will also need the
:class:`platypush.backend.http.HttpBackend` backend enabled.
@@ -154,12 +157,12 @@ class MediaPlugin(Plugin):
# The Chromecast has already its native way to handle YouTube
return resource
- resource = self._get_youtube_content(resource)
+ resource = self.get_youtube_url(resource).output
elif resource.startswith('magnet:?'):
try:
get_plugin('media.webtorrent')
return resource # media.webtorrent will handle this
- except:
+ except Exception:
pass
torrents = get_plugin('torrent')
@@ -448,20 +451,49 @@ class MediaPlugin(Plugin):
html = ''
self.logger.info('{} YouTube video results for the search query "{}"'
- .format(len(results), query))
+ .format(len(results), query))
return results
- @classmethod
- def _get_youtube_content(cls, url):
+ @action
+ def get_youtube_url(self, url):
m = re.match('youtube:video:(.*)', url)
- if m: url = 'https://www.youtube.com/watch?v={}'.format(m.group(1))
-
- proc = subprocess.Popen(['youtube-dl','-f','best', '-g', url],
- stdout=subprocess.PIPE)
+ if m:
+ url = 'https://www.youtube.com/watch?v={}'.format(m.group(1))
+ proc = subprocess.Popen(['youtube-dl', '-f', 'best', '-g', url], stdout=subprocess.PIPE)
return proc.stdout.read().decode("utf-8", "strict")[:-1]
+ @action
+ def get_youtube_info(self, url):
+ m = re.match('youtube:video:(.*)', url)
+ if m:
+ url = 'https://www.youtube.com/watch?v={}'.format(m.group(1))
+
+ proc = subprocess.Popen(['youtube-dl', '-j', url], stdout=subprocess.PIPE)
+ return proc.stdout.read().decode("utf-8", "strict")[:-1]
+
+ @action
+ def get_media_file_duration(self, filename):
+ """
+ Get the duration of a media file in seconds. Requires ffmpeg
+ """
+
+ if filename.startswith('file://'):
+ filename = filename[7:]
+
+ result = subprocess.Popen(["ffprobe", filename], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+
+ return functools.reduce(
+ lambda t, t_i: t + t_i,
+ [float(t) * pow(60, i) for (i, t) in enumerate(re.search(
+ '^Duration:\s*([^,]+)', [x.decode()
+ for x in result.stdout.readlines()
+ if "Duration" in x.decode()]
+ .pop().strip()
+ ).group(1).split(':')[::-1])]
+ )
+
def is_local(self):
return self._is_local
diff --git a/platypush/plugins/media/search/local.py b/platypush/plugins/media/search/local.py
index f35baa253..1faf58e6b 100644
--- a/platypush/plugins/media/search/local.py
+++ b/platypush/plugins/media/search/local.py
@@ -43,14 +43,15 @@ class LocalMediaSearcher(MediaSearcher):
if not self._db_engine:
self._db_engine = create_engine(
'sqlite:///{}'.format(self.db_file),
- connect_args = { 'check_same_thread': False })
+ connect_args = {'check_same_thread': False})
Base.metadata.create_all(self._db_engine)
Session.configure(bind=self._db_engine)
return Session()
- def _get_or_create_dir_entry(self, session, path):
+ @staticmethod
+ def _get_or_create_dir_entry(session, path):
record = session.query(MediaDirectory).filter_by(path=path).first()
if record is None:
record = MediaDirectory.build(path=path)
@@ -103,13 +104,11 @@ class LocalMediaSearcher(MediaSearcher):
return session.query(MediaToken).filter(
MediaToken.token.in_(tokens)).all()
-
@classmethod
def _get_file_records(cls, dir_record, session):
return session.query(MediaFile).filter_by(
directory_id=dir_record.id).all()
-
def scan(self, media_dir, session=None, dir_record=None):
"""
Scans a media directory and stores the search results in the internal
@@ -186,8 +185,7 @@ class LocalMediaSearcher(MediaSearcher):
session.commit()
-
- def search(self, query):
+ def search(self, query, **kwargs):
"""
Searches in the configured media directories given a query. It uses the
built-in SQLite index if available. If any directory has changed since
@@ -216,10 +214,12 @@ class LocalMediaSearcher(MediaSearcher):
join(MediaToken). \
filter(MediaToken.token.in_(query_tokens)). \
group_by(MediaFile.path). \
- order_by(func.count(MediaFileToken.token_id).desc()):
+ having(func.count(MediaFileToken.token_id) >= len(query_tokens)):
+ # order_by(func.count(MediaFileToken.token_id).desc()):
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/torrent.py b/platypush/plugins/media/search/torrent.py
index cfc393616..aa27b5232 100644
--- a/platypush/plugins/media/search/torrent.py
+++ b/platypush/plugins/media/search/torrent.py
@@ -8,7 +8,7 @@ class TorrentMediaSearcher(MediaSearcher):
torrents = get_plugin('torrent')
if not torrents:
raise RuntimeError('Torrent plugin not available/configured')
- return torrents.search(query).output
+ return torrents.search(query, ).output