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] + \ self.uwsgi_args = [str(_) for _ in self.uwsgi_args] + \
['--module', 'platypush.backend.http.uwsgi', '--enable-threads'] ['--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): def send_message(self, msg, **kwargs):
self.logger.warning('Use cURL or any HTTP client to query the HTTP backend') 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: class MediaHandler:
""" """
Abstract class to manage media handlers that can be streamed over the HTTP Abstract class to manage media handlers that can be streamed over the HTTP
@ -20,7 +25,7 @@ class MediaHandler:
self.name = name self.name = name
self.path = None self.path = None
self.filename = name self.filename = filename
self.source = source self.source = source
self.url = url self.url = url
self.mime_type = mime_type self.mime_type = mime_type
@ -28,7 +33,6 @@ class MediaHandler:
self.content_length = 0 self.content_length = 0
self._matched_handler = matched_handlers[0] self._matched_handler = matched_handlers[0]
@classmethod @classmethod
def build(cls, source, *args, **kwargs): def build(cls, source, *args, **kwargs):
errors = {} errors = {}
@ -37,6 +41,7 @@ class MediaHandler:
try: try:
return hndl_class(source, *args, **kwargs) return hndl_class(source, *args, **kwargs)
except Exception as e: except Exception as e:
logging.exception(e)
errors[hndl_class.__name__] = str(e) errors[hndl_class.__name__] = str(e)
raise AttributeError(('The source {} has no handlers associated. ' + raise AttributeError(('The source {} has no handlers associated. ' +
@ -58,10 +63,6 @@ class MediaHandler:
yield (attr, getattr(self, attr)) yield (attr, getattr(self, attr))
from .file import FileHandler
__all__ = ['MediaHandler', 'FileHandler'] __all__ = ['MediaHandler', 'FileHandler']

View file

@ -23,7 +23,8 @@ class FileHandler(MediaHandler):
self.mime_type = get_mime_type(source) self.mime_type = get_mime_type(source)
if self.mime_type[:5] not in ['video', 'audio', 'image']: 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) self.extension = mimetypes.guess_extension(self.mime_type)
if self.url: if self.url:

View file

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

View file

@ -43,5 +43,10 @@
button { button {
border: 0; 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; $control-time-color: #666;
$empty-results-color: #506050; $empty-results-color: #506050;
$result-item-icon: #444;
$devices-dropdown-z-index: 2; $devices-dropdown-z-index: 2;
$devices-dropdown-refresh-fg: #666; $devices-dropdown-refresh-fg: #666;

View file

@ -22,14 +22,16 @@ Vue.component('dropdown', {
item.click(); item.click();
} }
if (!item.preventClose) {
closeDropdown(); closeDropdown();
}
}, },
}, },
}); });
var openedDropdown; let openedDropdown;
var clickHndl = function(event) { let clickHndl = function(event) {
if (!openedDropdown) { if (!openedDropdown) {
return; return;
} }

View file

@ -38,7 +38,12 @@ function initEvents() {
event = event.data; event = event.data;
if (typeof event === 'string') { if (typeof event === 'string') {
try {
event = JSON.parse(event); event = JSON.parse(event);
} catch (e) {
console.warn('Received invalid non-JSON event');
console.warn(event);
}
} }
if (event.type !== 'event') { if (event.type !== 'event') {

View file

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

View file

@ -1,11 +1,11 @@
// Will be filled by dynamically loading device scripts // Will be filled by dynamically loading device scripts
var mediaPlayers = {}; const MediaPlayers = {};
Vue.component('media-devices', { Vue.component('media-devices', {
template: '#tmpl-media-devices', template: '#tmpl-media-devices',
props: { props: {
bus: { type: Object }, bus: { type: Object },
playerPlugin: { type: String }, localPlayer: { type: String },
}, },
data: function() { data: function() {
@ -24,59 +24,38 @@ Vue.component('media-devices', {
text: 'Refresh', text: 'Refresh',
type: 'refresh', type: 'refresh',
icon: 'sync-alt', icon: 'sync-alt',
}, preventClose: true,
{
name: this.playerPlugin,
text: this.playerPlugin,
type: 'local',
icon: 'desktop',
},
{
name: 'browser',
text: 'Browser',
type: 'browser',
icon: 'laptop',
}, },
]; ];
}, },
dropdownItems: function() { 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 self = this;
const onClick = (menuItem) => {
const onClick = (item) => {
return () => { return () => {
if (self.loading) { if (self.loading) {
return; return;
} }
self.selectDevice(item); self.selectDevice(menuItem.device);
}; };
}; };
for (var i=0; i < items.length; i++) { return self.staticItems.concat(
if (items[i].type === 'refresh') { self.devices.map($dev => {
items[i].click = this.refreshDevices; return {
} else { name: $dev.name,
items[i].click = onClick(items[i]); text: $dev.text || $dev.name,
} icon: $dev.icon,
iconClass: $dev.iconClass,
items[i].disabled = this.loading; device: $dev,
} };
})
return items; ).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; this.loading = true;
var devices; const self = this;
try { try {
const promises = Object.entries(mediaPlayers).map((p) => { const promises = Object.entries(MediaPlayers).map((p) => {
const player = p[0]; const playerType = p[0];
const handler = p[1]; const Player = p[1];
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
handler.scan().then(devs => { const player = new Player();
for (var i=0; i < devs.length; i++) {
devs[i].__type__ = player;
if (handler.icon) { if (player.scan) {
devs[i].icon = handler.icon instanceof Function ? handler.icon(devs[i]) : handler.icon; player.scan().then(devs => {
} else if (handler.iconClass) { resolve(devs.map(device => {
devs[i].iconClass = handler.iconClass instanceof Function ? handler.iconClass(devs[i]) : handler.iconClass; 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) => { this.devices = (await Promise.all(promises)).reduce((list, devs) => {
for (const d of devs) { return [...list, ...devs];
list.push(d); }, []).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 { } finally {
this.loading = false; this.loading = false;
this.selectDevice(this.devices.filter(_ => _.type === 'local')[0]);
} }
}, },
@ -134,7 +141,6 @@ Vue.component('media-devices', {
}, },
created: function() { created: function() {
this.selectDevice(this.dropdownItems.filter(_ => _.type === 'local')[0]);
this.refreshDevices(); this.refreshDevices();
}, },
}); });

View file

@ -1,26 +1,46 @@
mediaHandlers.file = { MediaHandlers.file = Vue.extend({
iconClass: 'fa fa-hdd', props: {
bus: { type: Object },
iconClass: {
type: String,
default: 'fa fa-hdd',
},
},
actions: [ computed: {
dropdownItems: function() {
return [
{ {
text: 'Play', text: 'Play',
icon: 'play', icon: 'play',
action: 'play', action: this.play,
}, },
{ {
text: 'Download', text: 'Download',
icon: 'download', icon: 'download',
action: function(item, bus) { action: this.download,
bus.$emit('download', item);
},
}, },
{ {
text: 'View info', text: 'View info',
icon: 'info', icon: 'info',
action: 'info', action: this.info,
},
];
},
}, },
],
}; methods: {
play: function(item) {
this.bus.$emit('play', item);
},
download: function(item) {
},
info: function(item) {
},
},
});

View file

@ -1,25 +1,45 @@
mediaHandlers.torrent = { MediaHandlers.torrent = Vue.extend({
iconClass: 'fa fa-magnet', props: {
bus: { type: Object },
iconClass: {
type: String,
default: 'fa fa-magnet',
},
},
actions: [ computed: {
dropdownItems: function() {
return [
{ {
text: 'Play', text: 'Play',
icon: 'play', icon: 'play',
action: 'play', action: this.play,
}, },
{ {
text: 'Download', text: 'Download',
icon: 'download', icon: 'download',
action: function(item) { action: this.download,
},
}, },
{ {
text: 'View info', text: 'View info',
icon: 'info', icon: 'info',
action: 'info', action: this.info,
},
];
},
}, },
],
}; methods: {
play: function(item) {
},
download: function(item) {
},
info: function(item) {
},
},
});

View file

@ -1,25 +1,45 @@
mediaHandlers.youtube = { MediaHandlers.youtube = Vue.extend({
iconClass: 'fab fa-youtube', props: {
bus: { type: Object },
iconClass: {
type: String,
default: 'fab fa-youtube',
},
},
actions: [ computed: {
dropdownItems: function() {
return [
{ {
text: 'Play', text: 'Play',
icon: 'play', icon: 'play',
action: 'play', action: this.play,
}, },
{ {
text: 'Download', text: 'Download',
icon: 'download', icon: 'download',
action: function(item) { action: this.download,
},
}, },
{ {
text: 'View info', text: 'View info',
icon: 'info', icon: 'info',
action: 'info', action: this.info,
},
];
},
}, },
],
}; methods: {
play: function(item) {
},
download: function(item) {
},
info: function(item) {
},
},
});

View file

@ -1,5 +1,5 @@
// Will be filled by dynamically loading handler scripts // Will be filled by dynamically loading media type handler scripts
var mediaHandlers = {}; const MediaHandlers = {};
Vue.component('media', { Vue.component('media', {
template: '#tmpl-media', template: '#tmpl-media',
@ -8,8 +8,8 @@ Vue.component('media', {
return { return {
bus: new Vue({}), bus: new Vue({}),
results: [], results: [],
currentItem: {}, status: {},
selectedDevice: undefined, selectedDevice: {},
loading: { loading: {
results: false, results: false,
media: false, media: false,
@ -19,7 +19,7 @@ Vue.component('media', {
computed: { computed: {
types: function() { types: function() {
return mediaHandlers; return MediaHandlers;
}, },
}, },
@ -35,13 +35,23 @@ Vue.component('media', {
this.loading.results = false; this.loading.results = false;
for (var i=0; i < results.length; i++) { 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; this.results = results;
}, },
play: async function(item) { 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) { info: function(item) {
@ -49,19 +59,71 @@ Vue.component('media', {
console.log(item); console.log(item);
}, },
startStreaming: async function(item) {
return await request('media.start_streaming', {
media: item.url,
});
},
selectDevice: function(device) { selectDevice: function(device) {
this.selectedDevice = 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() { created: function() {
this.refresh(); 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('play', this.play);
this.bus.$on('info', this.info); this.bus.$on('info', this.info);
this.bus.$on('selected-device', this.selectDevice); this.bus.$on('selected-device', this.selectDevice);
this.bus.$on('results-loading', this.onResultsLoading); this.bus.$on('results-loading', this.onResultsLoading);
this.bus.$on('results-ready', this.onResultsReady); 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 = { MediaPlayers.chromecast = Vue.extend({
iconClass: function(item) { props: {
if (item.type === 'audio') { type: {
return 'fa fa-volume-up'; type: String,
} else { default: 'chromecast',
return 'fab fa-chromecast';
}
}, },
accepts: {
type: Object,
default: () => {
return {
youtube: true,
};
},
},
device: {
type: null,
address: null,
port: null,
uuid: null,
status: {},
name: '',
model_name: null,
},
},
computed: {
name: function() {
return this.device.name;
},
iconClass: function() {
return this.device.type === 'audio' ? 'fa fa-volume-up' : 'fab fa-chromecast';
},
},
methods: {
scan: async function() { scan: async function() {
return await request('media.chromecast.get_chromecasts'); return await request('media.chromecast.get_chromecasts');
}, },
status: function(device) { status: async function() {
return {};
}, },
play: function(item) { play: async function(item) {
}, },
stop: function() { 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, type: Array,
default: () => [], default: () => [],
}, },
status: {
type: Object,
default: () => {},
},
}, },
data: function() { data: function() {
return { return {
selectedItem: {}, selectedItem: {},
currentItem: {},
}; };
}, },
@ -31,16 +34,12 @@ Vue.component('media-results', {
const self = this; const self = this;
return this.selectedItem.handler.actions.map(action => { return this.selectedItem.handler.dropdownItems.map(item => {
return { return {
text: action.text, text: item.text,
icon: action.icon, icon: item.icon,
click: function() { click: function() {
if (action.action instanceof Function) { item.action(self.selectedItem);
action.action(self.selectedItem, self.bus);
} else if (typeof(action.action) === 'string') {
self[action.action](self.selectedItem);
}
}, },
}; };
}); });

View file

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

View file

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

View file

@ -10,7 +10,8 @@
class="devices" class="devices"
:class="{selected: selectedDevice.type !== 'local' && selectedDevice.type !== 'browser'}" :class="{selected: selectedDevice.type !== 'local' && selectedDevice.type !== 'browser'}"
:title="'Play on ' + (selectedDevice.name || '')" :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="fa" :class="'fa-' + (selectedDevice.icon || '')" v-if="selectedDevice.icon"></i>
<i :class="selectedDevice.iconClass" v-else-if="selectedDevice.iconClass"></i> <i :class="selectedDevice.iconClass" v-else-if="selectedDevice.iconClass"></i>
</button> </button>

View file

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

View file

@ -4,7 +4,7 @@
<div class="media-item" <div class="media-item"
:class="{selected: selected, active: active}" :class="{selected: selected, active: active}"
@click="onClick"> @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> <span v-text="item.title"></span>
</div> </div>
</script> </script>

View file

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

View file

@ -3,10 +3,8 @@
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/search.js') }}"></script> <script type="application/javascript" src="{{ url_for('static', filename='js/plugins/media/search.js') }}"></script>
<script type="text/x-template" id="tmpl-media-search"> <script type="text/x-template" id="tmpl-media-search">
<div class="search">
<form @submit.prevent="search"> <form @submit.prevent="search">
<div class="row"> <div class="row">
<div class="col-11 query-container">
<button type="button" title="Media type filter" class="filter" @click="showFilter = !showFilter"> <button type="button" title="Media type filter" class="filter" @click="showFilter = !showFilter">
<i class="fa fa-filter"></i> <i class="fa fa-filter"></i>
</button> </button>
@ -19,14 +17,6 @@
</button> </button>
</div> </div>
<div class="col-1 pull-right">
<media-devices
:bus="bus"
:playerPlugin="playerPlugin">
</media-devices>
</div>
</div>
<div class="row types fade-in" :class="{hidden: !showFilter}"> <div class="row types fade-in" :class="{hidden: !showFilter}">
<div class="type" v-for="config,type in types"> <div class="type" v-for="config,type in types">
<input type="checkbox" <input type="checkbox"
@ -37,6 +27,5 @@
</div> </div>
</div> </div>
</form> </form>
</div>
</script> </script>

View file

@ -13,13 +13,13 @@ class MediaPlayRequestEvent(MediaEvent):
Event triggered when a new media playback request is received 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 :param resource: File name or URI of the played video
:type resource: str :type resource: str
""" """
super().__init__(*args, resource=resource, **kwargs) super().__init__(*args, resource=resource, title=title, **kwargs)
class MediaPlayEvent(MediaEvent): class MediaPlayEvent(MediaEvent):
@ -27,13 +27,13 @@ class MediaPlayEvent(MediaEvent):
Event triggered when a new media content is played 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 :param resource: File name or URI of the played video
:type resource: str :type resource: str
""" """
super().__init__(*args, resource=resource, **kwargs) super().__init__(*args, resource=resource, title=title, **kwargs)
class MediaStopEvent(MediaEvent): class MediaStopEvent(MediaEvent):
@ -88,8 +88,8 @@ class NewPlayingMediaEvent(MediaEvent):
def __init__(self, resource=None, *args, **kwargs): def __init__(self, resource=None, *args, **kwargs):
""" """
:param video: File name or URI of the played resource :param resource: File name or URI of the played resource
:type video: str :type resource: str
""" """
super().__init__(*args, resource=resource, **kwargs) super().__init__(*args, resource=resource, **kwargs)

View file

@ -3,6 +3,7 @@ import os
import queue import queue
import re import re
import subprocess import subprocess
import tempfile
import threading import threading
import urllib.request import urllib.request
@ -17,6 +18,7 @@ class PlayerState(enum.Enum):
STOP = 'stop' STOP = 'stop'
PLAY = 'play' PLAY = 'play'
PAUSE = 'pause' PAUSE = 'pause'
IDLE = 'idle'
class MediaPlugin(Plugin): class MediaPlugin(Plugin):
@ -26,8 +28,8 @@ class MediaPlugin(Plugin):
Requires: Requires:
* A media player installed (supported so far: mplayer, vlc, mpv, omxplayer, chromecast) * 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) * The :class:`platypush.plugins.media.webtorrent` plugin for optional torrent support through webtorrent (recommended)
* **python-libtorrent** (``pip install python-libtorrent``), optional, for torrent support through the native Python plugin * **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 * **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 * **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'] _supported_media_types = ['file', 'torrent', 'youtube']
_default_search_timeout = 60 # 60 seconds _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): *args, **kwargs):
""" """
:param media_dirs: Directories that will be scanned for media files when :param media_dirs: Directories that will be scanned for media files when
@ -83,8 +85,10 @@ class MediaPlugin(Plugin):
:type env: dict :type env: dict
""" """
super().__init__(*args, **kwargs) super().__init__(**kwargs)
if media_dirs is None:
media_dirs = []
player = None player = None
player_config = {} player_config = {}
@ -108,9 +112,9 @@ class MediaPlugin(Plugin):
if self.__class__.__name__ == 'MediaPlugin': if self.__class__.__name__ == 'MediaPlugin':
# Populate this plugin with the actions of the configured player # Populate this plugin with the actions of the configured player
plugin = get_plugin(player) plugin = get_plugin(player)
for action in plugin.registered_actions: for act in plugin.registered_actions:
setattr(self, action, getattr(plugin, action)) setattr(self, act, getattr(plugin, act))
self.registered_actions.add(action) self.registered_actions.add(act)
self._env = env or {} self._env = env or {}
self.media_dirs = set( self.media_dirs = set(
@ -215,8 +219,7 @@ class MediaPlugin(Plugin):
@action @action
def next(self): def next(self):
""" Play the next item in the queue """ """ Play the next item in the queue """
if self.player: self.stop()
self.player.stop()
if self._videos_queue: if self._videos_queue:
video = self._videos_queue.pop(0) video = self._videos_queue.pop(0)
@ -255,7 +258,7 @@ class MediaPlugin(Plugin):
raise self._NOT_IMPLEMENTED_ERR raise self._NOT_IMPLEMENTED_ERR
@action @action
def set_volume(self, volume, *args, **kwargs): def set_volume(self, volume):
raise self._NOT_IMPLEMENTED_ERR raise self._NOT_IMPLEMENTED_ERR
@action @action
@ -324,7 +327,8 @@ class MediaPlugin(Plugin):
return results return results
def _search_worker(self, query, search_hndl, results_queue): @staticmethod
def _search_worker(query, search_hndl, results_queue):
def thread(): def thread():
results_queue.put(search_hndl.search(query)) results_queue.put(search_hndl.search(query))
return thread return thread
@ -389,7 +393,7 @@ class MediaPlugin(Plugin):
if not response.ok: if not response.ok:
self.logger.warning('Unable to start streaming: {}'. self.logger.warning('Unable to start streaming: {}'.
format(response.text or response.reason)) format(response.text or response.reason))
return return None, (response.text or response.reason)
return response.json() return response.json()
@ -413,8 +417,8 @@ class MediaPlugin(Plugin):
return response.json() return response.json()
@staticmethod
def _youtube_search_api(self, query): def _youtube_search_api(query):
return [ return [
{ {
'url': 'https://www.youtube.com/watch?v=' + item['id']['videoId'], 'url': 'https://www.youtube.com/watch?v=' + item['id']['videoId'],
@ -448,7 +452,6 @@ class MediaPlugin(Plugin):
return results return results
@classmethod @classmethod
def _get_youtube_content(cls, url): def _get_youtube_content(cls, url):
m = re.match('youtube:video:(.*)', url) m = re.match('youtube:video:(.*)', url)
@ -459,12 +462,11 @@ class MediaPlugin(Plugin):
return proc.stdout.read().decode("utf-8", "strict")[:-1] return proc.stdout.read().decode("utf-8", "strict")[:-1]
def is_local(self): def is_local(self):
return self._is_local return self._is_local
@staticmethod
def get_subtitles_file(self, subtitles): def get_subtitles_file(subtitles):
if not subtitles: if not subtitles:
return return

View file

@ -322,7 +322,7 @@ class MediaMplayerPlugin(MediaPlugin):
return self._exec('sub_visibility', int(not subs)) return self._exec('sub_visibility', int(not subs))
@action @action
def set_subtitles(self, filename): def set_subtitles(self, filename, **args):
""" Sets media subtitles from filename """ """ Sets media subtitles from filename """
self._exec('sub_visibility', 1) self._exec('sub_visibility', 1)
return self._exec('sub_load', filename) return self._exec('sub_load', filename)
@ -343,10 +343,12 @@ class MediaMplayerPlugin(MediaPlugin):
return self.get_property('pause').output.get('pause') == False return self.get_property('pause').output.get('pause') == False
@action @action
def load(self, resource, mplayer_args={}): def load(self, resource, mplayer_args=None, **kwargs):
""" """
Load a resource/video in the player. Load a resource/video in the player.
""" """
if mplayer_args is None:
mplayer_args = {}
return self.play(resource, mplayer_args=mplayer_args) return self.play(resource, mplayer_args=mplayer_args)
@action @action

View file

@ -5,7 +5,7 @@ import threading
from platypush.context import get_bus, get_plugin from platypush.context import get_bus, get_plugin
from platypush.plugins.media import PlayerState, MediaPlugin from platypush.plugins.media import PlayerState, MediaPlugin
from platypush.message.event.media import MediaPlayEvent, MediaPlayRequestEvent, \ from platypush.message.event.media import MediaPlayEvent, MediaPlayRequestEvent, \
MediaPauseEvent, MediaStopEvent, NewPlayingMediaEvent MediaPauseEvent, MediaStopEvent, NewPlayingMediaEvent, MediaSeekEvent
from platypush.plugins import action from platypush.plugins import action
@ -46,7 +46,6 @@ class MediaMpvPlugin(MediaPlugin):
self._playback_rebounce_event = threading.Event() self._playback_rebounce_event = threading.Event()
self._on_stop_callbacks = [] self._on_stop_callbacks = []
def _init_mpv(self, args=None): def _init_mpv(self, args=None):
import mpv import mpv
@ -72,17 +71,16 @@ class MediaMpvPlugin(MediaPlugin):
return return
bus = get_bus() 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() self._playback_rebounce_event.set()
bus.post(NewPlayingMediaEvent(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())) bus.post(MediaPlayEvent(resource=self._get_current_resource(), title=self._player.filename))
elif evt == Event.PLAYBACK_RESTART: elif evt == Event.PLAYBACK_RESTART:
self._playback_rebounce_event.set() self._playback_rebounce_event.set()
pass
elif evt == Event.PAUSE: 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: 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 ( elif evt == Event.SHUTDOWN or (
evt == Event.END_FILE and event.get('event', {}).get('reason') evt == Event.END_FILE and event.get('event', {}).get('reason')
in [EndFile.EOF_OR_INIT_FAILURE, EndFile.ABORTED, EndFile.QUIT]): in [EndFile.EOF_OR_INIT_FAILURE, EndFile.ABORTED, EndFile.QUIT]):
@ -96,9 +94,13 @@ class MediaMpvPlugin(MediaPlugin):
for callback in self._on_stop_callbacks: for callback in self._on_stop_callbacks:
callback() callback()
elif evt == Event.SEEK:
bus.post(MediaSeekEvent(position=self._player.playback_time))
return callback return callback
def _get_youtube_link(self, resource): @staticmethod
def _get_youtube_link(resource):
base_url = 'https://youtu.be/' base_url = 'https://youtu.be/'
regexes = ['^https://(www\.)?youtube.com/watch\?v=([^?&#]+)', regexes = ['^https://(www\.)?youtube.com/watch\?v=([^?&#]+)',
'^https://(www\.)?youtu.be.com/([^?&#]+)', '^https://(www\.)?youtu.be.com/([^?&#]+)',
@ -109,17 +111,15 @@ class MediaMpvPlugin(MediaPlugin):
if m: return base_url + m.group(2) if m: return base_url + m.group(2)
return None return None
@action @action
def execute(self, cmd, **args): def execute(self, cmd, **args):
""" """
Execute a raw mpv command. Execute a raw mpv command.
""" """
if not self._player: if not self._player:
return (None, 'No mpv instance is running') return None, 'No mpv instance is running'
return self._player.command(cmd, *args) return self._player.command(cmd, *args)
@action @action
def play(self, resource, subtitles=None, **args): def play(self, resource, subtitles=None, **args):
""" """
@ -154,50 +154,47 @@ class MediaMpvPlugin(MediaPlugin):
if yt_resource: resource = yt_resource if yt_resource: resource = yt_resource
self._is_playing_torrent = False self._is_playing_torrent = False
ret = self._player.play(resource) self._player.play(resource)
return self.status() return self.status()
@action @action
def pause(self): def pause(self):
""" Toggle the paused state """ """ Toggle the paused state """
if not self._player: 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 self._player.pause = not self._player.pause
return self.status() return self.status()
@action @action
def quit(self): def quit(self):
""" Quit the player (same as `stop`) """ """ Stop and quit the player """
self._stop_torrent() self._stop_torrent()
if not self._player: if not self._player:
return (None, 'No mpv instance is running') return None, 'No mpv instance is running'
self._player.quit() self._player.quit()
self._player.terminate()
self._player = None self._player = None
# self._player.terminate()
return {'state': PlayerState.STOP.value} return {'state': PlayerState.STOP.value}
@action @action
def stop(self): def stop(self):
""" Stop the application (same as `quit`) """ """ Stop and quit the player """
return self.quit() return self.quit()
@action @action
def voldown(self, step=10.0): def voldown(self, step=10.0):
""" Volume down by (default: 10)% """ """ Volume down by (default: 10)% """
if not self._player: 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) return self.set_volume(self._player.volume-step)
@action @action
def volup(self, step=10.0): def volup(self, step=10.0):
""" Volume up by (default: 10)% """ """ Volume up by (default: 10)% """
if not self._player: 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) return self.set_volume(self._player.volume+step)
@action @action
@ -209,9 +206,9 @@ class MediaMpvPlugin(MediaPlugin):
:type volume: float :type volume: float
""" """
if not self._player: 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 self._player.volume = volume
return {'volume': volume} return {'volume': volume}
@ -220,13 +217,13 @@ class MediaMpvPlugin(MediaPlugin):
""" """
Seek backward/forward by the specified number of seconds Seek backward/forward by the specified number of seconds
:param relative_position: Number of seconds relative to the current cursor :param position: Number of seconds relative to the current cursor
:type relative_position: int :type position: int
""" """
if not self._player: if not self._player:
return (None, 'No mpv instance is running') return None, 'No mpv instance is running'
if not self._player.seekable: 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, pos = min(self._player.time_pos+self._player.time_remaining,
max(0, position)) max(0, position))
self._player.time_pos = pos self._player.time_pos = pos
@ -236,9 +233,9 @@ class MediaMpvPlugin(MediaPlugin):
def back(self, offset=60.0): def back(self, offset=60.0):
""" Back by (default: 60) seconds """ """ Back by (default: 60) seconds """
if not self._player: if not self._player:
return (None, 'No mpv instance is running') return None, 'No mpv instance is running'
if not self._player.seekable: 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) pos = max(0, self._player.time_pos-offset)
return self.seek(pos) return self.seek(pos)
@ -246,9 +243,9 @@ class MediaMpvPlugin(MediaPlugin):
def forward(self, offset=60.0): def forward(self, offset=60.0):
""" Forward by (default: 60) seconds """ """ Forward by (default: 60) seconds """
if not self._player: if not self._player:
return (None, 'No mpv instance is running') return None, 'No mpv instance is running'
if not self._player.seekable: 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, pos = min(self._player.time_pos+self._player.time_remaining,
self._player.time_pos+offset) self._player.time_pos+offset)
return self.seek(pos) return self.seek(pos)
@ -257,18 +254,18 @@ class MediaMpvPlugin(MediaPlugin):
def next(self): def next(self):
""" Play the next item in the queue """ """ Play the next item in the queue """
if not self._player: if not self._player:
return (None, 'No mpv instance is running') return None, 'No mpv instance is running'
self._player.playlist_next() self._player.playlist_next()
@action @action
def prev(self): def prev(self):
""" Play the previous item in the queue """ """ Play the previous item in the queue """
if not self._player: if not self._player:
return (None, 'No mpv instance is running') return None, 'No mpv instance is running'
self._player.playlist_prev() self._player.playlist_prev()
@action @action
def toggle_subtitles(self, visibile=None): def toggle_subtitles(self, visible=None):
""" Toggle the subtitles visibility """ """ Toggle the subtitles visibility """
return self.toggle_property('sub_visibility') return self.toggle_property('sub_visibility')
@ -322,7 +319,7 @@ class MediaMpvPlugin(MediaPlugin):
return props return props
@action @action
def set_subtitles(self, filename): def set_subtitles(self, filename, *args, **kwargs):
""" Sets media subtitles from filename """ """ Sets media subtitles from filename """
return self.set_property(subfile=filename, sub_visibility=True) return self.set_property(subfile=filename, sub_visibility=True)
@ -349,7 +346,7 @@ class MediaMpvPlugin(MediaPlugin):
""" """
if not self._player: if not self._player:
return self.play(resource, **args) return self.play(resource, **args)
return self.loadfile(resource, mode='append-play', **args) return self._player.loadfile(resource, mode='append-play', **args)
@action @action
def mute(self): def mute(self):
@ -385,9 +382,119 @@ class MediaMpvPlugin(MediaPlugin):
return {'state': PlayerState.STOP.value} return {'state': PlayerState.STOP.value}
return { return {
'filename': self._get_current_resource(), 'alang': getattr(self._player, 'alang'),
'state': (PlayerState.PAUSE.value if self._player.pause else 'aspect': getattr(self._player, 'aspect'),
PlayerState.PLAY.value), '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): def on_stop(self, callback):
@ -401,5 +508,4 @@ class MediaMpvPlugin(MediaPlugin):
else '') + self._player.stream_path else '') + self._player.stream_path
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -174,7 +174,7 @@ class MediaOmxplayerPlugin(MediaPlugin):
:type pause: bool :type pause: bool
""" """
if self._player: self._player.load(resource, pause) if self._player: self._player.load(resource, )
return self.status() return self.status()
@action @action

View file

@ -1,6 +1,4 @@
import os import os
import re
import threading
from platypush.context import get_bus, get_plugin from platypush.context import get_bus, get_plugin
from platypush.plugins.media import PlayerState, MediaPlugin from platypush.plugins.media import PlayerState, MediaPlugin
@ -232,8 +230,8 @@ class MediaVlcPlugin(MediaPlugin):
""" """
Seek backward/forward by the specified number of seconds Seek backward/forward by the specified number of seconds
:param relative_position: Number of seconds relative to the current cursor :param position: Number of seconds relative to the current cursor
:type relative_position: int :type position: int
""" """
if not self._player: if not self._player:
return (None, 'No vlc instance is running') return (None, 'No vlc instance is running')
@ -304,7 +302,7 @@ class MediaVlcPlugin(MediaPlugin):
self._player.set_fullscreen(fullscreen) self._player.set_fullscreen(fullscreen)
@action @action
def set_subtitles(self, filename): def set_subtitles(self, filename, **args):
""" Sets media subtitles from filename """ """ Sets media subtitles from filename """
if not self._player: if not self._player:
return (None, 'No vlc instance is running') return (None, 'No vlc instance is running')

View file

@ -1,4 +1,3 @@
import datetime
import enum import enum
import os import os
import re import re
@ -9,18 +8,16 @@ import time
from platypush.config import Config from platypush.config import Config
from platypush.context import get_bus, get_plugin from platypush.context import get_bus, get_plugin
from platypush.message.response import Response
from platypush.plugins.media import PlayerState, MediaPlugin from platypush.plugins.media import PlayerState, MediaPlugin
from platypush.message.event.torrent import TorrentDownloadStartEvent, \ from platypush.message.event.torrent import TorrentDownloadStartEvent, \
TorrentDownloadCompletedEvent, TorrentDownloadProgressEvent, \ TorrentDownloadCompletedEvent, TorrentDownloadingMetadataEvent
TorrentDownloadingMetadataEvent
from platypush.plugins import action from platypush.plugins import action
from platypush.utils import find_bins_in_path, find_files_by_ext, \ from platypush.utils import find_bins_in_path, find_files_by_ext, \
is_process_alive, get_ip_or_hostname is_process_alive, get_ip_or_hostname
class TorrentState(enum.Enum): class TorrentState(enum.IntEnum):
IDLE = 1 IDLE = 1
DOWNLOADING_METADATA = 2 DOWNLOADING_METADATA = 2
DOWNLOADING = 3 DOWNLOADING = 3
@ -35,8 +32,7 @@ class MediaWebtorrentPlugin(MediaPlugin):
* **webtorrent** installed on your system (``npm install -g webtorrent``) * **webtorrent** installed on your system (``npm install -g webtorrent``)
* **webtorrent-cli** installed on your system (``npm install -g webtorrent-cli``) * **webtorrent-cli** installed on your system (``npm install -g webtorrent-cli``)
* A media plugin configured for streaming (e.g. media.mplayer * A media plugin configured for streaming (e.g. media.mplayer, media.vlc, media.mpv or media.omxplayer)
or media.omxplayer)
""" """
_supported_media_plugins = {'media.mplayer', 'media.omxplayer', 'media.mpv', _supported_media_plugins = {'media.mplayer', 'media.omxplayer', 'media.mpv',
@ -65,15 +61,13 @@ class MediaWebtorrentPlugin(MediaPlugin):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.webtorrent_port = webtorrent_port self.webtorrent_port = webtorrent_port
self._webtorrent_process = None
self._init_webtorrent_bin(webtorrent_bin=webtorrent_bin) self._init_webtorrent_bin(webtorrent_bin=webtorrent_bin)
self._init_media_player() self._init_media_player()
self._download_started_event = threading.Event() self._download_started_event = threading.Event()
self._torrent_stream_urls = {} self._torrent_stream_urls = {}
def _init_webtorrent_bin(self, webtorrent_bin=None): def _init_webtorrent_bin(self, webtorrent_bin=None):
self._webtorrent_process = None
if not webtorrent_bin: if not webtorrent_bin:
bin_name = 'webtorrent.exe' if os.name == 'nt' else 'webtorrent' bin_name = 'webtorrent.exe' if os.name == 'nt' else 'webtorrent'
bins = find_bins_in_path(bin_name) bins = find_bins_in_path(bin_name)
@ -97,7 +91,6 @@ class MediaWebtorrentPlugin(MediaPlugin):
def _init_media_player(self): def _init_media_player(self):
self._media_plugin = None self._media_plugin = None
plugin_name = None
for plugin_name in self._supported_media_plugins: for plugin_name in self._supported_media_plugins:
try: try:
@ -113,12 +106,10 @@ class MediaWebtorrentPlugin(MediaPlugin):
'supported media plugins: {}').format( 'supported media plugins: {}').format(
self._supported_media_plugins)) self._supported_media_plugins))
def _read_process_line(self): def _read_process_line(self):
line = self._webtorrent_process.stdout.readline().decode().strip() line = self._webtorrent_process.stdout.readline().decode().strip()
# Strip output of the colors # 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, def _process_monitor(self, resource, download_dir, download_only,
player_type, player_args): player_type, player_args):
@ -192,7 +183,6 @@ class MediaWebtorrentPlugin(MediaPlugin):
stream_url=webtorrent_url)) stream_url=webtorrent_url))
break break
if not output_dir: if not output_dir:
raise RuntimeError('Could not download torrent') raise RuntimeError('Could not download torrent')
if not download_only and (not media_file or not webtorrent_url): 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 stop_evt = player._mplayer_stopped_event
elif media_cls == 'MediaMpvPlugin' or media_cls == 'MediaVlcPlugin': elif media_cls == 'MediaMpvPlugin' or media_cls == 'MediaVlcPlugin':
stop_evt = threading.Event() stop_evt = threading.Event()
def stop_callback(): def stop_callback():
stop_evt.set() stop_evt.set()
player.on_stop(stop_callback) player.on_stop(stop_callback)
elif media_cls == 'MediaOmxplayerPlugin': elif media_cls == 'MediaOmxplayerPlugin':
stop_evt = threading.Event() stop_evt = threading.Event()
def stop_callback(): def stop_callback():
stop_evt.set() stop_evt.set()
player.add_handler('stop', stop_callback) player.add_handler('stop', stop_callback)
@ -280,7 +272,6 @@ class MediaWebtorrentPlugin(MediaPlugin):
# Fallback: wait for the webtorrent process to terminate # Fallback: wait for the webtorrent process to terminate
self._webtorrent_process.wait() self._webtorrent_process.wait()
def _get_torrent_download_dir(self): def _get_torrent_download_dir(self):
if self._media_plugin.download_dir: if self._media_plugin.download_dir:
return 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 :param player_args: Any arguments to pass to the player plugin's
play() method play() method
:type player_args: dict :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: if self._webtorrent_process:
try: try:
self.quit() self.quit()
except: except Exception as e:
self.logger.debug('Failed to quit the previous instance: {}'. self.logger.debug('Failed to quit the previous instance: {}'.
format(str)) format(str(e)))
download_dir = self._get_torrent_download_dir() download_dir = self._get_torrent_download_dir()
webtorrent_args = [self.webtorrent_bin, 'download', '-o', download_dir] webtorrent_args = [self.webtorrent_bin, 'download', '-o', download_dir]
@ -370,12 +365,10 @@ class MediaWebtorrentPlugin(MediaPlugin):
return {'resource': resource, 'url': stream_url} return {'resource': resource, 'url': stream_url}
@action @action
def download(self, resource): def download(self, resource):
return self.play(resource, download_only=True) return self.play(resource, download_only=True)
@action @action
def stop(self): def stop(self):
""" Stop the playback """ """ Stop the playback """
@ -393,7 +386,7 @@ class MediaWebtorrentPlugin(MediaPlugin):
self._webtorrent_process = None self._webtorrent_process = None
@action @action
def load(self, resource): def load(self, resource, **kwargs):
""" """
Load a torrent resource in the player. Load a torrent resource in the player.
""" """
@ -417,8 +410,7 @@ class MediaWebtorrentPlugin(MediaPlugin):
} }
""" """
return {'state': self._media_plugin.status() return {'state': self._media_plugin.status().get('state', PlayerState.STOP.value)}
.get('state', PlayerState.STOP.value)}
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -1,5 +1,4 @@
import ast import ast
import errno
import hashlib import hashlib
import importlib import importlib
import inspect import inspect
@ -130,8 +129,6 @@ def _get_ssl_context(context_type=None, ssl_cert=None, ssl_key=None,
ssl_context = ssl.create_default_context(cafile=ssl_cafile, ssl_context = ssl.create_default_context(cafile=ssl_cafile,
capath=ssl_capath) capath=ssl_capath)
else: 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: if ssl_cafile or ssl_capath:
@ -227,8 +224,9 @@ def get_mime_type(resource):
with urllib.request.urlopen(resource) as response: with urllib.request.urlopen(resource) as response:
return response.info().get_content_type() return response.info().get_content_type()
else: else:
mime = magic.Magic(mime=True) mime = magic.detect_from_filename(resource)
return mime.from_file(resource) if mime:
return mime.mime_type
def camel_case_to_snake_case(string): def camel_case_to_snake_case(string):
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', string) s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', string)