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 ```python
# Example 9: Get audio/video/all playlists # 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) print(playlist.title)
``` ```

View file

@ -1,17 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
PlexAPI Client PlexAPI Client
To understand how this works, read this page:
https://github.com/plexinc/plex-media-player/wiki/Remote-control-API https://github.com/plexinc/plex-media-player/wiki/Remote-control-API
""" """
import requests import requests
from requests.status_codes import _codes as codes from requests.status_codes import _codes as codes
from plexapi import TIMEOUT, log, utils from plexapi import TIMEOUT, log, utils
from plexapi.exceptions import BadRequest from plexapi.exceptions import BadRequest, Unsupported
from xml.etree import ElementTree from xml.etree import ElementTree
SERVER = 'server'
CLIENT = 'client'
class Client(object): class Client(object):
@ -31,26 +29,34 @@ class Client(object):
self.protocolVersion = data.attrib.get('protocolVersion') self.protocolVersion = data.attrib.get('protocolVersion')
self.protocolCapabilities = data.attrib.get('protocolCapabilities', '').split(',') self.protocolCapabilities = data.attrib.get('protocolCapabilities', '').split(',')
self.state = data.attrib.get('state') self.state = data.attrib.get('state')
self._sendCommandsTo = CLIENT self._proxyThroughServer = False
self._commandId = 0
def sendCommandsTo(self, value): @property
self._sendCommandsTo = value def quickName(self):
return self.name or self.product
def sendCommand(self, command, args=None, sendTo=None): def proxyThroughServer(self, value=True):
sendTo = sendTo or self._sendCommandsTo self._proxyThroughServer = value
if sendTo == CLIENT:
return self.sendClientCommand(command, args)
return self.sendServerCommand(command, args)
def sendClientCommand(self, command, args=None): def sendCommand(self, command, proxy=None, **params):
args = args or {} proxy = self._proxyThroughServer if proxy is None else proxy
args.update({ if proxy: return self.sendServerCommand(command, **params)
'X-Plex-Target-Client-Identifier': self.machineIdentifier, 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-Device-Name': self.name,
'X-Plex-Client-Identifier': self.server.machineIdentifier, '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) log.info('GET %s', url)
response = requests.get(url, timeout=TIMEOUT) response = requests.get(url, timeout=TIMEOUT)
if response.status_code != requests.codes.ok: if response.status_code != requests.codes.ok:
@ -59,54 +65,92 @@ class Client(object):
data = response.text.encode('utf8') data = response.text.encode('utf8')
return ElementTree.fromstring(data) if data else None return ElementTree.fromstring(data) if data else None
def sendServerCommand(self, command, args=None): def sendServerCommand(self, command, **params):
# TODO: Rip this out, server is throwing exceptions, maybe deprecated? params.update({'commandID': self._commandId})
path = '/system/players/%s/%s%s' % (self.address, command, utils.joinArgs(args)) path = '/system/players/%s/%s%s' % (self.address, command, utils.joinArgs(params))
self.server.query(path) self.server.query(path)
def url(self, path):
return 'http://%s:%s/player/%s' % (self.address, self.port, path.lstrip('/'))
# Navigation Commands # Navigation Commands
def moveUp(self): self.sendCommand('navigation/moveUp') # These commands navigate around the user interface.
def moveDown(self): self.sendCommand('navigation/moveDown') def contextMenu(self): self.sendCommand('player/navigation/contextMenu')
def moveLeft(self): self.sendCommand('navigation/moveLeft') def goBack(self): self.sendCommand('player/navigation/back')
def moveRight(self): self.sendCommand('navigation/moveRight') def goToHome(self): self.sendCommand('/player/navigation/home')
def pageUp(self): self.sendCommand('navigation/pageUp') def goToMusic(self): self.sendCommand('/player/navigation/music')
def pageDown(self): self.sendCommand('navigation/pageDown') def moveDown(self): self.sendCommand('player/navigation/moveDown')
def nextLetter(self): self.sendCommand('navigation/nextLetter') def moveLeft(self): self.sendCommand('player/navigation/moveLeft')
def previousLetter(self): self.sendCommand('navigation/previousLetter') def moveRight(self): self.sendCommand('player/navigation/moveRight')
def select(self): self.sendCommand('navigation/select') def moveUp(self): self.sendCommand('player/navigation/moveUp')
def back(self): self.sendCommand('navigation/back') def nextLetter(self): self.sendCommand('player/navigation/nextLetter')
def contextMenu(self): self.sendCommand('navigation/contextMenu') def pageDown(self): self.sendCommand('player/navigation/pageDown')
def toggleOSD(self): self.sendCommand('navigation/toggleOSD') 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 # Playback Commands
def play(self): self.sendCommand('playback/play') # most of the playback commands take a mandatory mtype {'music','photo','video'} argument,
def pause(self): self.sendCommand('playback/pause') # to specify which media type to apply the command to, (except for playMedia). This
def stop(self): self.sendCommand('playback/stop') # is in case there are multiple things happening (e.g. music in the background, photo
def stepForward(self): self.sendCommand('playback/stepForward') # slideshow in the foreground).
def bigStepForward(self): self.sendCommand('playback/bigStepForward') def pause(self, mtype): self.sendCommand('player/playback/pause', type=mtype)
def stepBack(self): self.sendCommand('playback/stepBack') def play(self, mtype): self.sendCommand('player/playback/play', type=mtype)
def bigStepBack(self): self.sendCommand('playback/bigStepBack') def refreshPlayQueue(self, playQueueID, mtype=None): self.sendCommand('player/playback/refreshPlayQueue', playQueueID=playQueueID, type=mtype)
def skipNext(self): self.sendCommand('playback/skipNext') def seekTo(self, offset, mtype=None): self.sendCommand('player/playback/seekTo', offset=offset, type=mtype) # offset in milliseconds
def skipPrevious(self): self.sendCommand('playback/skipPrevious') 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, video, viewOffset=0): def playMedia(self, media, **params):
playqueue = self.server.createPlayQueue(video) server_uri = media.server.baseuri.split(':')
self.sendCommand('playback/playMedia', { playqueue = self.server.createPlayQueue(media)
self.sendCommand('player/playback/playMedia', **dict({
'machineIdentifier': self.server.machineIdentifier, '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, 'containerKey': '/playQueues/%s?window=100&own=1' % playqueue.playQueueID,
'key': video.key, }, **params))
'offset': 0,
})
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): def timeline(self):
params = {'wait':1, 'commandID':4} self.sendCommand('timeline/poll', **{'wait':1, 'commandID':4})
return self.server.query('timeline/poll', params=params)
def isPlayingMedia(self): 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() timeline = self.timeline()
for media_type in timeline: for media_type in timeline:
if media_type.get('state') == 'playing': if media_type.get('state') == 'playing':

View file

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

View file

@ -172,18 +172,7 @@ class ResourceConnection(object):
return '<%s:%s>' % (self.__class__.__name__, self.uri.encode('utf8')) return '<%s:%s>' % (self.__class__.__name__, self.uri.encode('utf8'))
def _findResource(resources, search, port=32400): # TODO: Is this a plex client in disguise?
""" 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)
class MyPlexDevice(object): class MyPlexDevice(object):
DEVICES = 'https://plex.tv/devices.xml' 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 bigStepBack(self, args=None): self.sendCommand('playback/bigStepBack', args) # noqa
def skipNext(self, args=None): self.sendCommand('playback/skipNext', 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 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

@ -92,6 +92,15 @@ class PlexPartialObject(object):
return Client(self.server, elem) return Client(self.server, elem)
return None 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): def _findTranscodeSession(self, data):
elem = data.find('TranscodeSession') elem = data.find('TranscodeSession')
if elem is not None: if elem is not None:
@ -193,6 +202,21 @@ def listItems(server, path, libtype=None, watched=None):
return items 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): def searchType(libtype):
if libtype == 'movie': return 1 if libtype == 'movie': return 1
elif libtype == 'show': return 2 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.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.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.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 # data for active sessions
self.sessionKey = utils.cast(int, data.attrib.get('sessionKey', NA)) self.sessionKey = utils.cast(int, data.attrib.get('sessionKey', NA))
self.user = self._findUser(data) 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.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.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.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 # data for active sessions
self.sessionKey = utils.cast(int, data.attrib.get('sessionKey', NA)) self.sessionKey = utils.cast(int, data.attrib.get('sessionKey', NA))
self.user = self._findUser(data) self.user = self._findUser(data)

View file

@ -18,12 +18,12 @@ SHOW_TITLE = 'Game of Thrones'
SHOW_SEASON = 'Season 1' SHOW_SEASON = 'Season 1'
SHOW_EPISODE = 'Winter Is Coming' SHOW_EPISODE = 'Winter Is Coming'
MOVIE_SECTION = 'Movies' MOVIE_SECTION = 'Movies'
MOVIE_TITLE = 'Jurassic Park' MOVIE_TITLE = 'Jurassic World'
AUDIO_SECTION = 'Music' AUDIO_SECTION = 'Music'
AUDIO_ARTIST = 'Beastie Boys' AUDIO_ARTIST = 'Beastie Boys'
AUDIO_ALBUM = 'Licensed To Ill' AUDIO_ALBUM = 'Licensed To Ill'
AUDIO_TRACK = 'Brass Monkey' AUDIO_TRACK = 'Brass Monkey'
PLEX_CLIENT = 'iphone-mike' PLEX_CLIENT = 'pkkid-home'
#----------------------- #-----------------------
@ -263,6 +263,20 @@ def test_list_video_tags(plex, user=None):
assert movie in related, 'Movie was not found in related directors search.' 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') @register('meta,audio')
def test_list_audio_tags(plex, user=None): def test_list_audio_tags(plex, user=None):
section = plex.library.section(AUDIO_SECTION) section = plex.library.section(AUDIO_SECTION)
@ -330,23 +344,91 @@ def test_play_queues(plex, user=None):
#----------------------- #-----------------------
@register('client') @register('client')
def test_list_devices(plex, user=None): def test_list_clients(plex, user=None):
assert user, 'Must specify username, password & resource to run this test.' clients = [c.name or c.product for c in plex.clients()]
log(2, ', '.join([r.name or r.product for r in user.resources()])) log(2, ', '.join(clients))
@register('client') @register('client')
def test_client_play_media(plex, user=None): def test_client_navigation(plex, user=None):
episode = plex.library.section(SHOW_SECTION).get(SHOW_TITLE).get(SHOW_EPISODE)
client = plex.client(PLEX_CLIENT) client = plex.client(PLEX_CLIENT)
client.playMedia(episode); time.sleep(10) _navigate(plex, client)
client.pause(); time.sleep(3)
client.stepForward(); time.sleep(3)
client.play(); time.sleep(3) @register('client')
client.stop(); time.sleep(3) def test_client_navigation_via_proxy(plex, user=None):
movie = plex.library.get(MOVIE_TITLE) client = plex.client(PLEX_CLIENT)
movie.play(client); time.sleep(10) client.proxyThroughServer()
client.stop() _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): # 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) # 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__': if __name__ == '__main__':
# There are three ways to authenticate: # There are three ways to authenticate:
# 1. If the server is running on localhost, just run without any auth. # 1. If the server is running on localhost, just run without any auth.