From 4cd2e6949fa81fca33f685f8fb3c3ba6ac91c014 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Fri, 21 Jun 2019 02:13:14 +0200 Subject: [PATCH] New media webplugin WIP --- platypush/backend/http/__init__.py | 3 + .../backend/http/media/handlers/__init__.py | 13 +- platypush/backend/http/media/handlers/file.py | 3 +- .../webpanel/plugins/media/devices.scss | 2 + .../source/webpanel/plugins/media/index.scss | 5 + .../source/webpanel/plugins/media/vars.scss | 1 + .../http/static/js/elements/dropdown.js | 8 +- platypush/backend/http/static/js/events.js | 7 +- .../http/static/js/plugins/media/controls.js | 2 +- .../http/static/js/plugins/media/devices.js | 130 ++++++------ .../static/js/plugins/media/handlers/file.js | 58 +++-- .../js/plugins/media/handlers/torrent.js | 56 +++-- .../js/plugins/media/handlers/youtube.js | 56 +++-- .../http/static/js/plugins/media/index.js | 74 ++++++- .../js/plugins/media/players/browser.js | 40 ++++ .../js/plugins/media/players/chromecast.js | 63 ++++-- .../static/js/plugins/media/players/local.js | 78 +++++++ .../http/static/js/plugins/media/results.js | 17 +- .../http/static/js/plugins/media/search.js | 1 - .../templates/plugins/media/controls.html | 2 +- .../http/templates/plugins/media/devices.html | 3 +- .../http/templates/plugins/media/index.html | 21 +- .../http/templates/plugins/media/item.html | 2 +- .../http/templates/plugins/media/results.html | 4 +- .../http/templates/plugins/media/search.html | 51 ++--- platypush/message/event/media.py | 12 +- platypush/plugins/media/__init__.py | 44 ++-- platypush/plugins/media/mplayer.py | 6 +- platypush/plugins/media/mpv.py | 198 ++++++++++++++---- platypush/plugins/media/omxplayer.py | 2 +- platypush/plugins/media/vlc.py | 8 +- platypush/plugins/media/webtorrent.py | 44 ++-- platypush/utils/__init__.py | 10 +- 33 files changed, 708 insertions(+), 316 deletions(-) create mode 100644 platypush/backend/http/static/js/plugins/media/players/browser.js create mode 100644 platypush/backend/http/static/js/plugins/media/players/local.js 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)