"""Support to interface with the Plex API."""
from datetime import timedelta
import json
import logging

import requests
import voluptuous as vol

from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
from homeassistant.components.media_player.const import (
    MEDIA_TYPE_MOVIE,
    MEDIA_TYPE_MUSIC,
    MEDIA_TYPE_TVSHOW,
    SUPPORT_NEXT_TRACK,
    SUPPORT_PAUSE,
    SUPPORT_PLAY,
    SUPPORT_PREVIOUS_TRACK,
    SUPPORT_STOP,
    SUPPORT_TURN_OFF,
    SUPPORT_VOLUME_MUTE,
    SUPPORT_VOLUME_SET,
)
from homeassistant.const import (
    DEVICE_DEFAULT_NAME,
    STATE_IDLE,
    STATE_OFF,
    STATE_PAUSED,
    STATE_PLAYING,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import track_time_interval
from homeassistant.util import dt as dt_util
from homeassistant.util.json import load_json, save_json

_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)

NAME_FORMAT = "Plex {}"
PLEX_CONFIG_FILE = "plex.conf"
PLEX_DATA = "plex"

CONF_USE_EPISODE_ART = "use_episode_art"
CONF_SHOW_ALL_CONTROLS = "show_all_controls"
CONF_REMOVE_UNAVAILABLE_CLIENTS = "remove_unavailable_clients"
CONF_CLIENT_REMOVE_INTERVAL = "client_remove_interval"

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
    {
        vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean,
        vol.Optional(CONF_SHOW_ALL_CONTROLS, default=False): cv.boolean,
        vol.Optional(CONF_REMOVE_UNAVAILABLE_CLIENTS, default=True): cv.boolean,
        vol.Optional(
            CONF_CLIENT_REMOVE_INTERVAL, default=timedelta(seconds=600)
        ): vol.All(cv.time_period, cv.positive_timedelta),
    }
)


def setup_platform(hass, config, add_entities_callback, discovery_info=None):
    """Set up the Plex platform."""
    if PLEX_DATA not in hass.data:
        hass.data[PLEX_DATA] = {}

    # get config from plex.conf
    file_config = load_json(hass.config.path(PLEX_CONFIG_FILE))

    if file_config:
        # Setup a configured PlexServer
        host, host_config = file_config.popitem()
        token = host_config["token"]
        try:
            has_ssl = host_config["ssl"]
        except KeyError:
            has_ssl = False
        try:
            verify_ssl = host_config["verify"]
        except KeyError:
            verify_ssl = True

    # Via discovery
    elif discovery_info is not None:
        # Parse discovery data
        host = discovery_info.get("host")
        port = discovery_info.get("port")
        host = "%s:%s" % (host, port)
        _LOGGER.info("Discovered PLEX server: %s", host)

        if host in _CONFIGURING:
            return
        token = None
        has_ssl = False
        verify_ssl = True
    else:
        return

    setup_plexserver(
        host, token, has_ssl, verify_ssl, hass, config, add_entities_callback
    )


def setup_plexserver(
    host, token, has_ssl, verify_ssl, hass, config, add_entities_callback
):
    """Set up a plexserver based on host parameter."""
    import plexapi.server
    import plexapi.exceptions

    cert_session = None
    http_prefix = "https" if has_ssl else "http"
    if has_ssl and (verify_ssl is False):
        _LOGGER.info("Ignoring SSL verification")
        cert_session = requests.Session()
        cert_session.verify = False
    try:
        plexserver = plexapi.server.PlexServer(
            "%s://%s" % (http_prefix, host), token, cert_session
        )
        _LOGGER.info("Discovery configuration done (no token needed)")
    except (
        plexapi.exceptions.BadRequest,
        plexapi.exceptions.Unauthorized,
        plexapi.exceptions.NotFound,
    ) as error:
        _LOGGER.info(error)
        # No token or wrong token
        request_configuration(host, hass, config, add_entities_callback)
        return

    # If we came here and configuring this host, mark as done
    if host in _CONFIGURING:
        request_id = _CONFIGURING.pop(host)
        configurator = hass.components.configurator
        configurator.request_done(request_id)
        _LOGGER.info("Discovery configuration done")

    # Save config
    save_json(
        hass.config.path(PLEX_CONFIG_FILE),
        {host: {"token": token, "ssl": has_ssl, "verify": verify_ssl}},
    )

    _LOGGER.info("Connected to: %s://%s", http_prefix, host)

    plex_clients = hass.data[PLEX_DATA]
    plex_sessions = {}
    track_time_interval(hass, lambda now: update_devices(), timedelta(seconds=10))

    def update_devices():
        """Update the devices objects."""
        try:
            devices = plexserver.clients()
        except plexapi.exceptions.BadRequest:
            _LOGGER.exception("Error listing plex devices")
            return
        except requests.exceptions.RequestException as ex:
            _LOGGER.warning(
                "Could not connect to plex server at http://%s (%s)", host, ex
            )
            return

        new_plex_clients = []
        available_client_ids = []
        for device in devices:
            # For now, let's allow all deviceClass types
            if device.deviceClass in ["badClient"]:
                continue

            available_client_ids.append(device.machineIdentifier)

            if device.machineIdentifier not in plex_clients:
                new_client = PlexClient(
                    config, device, None, plex_sessions, update_devices
                )
                plex_clients[device.machineIdentifier] = new_client
                _LOGGER.debug("New device: %s", device.machineIdentifier)
                new_plex_clients.append(new_client)
            else:
                _LOGGER.debug("Refreshing device: %s", device.machineIdentifier)
                plex_clients[device.machineIdentifier].refresh(device, None)

        # add devices with a session and no client (ex. PlexConnect Apple TV's)
        try:
            sessions = plexserver.sessions()
        except plexapi.exceptions.BadRequest:
            _LOGGER.exception("Error listing plex sessions")
            return
        except requests.exceptions.RequestException as ex:
            _LOGGER.warning(
                "Could not connect to plex server at http://%s (%s)", host, ex
            )
            return

        plex_sessions.clear()
        for session in sessions:
            for player in session.players:
                plex_sessions[player.machineIdentifier] = session, player

        for machine_identifier, (session, player) in plex_sessions.items():
            if machine_identifier in available_client_ids:
                # Avoid using session if already added as a device.
                _LOGGER.debug("Skipping session, device exists: %s", machine_identifier)
                continue

            if (
                machine_identifier not in plex_clients
                and machine_identifier is not None
            ):
                new_client = PlexClient(
                    config, player, session, plex_sessions, update_devices
                )
                plex_clients[machine_identifier] = new_client
                _LOGGER.debug("New session: %s", machine_identifier)
                new_plex_clients.append(new_client)
            else:
                _LOGGER.debug("Refreshing session: %s", machine_identifier)
                plex_clients[machine_identifier].refresh(None, session)

        clients_to_remove = []
        for client in plex_clients.values():
            # force devices to idle that do not have a valid session
            if client.session is None:
                client.force_idle()

            client.set_availability(
                client.machine_identifier in available_client_ids
                or client.machine_identifier in plex_sessions
            )

            if client not in new_plex_clients:
                client.schedule_update_ha_state()

            if not config.get(CONF_REMOVE_UNAVAILABLE_CLIENTS) or client.available:
                continue

            if (dt_util.utcnow() - client.marked_unavailable) >= (
                config.get(CONF_CLIENT_REMOVE_INTERVAL)
            ):
                hass.add_job(client.async_remove())
                clients_to_remove.append(client.machine_identifier)

        while clients_to_remove:
            del plex_clients[clients_to_remove.pop()]

        if new_plex_clients:
            add_entities_callback(new_plex_clients)


def request_configuration(host, hass, config, add_entities_callback):
    """Request configuration steps from the user."""
    configurator = hass.components.configurator
    # We got an error if this method is called while we are configuring
    if host in _CONFIGURING:
        configurator.notify_errors(
            _CONFIGURING[host], "Failed to register, please try again."
        )

        return

    def plex_configuration_callback(data):
        """Handle configuration changes."""
        setup_plexserver(
            host,
            data.get("token"),
            cv.boolean(data.get("has_ssl")),
            cv.boolean(data.get("do_not_verify_ssl")),
            hass,
            config,
            add_entities_callback,
        )

    _CONFIGURING[host] = configurator.request_config(
        "Plex Media Server",
        plex_configuration_callback,
        description="Enter the X-Plex-Token",
        entity_picture="/static/images/logo_plex_mediaserver.png",
        submit_caption="Confirm",
        fields=[
            {"id": "token", "name": "X-Plex-Token", "type": ""},
            {"id": "has_ssl", "name": "Use SSL", "type": ""},
            {"id": "do_not_verify_ssl", "name": "Do not verify SSL", "type": ""},
        ],
    )


class PlexClient(MediaPlayerDevice):
    """Representation of a Plex device."""

    def __init__(self, config, device, session, plex_sessions, update_devices):
        """Initialize the Plex device."""
        self._app_name = ""
        self._device = None
        self._available = False
        self._marked_unavailable = None
        self._device_protocol_capabilities = None
        self._is_player_active = False
        self._is_player_available = False
        self._player = None
        self._machine_identifier = None
        self._make = ""
        self._name = None
        self._player_state = "idle"
        self._previous_volume_level = 1  # Used in fake muting
        self._session = None
        self._session_type = None
        self._session_username = None
        self._state = STATE_IDLE
        self._volume_level = 1  # since we can't retrieve remotely
        self._volume_muted = False  # since we can't retrieve remotely
        self.config = config
        self.plex_sessions = plex_sessions
        self.update_devices = update_devices
        # General
        self._media_content_id = None
        self._media_content_rating = None
        self._media_content_type = None
        self._media_duration = None
        self._media_image_url = None
        self._media_title = None
        self._media_position = None
        self._media_position_updated_at = None
        # Music
        self._media_album_artist = None
        self._media_album_name = None
        self._media_artist = None
        self._media_track = None
        # TV Show
        self._media_episode = None
        self._media_season = None
        self._media_series_title = None

        self.refresh(device, session)

    def _clear_media_details(self):
        """Set all Media Items to None."""
        # General
        self._media_content_id = None
        self._media_content_rating = None
        self._media_content_type = None
        self._media_duration = None
        self._media_image_url = None
        self._media_title = None
        # Music
        self._media_album_artist = None
        self._media_album_name = None
        self._media_artist = None
        self._media_track = None
        # TV Show
        self._media_episode = None
        self._media_season = None
        self._media_series_title = None

        # Clear library Name
        self._app_name = ""

    def refresh(self, device, session):
        """Refresh key device data."""
        import plexapi.exceptions

        # new data refresh
        self._clear_media_details()

        if session:  # Not being triggered by Chrome or FireTablet Plex App
            self._session = session
        if device:
            self._device = device
            try:
                device_url = self._device.url("/")
            except plexapi.exceptions.BadRequest:
                device_url = "127.0.0.1"
            if "127.0.0.1" in device_url:
                self._device.proxyThroughServer()
            self._session = None
            self._machine_identifier = self._device.machineIdentifier
            self._name = NAME_FORMAT.format(self._device.title or DEVICE_DEFAULT_NAME)
            self._device_protocol_capabilities = self._device.protocolCapabilities

            # set valid session, preferring device session
            if self._device.machineIdentifier in self.plex_sessions:
                self._session = self.plex_sessions.get(
                    self._device.machineIdentifier, [None, None]
                )[0]

        if self._session:
            if (
                self._device is not None
                and self._device.machineIdentifier is not None
                and self._session.players
            ):
                self._is_player_available = True
                self._player = [
                    p
                    for p in self._session.players
                    if p.machineIdentifier == self._device.machineIdentifier
                ][0]
                self._name = NAME_FORMAT.format(self._player.title)
                self._player_state = self._player.state
                self._session_username = self._session.usernames[0]
                self._make = self._player.device
            else:
                self._is_player_available = False

            # Calculate throttled position for proper progress display.
            position = int(self._session.viewOffset / 1000)
            now = dt_util.utcnow()
            if self._media_position is not None:
                pos_diff = position - self._media_position
                time_diff = now - self._media_position_updated_at
                if pos_diff != 0 and abs(time_diff.total_seconds() - pos_diff) > 5:
                    self._media_position_updated_at = now
                    self._media_position = position
            else:
                self._media_position_updated_at = now
                self._media_position = position

            self._media_content_id = self._session.ratingKey
            self._media_content_rating = getattr(self._session, "contentRating", None)

        self._set_player_state()

        if self._is_player_active and self._session is not None:
            self._session_type = self._session.type
            self._media_duration = int(self._session.duration / 1000)
            #  title (movie name, tv episode name, music song name)
            self._media_title = self._session.title
            # media type
            self._set_media_type()
            self._app_name = (
                self._session.section().title
                if self._session.section() is not None
                else ""
            )
            self._set_media_image()
        else:
            self._session_type = None

    def _set_media_image(self):
        thumb_url = self._session.thumbUrl
        if self.media_content_type is MEDIA_TYPE_TVSHOW and not self.config.get(
            CONF_USE_EPISODE_ART
        ):
            thumb_url = self._session.url(self._session.grandparentThumb)

        if thumb_url is None:
            _LOGGER.debug(
                "Using media art because media thumb " "was not found: %s",
                self.entity_id,
            )
            thumb_url = self.session.url(self._session.art)

        self._media_image_url = thumb_url

    def set_availability(self, available):
        """Set the device as available/unavailable noting time."""
        if not available:
            self._clear_media_details()
            if self._marked_unavailable is None:
                self._marked_unavailable = dt_util.utcnow()
        else:
            self._marked_unavailable = None

        self._available = available

    def _set_player_state(self):
        if self._player_state == "playing":
            self._is_player_active = True
            self._state = STATE_PLAYING
        elif self._player_state == "paused":
            self._is_player_active = True
            self._state = STATE_PAUSED
        elif self.device:
            self._is_player_active = False
            self._state = STATE_IDLE
        else:
            self._is_player_active = False
            self._state = STATE_OFF

    def _set_media_type(self):
        if self._session_type in ["clip", "episode"]:
            self._media_content_type = MEDIA_TYPE_TVSHOW

            # season number (00)
            if callable(self._session.season):
                self._media_season = str((self._session.season()).index).zfill(2)
            elif self._session.parentIndex is not None:
                self._media_season = self._session.parentIndex.zfill(2)
            else:
                self._media_season = None
            # show name
            self._media_series_title = self._session.grandparentTitle
            # episode number (00)
            if self._session.index is not None:
                self._media_episode = str(self._session.index).zfill(2)

        elif self._session_type == "movie":
            self._media_content_type = MEDIA_TYPE_MOVIE
            if self._session.year is not None and self._media_title is not None:
                self._media_title += " (" + str(self._session.year) + ")"

        elif self._session_type == "track":
            self._media_content_type = MEDIA_TYPE_MUSIC
            self._media_album_name = self._session.parentTitle
            self._media_album_artist = self._session.grandparentTitle
            self._media_track = self._session.index
            self._media_artist = self._session.originalTitle
            # use album artist if track artist is missing
            if self._media_artist is None:
                _LOGGER.debug(
                    "Using album artist because track artist " "was not found: %s",
                    self.entity_id,
                )
                self._media_artist = self._media_album_artist

    def force_idle(self):
        """Force client to idle."""
        self._state = STATE_IDLE
        self._session = None
        self._clear_media_details()

    @property
    def should_poll(self):
        """Return True if entity has to be polled for state."""
        return False

    @property
    def unique_id(self):
        """Return the id of this plex client."""
        return self.machine_identifier

    @property
    def available(self):
        """Return the availability of the client."""
        return self._available

    @property
    def name(self):
        """Return the name of the device."""
        return self._name

    @property
    def machine_identifier(self):
        """Return the machine identifier of the device."""
        return self._machine_identifier

    @property
    def app_name(self):
        """Return the library name of playing media."""
        return self._app_name

    @property
    def device(self):
        """Return the device, if any."""
        return self._device

    @property
    def marked_unavailable(self):
        """Return time device was marked unavailable."""
        return self._marked_unavailable

    @property
    def session(self):
        """Return the session, if any."""
        return self._session

    @property
    def state(self):
        """Return the state of the device."""
        return self._state

    @property
    def _active_media_plexapi_type(self):
        """Get the active media type required by PlexAPI commands."""
        if self.media_content_type is MEDIA_TYPE_MUSIC:
            return "music"

        return "video"

    @property
    def media_content_id(self):
        """Return the content ID of current playing media."""
        return self._media_content_id

    @property
    def media_content_type(self):
        """Return the content type of current playing media."""
        if self._session_type == "clip":
            _LOGGER.debug(
                "Clip content type detected, " "compatibility may vary: %s",
                self.entity_id,
            )
            return MEDIA_TYPE_TVSHOW
        if self._session_type == "episode":
            return MEDIA_TYPE_TVSHOW
        if self._session_type == "movie":
            return MEDIA_TYPE_MOVIE
        if self._session_type == "track":
            return MEDIA_TYPE_MUSIC

        return None

    @property
    def media_artist(self):
        """Return the artist of current playing media, music track only."""
        return self._media_artist

    @property
    def media_album_name(self):
        """Return the album name of current playing media, music track only."""
        return self._media_album_name

    @property
    def media_album_artist(self):
        """Return the album artist of current playing media, music only."""
        return self._media_album_artist

    @property
    def media_track(self):
        """Return the track number of current playing media, music only."""
        return self._media_track

    @property
    def media_duration(self):
        """Return the duration of current playing media in seconds."""
        return self._media_duration

    @property
    def media_position(self):
        """Return the duration of current playing media in seconds."""
        return self._media_position

    @property
    def media_position_updated_at(self):
        """When was the position of the current playing media valid."""
        return self._media_position_updated_at

    @property
    def media_image_url(self):
        """Return the image URL of current playing media."""
        return self._media_image_url

    @property
    def media_title(self):
        """Return the title of current playing media."""
        return self._media_title

    @property
    def media_season(self):
        """Return the season of current playing media (TV Show only)."""
        return self._media_season

    @property
    def media_series_title(self):
        """Return the title of the series of current playing media."""
        return self._media_series_title

    @property
    def media_episode(self):
        """Return the episode of current playing media (TV Show only)."""
        return self._media_episode

    @property
    def make(self):
        """Return the make of the device (ex. SHIELD Android TV)."""
        return self._make

    @property
    def supported_features(self):
        """Flag media player features that are supported."""
        if not self._is_player_active:
            return 0

        # force show all controls
        if self.config.get(CONF_SHOW_ALL_CONTROLS):
            return (
                SUPPORT_PAUSE
                | SUPPORT_PREVIOUS_TRACK
                | SUPPORT_NEXT_TRACK
                | SUPPORT_STOP
                | SUPPORT_VOLUME_SET
                | SUPPORT_PLAY
                | SUPPORT_TURN_OFF
                | SUPPORT_VOLUME_MUTE
            )

        # only show controls when we know what device is connecting
        if not self._make:
            return 0
        # no mute support
        if self.make.lower() == "shield android tv":
            _LOGGER.debug(
                "Shield Android TV client detected, disabling mute " "controls: %s",
                self.entity_id,
            )
            return (
                SUPPORT_PAUSE
                | SUPPORT_PREVIOUS_TRACK
                | SUPPORT_NEXT_TRACK
                | SUPPORT_STOP
                | SUPPORT_VOLUME_SET
                | SUPPORT_PLAY
                | SUPPORT_TURN_OFF
            )
        # Only supports play,pause,stop (and off which really is stop)
        if self.make.lower().startswith("tivo"):
            _LOGGER.debug(
                "Tivo client detected, only enabling pause, play, "
                "stop, and off controls: %s",
                self.entity_id,
            )
            return SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP | SUPPORT_TURN_OFF
        # Not all devices support playback functionality
        # Playback includes volume, stop/play/pause, etc.
        if self.device and "playback" in self._device_protocol_capabilities:
            return (
                SUPPORT_PAUSE
                | SUPPORT_PREVIOUS_TRACK
                | SUPPORT_NEXT_TRACK
                | SUPPORT_STOP
                | SUPPORT_VOLUME_SET
                | SUPPORT_PLAY
                | SUPPORT_TURN_OFF
                | SUPPORT_VOLUME_MUTE
            )

        return 0

    def set_volume_level(self, volume):
        """Set volume level, range 0..1."""
        if self.device and "playback" in self._device_protocol_capabilities:
            self.device.setVolume(int(volume * 100), self._active_media_plexapi_type)
            self._volume_level = volume  # store since we can't retrieve
            self.update_devices()

    @property
    def volume_level(self):
        """Return the volume level of the client (0..1)."""
        if (
            self._is_player_active
            and self.device
            and "playback" in self._device_protocol_capabilities
        ):
            return self._volume_level

    @property
    def is_volume_muted(self):
        """Return boolean if volume is currently muted."""
        if self._is_player_active and self.device:
            return self._volume_muted

    def mute_volume(self, mute):
        """Mute the volume.

        Since we can't actually mute, we'll:
        - On mute, store volume and set volume to 0
        - On unmute, set volume to previously stored volume
        """
        if not (self.device and "playback" in self._device_protocol_capabilities):
            return

        self._volume_muted = mute
        if mute:
            self._previous_volume_level = self._volume_level
            self.set_volume_level(0)
        else:
            self.set_volume_level(self._previous_volume_level)

    def media_play(self):
        """Send play command."""
        if self.device and "playback" in self._device_protocol_capabilities:
            self.device.play(self._active_media_plexapi_type)
            self.update_devices()

    def media_pause(self):
        """Send pause command."""
        if self.device and "playback" in self._device_protocol_capabilities:
            self.device.pause(self._active_media_plexapi_type)
            self.update_devices()

    def media_stop(self):
        """Send stop command."""
        if self.device and "playback" in self._device_protocol_capabilities:
            self.device.stop(self._active_media_plexapi_type)
            self.update_devices()

    def turn_off(self):
        """Turn the client off."""
        # Fake it since we can't turn the client off
        self.media_stop()

    def media_next_track(self):
        """Send next track command."""
        if self.device and "playback" in self._device_protocol_capabilities:
            self.device.skipNext(self._active_media_plexapi_type)
            self.update_devices()

    def media_previous_track(self):
        """Send previous track command."""
        if self.device and "playback" in self._device_protocol_capabilities:
            self.device.skipPrevious(self._active_media_plexapi_type)
            self.update_devices()

    def play_media(self, media_type, media_id, **kwargs):
        """Play a piece of media."""
        if not (self.device and "playback" in self._device_protocol_capabilities):
            return

        src = json.loads(media_id)

        media = None
        if media_type == "MUSIC":
            media = (
                self.device.server.library.section(src["library_name"])
                .get(src["artist_name"])
                .album(src["album_name"])
                .get(src["track_name"])
            )
        elif media_type == "EPISODE":
            media = self._get_tv_media(
                src["library_name"],
                src["show_name"],
                src["season_number"],
                src["episode_number"],
            )
        elif media_type == "PLAYLIST":
            media = self.device.server.playlist(src["playlist_name"])
        elif media_type == "VIDEO":
            media = self.device.server.library.section(src["library_name"]).get(
                src["video_name"]
            )

        import plexapi.playlist

        if (
            media
            and media_type == "EPISODE"
            and isinstance(media, plexapi.playlist.Playlist)
        ):
            # delete episode playlist after being loaded into a play queue
            self._client_play_media(media=media, delete=True, shuffle=src["shuffle"])
        elif media:
            self._client_play_media(media=media, shuffle=src["shuffle"])

    def _get_tv_media(self, library_name, show_name, season_number, episode_number):
        """Find TV media and return a Plex media object."""
        target_season = None
        target_episode = None

        show = self.device.server.library.section(library_name).get(show_name)

        if not season_number:
            playlist_name = "{} - {} Episodes".format(self.entity_id, show_name)
            return self.device.server.createPlaylist(playlist_name, show.episodes())

        for season in show.seasons():
            if int(season.seasonNumber) == int(season_number):
                target_season = season
                break

        if target_season is None:
            _LOGGER.error(
                "Season not found: %s\\%s - S%sE%s",
                library_name,
                show_name,
                str(season_number).zfill(2),
                str(episode_number).zfill(2),
            )
        else:
            if not episode_number:
                playlist_name = "{} - {} Season {} Episodes".format(
                    self.entity_id, show_name, str(season_number)
                )
                return self.device.server.createPlaylist(
                    playlist_name, target_season.episodes()
                )

            for episode in target_season.episodes():
                if int(episode.index) == int(episode_number):
                    target_episode = episode
                    break

            if target_episode is None:
                _LOGGER.error(
                    "Episode not found: %s\\%s - S%sE%s",
                    library_name,
                    show_name,
                    str(season_number).zfill(2),
                    str(episode_number).zfill(2),
                )

        return target_episode

    def _client_play_media(self, media, delete=False, **params):
        """Instruct Plex client to play a piece of media."""
        if not (self.device and "playback" in self._device_protocol_capabilities):
            _LOGGER.error("Client cannot play media: %s", self.entity_id)
            return

        import plexapi.playqueue

        playqueue = plexapi.playqueue.PlayQueue.create(
            self.device.server, media, **params
        )

        # Delete dynamic playlists used to build playqueue (ex. play tv season)
        if delete:
            media.delete()

        server_url = self.device.server.baseurl.split(":")
        self.device.sendCommand(
            "playback/playMedia",
            **dict(
                {
                    "machineIdentifier": self.device.server.machineIdentifier,
                    "address": server_url[1].strip("/"),
                    "port": server_url[-1],
                    "key": media.key,
                    "containerKey": "/playQueues/{}?window=100&own=1".format(
                        playqueue.playQueueID
                    ),
                },
                **params,
            ),
        )
        self.update_devices()

    @property
    def device_state_attributes(self):
        """Return the scene state attributes."""
        attr = {
            "media_content_rating": self._media_content_rating,
            "session_username": self._session_username,
            "media_library_name": self._app_name,
        }

        return attr
