Added Plex and Chromecast plugins
This commit is contained in:
parent
9a88f85cda
commit
1459630661
6 changed files with 447 additions and 1 deletions
|
@ -195,6 +195,7 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
|
|||
'inputs',
|
||||
'inotify',
|
||||
'omxplayer',
|
||||
'plexapi',
|
||||
]
|
||||
|
||||
sys.path.insert(0, os.path.abspath('../..'))
|
||||
|
|
262
platypush/plugins/media/chromecast.py
Normal file
262
platypush/plugins/media/chromecast.py
Normal file
|
@ -0,0 +1,262 @@
|
|||
import pychromecast
|
||||
|
||||
from platypush.plugins import Plugin, action
|
||||
|
||||
|
||||
class MediaChromecastPlugin(Plugin):
|
||||
"""
|
||||
Plugin to interact with Chromecast devices
|
||||
|
||||
Requires:
|
||||
|
||||
* **pychromecast** (``pip install pychromecast``)
|
||||
"""
|
||||
|
||||
STREAM_TYPE_UNKNOWN = "UNKNOWN"
|
||||
STREAM_TYPE_BUFFERED = "BUFFERED"
|
||||
STREAM_TYPE_LIVE = "LIVE"
|
||||
|
||||
def __init__(self, chromecast=None, *args, **kwargs):
|
||||
"""
|
||||
:param chromecast: Default Chromecast to cast to if no name is specified
|
||||
:type chromecast: str
|
||||
"""
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.chromecast = chromecast
|
||||
self.chromecasts = {}
|
||||
|
||||
|
||||
@action
|
||||
def get_chromecasts(self):
|
||||
"""
|
||||
Get the list of Chromecast devices
|
||||
"""
|
||||
|
||||
return [ {
|
||||
'type': cc.cast_type,
|
||||
'name': cc.name,
|
||||
'manufacturer': cc.device.manufacturer,
|
||||
'model_name': cc.model_name,
|
||||
'uuid': str(cc.device.uuid),
|
||||
'address': cc.host,
|
||||
'port': cc.port,
|
||||
|
||||
'status': ({
|
||||
'app': {
|
||||
'id': cc.app_id,
|
||||
'name': cc.app_display_name,
|
||||
},
|
||||
|
||||
'is_active_input': cc.status.is_active_input,
|
||||
'is_stand_by': cc.status.is_stand_by,
|
||||
'is_idle': cc.is_idle,
|
||||
'namespaces': cc.status.namespaces,
|
||||
'volume': round(100*cc.status.volume_level, 2),
|
||||
'muted': cc.status.volume_muted,
|
||||
} if cc.status else {}),
|
||||
} for cc in pychromecast.get_chromecasts() ]
|
||||
|
||||
|
||||
def _get_chromecast(self, chromecast=None):
|
||||
if not chromecast:
|
||||
if not self.chromecast:
|
||||
raise RuntimeError('No Chromecast specified nor default Chromecast configured')
|
||||
chromecast = self.chromecast
|
||||
|
||||
|
||||
if chromecast not in self.chromecasts:
|
||||
chromecasts = pychromecast.get_chromecasts()
|
||||
cast = next(cc for cc in pychromecast.get_chromecasts()
|
||||
if cc.device.friendly_name == chromecast)
|
||||
self.chromecasts[chromecast] = cast
|
||||
else:
|
||||
cast = self.chromecasts[chromecast]
|
||||
|
||||
if not cast:
|
||||
raise RuntimeError('No such Chromecast: {}'.format(chromecast))
|
||||
|
||||
return cast
|
||||
|
||||
@action
|
||||
def cast_media(self, media, content_type, chromecast=None, title=None,
|
||||
image_url=None, autoplay=True, current_time=0,
|
||||
stream_type=STREAM_TYPE_BUFFERED, subtitles=None,
|
||||
subtitles_lang='en-US', subtitles_mime='text/vtt',
|
||||
subtitle_id=1):
|
||||
"""
|
||||
Cast media to a visible Chromecast
|
||||
|
||||
:param media: Media to cast
|
||||
:type media: str
|
||||
|
||||
:param content_type: Content type
|
||||
:type content_type: str
|
||||
|
||||
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast will be used.
|
||||
:type chromecast: str
|
||||
|
||||
:param title: Optional title
|
||||
:type title: str
|
||||
|
||||
:param image_url: URL of the image to use for the thumbnail
|
||||
:type image_url: str
|
||||
|
||||
:param autoplay: Set it to false if you don't want the content to start playing immediately (default: true)
|
||||
:type autoplay: bool
|
||||
|
||||
:param current_time: Time to start the playback in seconds (default: 0)
|
||||
:type current_time: int
|
||||
|
||||
:param stream_type: Type of stream to cast. Can be BUFFERED (default), LIVE or UNKNOWN
|
||||
:type stream_type: str
|
||||
|
||||
:param subtitles: URL of the subtitles to be shown
|
||||
:type subtitles: str
|
||||
|
||||
:param subtitles_lang: Subtitles language (default: en-US)
|
||||
:type subtitles_lang: str
|
||||
|
||||
:param subtitles_mime: Subtitles MIME type (default: text/vtt)
|
||||
:type subtitles_mime: str
|
||||
|
||||
:param subtitle_id: ID of the subtitles to be loaded (default: 1)
|
||||
:type subtitle_id: int
|
||||
"""
|
||||
|
||||
cast = self._get_chromecast(chromecast)
|
||||
cast.wait()
|
||||
mc = cast.media_controller
|
||||
mc.play_media(media, content_type, title=title, thumb=image_url,
|
||||
current_time=current_time, autoplay=autoplay,
|
||||
stream_type=stream_type, subtitles=subtitles,
|
||||
subtitles_lang=subtitles_lang, subtitles_mime=subtitles_mime,
|
||||
subtitle_id=subtitle_id)
|
||||
|
||||
mc.block_until_active()
|
||||
|
||||
@action
|
||||
def disconnect(self, chromecast=None, timeout=None, blocking=True):
|
||||
"""
|
||||
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.
|
||||
:type chromecast: str
|
||||
|
||||
:param timeout: Number of seconds to wait for disconnection (default: None: block until termination)
|
||||
: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.disconnect(timeout=timeout, blocking=blocking)
|
||||
|
||||
@action
|
||||
def join(self, chromecast=None, timeout=None, blocking=True):
|
||||
"""
|
||||
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.
|
||||
:type chromecast: str
|
||||
|
||||
:param timeout: Number of seconds to wait for disconnection (default: None: block until termination)
|
||||
: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.join(timeout=timeout, blocking=blocking)
|
||||
|
||||
@action
|
||||
def quit_app(self, chromecast=None):
|
||||
"""
|
||||
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.
|
||||
:type chromecast: str
|
||||
"""
|
||||
|
||||
cast = self._get_chromecast(chromecast)
|
||||
cast.quit_app()
|
||||
|
||||
@action
|
||||
def reboot(self, chromecast=None):
|
||||
"""
|
||||
Reboots the Chromecast
|
||||
|
||||
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast will be used.
|
||||
:type chromecast: str
|
||||
"""
|
||||
|
||||
cast = self._get_chromecast(chromecast)
|
||||
cast.reboot()
|
||||
|
||||
@action
|
||||
def set_volume(self, volume, chromecast=None):
|
||||
"""
|
||||
Set the Chromecast volume
|
||||
|
||||
:param volume: Volume to be set, between 0 and 100
|
||||
:type volume: float
|
||||
|
||||
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast will be used.
|
||||
:type chromecast: str
|
||||
"""
|
||||
|
||||
cast = self._get_chromecast(chromecast)
|
||||
cast.set_volume(volume/100)
|
||||
|
||||
@action
|
||||
def volume_up(self, chromecast=None, delta=10):
|
||||
"""
|
||||
Turn up the Chromecast volume by 10% or delta.
|
||||
|
||||
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast will be used.
|
||||
:type chromecast: str
|
||||
|
||||
:param delta: Volume increment between 0 and 100 (default: 100%)
|
||||
:type delta: float
|
||||
"""
|
||||
|
||||
cast = self._get_chromecast(chromecast)
|
||||
delta /= 100
|
||||
cast.volume_up(min(delta, 1))
|
||||
|
||||
|
||||
@action
|
||||
def volume_down(self, chromecast=None, delta=10):
|
||||
"""
|
||||
Turn down the Chromecast volume by 10% or delta.
|
||||
|
||||
:param chromecast: Chromecast to cast to. If none is specified, then the default configured Chromecast will be used.
|
||||
:type chromecast: str
|
||||
|
||||
:param delta: Volume decrement between 0 and 100 (default: 100%)
|
||||
:type delta: float
|
||||
"""
|
||||
|
||||
cast = self._get_chromecast(chromecast)
|
||||
delta /= 100
|
||||
cast.volume_down(max(delta, 0))
|
||||
|
||||
|
||||
@action
|
||||
def toggle_mute(self, chromecast=None):
|
||||
"""
|
||||
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.
|
||||
:type chromecast: str
|
||||
"""
|
||||
|
||||
cast = self._get_chromecast(chromecast)
|
||||
cast.set_volume_muted(not cast.status.volume_muted)
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
@ -9,7 +9,7 @@ class MediaKodiPlugin(Plugin):
|
|||
|
||||
Requires:
|
||||
|
||||
* **kodi-json** (``pip install kodi-rtmidi``)
|
||||
* **kodi-json** (``pip install kodi-json``)
|
||||
"""
|
||||
|
||||
def __init__(self, url, username=None, password=None, *args, **kwargs):
|
||||
|
|
171
platypush/plugins/media/plex.py
Normal file
171
platypush/plugins/media/plex.py
Normal file
|
@ -0,0 +1,171 @@
|
|||
from plexapi.myplex import MyPlexAccount
|
||||
from plexapi.video import Movie, Show
|
||||
|
||||
from platypush.plugins import Plugin, action
|
||||
|
||||
|
||||
class MediaPlexPlugin(Plugin):
|
||||
"""
|
||||
Plugin to interact with a Plex media server
|
||||
|
||||
Requires:
|
||||
|
||||
* **plexapi** (``pip install plexapi``)
|
||||
"""
|
||||
|
||||
def __init__(self, server, username, password, *args, **kwargs):
|
||||
"""
|
||||
:param server: Plex server name
|
||||
:type server: str
|
||||
|
||||
:param username: Plex username
|
||||
:type username: str
|
||||
|
||||
:param password: Plex password
|
||||
:type username: str
|
||||
"""
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.resource = MyPlexAccount(username, password).resource(server)
|
||||
self._plex = None
|
||||
|
||||
|
||||
@property
|
||||
def plex(self):
|
||||
if not self._plex:
|
||||
self._plex = self.resource.connect()
|
||||
|
||||
return self._plex
|
||||
|
||||
|
||||
@action
|
||||
def get_clients(self):
|
||||
"""
|
||||
Get the list of active clients
|
||||
"""
|
||||
|
||||
return [{
|
||||
'device': c.device,
|
||||
'device_class': c.deviceClass,
|
||||
'local': c.local,
|
||||
'model': c.model,
|
||||
'platform': c.platform,
|
||||
'platform_version': c.platformVersion,
|
||||
'product': c.product,
|
||||
'state': c.state,
|
||||
'title': c.title,
|
||||
'version': c.version,
|
||||
} for c in self.plex.clients()]
|
||||
|
||||
|
||||
def _get_client(self, name):
|
||||
return self.plex.client(name)
|
||||
|
||||
|
||||
@action
|
||||
def search(self, section=None, title=None, **kwargs):
|
||||
"""
|
||||
Return all the items matching the search criteria (default: all library items)
|
||||
|
||||
:param section: Section to search (Movies, Shows etc.)
|
||||
:type section: str
|
||||
|
||||
:param title: Full or partial title
|
||||
:type title: str
|
||||
|
||||
:param kwargs: Search criteria - includes e.g. title, unwatched, director, genre etc.
|
||||
:type kwargs: dict
|
||||
"""
|
||||
|
||||
ret = []
|
||||
library = self.plex.library
|
||||
|
||||
if section:
|
||||
library = library.section(section)
|
||||
|
||||
if title or kwargs:
|
||||
items = library.search(title, **kwargs)
|
||||
else:
|
||||
items = library.all()
|
||||
|
||||
for item in items:
|
||||
video_item = {
|
||||
'summary': item.summary,
|
||||
'title': item.title,
|
||||
'type': item.type,
|
||||
'rating': item.rating,
|
||||
}
|
||||
|
||||
if isinstance(item, Movie):
|
||||
video_item['is_watched'] = item.isWatched
|
||||
video_item['view_offset'] = item.viewOffset
|
||||
video_item['view_count'] = item.viewCount
|
||||
video_item['year'] = item.year
|
||||
|
||||
video_item['media'] = [
|
||||
{
|
||||
'duration': (item.media[i].duration or 0)/1000,
|
||||
'width': item.media[i].width,
|
||||
'height': item.media[i].height,
|
||||
'audio_channels': item.media[i].audioChannels,
|
||||
'audio_codec': item.media[i].audioCodec,
|
||||
'video_codec': item.media[i].videoCodec,
|
||||
'video_resolution': item.media[i].videoResolution,
|
||||
'video_frame_rate': item.media[i].videoFrameRate,
|
||||
'parts': [
|
||||
{
|
||||
'file': part.file,
|
||||
'duration': (part.duration or 0)/1000,
|
||||
} for part in item.media[i].parts
|
||||
]
|
||||
} for i in range(0, len(item.media))
|
||||
]
|
||||
elif isinstance(item, Show):
|
||||
video_item['media'] = [
|
||||
{
|
||||
'title': season.title,
|
||||
'season_number': season.seasonNumber,
|
||||
'summary': season.summary,
|
||||
'episodes': [
|
||||
{
|
||||
'duration': episode.duration/1000,
|
||||
'index': episode.index,
|
||||
'year': episode.year,
|
||||
'season_number': episode.seasonNumber,
|
||||
'season_episode': episode.seasonEpisode,
|
||||
'summary': episode.summary,
|
||||
'is_watched': episode.isWatched,
|
||||
'view_count': episode.viewCount,
|
||||
'view_offset': episode.viewOffset,
|
||||
'media': [
|
||||
{
|
||||
'duration': episode.media[i].duration/1000,
|
||||
'width': episode.media[i].width,
|
||||
'height': episode.media[i].height,
|
||||
'audio_channels': episode.media[i].audioChannels,
|
||||
'audio_codec': episode.media[i].audioCodec,
|
||||
'video_codec': episode.media[i].videoCodec,
|
||||
'video_resolution': episode.media[i].videoResolution,
|
||||
'video_frame_rate': episode.media[i].videoFrameRate,
|
||||
'title': episode.title,
|
||||
'parts': [
|
||||
{
|
||||
'file': part.file,
|
||||
'duration': part.duration/1000,
|
||||
} for part in episode.media[i].parts
|
||||
]
|
||||
} for i in range(0, len(episode.locations))
|
||||
]
|
||||
} for episode in season.episodes()
|
||||
]
|
||||
} for season in item.seasons()
|
||||
]
|
||||
|
||||
ret.append(video_item)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
@ -99,3 +99,12 @@ pyHS100
|
|||
# Support for joystick backend
|
||||
inputs
|
||||
|
||||
# Support for Kodi
|
||||
kodi-json
|
||||
|
||||
# Support for Plex
|
||||
plexapi
|
||||
|
||||
# Support for Chromecast
|
||||
pychromecast
|
||||
|
||||
|
|
3
setup.py
3
setup.py
|
@ -88,6 +88,9 @@ setup(
|
|||
'Support for smart cards detection': ['pyscard'],
|
||||
'Support for ICal calendars': ['icalendar', 'python-dateutil'],
|
||||
'Support for joystick backend': ['inputs'],
|
||||
'Support for Kodi plugin': ['kodi-json'],
|
||||
'Support for Plex plugin': ['plexapi'],
|
||||
'Support for Chromecast plugin': ['pychromecast'],
|
||||
# 'Support for Leap Motion backend': ['git+ssh://git@github.com:BlackLight/leap-sdk-python3.git'],
|
||||
# 'Support for Flic buttons': ['git+ssh://git@github.com/50ButtonsEach/fliclib-linux-hci']
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue