New media webplugin WIP
This commit is contained in:
parent
37282ed9d5
commit
f86eeef549
6 changed files with 251 additions and 124 deletions
|
@ -38,7 +38,7 @@ MediaHandlers.youtube = MediaHandlers.base.extend({
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
matchesUrl: function(url) {
|
matchesUrl: function(url) {
|
||||||
return !!(url.match('^https?://(www\.)?youtube.com/') || url.match('^https?://youtu.be/'));
|
return !!(url.match('^https?://(www\.)?youtube.com/') || url.match('^https?://youtu.be/') || url.match('^https?://.*googlevideo.com/'));
|
||||||
},
|
},
|
||||||
|
|
||||||
getMetadata: function(url) {
|
getMetadata: function(url) {
|
||||||
|
|
|
@ -48,6 +48,7 @@ Vue.component('media', {
|
||||||
results: [],
|
results: [],
|
||||||
status: {},
|
status: {},
|
||||||
selectedDevice: {},
|
selectedDevice: {},
|
||||||
|
deviceHandlers: {},
|
||||||
|
|
||||||
loading: {
|
loading: {
|
||||||
results: false,
|
results: false,
|
||||||
|
@ -175,19 +176,13 @@ Vue.component('media', {
|
||||||
subtitles: item.subtitles,
|
subtitles: item.subtitles,
|
||||||
});
|
});
|
||||||
|
|
||||||
const hostRegex = /^(https?:\/\/[^:/]+(:[0-9]+)?\/?)/;
|
|
||||||
const baseURL = window.location.href.match(hostRegex)[1];
|
|
||||||
|
|
||||||
ret.url = ret.url.replace(hostRegex, baseURL);
|
|
||||||
if (ret.subtitles_url)
|
|
||||||
ret.subtitles_url = ret.subtitles_url.replace(hostRegex, baseURL);
|
|
||||||
|
|
||||||
this.bus.$emit('streaming-started', {
|
this.bus.$emit('streaming-started', {
|
||||||
url: ret.url,
|
url: ret.url,
|
||||||
resource: item.url,
|
resource: item.url,
|
||||||
|
subtitles_url: ret.subtiles_url,
|
||||||
});
|
});
|
||||||
|
|
||||||
return ret;
|
return {...item, ...ret};
|
||||||
},
|
},
|
||||||
|
|
||||||
searchSubs: function(item) {
|
searchSubs: function(item) {
|
||||||
|
@ -209,6 +204,9 @@ Vue.component('media', {
|
||||||
},
|
},
|
||||||
|
|
||||||
syncPosition: function(status) {
|
syncPosition: function(status) {
|
||||||
|
if (!status)
|
||||||
|
return;
|
||||||
|
|
||||||
status._syncTime = {
|
status._syncTime = {
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
position: status.position,
|
position: status.position,
|
||||||
|
@ -227,10 +225,36 @@ Vue.component('media', {
|
||||||
if (!this.status[dev.type])
|
if (!this.status[dev.type])
|
||||||
Vue.set(this.status, dev.type, {});
|
Vue.set(this.status, dev.type, {});
|
||||||
Vue.set(this.status[dev.type], dev.name, status);
|
Vue.set(this.status[dev.type], dev.name, status);
|
||||||
|
|
||||||
|
if (!this.deviceHandlers[dev.type])
|
||||||
|
Vue.set(this.deviceHandlers, dev.type, {});
|
||||||
|
Vue.set(this.deviceHandlers[dev.type], dev.name, dev);
|
||||||
},
|
},
|
||||||
|
|
||||||
onMediaEvent: async function(event) {
|
onMediaEvent: async function(event) {
|
||||||
let status = await request(event.plugin + '.status');
|
var type, player;
|
||||||
|
const plugin = event.plugin.replace(/^media\./, '');
|
||||||
|
|
||||||
|
if (this.status[event.player] && this.status[event.player][plugin]) {
|
||||||
|
type = event.player;
|
||||||
|
player = plugin;
|
||||||
|
} else if (this.status[plugin] && this.status[plugin][event.player]) {
|
||||||
|
type = plugin;
|
||||||
|
player = event.player;
|
||||||
|
}
|
||||||
|
|
||||||
|
var handler;
|
||||||
|
if (this.deviceHandlers[event.player] && this.deviceHandlers[event.player][plugin]) {
|
||||||
|
handler = this.deviceHandlers[event.player][plugin];
|
||||||
|
} else if (this.deviceHandlers[plugin] && this.deviceHandlers[plugin][event.player]) {
|
||||||
|
handler = this.deviceHandlers[plugin][event.player];
|
||||||
|
} else {
|
||||||
|
// No handlers
|
||||||
|
console.warn('No handlers found for device type '.concat(event.plugin, ' and player ', event.player));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = await handler.status(event.player);
|
||||||
this.syncPosition(status);
|
this.syncPosition(status);
|
||||||
|
|
||||||
if (event.resource) {
|
if (event.resource) {
|
||||||
|
@ -238,18 +262,6 @@ Vue.component('media', {
|
||||||
delete event.resource;
|
delete event.resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.plugin.startsWith('media.'))
|
|
||||||
event.plugin = event.plugin.substr(6);
|
|
||||||
|
|
||||||
var type, player;
|
|
||||||
if (this.status[event.player] && this.status[event.player][event.plugin]) {
|
|
||||||
type = event.player;
|
|
||||||
player = event.plugin;
|
|
||||||
} else if (this.status[event.plugin] && this.status[event.plugin][event.player]) {
|
|
||||||
type = event.plugin;
|
|
||||||
player = event.player;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.state !== 'stop') {
|
if (status.state !== 'stop') {
|
||||||
status.title = status.title || this.status[type][player].title;
|
status.title = status.title || this.status[type][player].title;
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,16 +40,17 @@ MediaPlayers.browser = Vue.extend({
|
||||||
|
|
||||||
play: async function(item, subtitles) {
|
play: async function(item, subtitles) {
|
||||||
let url = item.url;
|
let url = item.url;
|
||||||
if (item.source && item.source.startsWith('file://'))
|
|
||||||
url += '?webplayer'
|
|
||||||
|
|
||||||
let playerWindow = window.open(url, '_blank');
|
if (item.source && !item.source.match('https?://')) {
|
||||||
console.log(playerWindow);
|
// Non-HTTP resource streamed over HTTP
|
||||||
|
const hostRegex = /^(https?:\/\/[^:/]+(:[0-9]+)?\/?)/;
|
||||||
|
const baseURL = window.location.href.match(hostRegex)[1];
|
||||||
|
url = url.replace(hostRegex, baseURL) + '?webplayer';
|
||||||
|
}
|
||||||
|
|
||||||
|
window.open(url, '_blank');
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
|
||||||
stop: async function() {
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -48,14 +48,40 @@ MediaPlayers.chromecast = Vue.extend({
|
||||||
return await request('media.chromecast.get_chromecasts');
|
return await request('media.chromecast.get_chromecasts');
|
||||||
},
|
},
|
||||||
|
|
||||||
status: async function() {
|
status: async function(device) {
|
||||||
return {};
|
return await request('media.chromecast.status', {chromecast: device || this.device.name});
|
||||||
},
|
},
|
||||||
|
|
||||||
play: async function(item) {
|
play: async function(item) {
|
||||||
|
return await request('media.chromecast.play', {
|
||||||
|
resource: item.url,
|
||||||
|
chromecast: this.device.name,
|
||||||
|
title: item.title || item.url,
|
||||||
|
subtitles: item.subtitles_url,
|
||||||
|
content_type: item.mime_type,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
pause: async function() {
|
||||||
|
return await request('media.chromecast.pause', {chromecast: this.device.name});
|
||||||
},
|
},
|
||||||
|
|
||||||
stop: async function() {
|
stop: async function() {
|
||||||
|
return await request('media.chromecast.stop', {chromecast: this.device.name});
|
||||||
|
},
|
||||||
|
|
||||||
|
seek: async function(position) {
|
||||||
|
return await request('media.chromecast.set_position', {
|
||||||
|
position: position,
|
||||||
|
chromecast: this.device.name,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setVolume: async function(volume) {
|
||||||
|
return await request('media.chromecast.set_volume', {
|
||||||
|
volume: volume,
|
||||||
|
chromecast: this.device.name,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,15 +1,56 @@
|
||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
import pychromecast
|
import pychromecast
|
||||||
|
import time
|
||||||
|
|
||||||
from pychromecast.controllers.youtube import YouTubeController
|
from pychromecast.controllers.youtube import YouTubeController
|
||||||
|
|
||||||
from platypush.context import get_plugin, get_bus
|
from platypush.context import get_plugin, get_bus
|
||||||
from platypush.plugins import Plugin, action
|
from platypush.plugins import action
|
||||||
from platypush.plugins.media import MediaPlugin
|
from platypush.plugins.media import MediaPlugin
|
||||||
from platypush.utils import get_mime_type
|
from platypush.utils import get_mime_type
|
||||||
from platypush.message.event.media import MediaPlayEvent, MediaPlayRequestEvent, \
|
from platypush.message.event.media import MediaPlayEvent, MediaPlayRequestEvent, \
|
||||||
MediaStopEvent, MediaPauseEvent, NewPlayingMediaEvent
|
MediaStopEvent, MediaPauseEvent, NewPlayingMediaEvent, MediaVolumeChangedEvent, MediaSeekEvent
|
||||||
|
|
||||||
|
|
||||||
|
def convert_status(status):
|
||||||
|
attrs = [a for a in dir(status) if not a.startswith('_')
|
||||||
|
and not callable(getattr(status, a))]
|
||||||
|
|
||||||
|
renamed_attrs = {
|
||||||
|
'current_time': 'position',
|
||||||
|
'player_state': 'state',
|
||||||
|
'supports_seek': 'seekable',
|
||||||
|
'volume_level': 'volume',
|
||||||
|
'volume_muted': 'mute',
|
||||||
|
'content_id': 'url',
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = {}
|
||||||
|
for attr in attrs:
|
||||||
|
value = getattr(status, attr)
|
||||||
|
if attr == 'volume_level':
|
||||||
|
value *= 100
|
||||||
|
if attr == 'player_state':
|
||||||
|
value = value.lower()
|
||||||
|
if value == 'paused':
|
||||||
|
value = 'pause'
|
||||||
|
if value == 'playing':
|
||||||
|
value = 'play'
|
||||||
|
if isinstance(value, datetime.datetime):
|
||||||
|
value = value.isoformat()
|
||||||
|
|
||||||
|
if attr in renamed_attrs:
|
||||||
|
ret[renamed_attrs[attr]] = value
|
||||||
|
else:
|
||||||
|
ret[attr] = value
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def post_event(evt_type, **evt):
|
||||||
|
bus = get_bus()
|
||||||
|
bus.post(evt_type(player=evt.get('device'), plugin='media.chromecast', **evt))
|
||||||
|
|
||||||
|
|
||||||
class MediaChromecastPlugin(MediaPlugin):
|
class MediaChromecastPlugin(MediaPlugin):
|
||||||
|
@ -31,6 +72,47 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
STREAM_TYPE_BUFFERED = "BUFFERED"
|
STREAM_TYPE_BUFFERED = "BUFFERED"
|
||||||
STREAM_TYPE_LIVE = "LIVE"
|
STREAM_TYPE_LIVE = "LIVE"
|
||||||
|
|
||||||
|
class MediaListener:
|
||||||
|
def __init__(self, name, cast):
|
||||||
|
self.name = name
|
||||||
|
self.cast = cast
|
||||||
|
self.status = convert_status(cast.media_controller.status)
|
||||||
|
self.last_status_timestamp = time.time()
|
||||||
|
|
||||||
|
def new_media_status(self, status):
|
||||||
|
status = convert_status(status)
|
||||||
|
|
||||||
|
if status.get('url') and status.get('url') != self.status.get('url'):
|
||||||
|
post_event(NewPlayingMediaEvent, resource=status['url'],
|
||||||
|
title=status.get('title'), device=self.name)
|
||||||
|
if status.get('state') != self.status.get('state'):
|
||||||
|
if status.get('state') == 'play':
|
||||||
|
post_event(MediaPlayEvent, resource=status['url'], device=self.name)
|
||||||
|
elif status.get('state') == 'pause':
|
||||||
|
post_event(MediaPauseEvent, resource=status['url'], device=self.name)
|
||||||
|
elif status.get('state') in ['stop', 'idle']:
|
||||||
|
post_event(MediaStopEvent, device=self.name)
|
||||||
|
if status.get('volume') != self.status.get('volume'):
|
||||||
|
post_event(MediaVolumeChangedEvent, volume=status.get('volume'), device=self.name)
|
||||||
|
if abs(status.get('position') - self.status.get('position')) > time.time() - self.last_status_timestamp + 5:
|
||||||
|
post_event(MediaSeekEvent, position=status.get('position'), device=self.name)
|
||||||
|
|
||||||
|
self.last_status_timestamp = time.time()
|
||||||
|
self.status = status
|
||||||
|
|
||||||
|
class SubtitlesAsyncHandler:
|
||||||
|
def __init__(self, mc, subtitle_id):
|
||||||
|
self.mc = mc
|
||||||
|
self.subtitle_id = subtitle_id
|
||||||
|
self.initialized = False
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def new_media_status(self, status):
|
||||||
|
if self.subtitle_id and not self.initialized:
|
||||||
|
self.mc.update_status()
|
||||||
|
self.mc.enable_subtitle(self.subtitle_id)
|
||||||
|
self.initialized = True
|
||||||
|
|
||||||
def __init__(self, chromecast=None, *args, **kwargs):
|
def __init__(self, chromecast=None, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
:param chromecast: Default Chromecast to cast to if no name is specified
|
:param chromecast: Default Chromecast to cast to if no name is specified
|
||||||
|
@ -42,7 +124,7 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
self._is_local = False
|
self._is_local = False
|
||||||
self.chromecast = chromecast
|
self.chromecast = chromecast
|
||||||
self.chromecasts = {}
|
self.chromecasts = {}
|
||||||
|
self._media_listeners = {}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def get_chromecasts(self, tries=2, retry_wait=10, timeout=60,
|
def get_chromecasts(self, tries=2, retry_wait=10, timeout=60,
|
||||||
|
@ -76,7 +158,10 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
callback=callback)
|
callback=callback)
|
||||||
})
|
})
|
||||||
|
|
||||||
return [ {
|
for name, cast in self.chromecasts.items():
|
||||||
|
self._update_listeners(name, cast)
|
||||||
|
|
||||||
|
return [{
|
||||||
'type': cc.cast_type,
|
'type': cc.cast_type,
|
||||||
'name': cc.name,
|
'name': cc.name,
|
||||||
'manufacturer': cc.device.manufacturer,
|
'manufacturer': cc.device.manufacturer,
|
||||||
|
@ -99,8 +184,13 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
'volume': round(100*cc.status.volume_level, 2),
|
'volume': round(100*cc.status.volume_level, 2),
|
||||||
'muted': cc.status.volume_muted,
|
'muted': cc.status.volume_muted,
|
||||||
} if cc.status else {}),
|
} if cc.status else {}),
|
||||||
} for cc in self.chromecasts.values() ]
|
} for cc in self.chromecasts.values()]
|
||||||
|
|
||||||
|
def _update_listeners(self, name, cast):
|
||||||
|
if name not in self._media_listeners:
|
||||||
|
cast.start()
|
||||||
|
self._media_listeners[name] = self.MediaListener(name=name, cast=cast)
|
||||||
|
cast.media_controller.register_status_listener(self._media_listeners[name])
|
||||||
|
|
||||||
def get_chromecast(self, chromecast=None, n_tries=2):
|
def get_chromecast(self, chromecast=None, n_tries=2):
|
||||||
if isinstance(chromecast, pychromecast.Chromecast):
|
if isinstance(chromecast, pychromecast.Chromecast):
|
||||||
|
@ -111,7 +201,6 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
raise RuntimeError('No Chromecast specified nor default Chromecast configured')
|
raise RuntimeError('No Chromecast specified nor default Chromecast configured')
|
||||||
chromecast = self.chromecast
|
chromecast = self.chromecast
|
||||||
|
|
||||||
|
|
||||||
if chromecast not in self.chromecasts:
|
if chromecast not in self.chromecasts:
|
||||||
casts = {}
|
casts = {}
|
||||||
while n_tries > 0:
|
while n_tries > 0:
|
||||||
|
@ -128,8 +217,9 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
if chromecast not in self.chromecasts:
|
if chromecast not in self.chromecasts:
|
||||||
raise RuntimeError('Device {} not found'.format(chromecast))
|
raise RuntimeError('Device {} not found'.format(chromecast))
|
||||||
|
|
||||||
return self.chromecasts[chromecast]
|
cast = self.chromecasts[chromecast]
|
||||||
|
cast.wait()
|
||||||
|
return cast
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def play(self, resource, content_type=None, chromecast=None, title=None,
|
def play(self, resource, content_type=None, chromecast=None, title=None,
|
||||||
|
@ -146,7 +236,8 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
:param content_type: Content type as a MIME type string
|
:param content_type: Content type as a MIME type string
|
||||||
:type content_type: str
|
:type content_type: str
|
||||||
|
|
||||||
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast will be used.
|
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast
|
||||||
|
will be used.
|
||||||
:type chromecast: str
|
:type chromecast: str
|
||||||
|
|
||||||
:param title: Optional title
|
:param title: Optional title
|
||||||
|
@ -180,11 +271,8 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
if not chromecast:
|
if not chromecast:
|
||||||
chromecast = self.chromecast
|
chromecast = self.chromecast
|
||||||
|
|
||||||
get_bus().post(MediaPlayRequestEvent(resource=resource,
|
post_event(MediaPlayRequestEvent, resource=resource, device=chromecast)
|
||||||
device=chromecast))
|
|
||||||
|
|
||||||
cast = self.get_chromecast(chromecast)
|
cast = self.get_chromecast(chromecast)
|
||||||
cast.wait()
|
|
||||||
|
|
||||||
mc = cast.media_controller
|
mc = cast.media_controller
|
||||||
yt = self._get_youtube_url(resource)
|
yt = self._get_youtube_url(resource)
|
||||||
|
@ -201,7 +289,7 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
resource = self._get_resource(resource)
|
resource = self._get_resource(resource)
|
||||||
|
|
||||||
if resource.startswith('magnet:?'):
|
if resource.startswith('magnet:?'):
|
||||||
player_args = { 'chromecast': cast }
|
player_args = {'chromecast': chromecast}
|
||||||
return get_plugin('media.webtorrent').play(resource,
|
return get_plugin('media.webtorrent').play(resource,
|
||||||
player='chromecast',
|
player='chromecast',
|
||||||
**player_args)
|
**player_args)
|
||||||
|
@ -226,11 +314,11 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
subtitles_lang=subtitles_lang,
|
subtitles_lang=subtitles_lang,
|
||||||
subtitles_mime=subtitles_mime, subtitle_id=subtitle_id)
|
subtitles_mime=subtitles_mime, subtitle_id=subtitle_id)
|
||||||
|
|
||||||
|
if subtitles:
|
||||||
|
mc.register_status_listener(self.SubtitlesAsyncHandler(mc, subtitle_id))
|
||||||
|
|
||||||
mc.block_until_active()
|
mc.block_until_active()
|
||||||
get_bus().post(MediaPlayEvent(resource=resource,
|
return self.status(chromecast=chromecast)
|
||||||
device=chromecast))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_youtube_url(cls, url):
|
def _get_youtube_url(cls, url):
|
||||||
|
@ -250,32 +338,40 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def pause(self, chromecast=None):
|
def pause(self, chromecast=None):
|
||||||
cast = self.get_chromecast(chromecast or self.chromecast)
|
chromecast = chromecast or self.chromecast
|
||||||
if cast.media_controller.is_paused:
|
cast = self.get_chromecast(chromecast)
|
||||||
ret = cast.media_controller.play()
|
|
||||||
get_bus().post(MediaPlayEvent(device=chromecast or self.chromecast))
|
|
||||||
return ret
|
|
||||||
elif cast.media_controller.is_playing:
|
|
||||||
ret = cast.media_controller.pause()
|
|
||||||
get_bus().post(MediaPauseEvent(device=chromecast or self.chromecast))
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
if cast.media_controller.is_paused:
|
||||||
|
cast.media_controller.play()
|
||||||
|
elif cast.media_controller.is_playing:
|
||||||
|
cast.media_controller.pause()
|
||||||
|
|
||||||
|
cast.wait()
|
||||||
|
return self.status(chromecast=chromecast)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def stop(self, chromecast=None):
|
def stop(self, chromecast=None):
|
||||||
ret = self.get_chromecast(chromecast or self.chromecast).media_controller.stop()
|
chromecast = chromecast or self.chromecast
|
||||||
get_bus().post(MediaStopEvent(device=chromecast or self.chromecast))
|
cast = self.get_chromecast(chromecast)
|
||||||
return ret
|
cast.media_controller.stop()
|
||||||
|
cast.wait()
|
||||||
|
|
||||||
|
return self.status(chromecast=chromecast)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def rewind(self, chromecast=None):
|
def rewind(self, chromecast=None):
|
||||||
return self.get_chromecast(chromecast or self.chromecast).media_controller.rewind()
|
chromecast = chromecast or self.chromecast
|
||||||
|
cast = self.get_chromecast(chromecast)
|
||||||
|
cast.media_controller.rewind()
|
||||||
|
cast.wait()
|
||||||
|
return self.status(chromecast=chromecast)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def set_position(self, position, chromecast=None):
|
def set_position(self, position, chromecast=None):
|
||||||
return self.get_chromecast(chromecast or self.chromecast).media_controller.seek(position)
|
cast = self.get_chromecast(chromecast or self.chromecast)
|
||||||
|
cast.media_controller.seek(position)
|
||||||
|
cast.wait()
|
||||||
|
return self.status(chromecast=chromecast)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def seek(self, position, chromecast=None):
|
def seek(self, position, chromecast=None):
|
||||||
|
@ -283,39 +379,41 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def back(self, chromecast=None, offset=60):
|
def back(self, chromecast=None, offset=60):
|
||||||
mc = self.get_chromecast(chromecast or self.chromecast).media_controller
|
cast = self.get_chromecast(chromecast or self.chromecast)
|
||||||
|
mc = cast.media_controller
|
||||||
if mc.status.current_time:
|
if mc.status.current_time:
|
||||||
return mc.seek(mc.status.current_time-offset)
|
mc.seek(mc.status.current_time-offset)
|
||||||
|
cast.wait()
|
||||||
|
|
||||||
|
return self.status(chromecast=chromecast)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def forward(self, chromecast=None, offset=60):
|
def forward(self, chromecast=None, offset=60):
|
||||||
mc = self.get_chromecast(chromecast or self.chromecast).media_controller
|
cast = self.get_chromecast(chromecast or self.chromecast)
|
||||||
|
mc = cast.media_controller
|
||||||
if mc.status.current_time:
|
if mc.status.current_time:
|
||||||
return mc.seek(mc.status.current_time+offset)
|
mc.seek(mc.status.current_time+offset)
|
||||||
|
cast.wait()
|
||||||
|
|
||||||
|
return self.status(chromecast=chromecast)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def is_playing(self, chromecast=None):
|
def is_playing(self, chromecast=None):
|
||||||
return self.get_chromecast(chromecast or self.chromecast).media_controller.is_playing
|
return self.get_chromecast(chromecast or self.chromecast).media_controller.is_playing
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def is_paused(self, chromecast=None):
|
def is_paused(self, chromecast=None):
|
||||||
return self.get_chromecast(chromecast or self.chromecast).media_controller.is_paused
|
return self.get_chromecast(chromecast or self.chromecast).media_controller.is_paused
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def is_idle(self, chromecast=None):
|
def is_idle(self, chromecast=None):
|
||||||
return self.get_chromecast(chromecast or self.chromecast).media_controller.is_idle
|
return self.get_chromecast(chromecast or self.chromecast).media_controller.is_idle
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def list_subtitles(self, chromecast=None):
|
def list_subtitles(self, chromecast=None):
|
||||||
return self.get_chromecast(chromecast or self.chromecast) \
|
return self.get_chromecast(chromecast or self.chromecast) \
|
||||||
.media_controller.subtitle_tracks
|
.media_controller.subtitle_tracks
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def enable_subtitles(self, chromecast=None, track_id=None):
|
def enable_subtitles(self, chromecast=None, track_id=None):
|
||||||
mc = self.get_chromecast(chromecast or self.chromecast).media_controller
|
mc = self.get_chromecast(chromecast or self.chromecast).media_controller
|
||||||
|
@ -324,7 +422,6 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
elif mc.subtitle_tracks:
|
elif mc.subtitle_tracks:
|
||||||
return mc.enable_subtitle(mc.subtitle_tracks[0].get('trackId'))
|
return mc.enable_subtitle(mc.subtitle_tracks[0].get('trackId'))
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def disable_subtitles(self, chromecast=None, track_id=None):
|
def disable_subtitles(self, chromecast=None, track_id=None):
|
||||||
mc = self.get_chromecast(chromecast or self.chromecast).media_controller
|
mc = self.get_chromecast(chromecast or self.chromecast).media_controller
|
||||||
|
@ -344,51 +441,26 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
else:
|
else:
|
||||||
return self.enable_subtitles(chromecast, all_subs[0].get('trackId'))
|
return self.enable_subtitles(chromecast, all_subs[0].get('trackId'))
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def status(self, chromecast=None):
|
def status(self, chromecast=None):
|
||||||
status = self.get_chromecast(chromecast or self.chromecast) \
|
cast = self.get_chromecast(chromecast or self.chromecast)
|
||||||
.media_controller.status
|
status = cast.media_controller.status
|
||||||
attrs = [a for a in dir(status) if not a.startswith('_')
|
return convert_status(status)
|
||||||
and not callable(getattr(status, a))]
|
|
||||||
renamed_attrs = {
|
|
||||||
'player_state': 'state',
|
|
||||||
'volume_level': 'volume',
|
|
||||||
'volume_muted': 'muted',
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = {}
|
|
||||||
for attr in attrs:
|
|
||||||
value = getattr(status, attr)
|
|
||||||
if attr == 'volume_level':
|
|
||||||
value *= 100
|
|
||||||
if attr == 'player_state':
|
|
||||||
value = value.lower()
|
|
||||||
if value == 'paused': value = 'pause'
|
|
||||||
if value == 'playing': value = 'play'
|
|
||||||
if isinstance(value, datetime.datetime):
|
|
||||||
value = value.isoformat()
|
|
||||||
|
|
||||||
if attr in renamed_attrs:
|
|
||||||
ret[renamed_attrs[attr]] = value
|
|
||||||
else:
|
|
||||||
ret[attr] = value
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def disconnect(self, chromecast=None, timeout=None, blocking=True):
|
def disconnect(self, chromecast=None, timeout=None, blocking=True):
|
||||||
"""
|
"""
|
||||||
Disconnect a Chromecast and wait for it to terminate
|
Disconnect a Chromecast and wait for it to terminate
|
||||||
|
|
||||||
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast will be used.
|
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast
|
||||||
|
will be used.
|
||||||
:type chromecast: str
|
:type chromecast: str
|
||||||
|
|
||||||
:param timeout: Number of seconds to wait for disconnection (default: None: block until termination)
|
:param timeout: Number of seconds to wait for disconnection (default: None: block until termination)
|
||||||
:type timeout: float
|
:type timeout: float
|
||||||
|
|
||||||
:param blocking: If set (default), then the code will wait until disconnection, otherwise it will return immediately.
|
:param blocking: If set (default), then the code will wait until disconnection, otherwise it will return
|
||||||
|
immediately.
|
||||||
:type blocking: bool
|
:type blocking: bool
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -396,29 +468,28 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
cast.disconnect(timeout=timeout, blocking=blocking)
|
cast.disconnect(timeout=timeout, blocking=blocking)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def join(self, chromecast=None, timeout=None, blocking=True):
|
def join(self, chromecast=None, timeout=None):
|
||||||
"""
|
"""
|
||||||
Blocks the thread until the Chromecast connection is terminated.
|
Blocks the thread until the Chromecast connection is terminated.
|
||||||
|
|
||||||
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast will be used.
|
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast
|
||||||
|
will be used.
|
||||||
:type chromecast: str
|
:type chromecast: str
|
||||||
|
|
||||||
:param timeout: Number of seconds to wait for disconnection (default: None: block until termination)
|
:param timeout: Number of seconds to wait for disconnection (default: None: block until termination)
|
||||||
:type timeout: float
|
:type timeout: float
|
||||||
|
|
||||||
:param blocking: If set (default), then the code will wait until disconnection, otherwise it will return immediately.
|
|
||||||
:type blocking: bool
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
cast = self.get_chromecast(chromecast)
|
cast = self.get_chromecast(chromecast)
|
||||||
cast.join(timeout=timeout, blocking=blocking)
|
cast.join(timeout=timeout)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def quit(self, chromecast=None):
|
def quit(self, chromecast=None):
|
||||||
"""
|
"""
|
||||||
Exits the current app on the Chromecast
|
Exits the current app on the Chromecast
|
||||||
|
|
||||||
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast will be used.
|
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast
|
||||||
|
will be used.
|
||||||
:type chromecast: str
|
:type chromecast: str
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -430,7 +501,8 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
"""
|
"""
|
||||||
Reboots the Chromecast
|
Reboots the Chromecast
|
||||||
|
|
||||||
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast will be used.
|
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast
|
||||||
|
will be used.
|
||||||
:type chromecast: str
|
:type chromecast: str
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -445,58 +517,74 @@ class MediaChromecastPlugin(MediaPlugin):
|
||||||
:param volume: Volume to be set, between 0 and 100
|
:param volume: Volume to be set, between 0 and 100
|
||||||
:type volume: float
|
:type volume: float
|
||||||
|
|
||||||
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast will be used.
|
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast
|
||||||
|
will be used.
|
||||||
:type chromecast: str
|
:type chromecast: str
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
chromecast = chromecast or self.chromecast
|
||||||
cast = self.get_chromecast(chromecast)
|
cast = self.get_chromecast(chromecast)
|
||||||
cast.set_volume(volume/100)
|
cast.set_volume(volume/100)
|
||||||
|
cast.wait()
|
||||||
|
status = self.status(chromecast=chromecast)
|
||||||
|
status.output['volume'] = volume
|
||||||
|
return status
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def volup(self, chromecast=None, step=10):
|
def volup(self, chromecast=None, step=10):
|
||||||
"""
|
"""
|
||||||
Turn up the Chromecast volume by 10% or step.
|
Turn up the Chromecast volume by 10% or step.
|
||||||
|
|
||||||
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast will be used.
|
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast
|
||||||
|
will be used.
|
||||||
:type chromecast: str
|
:type chromecast: str
|
||||||
|
|
||||||
:param step: Volume increment between 0 and 100 (default: 100%)
|
:param step: Volume increment between 0 and 100 (default: 100%)
|
||||||
:type step: float
|
:type step: float
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
chromecast = chromecast or self.chromecast
|
||||||
cast = self.get_chromecast(chromecast)
|
cast = self.get_chromecast(chromecast)
|
||||||
step /= 100
|
step /= 100
|
||||||
cast.volume_up(min(step, 1))
|
cast.volume_up(min(step, 1))
|
||||||
|
cast.wait()
|
||||||
|
return self.status(chromecast=chromecast)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def voldown(self, chromecast=None, step=10):
|
def voldown(self, chromecast=None, step=10):
|
||||||
"""
|
"""
|
||||||
Turn down the Chromecast volume by 10% or step.
|
Turn down the Chromecast volume by 10% or step.
|
||||||
|
|
||||||
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast will be used.
|
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast
|
||||||
|
will be used.
|
||||||
:type chromecast: str
|
:type chromecast: str
|
||||||
|
|
||||||
:param step: Volume decrement between 0 and 100 (default: 100%)
|
:param step: Volume decrement between 0 and 100 (default: 100%)
|
||||||
:type step: float
|
:type step: float
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
chromecast = chromecast or self.chromecast
|
||||||
cast = self.get_chromecast(chromecast)
|
cast = self.get_chromecast(chromecast)
|
||||||
step /= 100
|
step /= 100
|
||||||
cast.volume_down(max(step, 0))
|
cast.volume_down(max(step, 0))
|
||||||
|
cast.wait()
|
||||||
|
return self.status(chromecast=chromecast)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def mute(self, chromecast=None):
|
def mute(self, chromecast=None):
|
||||||
"""
|
"""
|
||||||
Toggle the mute status on the Chromecast
|
Toggle the mute status on the Chromecast
|
||||||
|
|
||||||
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast will be used.
|
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast
|
||||||
|
will be used.
|
||||||
:type chromecast: str
|
:type chromecast: str
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
chromecast = chromecast or self.chromecast
|
||||||
cast = self.get_chromecast(chromecast)
|
cast = self.get_chromecast(chromecast)
|
||||||
cast.set_volume_muted(not cast.status.volume_muted)
|
cast.set_volume_muted(not cast.status.volume_muted)
|
||||||
|
cast.wait()
|
||||||
|
return self.status(chromecast=chromecast)
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -366,7 +366,7 @@ class MediaWebtorrentPlugin(MediaPlugin):
|
||||||
return {'resource': resource, 'url': stream_url}
|
return {'resource': resource, 'url': stream_url}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def download(self, resource):
|
def download(self, resource, **kwargs):
|
||||||
return self.play(resource, download_only=True)
|
return self.play(resource, download_only=True)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|
Loading…
Reference in a new issue