Revisit client code; Add more testing around clients; Still cant get playback working through server proxy, but it works directly

This commit is contained in:
Michael Shepanski 2016-03-24 02:20:08 -04:00
parent e41e8676c3
commit b277facf10
7 changed files with 290 additions and 127 deletions

View file

@ -103,7 +103,7 @@ print 'vlc "%s"' % jurassic_park.getStreamUrl(videoResolution='800x600')
```python
# Example 9: Get audio/video/all playlists
for playlist in self.plex.playlists(playlisttype='audio'): # or playlisttype='video' or playlisttype=None
for playlist in self.plex.playlists():
print(playlist.title)
```

View file

@ -1,17 +1,15 @@
# -*- coding: utf-8 -*-
"""
PlexAPI Client
To understand how this works, read this page:
https://github.com/plexinc/plex-media-player/wiki/Remote-control-API
"""
import requests
from requests.status_codes import _codes as codes
from plexapi import TIMEOUT, log, utils
from plexapi.exceptions import BadRequest
from plexapi.exceptions import BadRequest, Unsupported
from xml.etree import ElementTree
SERVER = 'server'
CLIENT = 'client'
class Client(object):
@ -31,26 +29,34 @@ class Client(object):
self.protocolVersion = data.attrib.get('protocolVersion')
self.protocolCapabilities = data.attrib.get('protocolCapabilities', '').split(',')
self.state = data.attrib.get('state')
self._sendCommandsTo = CLIENT
self._proxyThroughServer = False
self._commandId = 0
def sendCommandsTo(self, value):
self._sendCommandsTo = value
@property
def quickName(self):
return self.name or self.product
def sendCommand(self, command, args=None, sendTo=None):
sendTo = sendTo or self._sendCommandsTo
if sendTo == CLIENT:
return self.sendClientCommand(command, args)
return self.sendServerCommand(command, args)
def proxyThroughServer(self, value=True):
self._proxyThroughServer = value
def sendClientCommand(self, command, args=None):
args = args or {}
args.update({
'X-Plex-Target-Client-Identifier': self.machineIdentifier,
def sendCommand(self, command, proxy=None, **params):
proxy = self._proxyThroughServer if proxy is None else proxy
if proxy: return self.sendServerCommand(command, **params)
return self.sendClientCommand(command, **params)
def sendClientCommand(self, command, **params):
command = command.strip('/')
controller = command.split('/')[1]
if controller not in self.protocolCapabilities:
raise Unsupported('Client %s does not support the %s controller.' % (self.quickName, controller))
self._commandId += 1
params.update({
'X-Plex-Device-Name': self.name,
'X-Plex-Client-Identifier': self.server.machineIdentifier,
'type': 'video', # TODO: Make this with any media type or passed in as an arg
'X-Plex-Target-Client-Identifier': self.machineIdentifier,
'commandID': self._commandId,
})
url = '%s%s' % (self.url(command), utils.joinArgs(args))
url = 'http://%s:%s/%s%s' % (self.address, self.port, command.lstrip('/'), utils.joinArgs(params))
log.info('GET %s', url)
response = requests.get(url, timeout=TIMEOUT)
if response.status_code != requests.codes.ok:
@ -59,54 +65,92 @@ class Client(object):
data = response.text.encode('utf8')
return ElementTree.fromstring(data) if data else None
def sendServerCommand(self, command, args=None):
# TODO: Rip this out, server is throwing exceptions, maybe deprecated?
path = '/system/players/%s/%s%s' % (self.address, command, utils.joinArgs(args))
def sendServerCommand(self, command, **params):
params.update({'commandID': self._commandId})
path = '/system/players/%s/%s%s' % (self.address, command, utils.joinArgs(params))
self.server.query(path)
def url(self, path):
return 'http://%s:%s/player/%s' % (self.address, self.port, path.lstrip('/'))
# Navigation Commands
def moveUp(self): self.sendCommand('navigation/moveUp')
def moveDown(self): self.sendCommand('navigation/moveDown')
def moveLeft(self): self.sendCommand('navigation/moveLeft')
def moveRight(self): self.sendCommand('navigation/moveRight')
def pageUp(self): self.sendCommand('navigation/pageUp')
def pageDown(self): self.sendCommand('navigation/pageDown')
def nextLetter(self): self.sendCommand('navigation/nextLetter')
def previousLetter(self): self.sendCommand('navigation/previousLetter')
def select(self): self.sendCommand('navigation/select')
def back(self): self.sendCommand('navigation/back')
def contextMenu(self): self.sendCommand('navigation/contextMenu')
def toggleOSD(self): self.sendCommand('navigation/toggleOSD')
# These commands navigate around the user interface.
def contextMenu(self): self.sendCommand('player/navigation/contextMenu')
def goBack(self): self.sendCommand('player/navigation/back')
def goToHome(self): self.sendCommand('/player/navigation/home')
def goToMusic(self): self.sendCommand('/player/navigation/music')
def moveDown(self): self.sendCommand('player/navigation/moveDown')
def moveLeft(self): self.sendCommand('player/navigation/moveLeft')
def moveRight(self): self.sendCommand('player/navigation/moveRight')
def moveUp(self): self.sendCommand('player/navigation/moveUp')
def nextLetter(self): self.sendCommand('player/navigation/nextLetter')
def pageDown(self): self.sendCommand('player/navigation/pageDown')
def pageUp(self): self.sendCommand('player/navigation/pageUp')
def previousLetter(self): self.sendCommand('player/navigation/previousLetter')
def select(self): self.sendCommand('player/navigation/select')
def toggleOSD(self): self.sendCommand('player/navigation/toggleOSD')
def goToMedia(self, media, **params):
server_uri = media.server.baseuri.split(':')
self.sendCommand('player/mirror/details', **dict({
'machineIdentifier': self.server.machineIdentifier,
'address': server_uri[1].strip('/'),
'port': server_uri[-1],
'key': media.key,
}, **params))
# Playback Commands
def play(self): self.sendCommand('playback/play')
def pause(self): self.sendCommand('playback/pause')
def stop(self): self.sendCommand('playback/stop')
def stepForward(self): self.sendCommand('playback/stepForward')
def bigStepForward(self): self.sendCommand('playback/bigStepForward')
def stepBack(self): self.sendCommand('playback/stepBack')
def bigStepBack(self): self.sendCommand('playback/bigStepBack')
def skipNext(self): self.sendCommand('playback/skipNext')
def skipPrevious(self): self.sendCommand('playback/skipPrevious')
def playMedia(self, video, viewOffset=0):
playqueue = self.server.createPlayQueue(video)
self.sendCommand('playback/playMedia', {
# most of the playback commands take a mandatory mtype {'music','photo','video'} argument,
# to specify which media type to apply the command to, (except for playMedia). This
# is in case there are multiple things happening (e.g. music in the background, photo
# slideshow in the foreground).
def pause(self, mtype): self.sendCommand('player/playback/pause', type=mtype)
def play(self, mtype): self.sendCommand('player/playback/play', type=mtype)
def refreshPlayQueue(self, playQueueID, mtype=None): self.sendCommand('player/playback/refreshPlayQueue', playQueueID=playQueueID, type=mtype)
def seekTo(self, offset, mtype=None): self.sendCommand('player/playback/seekTo', offset=offset, type=mtype) # offset in milliseconds
def skipNext(self, mtype=None): self.sendCommand('player/playback/skipNext', type=mtype)
def skipPrevious(self, mtype=None): self.sendCommand('player/playback/skipPrevious', type=mtype)
def skipTo(self, key, mtype=None): self.sendCommand('player/playback/skipTo', key=key, type=mtype) # skips to item with matching key
def stepBack(self, mtype=None): self.sendCommand('player/playback/stepBack', type=mtype)
def stepForward(self, mtype): self.sendCommand('player/playback/stepForward', type=mtype)
def stop(self, mtype): self.sendCommand('player/playback/stop', type=mtype)
def setRepeat(self, repeat, mtype): self.setParameters(repeat=repeat, mtype=mtype) # 0=off, 1=repeatone, 2=repeatall
def setShuffle(self, shuffle, mtype): self.setParameters(shuffle=shuffle, mtype=mtype) # 0=off, 1=on
def setVolume(self, volume, mtype): self.setParameters(volume=volume, mtype=mtype) # 0-100
def setAudioStream(self, audioStreamID, mtype): self.setStreams(audioStreamID=audioStreamID, mtype=mtype)
def setSubtitleStream(self, subtitleStreamID, mtype): self.setStreams(subtitleStreamID=subtitleStreamID, mtype=mtype)
def setVideoStream(self, videoStreamID, mtype): self.setStreams(videoStreamID=videoStreamID, mtype=mtype)
def playMedia(self, media, **params):
server_uri = media.server.baseuri.split(':')
playqueue = self.server.createPlayQueue(media)
self.sendCommand('player/playback/playMedia', **dict({
'machineIdentifier': self.server.machineIdentifier,
'address': server_uri[1].strip('/'),
'port': server_uri[-1],
'key': media.key,
'containerKey': '/playQueues/%s?window=100&own=1' % playqueue.playQueueID,
'key': video.key,
'offset': 0,
})
}, **params))
def setParameters(self, volume=None, shuffle=None, repeat=None, mtype=None):
params = {}
if repeat is not None: params['repeat'] = repeat # 0=off, 1=repeatone, 2=repeatall
if shuffle is not None: params['shuffle'] = shuffle # 0=off, 1=on
if volume is not None: params['volume'] = volume # 0-100
if mtype is not None: params['type'] = mtype # music,photo,video
self.sendCommand('player/playback/setParameters', **params)
def setStreams(self, audioStreamID=None, subtitleStreamID=None, videoStreamID=None, mtype=None):
# Can possibly send {next,on,off}
params = {}
if audioStreamID is not None: params['audioStreamID'] = audioStreamID
if subtitleStreamID is not None: params['subtitleStreamID'] = subtitleStreamID
if videoStreamID is not None: params['videoStreamID'] = videoStreamID
if mtype is not None: params['type'] = mtype # music,photo,video
self.sendCommand('player/playback/setStreams', **params)
# Timeline Commands
def timeline(self):
params = {'wait':1, 'commandID':4}
return self.server.query('timeline/poll', params=params)
self.sendCommand('timeline/poll', **{'wait':1, 'commandID':4})
def isPlayingMedia(self):
# http://192.168.1.31:32500/player/timeline/poll?commandID=4&wait=1&X-Plex-Target-Client-Identifier=198D670A-DE1B-4BF2-BE55-10B4D98E1532&X-Plex-Device-Name=iphone-mike&X-Plex-Client-Identifier=792f0ff5fa644d63ff1e6ea8b130dade08716cb1
timeline = self.timeline()
for media_type in timeline:
if media_type.get('state') == 'playing':

View file

@ -46,10 +46,7 @@ class MediaPart(object):
self.file = data.attrib.get('file')
self.size = cast(int, data.attrib.get('size'))
self.container = data.attrib.get('container')
self.streams = [
MediaPartStream.parse(self.server, elem, self.initpath, self)
for elem in data if elem.tag == MediaPartStream.TYPE
]
self.streams = [MediaPartStream.parse(self.server, e, self.initpath, self) for e in data if e.tag == 'Stream']
def __repr__(self):
return '<%s:%s>' % (self.__class__.__name__, self.id)
@ -64,86 +61,84 @@ class MediaPart(object):
class MediaPartStream(object):
TYPE = 'Stream'
TYPE = None
STREAMTYPE = None
def __init__(self, server, data, initpath, part):
self.server = server
self.initpath = initpath
self.part = part
self.id = cast(int, data.attrib.get('id'))
self.type = cast(int, data.attrib.get('streamType'))
self.codec = data.attrib.get('codec')
self.selected = cast(bool, data.attrib.get('selected', '0'))
self.codecID = data.attrib.get('codecID')
self.id = cast(int, data.attrib.get('id'))
self.index = cast(int, data.attrib.get('index', '-1'))
self.language = data.attrib.get('language')
self.languageCode = data.attrib.get('languageCode')
self.selected = cast(bool, data.attrib.get('selected', '0'))
self.streamType = cast(int, data.attrib.get('streamType'))
self.type = cast(int, data.attrib.get('streamType'))
@staticmethod
def parse(server, data, initpath, part):
STREAMCLS = {
StreamVideo.TYPE: StreamVideo,
StreamAudio.TYPE: StreamAudio,
StreamSubtitle.TYPE: StreamSubtitle
}
STREAMCLS = {1:VideoStream, 2:AudioStream, 3:SubtitleStream}
stype = cast(int, data.attrib.get('streamType'))
cls = STREAMCLS.get(stype, MediaPartStream)
# return generic MediaPartStream if type is unknown
return cls(server, data, initpath, part)
def __repr__(self):
return '<%s:%s>' % (self.__class__.__name__, self.id)
class StreamVideo(MediaPartStream):
TYPE = 1
class VideoStream(MediaPartStream):
TYPE = 'videostream'
STREAMTYPE = 1
def __init__(self, server, data, initpath, part):
super(StreamVideo, self).__init__(server, data, initpath, part)
super(VideoStream, self).__init__(server, data, initpath, part)
self.bitrate = cast(int, data.attrib.get('bitrate'))
self.language = data.attrib.get('langauge')
self.language_code = data.attrib.get('languageCode')
self.bit_depth = cast(int, data.attrib.get('bitDepth'))
self.bitDepth = cast(int, data.attrib.get('bitDepth'))
self.cabac = cast(int, data.attrib.get('cabac'))
self.chroma_subsampling = data.attrib.get('chromaSubsampling')
self.codec_id = data.attrib.get('codecID')
self.color_space = data.attrib.get('colorSpace')
self.chromaSubsampling = data.attrib.get('chromaSubsampling')
self.colorSpace = data.attrib.get('colorSpace')
self.duration = cast(int, data.attrib.get('duration'))
self.frame_rate = cast(float, data.attrib.get('frameRate'))
self.frame_rate_mode = data.attrib.get('frameRateMode')
self.has_scalling_matrix = cast(bool, data.attrib.get('hasScallingMatrix'))
self.frameRate = cast(float, data.attrib.get('frameRate'))
self.frameRateMode = data.attrib.get('frameRateMode')
self.hasScallingMatrix = cast(bool, data.attrib.get('hasScallingMatrix'))
self.height = cast(int, data.attrib.get('height'))
self.level = cast(int, data.attrib.get('level'))
self.profile = data.attrib.get('profile')
self.ref_frames = cast(int, data.attrib.get('refFrames'))
self.scan_type = data.attrib.get('scanType')
self.refFrames = cast(int, data.attrib.get('refFrames'))
self.scanType = data.attrib.get('scanType')
self.title = data.attrib.get('title')
self.width = cast(int, data.attrib.get('width'))
class StreamAudio(MediaPartStream):
TYPE = 2
class AudioStream(MediaPartStream):
TYPE = 'audiostream'
STREAMTYPE = 2
def __init__(self, server, data, initpath, part):
super(StreamAudio, self).__init__(server, data, initpath, part)
super(AudioStream, self).__init__(server, data, initpath, part)
self.audioChannelLayout = data.attrib.get('audioChannelLayout')
self.channels = cast(int, data.attrib.get('channels'))
self.bitrate = cast(int, data.attrib.get('bitrate'))
self.bit_depth = cast(int, data.attrib.get('bitDepth'))
self.bitrate_mode = data.attrib.get('bitrateMode')
self.codec_id = data.attrib.get('codecID')
self.dialog_norm = cast(int, data.attrib.get('dialogNorm'))
self.bitDepth = cast(int, data.attrib.get('bitDepth'))
self.bitrateMode = data.attrib.get('bitrateMode')
self.dialogNorm = cast(int, data.attrib.get('dialogNorm'))
self.duration = cast(int, data.attrib.get('duration'))
self.sampling_rate = cast(int, data.attrib.get('samplingRate'))
self.samplingRate = cast(int, data.attrib.get('samplingRate'))
self.title = data.attrib.get('title')
class StreamSubtitle(MediaPartStream):
TYPE = 3
class SubtitleStream(MediaPartStream):
TYPE = 'subtitlestream'
STREAMTYPE = 3
def __init__(self, server, data, initpath, part):
super(StreamSubtitle, self).__init__(server, data, initpath, part)
super(SubtitleStream, self).__init__(server, data, initpath, part)
self.key = data.attrib.get('key')
self.language = data.attrib.get('langauge')
self.language_code = data.attrib.get('languageCode')
self.format = data.attrib.get('format')
self.title = data.attrib.get('title')
class TranscodeSession(object):

View file

@ -172,18 +172,7 @@ class ResourceConnection(object):
return '<%s:%s>' % (self.__class__.__name__, self.uri.encode('utf8'))
def _findResource(resources, search, port=32400):
""" Searches server.name """
search = search.lower()
log.info('Looking for server: %s', search)
for server in resources:
if search == server.name.lower():
log.info('Server found: %s', server)
return server
log.info('Unable to find server: %s', search)
raise NotFound('Unable to find server: %s' % search)
# TODO: Is this a plex client in disguise?
class MyPlexDevice(object):
DEVICES = 'https://plex.tv/devices.xml'
@ -272,3 +261,15 @@ class MyPlexDevice(object):
def bigStepBack(self, args=None): self.sendCommand('playback/bigStepBack', args) # noqa
def skipNext(self, args=None): self.sendCommand('playback/skipNext', args) # noqa
def skipPrevious(self, args=None): self.sendCommand('playback/skipPrevious', args) # noqa
def _findResource(resources, search, port=32400):
""" Searches server.name """
search = search.lower()
log.info('Looking for server: %s', search)
for server in resources:
if search == server.name.lower():
log.info('Server found: %s', server)
return server
log.info('Unable to find server: %s', search)
raise NotFound('Unable to find server: %s' % search)

View file

@ -91,6 +91,15 @@ class PlexPartialObject(object):
from plexapi.client import Client
return Client(self.server, elem)
return None
def _findStreams(self, streamtype):
streams = []
for media in self.media:
for part in media.parts:
for stream in part.streams:
if stream.TYPE == streamtype:
streams.append(stream)
return streams
def _findTranscodeSession(self, data):
elem = data.find('TranscodeSession')
@ -193,6 +202,21 @@ def listItems(server, path, libtype=None, watched=None):
return items
def rget(obj, attrstr, default=None, delim='.'):
try:
parts = attrstr.split(delim, 1)
attr = parts[0]
attrstr = parts[1] if len(parts) == 2 else None
if isinstance(obj, dict): value = obj[attr]
elif isinstance(obj, list): value = obj[int(attr)]
elif isinstance(obj, tuple): value = obj[int(attr)]
elif isinstance(obj, object): value = getattr(obj, attr)
if attrstr: return rget(value, attrstr, default, delim)
return value
except:
return default
def searchType(libtype):
if libtype == 'movie': return 1
elif libtype == 'show': return 2

View file

@ -79,6 +79,9 @@ class Movie(Video):
self.producers = [media.Producer(self.server, e) for e in data if e.tag == media.Producer.TYPE]
self.roles = [media.Role(self.server, e) for e in data if e.tag == media.Role.TYPE]
self.writers = [media.Writer(self.server, e) for e in data if e.tag == media.Writer.TYPE]
self.videoStreams = self._findStreams('videostream')
self.audioStreams = self._findStreams('audiostream')
self.subtitleStreams = self._findStreams('subtitlestream')
# data for active sessions
self.sessionKey = utils.cast(int, data.attrib.get('sessionKey', NA))
self.user = self._findUser(data)
@ -223,6 +226,9 @@ class Episode(Video):
self.directors = [media.Director(self.server, e) for e in data if e.tag == media.Director.TYPE]
self.media = [media.Media(self.server, e, self.initpath, self) for e in data if e.tag == media.Media.TYPE]
self.writers = [media.Writer(self.server, e) for e in data if e.tag == media.Writer.TYPE]
self.videoStreams = self._findStreams('videostream')
self.audioStreams = self._findStreams('audiostream')
self.subtitleStreams = self._findStreams('subtitlestream')
# data for active sessions
self.sessionKey = utils.cast(int, data.attrib.get('sessionKey', NA))
self.user = self._findUser(data)

View file

@ -18,12 +18,12 @@ SHOW_TITLE = 'Game of Thrones'
SHOW_SEASON = 'Season 1'
SHOW_EPISODE = 'Winter Is Coming'
MOVIE_SECTION = 'Movies'
MOVIE_TITLE = 'Jurassic Park'
MOVIE_TITLE = 'Jurassic World'
AUDIO_SECTION = 'Music'
AUDIO_ARTIST = 'Beastie Boys'
AUDIO_ALBUM = 'Licensed To Ill'
AUDIO_TRACK = 'Brass Monkey'
PLEX_CLIENT = 'iphone-mike'
PLEX_CLIENT = 'pkkid-home'
#-----------------------
@ -261,7 +261,21 @@ def test_list_video_tags(plex, user=None):
related = movies.search(None, director=movie.directors[0])
log(4, related[0:3])
assert movie in related, 'Movie was not found in related directors search.'
@register('client')
def test_list_video_streams(plex, user=None):
movie = plex.library.section(MOVIE_SECTION).get('John Wick')
videostreams = [s.language for s in movie.videoStreams]
audiostreams = [s.language for s in movie.audioStreams]
subtitlestreams = [s.language for s in movie.subtitleStreams]
log(2, 'Video Streams: %s' % ', '.join(videostreams[0:5]))
log(2, 'Audio Streams: %s' % ', '.join(audiostreams[0:5]))
log(2, 'Subtitle Streams: %s' % ', '.join(subtitlestreams[0:5]))
assert filter(None, videostreams), 'No video streams listed for movie.'
assert filter(None, audiostreams), 'No audio streams listed for movie.'
assert filter(None, subtitlestreams), 'No subtitle streams listed for movie.'
@register('meta,audio')
def test_list_audio_tags(plex, user=None):
@ -330,23 +344,91 @@ def test_play_queues(plex, user=None):
#-----------------------
@register('client')
def test_list_devices(plex, user=None):
assert user, 'Must specify username, password & resource to run this test.'
log(2, ', '.join([r.name or r.product for r in user.resources()]))
def test_list_clients(plex, user=None):
clients = [c.name or c.product for c in plex.clients()]
log(2, ', '.join(clients))
@register('client')
def test_client_play_media(plex, user=None):
episode = plex.library.section(SHOW_SECTION).get(SHOW_TITLE).get(SHOW_EPISODE)
def test_client_navigation(plex, user=None):
client = plex.client(PLEX_CLIENT)
client.playMedia(episode); time.sleep(10)
client.pause(); time.sleep(3)
client.stepForward(); time.sleep(3)
client.play(); time.sleep(3)
client.stop(); time.sleep(3)
movie = plex.library.get(MOVIE_TITLE)
movie.play(client); time.sleep(10)
client.stop()
_navigate(plex, client)
@register('client')
def test_client_navigation_via_proxy(plex, user=None):
client = plex.client(PLEX_CLIENT)
client.proxyThroughServer()
_navigate(plex, client)
def _navigate(plex, client):
episode = plex.library.section(SHOW_SECTION).get(SHOW_TITLE).get(SHOW_EPISODE)
artist = plex.library.section(AUDIO_SECTION).get(AUDIO_ARTIST)
log(2, 'Client: %s (%s)' % (client.name, client.product))
log(2, 'Capabilities: %s' % client.protocolCapabilities)
# Move around a bit
log(2, 'Browsing around..')
client.moveDown(); time.sleep(0.5)
client.moveDown(); time.sleep(0.5)
client.moveDown(); time.sleep(0.5)
client.select(); time.sleep(3)
client.moveRight(); time.sleep(0.5)
client.moveRight(); time.sleep(0.5)
client.moveLeft(); time.sleep(0.5)
client.select(); time.sleep(3)
client.goBack(); time.sleep(1)
client.goBack(); time.sleep(3)
# Go directly to media
log(2, 'Navigating to %s..' % episode.title)
client.goToMedia(episode); time.sleep(5)
log(2, 'Navigating to %s..' % artist.title)
client.goToMedia(artist); time.sleep(5)
log(2, 'Navigating home..')
client.goToHome(); time.sleep(5)
client.moveUp(); time.sleep(0.5)
client.moveUp(); time.sleep(0.5)
client.moveUp(); time.sleep(0.5)
# Show context menu
client.contextMenu(); time.sleep(3)
client.goBack(); time.sleep(5)
@register('client')
def test_video_playback(plex, user=None):
client = plex.client(PLEX_CLIENT)
_video_playback(plex, client)
@register('client')
def test_video_playback_via_proxy(plex, user=None):
client = plex.client(PLEX_CLIENT)
client.proxyThroughServer()
_video_playback(plex, client)
def _video_playback(plex, client):
mtype = 'video'
movie = plex.library.section(MOVIE_SECTION).get(MOVIE_TITLE)
subs = [s for s in movie.subtitleStreams if s.language == 'English']
log(2, 'Client: %s (%s)' % (client.name, client.product))
log(2, 'Capabilities: %s' % client.protocolCapabilities)
log(2, 'Playing to %s..' % movie.title)
client.playMedia(movie); time.sleep(5)
log(2, 'Pause..')
client.pause(mtype); time.sleep(2)
log(2, 'Step Forward..')
client.stepForward(mtype); time.sleep(5)
log(2, 'Play..')
client.play(mtype); time.sleep(3)
log(2, 'Seek to 10m..')
client.seekTo(10*60*1000); time.sleep(5)
log(2, 'Disable Subtitles..')
client.setSubtitleStream(0, mtype); time.sleep(10)
log(2, 'Load English Subtitles %s..' % subs[0].id)
client.setSubtitleStream(subs[0].id, mtype); time.sleep(10)
log(2, 'Stop..')
client.stop(mtype); time.sleep(1)
# def test_sync_items(plex, user=None):
@ -364,6 +446,17 @@ def test_client_play_media(plex, user=None):
# item.mark_as_done(part.sync_id)
#-----------------------
# Resource
#-----------------------
@register('resource')
def test_list_resources(plex, user=None):
assert user, 'Must specify username, password & resource to run this test.'
resources = [r.name or r.product for r in user.resources()]
log(2, ', '.join(resources))
if __name__ == '__main__':
# There are three ways to authenticate:
# 1. If the server is running on localhost, just run without any auth.