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