PlexDevices - Custom Media Player Component

@JesseWebDotCom - First this is an AMAZING upgrade. Thank you so much for the contributions.

I had one quick question - what is the point of ‘shuffle’ when you have to pass in a season/episode for the TV/EPISODE PLAY_MEDIA? What is it going to shuffle?

We like to turn on some shows for background noise, and usually just ‘shuffle’ a show or a season. I’ve tried to accomplish this with the PLAY_MEDIA, but if I don’t provide a episode/season, it fails (as expected since it tries to find SXXEYY).

Is this something that is possible today, or could be updated by chance?

I have shared the time limit automation I told you before at Plex media player time limit control automation (parental control)

Can’t wait to put it in good use. The missing piece is the username in the attributes. I am wondering is it possible to share the changes you made to the component that support username to me first so that I can use it as custom component until the official version is released.

I plan to stick to Rasplex for long term. It seems far superior than any players out there including the official Plex Media Player.

Sure - I’ll post that when I get home tonight

1 Like

Just add this to the end of your custom component:

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

        return attr

Thanks.

Since https://github.com/JesseWebDotCom/home-assistant-configuration/blob/master/custom_components/media_player/plexdevices.py is no longer available, I went to get a copy of plex.py from /usr/local/lib/python3.4/dist-packages/homeassistant/components/media_player and paste it to /home/pi/.homeassistant/custom_components/media_player.

From there, I added the code to the last line and restart HA. The Plex media player component couldn’t load and gave this error in the log…

ERROR (MainThread) [homeassistant.core] Error doing job: Task exception was never retrieved
Traceback (most recent call last):
  File "/usr/lib/python3.4/asyncio/tasks.py", line 237, in _step
    result = next(coro)
  File "/usr/local/lib/python3.4/dist-packages/homeassistant/helpers/entity_component.py", line 359, in async_process_entity
    new_entity, self, update_before_add=update_before_add
  File "/usr/local/lib/python3.4/dist-packages/homeassistant/helpers/entity_component.py", line 215, in async_add_entity
    yield from entity.async_update_ha_state()
  File "/usr/local/lib/python3.4/dist-packages/homeassistant/helpers/entity.py", line 247, in async_update_ha_state
    device_attr = self.device_state_attributes
  File "/home/pi/.homeassistant/custom_components/media_player/plex.py", line 862, in device_state_attributes
    attr['media_content_rating'] = self._media_content_rating
AttributeError: 'PlexClient' object has no attribute '_media_content_rating'

Download, rename to plex.py, and place in your custom_components/media_player folder:
plex.py.yaml (31.7 KB)

thank you so much! that works marvelously. now, my kids gonna hate me more.

I also added the rating so you can alert or stop when they watch media rated higher than you want them to

1 Like

Thanks. That can be useful. You can make it even better if you can include genre in there as well.

Can you you help me understand how I can modify the code so I can request a PLEX player to shuffle a ‘show or season’ instead of always having to provide an episode number? Or is it possible to get this feature updated in a future release? Not sure if I’m the only one trying to do this…

I’ve been looking into it, tested some things, but no luck. I’ll keep looking but I’ll take any working sample code you find.

I was able to get this working with my previous automation system by first having plex generate a playqueue - with a URL like below:

https://PLEXSERVERIP:32400/playQueues?shuffle=1&type=video&uri=library%3A%2F%2F61d6d471-b0e9-4018-92f1-b84584fbbbea%2Fdirectory%2F%252Flibrary%252Fmetadata%252F1702&X-Plex-Client-Identifier=72c64643c33ce2dfd70eba6fbc74d613&X-Plex-Token=TOKEN

I then parsed out the playQueueID (queueId), and playQueueSelectedMetadataItemID (playId) from the XML response.

Then I would hit the player up (a Roku, in this case) with the below URL and passed in the above parameters.

http://ROKUIP:8324/player/playback/playMedia?key=/library/metadata/“+playId+”&offset=0&machineIdentifier=cf0635a38663bbf7646943a24dfd35e00c09a9d6&protocol=http&address=10.0.0.100&port=32400&containerKey=/playQueues/“+queueId+”&type=video&token=TOKEN&clientidentifier=72c64643c33ce2dfd70eba6fbc74d613

I was able to track this down by using fiddler/chrome dev network tools to see what Plex Web was doing to make this happen when I had the Plex Web player ‘remote control’ my roku, and hit play on a show/season.

If we can’t get this working as part of the media_player, I can proceed with just making it a shell_command to run a couple curl calls, etc.

Thanks,
Brandon

Got something working via playlists but with the side effect of all these playlists being created. I’ll try a different route with just a play queue.

Got it. Will post in a bit.

@lethic - Test this code out. Follow the instructions with the file I posted earlier. Then rename _get_episode (should be about 2 references found to _get_tv_media and then replace the functions _get_tv_media and _client_play_media (both towards the end of the file) with this:

    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 = (self.entity_id + ' - ' + show_name + ' Episodes')
            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 = (self.entity_id + ' - ' + show_name +
                                 ' Season ' + str(season_number) + ' Episodes')
                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)

        self._local_client_control_fix()

        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/%s?window=100&own=1' % playqueue.playQueueID,
        }, **params))

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

Then just call the service like you previously did but leave season_number OR season_number and episode_number blank like this:

{
    "entity_id" : "media_player.plex_7b8c4c63_38ff_417d_ac14_0457796ee1c1",
    "media_content_id": "{ \"library_name\" : \"Adult TV\", \"show_name\" : \"Rick and Morty\", \"season_number\" : \"\", \"episode_number\" : \"\", \"shuffle\": \"1\" }",
    "media_content_type": "EPISODE"
}
1 Like

Submitted for official HA review and incorporation here:

1 Like

Gave it a try, and it works like a charm. You just improved the WAF by 10! :slight_smile:

I’m going to set up some automation rules to include this, so I’ll let you know if I encounter any issues. Can’t wait to see this in the main branch.

Thanks again for the awesome updates to the PLEX component.

I did notice it is creating the playlists… so I assume doing a playqueue wasn’t possible or as easy. Should I update the code to also pass in true for delete, so it cleans them up?

The playlists get cleaned up in my testing as I do have a line of code that deletes the playlist as soon as the play queue starts.

It doesn’t look like the play queue supports ITEMS as the code behind the api shows accepting a playlist or a single media. So I create a playlist, feed it to the queue, and then delete the playlist.

If the playlist is not being deleted, can you try the following? Move this code:

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

Directly under (i.e. the line right after) this code:

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

I don’t think it is a placement issue, the default value for ‘delete’ is set to false, and the only place I see calling the _client_play_media function is inside play_media, and it doesn’t provide a value… so it will always be false. Did you modify that on your side maybe? If so, I can do the same and see if it works.

        if media:
        self._client_play_media(media, shuffle=src['shuffle'])

I think you’re right - I may have missed copying a line from my dev box. I’ll look in a bit