New media webplugin WIP

This commit is contained in:
Fabio Manganiello 2019-06-21 02:13:14 +02:00
parent 9805ed0479
commit 4cd2e6949f
33 changed files with 708 additions and 316 deletions

View File

@ -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')

View File

@ -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']

View File

@ -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:

View File

@ -17,6 +17,8 @@
}
.dropdown {
white-space: nowrap;
.item {
display: flex;
align-items: center;

View File

@ -43,5 +43,10 @@
button {
border: 0;
}
.icon {
color: $result-item-icon;
margin-right: .5em;
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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') {

View File

@ -2,7 +2,7 @@ Vue.component('media-controls', {
template: '#tmpl-media-controls',
props: {
bus: { type: Object },
item: {
status: {
type: Object,
default: () => {},
},

View File

@ -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();
},
});

View File

@ -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) {
},
],
};
},
});

View File

@ -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) {
},
],
};
},
});

View File

@ -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) {
},
],
};
},
});

View File

@ -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);
},
});

View File

@ -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() {
},
},
});

View File

@ -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() {
},
},
});

View File

@ -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}
);
},
},
});

View File

@ -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);
},
};
});

View File

@ -3,7 +3,6 @@ Vue.component('media-search', {
props: {
bus: { type: Object },
supportedTypes: { type: Object },
playerPlugin: { type: String },
},
data: function() {

View File

@ -4,7 +4,7 @@
<div class="controls">
<div class="col-3 item-container">
<div class="item-info">
<span v-text="item.name"></span>
<span v-text="status.title" v-if="status.title"></span>
</div>
</div>

View File

@ -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">
<i class="fa" :class="'fa-' + (selectedDevice.icon || '')" v-if="selectedDevice.icon"></i>
<i :class="selectedDevice.iconClass" v-else-if="selectedDevice.iconClass"></i>
</button>

View File

@ -9,20 +9,29 @@
<script type="text/x-template" id="tmpl-media">
<div class="plugin media-plugin">
<media-search :bus="bus"
:playerPlugin="player"
:supportedTypes="types">
</media-search>
<div class="search">
<div class="col-11">
<media-search :bus="bus"
:supportedTypes="types">
</media-search>
</div>
<div class="col-1 pull-right">
<media-devices :bus="bus"
:localPlayer="player">
</media-devices>
</div>
</div>
<media-results :bus="bus"
:currentItem="currentItem"
:status="selectedDevice && status[selectedDevice.type] && status[selectedDevice.type][selectedDevice.name] ? status[selectedDevice.type][selectedDevice.name] : {}"
:searching="loading.results"
:loading="loading.media"
:results="results">
</media-results>
<media-controls :bus="bus"
:item="currentItem">
:status="selectedDevice && status[selectedDevice.type] && status[selectedDevice.type][selectedDevice.name] ? status[selectedDevice.type][selectedDevice.name] : {}">
</media-controls>
</div>
</script>

View File

@ -4,7 +4,7 @@
<div class="media-item"
:class="{selected: selected, active: active}"
@click="onClick">
<i :class="item.handler.iconClass" v-if="Object.keys(item.handler).length">&nbsp; </i>
<i class="icon" :class="item.handler.iconClass" v-if="item.handler">&nbsp; </i>
<span v-text="item.title"></span>
</div>
</script>

View File

@ -11,8 +11,8 @@
<media-item v-for="item in results"
:key="item.url"
:bus="bus"
:selected="Object.keys(selectedItem).length > 0 && item.url === selectedItem.url"
:active="Object.keys(currentItem).length > 0 && item.url === currentItem.url"
:selected="item.url && item.url === selectedItem.url"
:active="item.url && item.url === status.url"
:item="item"
v-else>
</media-item>

View File

@ -3,40 +3,29 @@
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/search.js') }}"></script>
<script type="text/x-template" id="tmpl-media-search">
<div class="search">
<form @submit.prevent="search">
<div class="row">
<div class="col-11 query-container">
<button type="button" title="Media type filter" class="filter" @click="showFilter = !showFilter">
<i class="fa fa-filter"></i>
</button>
<form @submit.prevent="search">
<div class="row">
<button type="button" title="Media type filter" class="filter" @click="showFilter = !showFilter">
<i class="fa fa-filter"></i>
</button>
<input type="text" name="query" v-model.lazy.trim="query"
:disabled="searching" placeholder="Search query or video URL">
<input type="text" name="query" v-model.lazy.trim="query"
:disabled="searching" placeholder="Search query or video URL">
<button type="submit" :disabled="searching" title="Search">
<i class="fa fa-search"></i>
</button>
</div>
<button type="submit" :disabled="searching" title="Search">
<i class="fa fa-search"></i>
</button>
</div>
<div class="col-1 pull-right">
<media-devices
:bus="bus"
:playerPlugin="playerPlugin">
</media-devices>
</div>
<div class="row types fade-in" :class="{hidden: !showFilter}">
<div class="type" v-for="config,type in types">
<input type="checkbox"
name="type"
:id="'media-type-' + type"
v-model.lazy="types[type]">
<label :for="'media-type-' + type" v-text="type"></label>
</div>
<div class="row types fade-in" :class="{hidden: !showFilter}">
<div class="type" v-for="config,type in types">
<input type="checkbox"
name="type"
:id="'media-type-' + type"
v-model.lazy="types[type]">
<label :for="'media-type-' + type" v-text="type"></label>
</div>
</div>
</form>
</div>
</div>
</form>
</script>

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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')

View File

@ -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:

View File

@ -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)