New media webplugin WIP
This commit is contained in:
parent
9805ed0479
commit
4cd2e6949f
33 changed files with 708 additions and 316 deletions
|
@ -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')
|
||||||
|
|
||||||
|
|
|
@ -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']
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -43,5 +43,10 @@
|
||||||
button {
|
button {
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: $result-item-icon;
|
||||||
|
margin-right: .5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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') {
|
||||||
|
|
|
@ -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: () => {},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -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() {
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"> </i>
|
<i class="icon" :class="item.handler.iconClass" v-if="item.handler"> </i>
|
||||||
<span v-text="item.title"></span>
|
<span v-text="item.title"></span>
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
@ -384,12 +388,12 @@ class MediaPlugin(Plugin):
|
||||||
self.logger.info('Starting streaming {}'.format(media))
|
self.logger.info('Starting streaming {}'.format(media))
|
||||||
response = requests.put('{url}/media{download}'.format(
|
response = requests.put('{url}/media{download}'.format(
|
||||||
url=http.local_base_url, download='?download' if download else ''),
|
url=http.local_base_url, download='?download' if download else ''),
|
||||||
json = { 'source': media })
|
json={'source': media})
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,36 +206,36 @@ 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}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def seek(self, position):
|
def seek(self, position):
|
||||||
"""
|
"""
|
||||||
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
|
||||||
return { 'position': pos }
|
return {'position': pos}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
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):
|
||||||
|
@ -382,12 +379,122 @@ class MediaMpvPlugin(MediaPlugin):
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
if not self._player or not hasattr(self._player, 'pause'):
|
if not self._player or not hasattr(self._player, 'pause'):
|
||||||
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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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]
|
||||||
|
@ -368,14 +363,12 @@ class MediaWebtorrentPlugin(MediaPlugin):
|
||||||
'streaming after {} seconds').format(
|
'streaming after {} seconds').format(
|
||||||
self._web_stream_ready_timeout))
|
self._web_stream_ready_timeout))
|
||||||
|
|
||||||
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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue