mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-12 23:17:07 +00:00
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:
parent
e41e8676c3
commit
b277facf10
7 changed files with 290 additions and 127 deletions
|
@ -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)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -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 playMedia(self, video, viewOffset=0):
|
def skipTo(self, key, mtype=None): self.sendCommand('player/playback/skipTo', key=key, type=mtype) # skips to item with matching key
|
||||||
playqueue = self.server.createPlayQueue(video)
|
def stepBack(self, mtype=None): self.sendCommand('player/playback/stepBack', type=mtype)
|
||||||
self.sendCommand('playback/playMedia', {
|
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,
|
'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':
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -91,6 +91,15 @@ class PlexPartialObject(object):
|
||||||
from plexapi.client import Client
|
from plexapi.client import Client
|
||||||
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')
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
125
tests/tests.py
125
tests/tests.py
|
@ -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'
|
||||||
|
|
||||||
|
|
||||||
#-----------------------
|
#-----------------------
|
||||||
|
@ -261,7 +261,21 @@ def test_list_video_tags(plex, user=None):
|
||||||
related = movies.search(None, director=movie.directors[0])
|
related = movies.search(None, director=movie.directors[0])
|
||||||
log(4, related[0:3])
|
log(4, related[0:3])
|
||||||
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):
|
||||||
|
@ -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.
|
||||||
|
|
Loading…
Reference in a new issue