mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-26 05:30:20 +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
|
||||
# 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)
|
||||
```
|
||||
|
||||
|
|
|
@ -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')
|
||||
# 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, video, viewOffset=0):
|
||||
playqueue = self.server.createPlayQueue(video)
|
||||
self.sendCommand('playback/playMedia', {
|
||||
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':
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -92,6 +92,15 @@ class PlexPartialObject(object):
|
|||
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')
|
||||
if elem is not None:
|
||||
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
123
tests/tests.py
123
tests/tests.py
|
@ -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'
|
||||
|
||||
|
||||
#-----------------------
|
||||
|
@ -263,6 +263,20 @@ def test_list_video_tags(plex, user=None):
|
|||
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):
|
||||
section = plex.library.section(AUDIO_SECTION)
|
||||
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue