Workout Script w/ Plex Video Control

I’ve made a modification to the Plex component to allow me to pass it the content I want it to play.

This script is invoked when I tell my Echo - “Alexa, turn on video workout”. It turns on the the lights in the workout room, turns on the whole home audio speaker for the workout room, sets the speaker to the workout room tv source, launches Plex on the Roku attached to the workout room tv, then starts the workout video on Plex.

For this last bit I overloaded the Plex component ‘source’ function to support a bit of json that contains the library and content to play. It seems to be working. I’m going to play with some variants for different media types (e.g. movies, music, etc.)

script:
  video_workout:
    sequence:
      - service: light.turn_on
        entity_id: light.workout
      - service: media_player.turn_on
        entity_id: media_player.workout_speaker
      - service: media_player.select_source
        entity_id: media_player.workout_speaker
        data:
          source: Workout
      - service: media_player.select_source
        entity_id: media_player.roku_workout
        data:
          source: 'Plex'
      - service: media_player.select_source
        entity_id: media_playerr.roku_workout
        data:
          source: '{ "library" : "Fitness", "title" : "Jillian Michaels 30 Day Shred" }'

Now I just need to stop sitting around hacking on HA and actually workout.

1 Like

This is neat. Gonna play with this to see if I can get it working on other players.

I was just looking at other media_players and it looks like the ‘play_media’ may be the right service to use. I’ll look into it.

What kind of modification did you made to the component? Do you mind to share it with us?

1 Like

I’d like to see this too. I feel like if enough of us can test it out, maybe a PR could be generated!

OK - I will write something up soon. I just pulled 0.38 and am working through some issues.

2 Likes

I reworked my code to work with the MediaPlayerDevice ‘play_media’ service method. The changes were very simple.

Here are the changes I made to the plex.py component.

Added import -

import asyncio

Added PlexClient method for ‘async_play_media’

@asyncio.coroutine
def async_play_media(self, media_type, media_id, **kwargs):
    src = json.loads(media_id)
    vid = self.device.server.library.section(src['library']).get(src['title']).episodes()[src['episode']]
    self.device.playMedia(vid)

I did this as a new custom component in the /config/custom_component/media_player directory. I called my modified plex component ‘z_plex.py’. This let me use a configuration like this:

media_player:
  - platform: z_plex

Then I used a script configuration that looks like this:

script:
  video_workout:
    sequence:
      - service: media_player.select_source
        entity_id: media_player.roku_roku_4_xyz
        data:
          source: Plex

      - service: media_player.play_media
        entity_id: media_player.roku_4__zzz
        data:
          media_content_type: Video
          media_content_id:  '{ "library" : "Fitness", "title" : "Jillian Michaels 30 Day Shred", "episode": 0 }'

Your entity_id’s will be different. To find the plex entity_id you might want to turn your device on to Plex then check the /api/states on HA to see what the entity_id is for the plex instance.

Note: this is very hacky code. I’m not checking parameters, handling different media types, or even dealing with different library options (e.g. I only handle ‘episodes’). But, I think you get the idea.

Also note: There is a bug in the python-roku code that if the Roku is already running Plex and it is directed to launch() it again the Roku will return an HTTP 204 code. This is fine. But, the library expects a 200 or else it throws an exception. I entered an Issue. But, I reported it against the wrong repository. I’ll have to do it again. The fix is very simple. Otherwise, you will need to make sure your Roku (if you’re using a Roku) is not already running Plex.

1 Like

Thanks.

So from my understanding, I need to copy /usr/local/lib/python3.4/dist-packages/homeassistant/components/media_player/plex.py to /home/pi/.homeassistant/custom_components/media_player/z_plex.py and then edit the z_plex.py file to add import asyncio somewhere below from urllib.parse import urlparse.

Finally add this inside class PlexClient(MediaPlayerDevice):

@asyncio.coroutine
def async_play_media(self, media_type, media_id, **kwargs):
    src = json.loads(media_id)
    vid = self.device.server.library.section(src['library']).get(src['title']).episodes()[src['episode']]
    self.device.playMedia(vid)

Do I understand correctly?

Instead of ‘episodes’, what if I want to play a song playlist?

Yep - that is basically it.

You could try out the z_plex.py before you make any changes to make sure HA is pulling the right component.

Don’t forget to use ‘z_plex’ in you configuration file.

Then make the changes, edit a simple script in the configuration file, then trigger it from the UI.

For playlists - it is pretty easy. You can take a look at the python-plex github (https://pypi.python.org/pypi/PlexAPI). It is a very good abstraction of the Plex API.

To make this fit in the async_play_media code above you might just hard code your function to play playlists. Let’s say just passing in the title of the playlist {"title" : "Christmas Music"}.

The code would then look like this:

@asyncio.coroutine
def async_play_media(self, media_type, media_id, **kwargs):
    src = json.loads(media_id)
    playlist = self.device.server.playlist(src['title'])
    self.device.playMedia(playlist)

Note: I’ve had some trouble finding the right entity_id for the plex client. I’m probably going to hack at my plex component to give me a better entity_id name.

1 Like

Thanks. I am trying to make sense of your example…

script:
  video_workout:
    sequence:
      - service: media_player.select_source
        entity_id: media_player.roku_roku_4_xyz
        data:
          source: Plex

      - service: media_player.play_media
        entity_id: media_player.roku_4__zzz
        data:
          media_content_type: Video
          media_content_id:  '{ "library" : "Fitness", "title" : "Jillian Michaels 30 Day Shred", "episode": 0 }'

I notice under media_player.select_source, your entity_id is media_player.roku_roku_4_xyz. But under media_player.play_media, your entity_id is totally different; media_player.roku_4__zzz. Aren’t they suppose to be the same?

For media_content_id, what I don’t understand is the part for title and episode. I assume the video “Jillian Michaels 30 Day Shred” is located under a library “Fitness” which the Library Type under Plex is TV Shows. All of the TV Shows in my Plex Library contain Seasons. For example, how should I write the media_content_id if I want to play this episode from Star Wars Rebel > Season 3 > Trials of the Darksaber (episode 14)?

I manage to successfully play a music playlist when I run this script…

plexdemo:
  alias: 'Plex Demo'
  sequence:
    - service: media_player.select_source
      entity_id: media_player.plex1
      data:
        source: Plex
    - service: media_player.play_media
      entity_id: media_player.plex1
      data:
        media_content_type: Music
        media_content_id:  '{ "title" : "Relaxing Musics" }'

…after I change the z_plex.py code to… vid = self.device.server.playlist(src['title'])

I am wondering won’t it be more flexible if it can use the correct code base on media_content_type: value in the script? For example…

@asyncio.coroutine
def async_play_media(self, media_type, media_id, **kwargs):
    src = json.loads(media_id)
    if media_content_type is 'Video'
        vid = self.device.server.library.section(src['library']).get(src['title']).episodes()[src['episode']]
    elif media_content_type is 'Music'
        vid = self.device.server.playlist(src['title'])
    self.device.playMedia(vid)

Also, is it possible to randomize the playlist?

2 Likes

@masterkenobi - great job getting this working!

Seems like you have answered some of your own questions.

There is some significant work left to make this a full-fledged feature. Part of that would be to use the media_content_type field to drive the interpretation of the json body.

Most of your questions are not necessarily HA questions but are more PlexAPI questions.

The github for PlexAPI is here: https://github.com/mjs7231/python-plexapi

On the question about TV Shows with Seasons. The PlexAPI Video class has methods for seasons() - as a list, or season(title) as a way to look up a season.

The PlexClient class (represented by 'self.device’ in this code) supports methods for setShuffle and setRepeat. Take a look at the PlexAPI code for other fun things to do to your Plex system.

On the question about why there are different entity_id’s - I’m going to look into how the entity_id for the Plex client is generated. It looks like it is derived from the media player entity_id. These entities should be different. Last night when I was messing around with this I notice that /api/states showed a generated entity_id that appended a ‘2’ to one of my devices. I’ll get back to you if I find out or change anything here.

I plan to test this, but should this also work on other Players? Chromecast, for example?

The chromecast player supports the play_media and consumes the options (media_id and media_type) directly:

def play_media(self, media_type, media_id, **kwargs):
    """Play media from a URL."""
    self.cast.media_controller.play_media(media_id, media_type)

If you wanted to keep the json schema consistent between the two you could:

  1. for Plex client - forgo json and use a media URL with client directives (e.g. shuffle) instead
  2. for Chromecast client - modify the play_media to support a similar json schema as input then translate it to a URL the Chromecast client understands

The interesting question is where do you want to content to come from? If you are trying to get the Chromecast to play Plex Server content then there is an example for doing something similar using VLC in the PlexAPI documentation.

Here’s that example code:

jurassic_park = plex.library.section('Movies').get('Jurassic Park')
print 'Run running the following command to play in VLC:'
print 'vlc "%s"' % jurassic_park.getStreamUrl(videoResolution='800x600')

So, here vlc is being invoked with a stream URL from Plex Server. You could use this same method to push a play directive to the Chromecast with a Plex Server URL. I don’t have a Chromecast so I can’t try this out.

I have looked into the PlexAPI page but I stared blankly on it. My understanding in Python is very basic and I have no idea where does setShuffle fits in the picture. Do I put it in z_plex.py? But where?

Here’s the link directly to the client code for PlexAPI: https://github.com/mjs7231/python-plexapi/blob/master/plexapi/client.py

The variable in the ‘async_play_media’ method called ‘self.device’ is an instance of the PlexAPI PlexClient class. This means you can call the ‘setShuffle’ using that variable before playing the media:

self.device.setShuffle(1,media_content_type.lower()) 
self.device.playMedia(vid)

Note: I did not get a chance to try this - but it goes something like that.

Also - just read this, from media_player/services.yaml:

play_media:
  description: Send the media player the command for playing media.
      fields:
        entity_id:
          description: Name(s) of entities to seek media on
          example: 'media_player.living_room_chromecast'
        media_content_id:
          description: The ID of the content to play. Platform dependent.
          example: 'https://home-assistant.io/images/cast/splash.png'
        media_content_type:
          description: The type of the content to play. Must be one of MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST
          example: 'MUSIC'

So, to be consistent the media_content_type should probably be capitalized - MUSIC, PLAYLIST, etc.

thanks but it is not working :sweat:

I’ll have to take a look later today.

1 Like

OK - I think the PlexAPI interface is flawed. There is no way to specify attributes like shuffle, repeat, etc.

And maybe there is some other way around it – but I ended up copying and modifying the playMedia method.

I added the following method to the PlexClient in z_plex.py:

def playMedia(self, media, **params):
    import plexapi.playqueue
    server_url = media.server.baseurl.split(':')
    playqueue = plexapi.playqueue.PlayQueue.create(self.device.server,media,**params)
    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))

This is just a lift out of the PlexAPI code but passes parameters to the “PlayQueue.create()” method. This method sends a message to the PMS to take the passed in playlist (media) and create a PlayQueue out it. This method has optional arguments:

def create(cls, server, item, shuffle=0, repeat=0, includeChapters=1, includeRelated=1):

But playMedia was not passing in arguments.

I know this is kind of broken. And I think the right approach would be to split the PlayQueue creation code from the playMedia code. And to get that updated in the PlexAPI library. But, for now - the above seems to work with audio playlists… I have no idea what it does for movies, tv shows, etc.

If you add the above code to your z_plex.py file you will then be able to call the function like this:

self.playMedia(vid,shuffle=1)

or

self.playMedia(vid,shuffle=src['shuffle'])

Try it out and let me know if it works.

I’ve tested it a few times. And since all our playlists are of Christmas music – I’m kind of getting tired of listening to it.

1 Like