diff --git a/platypush/backend/http/__init__.py b/platypush/backend/http/__init__.py
index 6d92acf52..ade182976 100644
--- a/platypush/backend/http/__init__.py
+++ b/platypush/backend/http/__init__.py
@@ -186,6 +186,9 @@ class HttpBackend(Backend):
self.uwsgi_args = [str(_) for _ in self.uwsgi_args] + \
['--module', 'platypush.backend.http.uwsgi', '--enable-threads']
+ self.local_base_url = '{proto}://localhost:{port}'.\
+ format(proto=('https' if ssl_cert else 'http'), port=self.port)
+
def send_message(self, msg, **kwargs):
self.logger.warning('Use cURL or any HTTP client to query the HTTP backend')
diff --git a/platypush/backend/http/media/handlers/__init__.py b/platypush/backend/http/media/handlers/__init__.py
index 7f264e96e..717fbb3ce 100644
--- a/platypush/backend/http/media/handlers/__init__.py
+++ b/platypush/backend/http/media/handlers/__init__.py
@@ -1,3 +1,8 @@
+import logging
+
+from .file import FileHandler
+
+
class MediaHandler:
"""
Abstract class to manage media handlers that can be streamed over the HTTP
@@ -20,7 +25,7 @@ class MediaHandler:
self.name = name
self.path = None
- self.filename = name
+ self.filename = filename
self.source = source
self.url = url
self.mime_type = mime_type
@@ -28,7 +33,6 @@ class MediaHandler:
self.content_length = 0
self._matched_handler = matched_handlers[0]
-
@classmethod
def build(cls, source, *args, **kwargs):
errors = {}
@@ -37,6 +41,7 @@ class MediaHandler:
try:
return hndl_class(source, *args, **kwargs)
except Exception as e:
+ logging.exception(e)
errors[hndl_class.__name__] = str(e)
raise AttributeError(('The source {} has no handlers associated. ' +
@@ -58,10 +63,6 @@ class MediaHandler:
yield (attr, getattr(self, attr))
-
-from .file import FileHandler
-
-
__all__ = ['MediaHandler', 'FileHandler']
diff --git a/platypush/backend/http/media/handlers/file.py b/platypush/backend/http/media/handlers/file.py
index 159066a0f..c3e95f62c 100644
--- a/platypush/backend/http/media/handlers/file.py
+++ b/platypush/backend/http/media/handlers/file.py
@@ -23,7 +23,8 @@ class FileHandler(MediaHandler):
self.mime_type = get_mime_type(source)
if self.mime_type[:5] not in ['video', 'audio', 'image']:
- raise AttributeError('{} is not a valid media file'.format(source))
+ raise AttributeError('{} is not a valid media file (detected format: {})'.
+ format(source, self.mime_type))
self.extension = mimetypes.guess_extension(self.mime_type)
if self.url:
diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/media/devices.scss b/platypush/backend/http/static/css/source/webpanel/plugins/media/devices.scss
index 97ed4c251..49f28aa89 100644
--- a/platypush/backend/http/static/css/source/webpanel/plugins/media/devices.scss
+++ b/platypush/backend/http/static/css/source/webpanel/plugins/media/devices.scss
@@ -17,6 +17,8 @@
}
.dropdown {
+ white-space: nowrap;
+
.item {
display: flex;
align-items: center;
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 a6338a447..ab9ab7864 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
@@ -43,5 +43,10 @@
button {
border: 0;
}
+
+ .icon {
+ color: $result-item-icon;
+ margin-right: .5em;
+ }
}
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 8f1695816..ca5942321 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
@@ -8,6 +8,7 @@ $control-panel-shadow: 0 -2.5px 4px 0 #c0c0c0;
$control-time-color: #666;
$empty-results-color: #506050;
+$result-item-icon: #444;
$devices-dropdown-z-index: 2;
$devices-dropdown-refresh-fg: #666;
diff --git a/platypush/backend/http/static/js/elements/dropdown.js b/platypush/backend/http/static/js/elements/dropdown.js
index dbba7a5bf..c1d98766e 100644
--- a/platypush/backend/http/static/js/elements/dropdown.js
+++ b/platypush/backend/http/static/js/elements/dropdown.js
@@ -22,14 +22,16 @@ Vue.component('dropdown', {
item.click();
}
- closeDropdown();
+ if (!item.preventClose) {
+ closeDropdown();
+ }
},
},
});
-var openedDropdown;
+let openedDropdown;
-var clickHndl = function(event) {
+let clickHndl = function(event) {
if (!openedDropdown) {
return;
}
diff --git a/platypush/backend/http/static/js/events.js b/platypush/backend/http/static/js/events.js
index 3d96bee39..266ecad43 100644
--- a/platypush/backend/http/static/js/events.js
+++ b/platypush/backend/http/static/js/events.js
@@ -38,7 +38,12 @@ function initEvents() {
event = event.data;
if (typeof event === 'string') {
- event = JSON.parse(event);
+ try {
+ event = JSON.parse(event);
+ } catch (e) {
+ console.warn('Received invalid non-JSON event');
+ console.warn(event);
+ }
}
if (event.type !== 'event') {
diff --git a/platypush/backend/http/static/js/plugins/media/controls.js b/platypush/backend/http/static/js/plugins/media/controls.js
index ce4ae69f2..ee9c2d498 100644
--- a/platypush/backend/http/static/js/plugins/media/controls.js
+++ b/platypush/backend/http/static/js/plugins/media/controls.js
@@ -2,7 +2,7 @@ Vue.component('media-controls', {
template: '#tmpl-media-controls',
props: {
bus: { type: Object },
- item: {
+ status: {
type: Object,
default: () => {},
},
diff --git a/platypush/backend/http/static/js/plugins/media/devices.js b/platypush/backend/http/static/js/plugins/media/devices.js
index ee17a5805..07093236a 100644
--- a/platypush/backend/http/static/js/plugins/media/devices.js
+++ b/platypush/backend/http/static/js/plugins/media/devices.js
@@ -1,11 +1,11 @@
// Will be filled by dynamically loading device scripts
-var mediaPlayers = {};
+const MediaPlayers = {};
Vue.component('media-devices', {
template: '#tmpl-media-devices',
props: {
bus: { type: Object },
- playerPlugin: { type: String },
+ localPlayer: { type: String },
},
data: function() {
@@ -24,59 +24,38 @@ Vue.component('media-devices', {
text: 'Refresh',
type: 'refresh',
icon: 'sync-alt',
- },
- {
- name: this.playerPlugin,
- text: this.playerPlugin,
- type: 'local',
- icon: 'desktop',
- },
- {
- name: 'browser',
- text: 'Browser',
- type: 'browser',
- icon: 'laptop',
+ preventClose: true,
},
];
},
dropdownItems: function() {
- const items = this.staticItems.concat(
- this.devices.map(dev => {
- return {
- name: dev.name,
- text: dev.name,
- type: dev.__type__,
- icon: dev.icon,
- iconClass: dev.iconClass,
- device: dev,
- };
- })
- );
-
const self = this;
-
- const onClick = (item) => {
+ const onClick = (menuItem) => {
return () => {
if (self.loading) {
return;
}
- self.selectDevice(item);
+ self.selectDevice(menuItem.device);
};
};
- for (var i=0; i < items.length; i++) {
- if (items[i].type === 'refresh') {
- items[i].click = this.refreshDevices;
- } else {
- items[i].click = onClick(items[i]);
- }
-
- items[i].disabled = this.loading;
- }
-
- return items;
+ return self.staticItems.concat(
+ self.devices.map($dev => {
+ return {
+ name: $dev.name,
+ text: $dev.text || $dev.name,
+ icon: $dev.icon,
+ iconClass: $dev.iconClass,
+ device: $dev,
+ };
+ })
+ ).map(item => {
+ item.click = item.type === 'refresh' ? self.refreshDevices : onClick(item);
+ item.disabled = self.loading;
+ return item;
+ });
},
},
@@ -87,39 +66,67 @@ Vue.component('media-devices', {
}
this.loading = true;
- var devices;
+ const self = this;
try {
- const promises = Object.entries(mediaPlayers).map((p) => {
- const player = p[0];
- const handler = p[1];
+ const promises = Object.entries(MediaPlayers).map((p) => {
+ const playerType = p[0];
+ const Player = p[1];
return new Promise((resolve, reject) => {
- handler.scan().then(devs => {
- for (var i=0; i < devs.length; i++) {
- devs[i].__type__ = player;
+ const player = new Player();
- if (handler.icon) {
- devs[i].icon = handler.icon instanceof Function ? handler.icon(devs[i]) : handler.icon;
- } else if (handler.iconClass) {
- devs[i].iconClass = handler.iconClass instanceof Function ? handler.iconClass(devs[i]) : handler.iconClass;
- }
- }
+ if (player.scan) {
+ player.scan().then(devs => {
+ resolve(devs.map(device => {
+ const handler = new Player();
+ handler.device = device;
+ return handler;
+ }));
+ });
- resolve(devs);
- });
+ return;
+ }
+
+ if (player.type === 'local') {
+ player.device = {
+ plugin: self.localPlayer,
+ };
+ } else {
+ player.device = {};
+ }
+
+ resolve([player]);
});
});
this.devices = (await Promise.all(promises)).reduce((list, devs) => {
- for (const d of devs) {
- list.push(d);
- }
+ return [...list, ...devs];
+ }, []).sort((a,b) => {
+ if (a.type === 'local')
+ return -1;
+ if (b.type === 'local')
+ return 1;
+ if (a.type === 'browser')
+ return -1;
+ if (b.type === 'browser')
+ return 1;
+ if (a.type !== b.type)
+ return b.type.localeCompare(a);
+ return b.name.localeCompare(a);
+ });
- return list;
- }, []);
+ this.devices.forEach(dev => {
+ dev.status().then(status => {
+ self.bus.$emit('status-update', {
+ device: dev,
+ status: status,
+ });
+ });
+ });
} finally {
this.loading = false;
+ this.selectDevice(this.devices.filter(_ => _.type === 'local')[0]);
}
},
@@ -134,7 +141,6 @@ Vue.component('media-devices', {
},
created: function() {
- this.selectDevice(this.dropdownItems.filter(_ => _.type === 'local')[0]);
this.refreshDevices();
},
});
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 029e84471..5dc78b156 100644
--- a/platypush/backend/http/static/js/plugins/media/handlers/file.js
+++ b/platypush/backend/http/static/js/plugins/media/handlers/file.js
@@ -1,26 +1,46 @@
-mediaHandlers.file = {
- iconClass: 'fa fa-hdd',
+MediaHandlers.file = Vue.extend({
+ props: {
+ bus: { type: Object },
+ iconClass: {
+ type: String,
+ default: 'fa fa-hdd',
+ },
+ },
- actions: [
- {
- text: 'Play',
- icon: 'play',
- action: 'play',
+ computed: {
+ dropdownItems: function() {
+ return [
+ {
+ text: 'Play',
+ icon: 'play',
+ action: this.play,
+ },
+
+ {
+ text: 'Download',
+ icon: 'download',
+ action: this.download,
+ },
+
+ {
+ text: 'View info',
+ icon: 'info',
+ action: this.info,
+ },
+ ];
+ },
+ },
+
+ methods: {
+ play: function(item) {
+ this.bus.$emit('play', item);
},
- {
- text: 'Download',
- icon: 'download',
- action: function(item, bus) {
- bus.$emit('download', item);
- },
+ download: function(item) {
},
- {
- text: 'View info',
- icon: 'info',
- action: 'info',
+ info: function(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 5787f89f2..d5211bff9 100644
--- a/platypush/backend/http/static/js/plugins/media/handlers/torrent.js
+++ b/platypush/backend/http/static/js/plugins/media/handlers/torrent.js
@@ -1,25 +1,45 @@
-mediaHandlers.torrent = {
- iconClass: 'fa fa-magnet',
+MediaHandlers.torrent = Vue.extend({
+ props: {
+ bus: { type: Object },
+ iconClass: {
+ type: String,
+ default: 'fa fa-magnet',
+ },
+ },
- actions: [
- {
- text: 'Play',
- icon: 'play',
- action: 'play',
+ computed: {
+ dropdownItems: function() {
+ return [
+ {
+ text: 'Play',
+ icon: 'play',
+ action: this.play,
+ },
+
+ {
+ text: 'Download',
+ icon: 'download',
+ action: this.download,
+ },
+
+ {
+ text: 'View info',
+ icon: 'info',
+ action: this.info,
+ },
+ ];
+ },
+ },
+
+ methods: {
+ play: function(item) {
},
- {
- text: 'Download',
- icon: 'download',
- action: function(item) {
- },
+ download: function(item) {
},
- {
- text: 'View info',
- icon: 'info',
- action: 'info',
+ info: function(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 bd33328e5..1b5270533 100644
--- a/platypush/backend/http/static/js/plugins/media/handlers/youtube.js
+++ b/platypush/backend/http/static/js/plugins/media/handlers/youtube.js
@@ -1,25 +1,45 @@
-mediaHandlers.youtube = {
- iconClass: 'fab fa-youtube',
+MediaHandlers.youtube = Vue.extend({
+ props: {
+ bus: { type: Object },
+ iconClass: {
+ type: String,
+ default: 'fab fa-youtube',
+ },
+ },
- actions: [
- {
- text: 'Play',
- icon: 'play',
- action: 'play',
+ computed: {
+ dropdownItems: function() {
+ return [
+ {
+ text: 'Play',
+ icon: 'play',
+ action: this.play,
+ },
+
+ {
+ text: 'Download',
+ icon: 'download',
+ action: this.download,
+ },
+
+ {
+ text: 'View info',
+ icon: 'info',
+ action: this.info,
+ },
+ ];
+ },
+ },
+
+ methods: {
+ play: function(item) {
},
- {
- text: 'Download',
- icon: 'download',
- action: function(item) {
- },
+ download: function(item) {
},
- {
- text: 'View info',
- icon: 'info',
- action: 'info',
+ info: function(item) {
},
- ],
-};
+ },
+});
diff --git a/platypush/backend/http/static/js/plugins/media/index.js b/platypush/backend/http/static/js/plugins/media/index.js
index edf3e3dd0..2e9a107b8 100644
--- a/platypush/backend/http/static/js/plugins/media/index.js
+++ b/platypush/backend/http/static/js/plugins/media/index.js
@@ -1,5 +1,5 @@
-// Will be filled by dynamically loading handler scripts
-var mediaHandlers = {};
+// Will be filled by dynamically loading media type handler scripts
+const MediaHandlers = {};
Vue.component('media', {
template: '#tmpl-media',
@@ -8,8 +8,8 @@ Vue.component('media', {
return {
bus: new Vue({}),
results: [],
- currentItem: {},
- selectedDevice: undefined,
+ status: {},
+ selectedDevice: {},
loading: {
results: false,
media: false,
@@ -19,7 +19,7 @@ Vue.component('media', {
computed: {
types: function() {
- return mediaHandlers;
+ return MediaHandlers;
},
},
@@ -35,13 +35,23 @@ Vue.component('media', {
this.loading.results = false;
for (var i=0; i < results.length; i++) {
- results[i].handler = mediaHandlers[results[i].type];
+ results[i].handler = MediaHandlers[results[i].type];
}
this.results = results;
},
play: async function(item) {
+ if (!this.selectedDevice.accepts[item.type]) {
+ item = await this.startStreaming(item.url);
+ }
+
+ let status = await this.selectedDevice.play(item.url);
+
+ this.onStatusUpdate({
+ device: this.selectedDevice,
+ status: status,
+ });
},
info: function(item) {
@@ -49,19 +59,71 @@ Vue.component('media', {
console.log(item);
},
+ startStreaming: async function(item) {
+ return await request('media.start_streaming', {
+ media: item.url,
+ });
+ },
+
selectDevice: function(device) {
this.selectedDevice = device;
},
+
+ onStatusUpdate: function(event) {
+ const dev = event.device;
+ const status = event.status;
+
+ if (!this.status[dev.type])
+ Vue.set(this.status, dev.type, {});
+ Vue.set(this.status[dev.type], dev.name, status);
+ },
+
+ onNewPlayingMedia: function(event) {
+ console.log('NEW MEDIA');
+ console.log(event);
+ },
+
+ onMediaPlay: function(event) {
+ console.log('PLAY');
+ console.log(event);
+ },
+
+ onMediaPause: function(event) {
+ console.log('PAUSE');
+ console.log(event);
+ },
+
+ onMediaStop: function(event) {
+ console.log('STOP');
+ console.log(event);
+ },
+
+ onMediaSeek: function(event) {
+ console.log('SEEK');
+ console.log(event);
+ },
},
created: function() {
this.refresh();
+ for (const [type, Handler] of Object.entries(MediaHandlers)) {
+ MediaHandlers[type] = new Handler();
+ MediaHandlers[type].bus = this.bus;
+ }
+
+ registerEventHandler(this.onNewPlayingMedia, 'platypush.message.event.media.NewPlayingMediaEvent');
+ registerEventHandler(this.onMediaPlay, 'platypush.message.event.media.MediaPlayEvent');
+ registerEventHandler(this.onMediaPause, 'platypush.message.event.media.MediaPauseEvent');
+ registerEventHandler(this.onMediaStop, 'platypush.message.event.media.MediaStopEvent');
+ registerEventHandler(this.onMediaSeek, 'platypush.message.event.media.MediaSeekEvent');
+
this.bus.$on('play', this.play);
this.bus.$on('info', this.info);
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);
},
});
diff --git a/platypush/backend/http/static/js/plugins/media/players/browser.js b/platypush/backend/http/static/js/plugins/media/players/browser.js
new file mode 100644
index 000000000..0ceb18b04
--- /dev/null
+++ b/platypush/backend/http/static/js/plugins/media/players/browser.js
@@ -0,0 +1,40 @@
+MediaPlayers.browser = Vue.extend({
+ props: {
+ type: {
+ type: String,
+ default: 'browser',
+ },
+
+ accepts: {
+ type: Object,
+ default: () => {
+ return {
+ youtube: true,
+ };
+ },
+ },
+
+ name: {
+ type: String,
+ default: 'Browser',
+ },
+
+ iconClass: {
+ type: String,
+ default: 'fa fa-laptop',
+ },
+ },
+
+ methods: {
+ status: async function() {
+ return {};
+ },
+
+ play: async function(item) {
+ },
+
+ stop: async function() {
+ },
+ },
+});
+
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 ef5fad334..499972324 100644
--- a/platypush/backend/http/static/js/plugins/media/players/chromecast.js
+++ b/platypush/backend/http/static/js/plugins/media/players/chromecast.js
@@ -1,23 +1,54 @@
-mediaPlayers.chromecast = {
- iconClass: function(item) {
- if (item.type === 'audio') {
- return 'fa fa-volume-up';
- } else {
- return 'fab fa-chromecast';
- }
+MediaPlayers.chromecast = Vue.extend({
+ props: {
+ type: {
+ type: String,
+ default: 'chromecast',
+ },
+
+ accepts: {
+ type: Object,
+ default: () => {
+ return {
+ youtube: true,
+ };
+ },
+ },
+
+ device: {
+ type: null,
+ address: null,
+ port: null,
+ uuid: null,
+ status: {},
+ name: '',
+ model_name: null,
+ },
},
- scan: async function() {
- return await request('media.chromecast.get_chromecasts');
+ computed: {
+ name: function() {
+ return this.device.name;
+ },
+
+ iconClass: function() {
+ return this.device.type === 'audio' ? 'fa fa-volume-up' : 'fab fa-chromecast';
+ },
},
- status: function(device) {
- },
+ methods: {
+ scan: async function() {
+ return await request('media.chromecast.get_chromecasts');
+ },
- play: function(item) {
- },
+ status: async function() {
+ return {};
+ },
- stop: function() {
- },
-};
+ play: async function(item) {
+ },
+
+ stop: async function() {
+ },
+ },
+});
diff --git a/platypush/backend/http/static/js/plugins/media/players/local.js b/platypush/backend/http/static/js/plugins/media/players/local.js
new file mode 100644
index 000000000..16f60fd9e
--- /dev/null
+++ b/platypush/backend/http/static/js/plugins/media/players/local.js
@@ -0,0 +1,78 @@
+MediaPlayers.local = Vue.extend({
+ props: {
+ type: {
+ type: String,
+ default: 'local',
+ },
+
+ accepts: {
+ type: Object,
+ default: () => {
+ return {
+ file: true,
+ youtube: true,
+ };
+ },
+ },
+
+ device: {
+ type: Object,
+ default: () => {
+ return {
+ plugin: undefined,
+ };
+ },
+ },
+
+ iconClass: {
+ type: String,
+ default: 'fa fa-desktop',
+ },
+ },
+
+ computed: {
+ name: function() {
+ return this.device.plugin;
+ },
+
+ pluginPrefix: function() {
+ return 'media.' + this.device.plugin;
+ },
+ },
+
+ methods: {
+ status: async function() {
+ return await request(this.pluginPrefix.concat('.status'));
+ },
+
+ play: async function(resource) {
+ return await request(
+ this.pluginPrefix.concat('.play'),
+ {resource: resource}
+ );
+ },
+
+ pause: async function() {
+ return await request(this.pluginPrefix.concat('.pause'));
+ },
+
+ stop: async function() {
+ return await request(this.pluginPrefix.concat('.stop'));
+ },
+
+ seek: async function(position) {
+ return await request(
+ this.pluginPrefix.concat('.seek'),
+ {position: position},
+ );
+ },
+
+ volume: async function(volume) {
+ return await request(
+ this.pluginPrefix.concat('.set_volume'),
+ {volume: volume}
+ );
+ },
+ },
+});
+
diff --git a/platypush/backend/http/static/js/plugins/media/results.js b/platypush/backend/http/static/js/plugins/media/results.js
index 74dde21e9..99dddca3e 100644
--- a/platypush/backend/http/static/js/plugins/media/results.js
+++ b/platypush/backend/http/static/js/plugins/media/results.js
@@ -14,12 +14,15 @@ Vue.component('media-results', {
type: Array,
default: () => [],
},
+ status: {
+ type: Object,
+ default: () => {},
+ },
},
data: function() {
return {
selectedItem: {},
- currentItem: {},
};
},
@@ -31,16 +34,12 @@ Vue.component('media-results', {
const self = this;
- return this.selectedItem.handler.actions.map(action => {
+ return this.selectedItem.handler.dropdownItems.map(item => {
return {
- text: action.text,
- icon: action.icon,
+ text: item.text,
+ icon: item.icon,
click: function() {
- if (action.action instanceof Function) {
- action.action(self.selectedItem, self.bus);
- } else if (typeof(action.action) === 'string') {
- self[action.action](self.selectedItem);
- }
+ item.action(self.selectedItem);
},
};
});
diff --git a/platypush/backend/http/static/js/plugins/media/search.js b/platypush/backend/http/static/js/plugins/media/search.js
index f3fd82ae9..09262bf3f 100644
--- a/platypush/backend/http/static/js/plugins/media/search.js
+++ b/platypush/backend/http/static/js/plugins/media/search.js
@@ -3,7 +3,6 @@ Vue.component('media-search', {
props: {
bus: { type: Object },
supportedTypes: { type: Object },
- playerPlugin: { type: String },
},
data: function() {
diff --git a/platypush/backend/http/templates/plugins/media/controls.html b/platypush/backend/http/templates/plugins/media/controls.html
index 89c75dc7e..44ead51da 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/devices.html b/platypush/backend/http/templates/plugins/media/devices.html
index 510098107..37064cdc2 100644
--- a/platypush/backend/http/templates/plugins/media/devices.html
+++ b/platypush/backend/http/templates/plugins/media/devices.html
@@ -10,7 +10,8 @@
class="devices"
:class="{selected: selectedDevice.type !== 'local' && selectedDevice.type !== 'browser'}"
:title="'Play on ' + (selectedDevice.name || '')"
- @click="openDevicesMenu">
+ @click="openDevicesMenu"
+ v-if="selectedDevice">
diff --git a/platypush/backend/http/templates/plugins/media/index.html b/platypush/backend/http/templates/plugins/media/index.html
index fb6b0b9ca..03541db89 100644
--- a/platypush/backend/http/templates/plugins/media/index.html
+++ b/platypush/backend/http/templates/plugins/media/index.html
@@ -9,20 +9,29 @@
diff --git a/platypush/backend/http/templates/plugins/media/item.html b/platypush/backend/http/templates/plugins/media/item.html
index 131db120b..45b63005e 100644
--- a/platypush/backend/http/templates/plugins/media/item.html
+++ b/platypush/backend/http/templates/plugins/media/item.html
@@ -4,7 +4,7 @@
-
+
diff --git a/platypush/backend/http/templates/plugins/media/results.html b/platypush/backend/http/templates/plugins/media/results.html
index d46848953..0ae9f1bc3 100644
--- a/platypush/backend/http/templates/plugins/media/results.html
+++ b/platypush/backend/http/templates/plugins/media/results.html
@@ -11,8 +11,8 @@
diff --git a/platypush/backend/http/templates/plugins/media/search.html b/platypush/backend/http/templates/plugins/media/search.html
index 98ed8192b..887bfaae9 100644
--- a/platypush/backend/http/templates/plugins/media/search.html
+++ b/platypush/backend/http/templates/plugins/media/search.html
@@ -3,40 +3,29 @@
diff --git a/platypush/message/event/media.py b/platypush/message/event/media.py
index 000d10ebf..12656d1cc 100644
--- a/platypush/message/event/media.py
+++ b/platypush/message/event/media.py
@@ -13,13 +13,13 @@ class MediaPlayRequestEvent(MediaEvent):
Event triggered when a new media playback request is received
"""
- def __init__(self, resource=None, *args, **kwargs):
+ def __init__(self, resource=None, title=None, *args, **kwargs):
"""
:param resource: File name or URI of the played video
:type resource: str
"""
- super().__init__(*args, resource=resource, **kwargs)
+ super().__init__(*args, resource=resource, title=title, **kwargs)
class MediaPlayEvent(MediaEvent):
@@ -27,13 +27,13 @@ class MediaPlayEvent(MediaEvent):
Event triggered when a new media content is played
"""
- def __init__(self, resource=None, *args, **kwargs):
+ def __init__(self, resource=None, title=None, *args, **kwargs):
"""
:param resource: File name or URI of the played video
:type resource: str
"""
- super().__init__(*args, resource=resource, **kwargs)
+ super().__init__(*args, resource=resource, title=title, **kwargs)
class MediaStopEvent(MediaEvent):
@@ -88,8 +88,8 @@ class NewPlayingMediaEvent(MediaEvent):
def __init__(self, resource=None, *args, **kwargs):
"""
- :param video: File name or URI of the played resource
- :type video: str
+ :param resource: File name or URI of the played resource
+ :type resource: str
"""
super().__init__(*args, resource=resource, **kwargs)
diff --git a/platypush/plugins/media/__init__.py b/platypush/plugins/media/__init__.py
index 1fcb5e050..78ac8a08f 100644
--- a/platypush/plugins/media/__init__.py
+++ b/platypush/plugins/media/__init__.py
@@ -3,6 +3,7 @@ import os
import queue
import re
import subprocess
+import tempfile
import threading
import urllib.request
@@ -14,9 +15,10 @@ from platypush.plugins import Plugin, action
class PlayerState(enum.Enum):
- STOP = 'stop'
- PLAY = 'play'
+ STOP = 'stop'
+ PLAY = 'play'
PAUSE = 'pause'
+ IDLE = 'idle'
class MediaPlugin(Plugin):
@@ -26,8 +28,8 @@ 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 (recommented)
- * **python-libtorrent** (``pip install python-libtorrent``), optional, for torrent support through the native Python plugin
+ * 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
@@ -67,7 +69,7 @@ class MediaPlugin(Plugin):
_supported_media_types = ['file', 'torrent', 'youtube']
_default_search_timeout = 60 # 60 seconds
- def __init__(self, media_dirs=[], download_dir=None, env=None,
+ def __init__(self, media_dirs=None, download_dir=None, env=None,
*args, **kwargs):
"""
:param media_dirs: Directories that will be scanned for media files when
@@ -83,8 +85,10 @@ class MediaPlugin(Plugin):
:type env: dict
"""
- super().__init__(*args, **kwargs)
+ super().__init__(**kwargs)
+ if media_dirs is None:
+ media_dirs = []
player = None
player_config = {}
@@ -108,9 +112,9 @@ class MediaPlugin(Plugin):
if self.__class__.__name__ == 'MediaPlugin':
# Populate this plugin with the actions of the configured player
plugin = get_plugin(player)
- for action in plugin.registered_actions:
- setattr(self, action, getattr(plugin, action))
- self.registered_actions.add(action)
+ for act in plugin.registered_actions:
+ setattr(self, act, getattr(plugin, act))
+ self.registered_actions.add(act)
self._env = env or {}
self.media_dirs = set(
@@ -215,8 +219,7 @@ class MediaPlugin(Plugin):
@action
def next(self):
""" Play the next item in the queue """
- if self.player:
- self.player.stop()
+ self.stop()
if self._videos_queue:
video = self._videos_queue.pop(0)
@@ -255,7 +258,7 @@ class MediaPlugin(Plugin):
raise self._NOT_IMPLEMENTED_ERR
@action
- def set_volume(self, volume, *args, **kwargs):
+ def set_volume(self, volume):
raise self._NOT_IMPLEMENTED_ERR
@action
@@ -324,7 +327,8 @@ class MediaPlugin(Plugin):
return results
- def _search_worker(self, query, search_hndl, results_queue):
+ @staticmethod
+ def _search_worker(query, search_hndl, results_queue):
def thread():
results_queue.put(search_hndl.search(query))
return thread
@@ -384,12 +388,12 @@ class MediaPlugin(Plugin):
self.logger.info('Starting streaming {}'.format(media))
response = requests.put('{url}/media{download}'.format(
url=http.local_base_url, download='?download' if download else ''),
- json = { 'source': media })
+ json={'source': media})
if not response.ok:
self.logger.warning('Unable to start streaming: {}'.
format(response.text or response.reason))
- return
+ return None, (response.text or response.reason)
return response.json()
@@ -413,8 +417,8 @@ class MediaPlugin(Plugin):
return response.json()
-
- def _youtube_search_api(self, query):
+ @staticmethod
+ def _youtube_search_api(query):
return [
{
'url': 'https://www.youtube.com/watch?v=' + item['id']['videoId'],
@@ -448,7 +452,6 @@ class MediaPlugin(Plugin):
return results
-
@classmethod
def _get_youtube_content(cls, url):
m = re.match('youtube:video:(.*)', url)
@@ -459,12 +462,11 @@ class MediaPlugin(Plugin):
return proc.stdout.read().decode("utf-8", "strict")[:-1]
-
def is_local(self):
return self._is_local
-
- def get_subtitles_file(self, subtitles):
+ @staticmethod
+ def get_subtitles_file(subtitles):
if not subtitles:
return
diff --git a/platypush/plugins/media/mplayer.py b/platypush/plugins/media/mplayer.py
index 0c93686a0..dd0d7be63 100644
--- a/platypush/plugins/media/mplayer.py
+++ b/platypush/plugins/media/mplayer.py
@@ -322,7 +322,7 @@ class MediaMplayerPlugin(MediaPlugin):
return self._exec('sub_visibility', int(not subs))
@action
- def set_subtitles(self, filename):
+ def set_subtitles(self, filename, **args):
""" Sets media subtitles from filename """
self._exec('sub_visibility', 1)
return self._exec('sub_load', filename)
@@ -343,10 +343,12 @@ class MediaMplayerPlugin(MediaPlugin):
return self.get_property('pause').output.get('pause') == False
@action
- def load(self, resource, mplayer_args={}):
+ def load(self, resource, mplayer_args=None, **kwargs):
"""
Load a resource/video in the player.
"""
+ if mplayer_args is None:
+ mplayer_args = {}
return self.play(resource, mplayer_args=mplayer_args)
@action
diff --git a/platypush/plugins/media/mpv.py b/platypush/plugins/media/mpv.py
index 1e996b837..1beab03b8 100644
--- a/platypush/plugins/media/mpv.py
+++ b/platypush/plugins/media/mpv.py
@@ -5,7 +5,7 @@ import threading
from platypush.context import get_bus, get_plugin
from platypush.plugins.media import PlayerState, MediaPlugin
from platypush.message.event.media import MediaPlayEvent, MediaPlayRequestEvent, \
- MediaPauseEvent, MediaStopEvent, NewPlayingMediaEvent
+ MediaPauseEvent, MediaStopEvent, NewPlayingMediaEvent, MediaSeekEvent
from platypush.plugins import action
@@ -46,7 +46,6 @@ class MediaMpvPlugin(MediaPlugin):
self._playback_rebounce_event = threading.Event()
self._on_stop_callbacks = []
-
def _init_mpv(self, args=None):
import mpv
@@ -72,17 +71,16 @@ class MediaMpvPlugin(MediaPlugin):
return
bus = get_bus()
- if evt == Event.FILE_LOADED or evt == Event.START_FILE:
+ if (evt == Event.FILE_LOADED or evt == Event.START_FILE) and self._get_current_resource():
self._playback_rebounce_event.set()
- bus.post(NewPlayingMediaEvent(resource=self._get_current_resource()))
- bus.post(MediaPlayEvent(resource=self._get_current_resource()))
+ bus.post(NewPlayingMediaEvent(resource=self._get_current_resource(), title=self._player.filename))
+ bus.post(MediaPlayEvent(resource=self._get_current_resource(), title=self._player.filename))
elif evt == Event.PLAYBACK_RESTART:
self._playback_rebounce_event.set()
- pass
elif evt == Event.PAUSE:
- bus.post(MediaPauseEvent(resource=self._get_current_resource()))
+ bus.post(MediaPauseEvent(resource=self._get_current_resource(), title=self._player.filename))
elif evt == Event.UNPAUSE:
- bus.post(MediaPlayEvent(resource=self._get_current_resource()))
+ bus.post(MediaPlayEvent(resource=self._get_current_resource(), title=self._player.filename))
elif evt == Event.SHUTDOWN or (
evt == Event.END_FILE and event.get('event', {}).get('reason')
in [EndFile.EOF_OR_INIT_FAILURE, EndFile.ABORTED, EndFile.QUIT]):
@@ -96,9 +94,13 @@ class MediaMpvPlugin(MediaPlugin):
for callback in self._on_stop_callbacks:
callback()
+ elif evt == Event.SEEK:
+ bus.post(MediaSeekEvent(position=self._player.playback_time))
+
return callback
- def _get_youtube_link(self, resource):
+ @staticmethod
+ def _get_youtube_link(resource):
base_url = 'https://youtu.be/'
regexes = ['^https://(www\.)?youtube.com/watch\?v=([^?]+)',
'^https://(www\.)?youtu.be.com/([^?]+)',
@@ -109,17 +111,15 @@ class MediaMpvPlugin(MediaPlugin):
if m: return base_url + m.group(2)
return None
-
@action
def execute(self, cmd, **args):
"""
Execute a raw mpv command.
"""
if not self._player:
- return (None, 'No mpv instance is running')
+ return None, 'No mpv instance is running'
return self._player.command(cmd, *args)
-
@action
def play(self, resource, subtitles=None, **args):
"""
@@ -154,50 +154,47 @@ class MediaMpvPlugin(MediaPlugin):
if yt_resource: resource = yt_resource
self._is_playing_torrent = False
- ret = self._player.play(resource)
+ self._player.play(resource)
return self.status()
-
@action
def pause(self):
""" Toggle the paused state """
if not self._player:
- return (None, 'No mpv instance is running')
+ return None, 'No mpv instance is running'
self._player.pause = not self._player.pause
return self.status()
-
@action
def quit(self):
- """ Quit the player (same as `stop`) """
+ """ Stop and quit the player """
self._stop_torrent()
if not self._player:
- return (None, 'No mpv instance is running')
+ return None, 'No mpv instance is running'
self._player.quit()
+ self._player.terminate()
self._player = None
- # self._player.terminate()
- return { 'state': PlayerState.STOP.value }
-
+ return {'state': PlayerState.STOP.value}
@action
def stop(self):
- """ Stop the application (same as `quit`) """
+ """ Stop and quit the player """
return self.quit()
@action
def voldown(self, step=10.0):
""" Volume down by (default: 10)% """
if not self._player:
- return (None, 'No mpv instance is running')
+ return None, 'No mpv instance is running'
return self.set_volume(self._player.volume-step)
@action
def volup(self, step=10.0):
""" Volume up by (default: 10)% """
if not self._player:
- return (None, 'No mpv instance is running')
+ return None, 'No mpv instance is running'
return self.set_volume(self._player.volume+step)
@action
@@ -209,36 +206,36 @@ class MediaMpvPlugin(MediaPlugin):
:type volume: float
"""
if not self._player:
- return (None, 'No mpv instance is running')
+ return None, 'No mpv instance is running'
- volume = max(0, min(self._player.volume_max, volume))
+ volume = max(0, min([self._player.volume_max, volume]))
self._player.volume = volume
- return { 'volume': volume }
+ return {'volume': volume}
@action
def seek(self, position):
"""
Seek backward/forward by the specified number of seconds
- :param relative_position: Number of seconds relative to the current cursor
- :type relative_position: int
+ :param position: Number of seconds relative to the current cursor
+ :type position: int
"""
if not self._player:
- return (None, 'No mpv instance is running')
+ return None, 'No mpv instance is running'
if not self._player.seekable:
- return (None, 'The resource is not seekable')
+ return None, 'The resource is not seekable'
pos = min(self._player.time_pos+self._player.time_remaining,
max(0, position))
self._player.time_pos = pos
- return { 'position': pos }
+ return {'position': pos}
@action
def back(self, offset=60.0):
""" Back by (default: 60) seconds """
if not self._player:
- return (None, 'No mpv instance is running')
+ return None, 'No mpv instance is running'
if not self._player.seekable:
- return (None, 'The resource is not seekable')
+ return None, 'The resource is not seekable'
pos = max(0, self._player.time_pos-offset)
return self.seek(pos)
@@ -246,9 +243,9 @@ class MediaMpvPlugin(MediaPlugin):
def forward(self, offset=60.0):
""" Forward by (default: 60) seconds """
if not self._player:
- return (None, 'No mpv instance is running')
+ return None, 'No mpv instance is running'
if not self._player.seekable:
- return (None, 'The resource is not seekable')
+ return None, 'The resource is not seekable'
pos = min(self._player.time_pos+self._player.time_remaining,
self._player.time_pos+offset)
return self.seek(pos)
@@ -257,18 +254,18 @@ class MediaMpvPlugin(MediaPlugin):
def next(self):
""" Play the next item in the queue """
if not self._player:
- return (None, 'No mpv instance is running')
+ return None, 'No mpv instance is running'
self._player.playlist_next()
@action
def prev(self):
""" Play the previous item in the queue """
if not self._player:
- return (None, 'No mpv instance is running')
+ return None, 'No mpv instance is running'
self._player.playlist_prev()
@action
- def toggle_subtitles(self, visibile=None):
+ def toggle_subtitles(self, visible=None):
""" Toggle the subtitles visibility """
return self.toggle_property('sub_visibility')
@@ -322,7 +319,7 @@ class MediaMpvPlugin(MediaPlugin):
return props
@action
- def set_subtitles(self, filename):
+ def set_subtitles(self, filename, *args, **kwargs):
""" Sets media subtitles from filename """
return self.set_property(subfile=filename, sub_visibility=True)
@@ -349,7 +346,7 @@ class MediaMpvPlugin(MediaPlugin):
"""
if not self._player:
return self.play(resource, **args)
- return self.loadfile(resource, mode='append-play', **args)
+ return self._player.loadfile(resource, mode='append-play', **args)
@action
def mute(self):
@@ -382,12 +379,122 @@ class MediaMpvPlugin(MediaPlugin):
}
"""
if not self._player or not hasattr(self._player, 'pause'):
- return { 'state': PlayerState.STOP.value }
+ return {'state': PlayerState.STOP.value}
return {
- 'filename': self._get_current_resource(),
- 'state': (PlayerState.PAUSE.value if self._player.pause else
- PlayerState.PLAY.value),
+ 'alang': getattr(self._player, 'alang'),
+ 'aspect': getattr(self._player, 'aspect'),
+ 'audio': getattr(self._player, 'audio'),
+ 'audio_bitrate': getattr(self._player, 'audio_bitrate'),
+ 'audio_buffer': getattr(self._player, 'audio_buffer'),
+ 'audio_channels': getattr(self._player, 'audio_channels'),
+ 'audio_client_name': getattr(self._player, 'audio_client_name'),
+ 'audio_codec': getattr(self._player, 'audio_codec'),
+ 'audio_codec_name': getattr(self._player, 'audio_codec_name'),
+ 'audio_delay': getattr(self._player, 'audio_delay'),
+ 'audio_device': getattr(self._player, 'audio_device'),
+ 'audio_device_list': getattr(self._player, 'audio_device_list'),
+ 'audio_exclusive': getattr(self._player, 'audio_exclusive'),
+ 'audio_file_paths': getattr(self._player, 'audio_file_paths'),
+ 'audio_files': getattr(self._player, 'audio_files'),
+ 'audio_format': getattr(self._player, 'audio_format'),
+ 'audio_out_params': getattr(self._player, 'audio_out_params'),
+ 'audio_params': getattr(self._player, 'audio_params'),
+ 'audio_mixer_device': getattr(self._player, 'alsa_mixer_device'),
+ 'audio_mixer_index': getattr(self._player, 'alsa_mixer_index'),
+ 'audio_mixer_name': getattr(self._player, 'alsa_mixer_name'),
+ 'autosub': getattr(self._player, 'autosub'),
+ 'autosync': getattr(self._player, 'autosync'),
+ 'background': getattr(self._player, 'background'),
+ 'border': getattr(self._player, 'border'),
+ 'brightness': getattr(self._player, 'brightness'),
+ 'chapter': getattr(self._player, 'chapter'),
+ 'chapter_list': getattr(self._player, 'chapter_list'),
+ 'chapter_metadata': getattr(self._player, 'chapter_metadata'),
+ 'chapters': getattr(self._player, 'chapters'),
+ 'chapters_file': getattr(self._player, 'chapters_file'),
+ 'clock': getattr(self._player, 'clock'),
+ 'cookies': getattr(self._player, 'cookies'),
+ 'cookies_file': getattr(self._player, 'cookies_file'),
+ 'current_ao': getattr(self._player, 'current_ao'),
+ 'current_vo': getattr(self._player, 'current_vo'),
+ 'delay': getattr(self._player, 'delay'),
+ 'display_names': getattr(self._player, 'display_names'),
+ 'end': getattr(self._player, 'end'),
+ 'endpos': getattr(self._player, 'endpos'),
+ 'eof_reached': getattr(self._player, 'eof_reached'),
+ 'file_format': getattr(self._player, 'file_format'),
+ 'filename': getattr(self._player, 'filename'),
+ 'file_size': getattr(self._player, 'file_size'),
+ 'font': getattr(self._player, 'font'),
+ 'fps': getattr(self._player, 'fps'),
+ 'fullscreen': getattr(self._player, 'fs'),
+ 'height': getattr(self._player, 'height'),
+ 'idle': getattr(self._player, 'idle'),
+ 'idle_active': getattr(self._player, 'idle_active'),
+ 'length': getattr(self._player, 'playback_time', 0) + getattr(self._player, 'playtime_remaining', 0)
+ if getattr(self._player, 'playtime_remaining') else None,
+ 'loop': getattr(self._player, 'loop'),
+ 'media_title': getattr(self._player, 'loop'),
+ 'mpv_configuration': getattr(self._player, 'mpv_configuration'),
+ 'mpv_version': getattr(self._player, 'mpv_version'),
+ 'mute': getattr(self._player, 'mute'),
+ 'name': getattr(self._player, 'name'),
+ 'pause': getattr(self._player, 'pause'),
+ 'percent_pos': getattr(self._player, 'percent_pos'),
+ 'playlist': getattr(self._player, 'playlist'),
+ 'playlist_pos': getattr(self._player, 'playlist_pos'),
+ 'position': getattr(self._player, 'playback_time'),
+ 'quiet': getattr(self._player, 'quiet'),
+ 'really_quiet': getattr(self._player, 'really_quiet'),
+ 'saturation': getattr(self._player, 'saturation'),
+ 'screen': getattr(self._player, 'screen'),
+ 'screenshot_directory': getattr(self._player, 'screenshot_directory'),
+ 'screenshot_format': getattr(self._player, 'screenshot_format'),
+ 'screenshot_template': getattr(self._player, 'screenshot_template'),
+ 'seekable': getattr(self._player, 'seekable'),
+ 'seeking': getattr(self._player, 'seeking'),
+ 'shuffle': getattr(self._player, 'shuffle'),
+ 'speed': getattr(self._player, 'speed'),
+ 'state': (PlayerState.PAUSE.value if self._player.pause else PlayerState.PLAY.value),
+ 'stream_pos': getattr(self._player, 'stream_pos'),
+ 'sub': getattr(self._player, 'sub'),
+ 'sub_file_paths': getattr(self._player, 'sub_file_paths'),
+ 'sub_files': getattr(self._player, 'sub_files'),
+ 'sub_paths': getattr(self._player, 'sub_paths'),
+ 'sub_text': getattr(self._player, 'sub_text'),
+ 'subdelay': getattr(self._player, 'subdelay'),
+ 'terminal': getattr(self._player, 'terminal'),
+ 'time_start': getattr(self._player, 'time_start'),
+ 'title': getattr(self._player, 'filename'),
+ 'tv_alsa': getattr(self._player, 'tv_alsa'),
+ 'tv_audio': getattr(self._player, 'tv_audio'),
+ 'tv_audiorate': getattr(self._player, 'tv_audiorate'),
+ 'tv_channels': getattr(self._player, 'tv_channels'),
+ 'tv_device': getattr(self._player, 'tv_device'),
+ 'tv_height': getattr(self._player, 'tv_height'),
+ 'tv_volume': getattr(self._player, 'tv_volume'),
+ 'tv_width': getattr(self._player, 'tv_width'),
+ 'url': self._get_current_resource(),
+ 'user_agent': getattr(self._player, 'user_agent'),
+ 'video': getattr(self._player, 'video'),
+ 'video_align_x': getattr(self._player, 'video_align_x'),
+ 'video_align_y': getattr(self._player, 'video_align_y'),
+ 'video_aspect': getattr(self._player, 'video_aspect'),
+ 'video_bitrate': getattr(self._player, 'video_bitrate'),
+ 'video_codec': getattr(self._player, 'video_codec'),
+ 'video_format': getattr(self._player, 'video_format'),
+ 'video_params': getattr(self._player, 'video_params'),
+ 'video_sync': getattr(self._player, 'video_sync'),
+ 'video_zoom': getattr(self._player, 'video_zoom'),
+ 'vlang': getattr(self._player, 'vlang'),
+ 'volume': getattr(self._player, 'volume'),
+ 'volume_max': getattr(self._player, 'volume_max'),
+ 'width': getattr(self._player, 'width'),
+ 'window_minimized': getattr(self._player, 'window_minimized'),
+ 'window_scale': getattr(self._player, 'window_scale'),
+ 'working_directory': getattr(self._player, 'working_directory'),
+ 'ytdl': getattr(self._player, 'ytdl'),
}
def on_stop(self, callback):
@@ -401,5 +508,4 @@ class MediaMpvPlugin(MediaPlugin):
else '') + self._player.stream_path
-
# vim:sw=4:ts=4:et:
diff --git a/platypush/plugins/media/omxplayer.py b/platypush/plugins/media/omxplayer.py
index 7c641bc3c..881d3148e 100644
--- a/platypush/plugins/media/omxplayer.py
+++ b/platypush/plugins/media/omxplayer.py
@@ -174,7 +174,7 @@ class MediaOmxplayerPlugin(MediaPlugin):
:type pause: bool
"""
- if self._player: self._player.load(resource, pause)
+ if self._player: self._player.load(resource, )
return self.status()
@action
diff --git a/platypush/plugins/media/vlc.py b/platypush/plugins/media/vlc.py
index b3725c3c0..55f1a0689 100644
--- a/platypush/plugins/media/vlc.py
+++ b/platypush/plugins/media/vlc.py
@@ -1,6 +1,4 @@
import os
-import re
-import threading
from platypush.context import get_bus, get_plugin
from platypush.plugins.media import PlayerState, MediaPlugin
@@ -232,8 +230,8 @@ class MediaVlcPlugin(MediaPlugin):
"""
Seek backward/forward by the specified number of seconds
- :param relative_position: Number of seconds relative to the current cursor
- :type relative_position: int
+ :param position: Number of seconds relative to the current cursor
+ :type position: int
"""
if not self._player:
return (None, 'No vlc instance is running')
@@ -304,7 +302,7 @@ class MediaVlcPlugin(MediaPlugin):
self._player.set_fullscreen(fullscreen)
@action
- def set_subtitles(self, filename):
+ def set_subtitles(self, filename, **args):
""" Sets media subtitles from filename """
if not self._player:
return (None, 'No vlc instance is running')
diff --git a/platypush/plugins/media/webtorrent.py b/platypush/plugins/media/webtorrent.py
index 921f51874..6c92e0c88 100644
--- a/platypush/plugins/media/webtorrent.py
+++ b/platypush/plugins/media/webtorrent.py
@@ -1,4 +1,3 @@
-import datetime
import enum
import os
import re
@@ -9,18 +8,16 @@ import time
from platypush.config import Config
from platypush.context import get_bus, get_plugin
-from platypush.message.response import Response
from platypush.plugins.media import PlayerState, MediaPlugin
from platypush.message.event.torrent import TorrentDownloadStartEvent, \
- TorrentDownloadCompletedEvent, TorrentDownloadProgressEvent, \
- TorrentDownloadingMetadataEvent
+ TorrentDownloadCompletedEvent, TorrentDownloadingMetadataEvent
from platypush.plugins import action
from platypush.utils import find_bins_in_path, find_files_by_ext, \
is_process_alive, get_ip_or_hostname
-class TorrentState(enum.Enum):
+class TorrentState(enum.IntEnum):
IDLE = 1
DOWNLOADING_METADATA = 2
DOWNLOADING = 3
@@ -35,8 +32,7 @@ class MediaWebtorrentPlugin(MediaPlugin):
* **webtorrent** installed on your system (``npm install -g webtorrent``)
* **webtorrent-cli** installed on your system (``npm install -g webtorrent-cli``)
- * A media plugin configured for streaming (e.g. media.mplayer
- or media.omxplayer)
+ * A media plugin configured for streaming (e.g. media.mplayer, media.vlc, media.mpv or media.omxplayer)
"""
_supported_media_plugins = {'media.mplayer', 'media.omxplayer', 'media.mpv',
@@ -65,15 +61,13 @@ class MediaWebtorrentPlugin(MediaPlugin):
super().__init__(*args, **kwargs)
self.webtorrent_port = webtorrent_port
+ self._webtorrent_process = None
self._init_webtorrent_bin(webtorrent_bin=webtorrent_bin)
self._init_media_player()
self._download_started_event = threading.Event()
self._torrent_stream_urls = {}
-
def _init_webtorrent_bin(self, webtorrent_bin=None):
- self._webtorrent_process = None
-
if not webtorrent_bin:
bin_name = 'webtorrent.exe' if os.name == 'nt' else 'webtorrent'
bins = find_bins_in_path(bin_name)
@@ -97,7 +91,6 @@ class MediaWebtorrentPlugin(MediaPlugin):
def _init_media_player(self):
self._media_plugin = None
- plugin_name = None
for plugin_name in self._supported_media_plugins:
try:
@@ -113,12 +106,10 @@ class MediaWebtorrentPlugin(MediaPlugin):
'supported media plugins: {}').format(
self._supported_media_plugins))
-
def _read_process_line(self):
line = self._webtorrent_process.stdout.readline().decode().strip()
# Strip output of the colors
- return re.sub('\x1b\[((\d+m)|(.{1,2}))', '', line).strip()
-
+ return re.sub('\x1b\[(([0-9]+m)|(.{1,2}))', '', line).strip()
def _process_monitor(self, resource, download_dir, download_only,
player_type, player_args):
@@ -192,7 +183,6 @@ class MediaWebtorrentPlugin(MediaPlugin):
stream_url=webtorrent_url))
break
-
if not output_dir:
raise RuntimeError('Could not download torrent')
if not download_only and (not media_file or not webtorrent_url):
@@ -265,11 +255,13 @@ class MediaWebtorrentPlugin(MediaPlugin):
stop_evt = player._mplayer_stopped_event
elif media_cls == 'MediaMpvPlugin' or media_cls == 'MediaVlcPlugin':
stop_evt = threading.Event()
+
def stop_callback():
stop_evt.set()
player.on_stop(stop_callback)
elif media_cls == 'MediaOmxplayerPlugin':
stop_evt = threading.Event()
+
def stop_callback():
stop_evt.set()
player.add_handler('stop', stop_callback)
@@ -280,7 +272,6 @@ class MediaWebtorrentPlugin(MediaPlugin):
# Fallback: wait for the webtorrent process to terminate
self._webtorrent_process.wait()
-
def _get_torrent_download_dir(self):
if self._media_plugin.download_dir:
return self._media_plugin.download_dir
@@ -325,14 +316,18 @@ class MediaWebtorrentPlugin(MediaPlugin):
:param player_args: Any arguments to pass to the player plugin's
play() method
:type player_args: dict
+
+ :param download_only: If false then it will start streaming the torrent on the local player once the
+ download starts, otherwise it will just download it (default: false)
+ :type download_only: bool
"""
if self._webtorrent_process:
try:
self.quit()
- except:
+ except Exception as e:
self.logger.debug('Failed to quit the previous instance: {}'.
- format(str))
+ format(str(e)))
download_dir = self._get_torrent_download_dir()
webtorrent_args = [self.webtorrent_bin, 'download', '-o', download_dir]
@@ -365,17 +360,15 @@ class MediaWebtorrentPlugin(MediaPlugin):
if not stream_url:
return (None, ('The webtorrent process hasn\'t started ' +
- 'streaming after {} seconds').format(
- self._web_stream_ready_timeout))
-
- return { 'resource': resource, 'url': stream_url }
+ 'streaming after {} seconds').format(
+ self._web_stream_ready_timeout))
+ return {'resource': resource, 'url': stream_url}
@action
def download(self, resource):
return self.play(resource, download_only=True)
-
@action
def stop(self):
""" Stop the playback """
@@ -393,7 +386,7 @@ class MediaWebtorrentPlugin(MediaPlugin):
self._webtorrent_process = None
@action
- def load(self, resource):
+ def load(self, resource, **kwargs):
"""
Load a torrent resource in the player.
"""
@@ -417,8 +410,7 @@ class MediaWebtorrentPlugin(MediaPlugin):
}
"""
- return {'state': self._media_plugin.status()
- .get('state', PlayerState.STOP.value)}
+ return {'state': self._media_plugin.status().get('state', PlayerState.STOP.value)}
# vim:sw=4:ts=4:et:
diff --git a/platypush/utils/__init__.py b/platypush/utils/__init__.py
index 791a95a9a..49cf6a169 100644
--- a/platypush/utils/__init__.py
+++ b/platypush/utils/__init__.py
@@ -1,5 +1,4 @@
import ast
-import errno
import hashlib
import importlib
import inspect
@@ -130,9 +129,7 @@ def _get_ssl_context(context_type=None, ssl_cert=None, ssl_key=None,
ssl_context = ssl.create_default_context(cafile=ssl_cafile,
capath=ssl_capath)
else:
- ssl_context = ssl.SSLContext(context_type)
-
- ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+ ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
if ssl_cafile or ssl_capath:
ssl_context.load_verify_locations(
@@ -227,8 +224,9 @@ def get_mime_type(resource):
with urllib.request.urlopen(resource) as response:
return response.info().get_content_type()
else:
- mime = magic.Magic(mime=True)
- return mime.from_file(resource)
+ mime = magic.detect_from_filename(resource)
+ if mime:
+ return mime.mime_type
def camel_case_to_snake_case(string):
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', string)