Workout Script w/ Plex Video Control

@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

Thanks man. I have tried it but it still doesn’t shuffle. This is what I did.

Added this code under class PlexClient(MediaPlayerDevice): 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))

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

My script remains the same. When run the script, it plays the playlist but it was never shuffled.

I’ll check out this code to see if there are any differences with what I have.

Meanwhile, can you take a look at your log files and see if you see a log output this looks like this?:

17-02-01 20:08:46 INFO (MainThread) [plexapi] POST http://<server-ip>:32400/playQueues?includeChapters=1&includeRelated=1&playlistID=136659&repeat=1&shuffle=1&type=audio&X-Plex-Token=...

Note the ‘shuffle=1’.

So, instead of using ’self.device.playMedia(vid,shuffle=1)' you need to be calling the new PlexClient 'playMedia' method:

self.playMedia(vid,shuffle=1) # w/o the 'device' part

I know I could have changed the name of the method - to be less confusing. But, it’s just the way I roll.

This is what I have in the log…

17-02-02 00:00:00 INFO (MainThread) [plexapi] GET http://<server-ip>:32400/playlists?X-Plex-Token=<token>
17-02-02 00:00:00 INFO (MainThread) [plexapi] POST http://<server-ip>:32400/playQueues?includeChapters=1&includeRelated=1&playlistID=360771&repeat=0&shuffle=0&type=audio&X-Plex-Token=<token>
17-02-02 00:00:00 INFO (MainThread) [plexapi] GET http://<client-ip>:32500/player/playback/playMedia?address=<server-ip>&commandID=1&containerKey=/playQueues/10868%3Fwindow%3D100%26own%3D1&key=/playlists/360771&machineIdentifier=<machineIdentifier>&port=32400&shuffle=1
17-02-02 00:00:00 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/core.py", line 1052, in _event_to_service_call
    yield from service_handler.func(service_call)
  File "/usr/local/lib/python3.4/dist-packages/homeassistant/components/media_player/__init__.py", line 366, in async_service_handler
    yield from getattr(player, method['method'])(**params)
  File "/usr/lib/python3.4/asyncio/coroutines.py", line 141, in coro
    res = func(*args, **kw)
  File "/home/pi/.homeassistant/custom_components/media_player/z_plex.py", line 390, in async_play_media
    self.device.playMedia(vid,shuffle=1)
  File "/home/pi/.homeassistant/deps/plexapi/client.py", line 154, in playMedia
    }, **params))
  File "/home/pi/.homeassistant/deps/plexapi/client.py", line 85, in sendCommand
    return self.query(path, headers=headers)
  File "/home/pi/.homeassistant/deps/plexapi/client.py", line 71, in query
    return ElementTree.fromstring(data) if data else None
  File "/usr/lib/python3.4/xml/etree/ElementTree.py", line 1325, in XML
    parser.feed(text)
  File "<string>", line None
xml.etree.ElementTree.ParseError: syntax error: line 1, column 0

Yep - that makes sense.

If you remove the “.device.” part of the call to playMedia() it should work.

1 Like

Yep. That works! Thanks a lot.

Now I wish this will make into the official Plex component

2 Likes

I’ll try to find some time soon to look into what that would take.

2 Likes

I have modified your code to this…

def async_play_media(self, media_type, media_id, **kwargs):
    src = json.loads(media_id)
    if media_type == 'EPISODE':
        media = self.device.server.library.section(src['library']).get(src['title']).episodes()[src['episode']]
    elif media_type == 'MUSIC':
        media = self.device.server.playlist(src['title'])
    self.playMedia(media,shuffle=src['shuffle'])

Now, the media variable is base on media_content_type in the script. Not sure about other media_type but you can get the idea.

1 Like

might want to have a look here: https://www.plex.tv/blog/alexa-ask-plex-get-party-started/

yeah man. i was super excited because i just got my first Alexa 12 hours before the announcement was made.

anyway, the skill is not really useful to HA. I still need this script for my automation.

I added this functionality (plus a little more) here (scroll to the bottom for the latest link):

Note: I also changed the input json format from your original examples. I documented the new formats in my link above.

2 Likes