From 06de7053ce3667ef26e9b8f3ba41894051855f89 Mon Sep 17 00:00:00 2001
From: John <mezz@johnmihalic.com>
Date: Mon, 31 Oct 2016 08:29:08 -0400
Subject: [PATCH] Add Emby Server media_player component (#3862)

* Add Emby Server media_player component

* Code cleanup, move to request sessions, generate UUID per session

* Make media image fetch more robust

* Allow for http or https

* Cleanup some Keyerror conditions found through more testing

* Move EmbyRemote to pip, update requirements

* Code cleanup, add SSL config option
---
 .coveragerc                                   |   1 +
 homeassistant/components/media_player/emby.py | 304 ++++++++++++++++++
 requirements_all.txt                          |   3 +
 3 files changed, 308 insertions(+)
 create mode 100644 homeassistant/components/media_player/emby.py

diff --git a/.coveragerc b/.coveragerc
index 5695c4fad0e..3bf0f7c8249 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -179,6 +179,7 @@ omit =
     homeassistant/components/media_player/cmus.py
     homeassistant/components/media_player/denon.py
     homeassistant/components/media_player/directv.py
+    homeassistant/components/media_player/emby.py
     homeassistant/components/media_player/firetv.py
     homeassistant/components/media_player/gpmdp.py
     homeassistant/components/media_player/itunes.py
diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py
new file mode 100644
index 00000000000..3422fadbc10
--- /dev/null
+++ b/homeassistant/components/media_player/emby.py
@@ -0,0 +1,304 @@
+"""
+Support to interface with the Emby API.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/media_player.emby/
+"""
+import logging
+
+from datetime import timedelta
+
+import voluptuous as vol
+
+import homeassistant.helpers.config_validation as cv
+from homeassistant.components.media_player import (
+    MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
+    SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK, MediaPlayerDevice,
+    PLATFORM_SCHEMA)
+from homeassistant.const import (
+    CONF_HOST, CONF_API_KEY, CONF_PORT, CONF_SSL, DEVICE_DEFAULT_NAME,
+    STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN)
+from homeassistant.helpers.event import (track_utc_time_change)
+from homeassistant.util import Throttle
+
+REQUIREMENTS = ['pyemby==0.1']
+
+MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
+MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
+
+DEFAULT_PORT = 8096
+
+_LOGGER = logging.getLogger(__name__)
+
+SUPPORT_EMBY = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
+    SUPPORT_STOP | SUPPORT_SEEK
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+    vol.Optional(CONF_HOST, default='localhost'): cv.string,
+    vol.Optional(CONF_SSL, default=False): cv.boolean,
+    vol.Required(CONF_API_KEY): cv.string,
+    vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
+})
+
+
+def setup_platform(hass, config, add_devices_callback, discovery_info=None):
+    """Setup the Emby platform."""
+    from pyemby.emby import EmbyRemote
+
+    _host = config.get(CONF_HOST)
+    _key = config.get(CONF_API_KEY)
+    _port = config.get(CONF_PORT)
+
+    if config.get(CONF_SSL):
+        _protocol = "https"
+    else:
+        _protocol = "http"
+
+    _url = '{}://{}:{}'.format(_protocol, _host, _port)
+
+    _LOGGER.debug('Setting up Emby server at: %s', _url)
+
+    embyserver = EmbyRemote(_key, _url)
+
+    emby_clients = {}
+    emby_sessions = {}
+    track_utc_time_change(hass, lambda now: update_devices(), second=30)
+
+    @Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
+    def update_devices():
+        """Update the devices objects."""
+        devices = embyserver.get_sessions()
+        if devices is None:
+            _LOGGER.error('Error listing Emby devices.')
+            return
+
+        new_emby_clients = []
+        for device in devices:
+            if device['DeviceId'] == embyserver.unique_id:
+                break
+
+            if device['DeviceId'] not in emby_clients:
+                _LOGGER.debug('New Emby DeviceID: %s. Adding to Clients.',
+                              device['DeviceId'])
+                new_client = EmbyClient(embyserver, device, emby_sessions,
+                                        update_devices, update_sessions)
+                emby_clients[device['DeviceId']] = new_client
+                new_emby_clients.append(new_client)
+            else:
+                emby_clients[device['DeviceId']].set_device(device)
+
+        if new_emby_clients:
+            add_devices_callback(new_emby_clients)
+
+    @Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
+    def update_sessions():
+        """Update the sessions objects."""
+        sessions = embyserver.get_sessions()
+        if sessions is None:
+            _LOGGER.error('Error listing Emby sessions')
+            return
+
+        emby_sessions.clear()
+        for session in sessions:
+            emby_sessions[session['DeviceId']] = session
+
+    update_devices()
+    update_sessions()
+
+
+class EmbyClient(MediaPlayerDevice):
+    """Representation of a Emby device."""
+
+    # pylint: disable=too-many-arguments, too-many-public-methods,
+    # pylint: disable=abstract-method
+    def __init__(self, client, device, emby_sessions, update_devices,
+                 update_sessions):
+        """Initialize the Emby device."""
+        self.emby_sessions = emby_sessions
+        self.update_devices = update_devices
+        self.update_sessions = update_sessions
+        self.client = client
+        self.set_device(device)
+
+    def set_device(self, device):
+        """Set the device property."""
+        self.device = device
+
+    @property
+    def unique_id(self):
+        """Return the id of this emby client."""
+        return '{}.{}'.format(
+            self.__class__, self.device['DeviceId'])
+
+    @property
+    def supports_remote_control(self):
+        """Return control ability."""
+        return self.device['SupportsRemoteControl']
+
+    @property
+    def name(self):
+        """Return the name of the device."""
+        return 'emby_{}'.format(self.device['DeviceName']) or \
+            DEVICE_DEFAULT_NAME
+
+    @property
+    def session(self):
+        """Return the session, if any."""
+        if self.device['DeviceId'] not in self.emby_sessions:
+            return None
+
+        return self.emby_sessions[self.device['DeviceId']]
+
+    @property
+    def now_playing_item(self):
+        """Return the currently playing item, if any."""
+        session = self.session
+        if session is not None and 'NowPlayingItem' in session:
+            return session['NowPlayingItem']
+
+    @property
+    def state(self):
+        """Return the state of the device."""
+        session = self.session
+        if session:
+            if 'NowPlayingItem' in session:
+                if session['PlayState']['IsPaused']:
+                    return STATE_PAUSED
+                else:
+                    return STATE_PLAYING
+            else:
+                return STATE_IDLE
+        # This is nasty. Need to find a way to determine alive
+        else:
+            return STATE_OFF
+
+        return STATE_UNKNOWN
+
+    def update(self):
+        """Get the latest details."""
+        self.update_devices(no_throttle=True)
+        self.update_sessions(no_throttle=True)
+
+    def play_percent(self):
+        """Return current media percent complete."""
+        if self.now_playing_item['RunTimeTicks'] and \
+                self.session['PlayState']['PositionTicks']:
+            try:
+                return int(self.session['PlayState']['PositionTicks']) / \
+                    int(self.now_playing_item['RunTimeTicks']) * 100
+            except KeyError:
+                return 0
+        else:
+            return 0
+
+    @property
+    def app_name(self):
+        """Return current user as app_name."""
+        # Ideally the media_player object would have a user property.
+        try:
+            return self.device['UserName']
+        except KeyError:
+            return None
+
+    @property
+    def media_content_id(self):
+        """Content ID of current playing media."""
+        if self.now_playing_item is not None:
+            try:
+                return self.now_playing_item['Id']
+            except KeyError:
+                return None
+
+    @property
+    def media_content_type(self):
+        """Content type of current playing media."""
+        if self.now_playing_item is None:
+            return None
+        try:
+            media_type = self.now_playing_item['Type']
+            if media_type == 'Episode':
+                return MEDIA_TYPE_TVSHOW
+            elif media_type == 'Movie':
+                return MEDIA_TYPE_VIDEO
+            return None
+        except KeyError:
+            return None
+
+    @property
+    def media_duration(self):
+        """Duration of current playing media in seconds."""
+        if self.now_playing_item and self.media_content_type:
+            try:
+                return int(self.now_playing_item['RunTimeTicks']) / 10000000
+            except KeyError:
+                return None
+
+    @property
+    def media_image_url(self):
+        """Image url of current playing media."""
+        if self.now_playing_item is not None:
+            try:
+                return self.client.get_image(
+                    self.now_playing_item['ThumbItemId'], 'Thumb',
+                    self.play_percent())
+            except KeyError:
+                try:
+                    return self.client.get_image(
+                        self.now_playing_item['PrimaryImageItemId'], 'Primary',
+                        self.play_percent())
+                except KeyError:
+                    return None
+
+    @property
+    def media_title(self):
+        """Title of current playing media."""
+        # find a string we can use as a title
+        if self.now_playing_item is not None:
+            return self.now_playing_item['Name']
+
+    @property
+    def media_season(self):
+        """Season of curent playing media (TV Show only)."""
+        if self.now_playing_item is not None and \
+           'ParentIndexNumber' in self.now_playing_item:
+            return self.now_playing_item['ParentIndexNumber']
+
+    @property
+    def media_series_title(self):
+        """The title of the series of current playing media (TV Show only)."""
+        if self.now_playing_item is not None and \
+           'SeriesName' in self.now_playing_item:
+            return self.now_playing_item['SeriesName']
+
+    @property
+    def media_episode(self):
+        """Episode of current playing media (TV Show only)."""
+        if self.now_playing_item is not None and \
+           'IndexNumber' in self.now_playing_item:
+            return self.now_playing_item['IndexNumber']
+
+    @property
+    def supported_media_commands(self):
+        """Flag of media commands that are supported."""
+        if self.supports_remote_control:
+            return SUPPORT_EMBY
+        else:
+            return None
+
+    def media_play(self):
+        """Send play command."""
+        if self.supports_remote_control:
+            self.client.play(self.session)
+
+    def media_pause(self):
+        """Send pause command."""
+        if self.supports_remote_control:
+            self.client.pause(self.session)
+
+    def media_next_track(self):
+        """Send next track command."""
+        self.client.next_track(self.session)
+
+    def media_previous_track(self):
+        """Send previous track command."""
+        self.client.previous_track(self.session)
diff --git a/requirements_all.txt b/requirements_all.txt
index b5c68ff4e7e..0c296981896 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -349,6 +349,9 @@ pycmus==0.1.0
 # homeassistant.components.zwave
 pydispatcher==2.0.5
 
+# homeassistant.components.media_player.emby
+pyemby==0.1
+
 # homeassistant.components.envisalink
 pyenvisalink==1.7
 
-- 
GitLab