Merge pull request #100 from Hellowlol/responses

Responses
This commit is contained in:
Michael Shepanski 2017-02-01 22:05:59 -05:00 committed by GitHub
commit 6e761f0fc5
26 changed files with 2354 additions and 62 deletions

9
.coveragerc Normal file
View file

@ -0,0 +1,9 @@
[report]
exclude_lines =
pragma: no cover
raise NotImplementedError
raise Unsupported
except ImportError
def __repr__
def __bool__
if __name__ == .__main__.:

4
.gitignore vendored
View file

@ -17,3 +17,7 @@ include/
lib/
pip-selfcheck.json
pyvenv.cfg
htmlcov
.coverage
*.orig

View file

@ -2,10 +2,18 @@ language: python
python:
- "2.7"
- "3.3"
- "3.4"
- "3.5"
- "3.6"
before_install:
- pip install -U pytest pytest-cov coveralls
install:
- pip install .
- pip install -r requirements.pip
- pip install -r requirements_dev.txt
script: python tests/runtests.py --query=test_core/test_server
script: py.test tests/tests_pytest --cov-config .coveragerc --cov=plexapi --cov-report=html
after_success:
- coveralls

View file

@ -131,6 +131,16 @@ class Artist(Audio):
""" Alias of :func:`~plexapi.audio.Artist.track`. """
return self.track(title)
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
downloaded = []
for album in self.albums():
for track in album.tracks():
dl = track.download(savepath=savepath, keep_orginal_name=keep_orginal_name, **kwargs)
if dl:
downloaded.extend(dl)
return downloaded
@utils.register_libtype
class Album(Audio):
@ -192,6 +202,15 @@ class Album(Audio):
""" Return :func:`~plexapi.audio.Artist` of this album. """
return utils.listItems(self.server, self.parentKey)[0]
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
downloaded = []
for ep in self.tracks():
dl = ep.download(savepath=savepath, keep_orginal_name=keep_orginal_name, **kwargs)
if dl:
downloaded.extend(dl)
return downloaded
@utils.register_libtype
class Track(Audio, Playable):
@ -255,9 +274,13 @@ class Track(Audio, Playable):
self.ratingCount = utils.cast(int, data.attrib.get('ratingCount', NA))
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
self.year = utils.cast(int, data.attrib.get('year', NA))
# media is included in /children
self.media = [media.Media(self.server, e, self.initpath, self)
for e in data if e.tag == media.Media.TYPE]
if self.isFullObject(): # check me
self.moods = [media.Mood(self.server, e) for e in data if e.tag == media.Mood.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]
# data for active sessions and history
self.sessionKey = utils.cast(int, data.attrib.get('sessionKey', NA))
self.username = utils.findUsername(data)
@ -277,3 +300,39 @@ class Track(Audio, Playable):
def artist(self):
""" Return this track's :class:`~plexapi.audio.Artist`. """
return utils.listItems(self.server, self.grandparentKey)[0]
def _prettyfilename(self):
return '%s - %s %s' % (self.grandparentTitle, self.parentTitle, self.title)
'''
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
"""Download a episode. If kwargs are passed your can download a trancoded file.
Args:
savepath (str): Abs path to savefolder
keep_orginal_name (bool): Use the mediafiles orginal name
kwargs:
See getStreamURL docs.
"""
downloaded = []
locs = [i for i in self.iterParts() if i]
for loc in locs:
if keep_orginal_name is False:
name = '%s.%s' % (self._prettyfilename(), loc.container)
else:
name = loc.file
# So this seems to be a alot slower but allows transcode.
if kwargs:
download_url = self.getStreamURL(**kwargs)
else:
download_url = self.server.url('%s?download=1' % loc.key)
dl = utils.download(download_url, filename=name, savepath=savepath, session=self.server.session)
if dl:
downloaded.append(dl)
return downloaded
'''

View file

@ -45,8 +45,12 @@ class PlexClient(object):
def __init__(self, baseurl, token=None, session=None, server=None, data=None):
self.baseurl = baseurl.strip('/')
self.token = token
self.session = session or requests.Session()
self.server = server
# session > server.session > requests.Session
if server:
self.session = session or server.session
else:
self.session = session or requests.Session()
self._loadData(data) if data is not None else self.connect()
self._proxyThroughServer = False
self._commandId = 0

View file

@ -23,3 +23,6 @@ class Unsupported(PlexApiException):
class Unauthorized(PlexApiException):
""" Invalid username or password. """
pass
class NotImplementedError(PlexApiException):
pass

View file

@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
from plexapi import log, utils
from plexapi import X_PLEX_CONTAINER_SIZE
from plexapi import X_PLEX_CONTAINER_SIZE, log, utils
from plexapi.compat import unquote
from plexapi.media import MediaTag
from plexapi.exceptions import BadRequest, NotFound
from plexapi.media import MediaTag
class Library(object):
@ -67,11 +66,11 @@ class Library(object):
def sectionByID(self, sectionID):
""" Returns the :class:`~plexapi.library.LibrarySection` that matches the specified sectionID.
Parameters:
sectionID (int): ID of the section to return.
"""
if not self._sectionsByID:
if not self._sectionsByID or sectionID not in self._sectionsByID:
self.sections()
return self._sectionsByID[sectionID]
@ -89,16 +88,18 @@ class Library(object):
""" Returns a list of all media items recently added. """
return utils.listItems(self.server, '/library/recentlyAdded')
def get(self, title):
""" Return the first item from all items with the specified title.
def get(self, title): # this should use hub search when its merged
""" Return the first item from all items with the specified title.
Parameters:
title (str): Title of the item to return.
"""
return utils.findItem(self.server, '/library/all', title)
for i in self.all():
if i.title.lower() == title.lower():
return i
def getByKey(self, key):
""" Return the first item from all items with the specified key.
""" Return the first item from all items with the specified key.
Parameters:
key (str): Key of the item to return.
@ -131,6 +132,8 @@ class Library(object):
server will automatically clean up old bundles once a week as part of Scheduled Tasks.
"""
self.server.query('/library/clean/bundles')
# Should this return true or false?
# check element if if has the correct mediaprefix?
def emptyTrash(self):
""" If a library has items in the Library Trash, use this option to empty the Trash. """
@ -226,7 +229,7 @@ class LibrarySection(object):
def recentlyAdded(self, maxresults=50):
""" Returns a list of media items recently added from this library section.
Parameters:
maxresults (int): Max number of items to return (default 50).
"""
@ -234,7 +237,7 @@ class LibrarySection(object):
def analyze(self):
""" Run an analysis on all of the items in this library section. """
self.server.query('/library/sections/%s/analyze' % self.key)
self.server.query('/library/sections/%s/analyze' % self.key, method=self.server.session.put)
def emptyTrash(self):
""" If a section has items in the Trash, use this option to empty the Trash. """
@ -366,7 +369,7 @@ class LibrarySection(object):
class MovieSection(LibrarySection):
""" Represents a :class:`~plexapi.library.LibrarySection` section containing movies.
Attributes:
ALLOWED_FILTERS (list<str>): List of allowed search filters. ('unwatched',
'duplicate', 'year', 'decade', 'genre', 'contentRating', 'collection',
@ -377,15 +380,15 @@ class MovieSection(LibrarySection):
TYPE (str): 'movie'
"""
ALLOWED_FILTERS = ('unwatched', 'duplicate', 'year', 'decade', 'genre', 'contentRating',
'collection', 'director', 'actor', 'country', 'studio', 'resolution')
'collection', 'director', 'actor', 'country', 'studio', 'resolution')
ALLOWED_SORT = ('addedAt', 'originallyAvailableAt', 'lastViewedAt', 'titleSort', 'rating',
'mediaHeight', 'duration')
'mediaHeight', 'duration')
TYPE = 'movie'
class ShowSection(LibrarySection):
""" Represents a :class:`~plexapi.library.LibrarySection` section containing tv shows.
Attributes:
ALLOWED_FILTERS (list<str>): List of allowed search filters. ('unwatched',
'year', 'genre', 'contentRating', 'network', 'collection')
@ -395,7 +398,7 @@ class ShowSection(LibrarySection):
"""
ALLOWED_FILTERS = ('unwatched', 'year', 'genre', 'contentRating', 'network', 'collection')
ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'originallyAvailableAt', 'titleSort',
'rating', 'unwatched')
'rating', 'unwatched')
TYPE = 'show'
def searchShows(self, **kwargs):
@ -408,7 +411,7 @@ class ShowSection(LibrarySection):
def recentlyAdded(self, libtype='episode', maxresults=50):
""" Returns a list of recently added episodes from this library section.
Parameters:
maxresults (int): Max number of items to return (default 50).
"""
@ -417,7 +420,7 @@ class ShowSection(LibrarySection):
class MusicSection(LibrarySection):
""" Represents a :class:`~plexapi.library.LibrarySection` section containing music artists.
Attributes:
ALLOWED_FILTERS (list<str>): List of allowed search filters. ('genre',
'country', 'collection')
@ -448,23 +451,25 @@ class MusicSection(LibrarySection):
class PhotoSection(LibrarySection):
""" Represents a :class:`~plexapi.library.LibrarySection` section containing photos.
Attributes:
ALLOWED_FILTERS (list<str>): List of allowed search filters. <NONE>
ALLOWED_SORT (list<str>): List of allowed sorting keys. <NONE>
TYPE (str): 'photo'
"""
ALLOWED_FILTERS = ()
ALLOWED_FILTERS = ('all', 'iso', 'make', 'lens', 'aperture', 'exposure')
ALLOWED_SORT = ()
TYPE = 'photo'
def searchAlbums(self, **kwargs):
def searchAlbums(self, title, **kwargs): # lets use this for now.
""" Search for an album. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
return self.search(libtype='photo', **kwargs)
albums = utils.listItems(self.server, '/library/sections/%s/all?type=14' % self.key)
return [i for i in albums if i.title.lower() == title.lower()]
def searchPhotos(self, **kwargs):
def searchPhotos(self, title, **kwargs):
""" Search for a photo. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
return self.search(libtype='photo', **kwargs)
photos = utils.listItems(self.server, '/library/sections/%s/all?type=13' % self.key)
return [i for i in photos if i.title.lower() == title.lower()]
@utils.register_libtype
@ -479,7 +484,7 @@ class FilterChoice(object):
fastKey (str): API path to quickly list all items in this filter
(/library/sections/<section>/all?genre=<key>)
key (str): Short key (id) of this filter option (used ad <key> in fastKey above).
thumb (str): Thumbnail used to represent this filter option.
thumb (str): Thumbnail used to represent this filter option.
title (str): Human readable name for this filter option.
type (str): Filter type (genre, contentRating, etc).
"""

View file

@ -45,7 +45,11 @@ class MyPlexAccount(object):
BASEURL = 'https://plex.tv/users/account'
SIGNIN = 'https://my.plexapp.com/users/sign_in.xml'
def __init__(self, data, initpath=None):
def __init__(self, data=None, initpath=None, username=None, password=None, session=None):
if data is None and username and password:
self.Signin(username, password)#
self._session = session or requests.Session()
self.authenticationToken = data.attrib.get('authenticationToken')
if self.authenticationToken:
logfilter.add_secret(self.authenticationToken)
@ -116,7 +120,7 @@ class MyPlexAccount(object):
return _findItem(self.users(), email, ['username', 'email'])
@classmethod
def signin(cls, username, password):
def signin(cls, username, password, session=None):
""" Returns a new :class:`~myplex.MyPlexAccount` object by connecting to MyPlex with the
specified username and password. This is essentially logging into MyPlex and often
the very first entry point to using this API.
@ -133,7 +137,8 @@ class MyPlexAccount(object):
del plexapi.BASE_HEADERS['X-Plex-Token']
auth = (username, password)
log.info('POST %s', cls.SIGNIN)
response = requests.post(
sess = session or requests.Session()
response = sess.post(
cls.SIGNIN, headers=plexapi.BASE_HEADERS, auth=auth, timeout=TIMEOUT)
if response.status_code != requests.codes.created:
codename = codes.get(response.status_code)[0]
@ -142,7 +147,7 @@ class MyPlexAccount(object):
(response.status_code, codename))
raise BadRequest('(%s) %s' % (response.status_code, codename))
data = ElementTree.fromstring(response.text.encode('utf8'))
return cls(data, cls.SIGNIN)
return MyPlexAccount(data, cls.SIGNIN, session=sess)
class MyPlexUser(object):

View file

@ -57,7 +57,7 @@ class Playlist(PlexPartialObject, Playable):
for item in items:
if item.listType != self.playlistType:
raise BadRequest('Can not mix media types when building a playlist: %s and %s' % (self.playlistType, item.listType))
ratingKeys.append(item.ratingKey)
ratingKeys.append(str(item.ratingKey))
uuid = items[0].section().uuid
ratingKeys = ','.join(ratingKeys)
path = '%s/items%s' % (self.key, utils.joinArgs({

View file

@ -206,9 +206,10 @@ class PlexServer(object):
if headers:
h.update(headers)
response = method(url, headers=h, timeout=TIMEOUT, **kwargs)
if response.status_code not in [200, 201]:
#print(response.url)
if response.status_code not in [200, 201]: # pragma: no cover
codename = codes.get(response.status_code)[0]
raise BadRequest('(%s) %s' % (response.status_code, codename))
raise BadRequest('(%s) %s %s' % (response.status_code, codename, response.url))
data = response.text.encode('utf8')
return ElementTree.fromstring(data) if data else None

View file

@ -1,13 +1,24 @@
# -*- coding: utf-8 -*-
import logging, re
import logging
import os
import re
from datetime import datetime
from plexapi.compat import quote, urlencode, string_type
from plexapi.exceptions import NotFound, UnknownType, Unsupported
from threading import Thread
import requests
from plexapi.compat import quote, string_type, urlencode
from plexapi.exceptions import NotFound, NotImplementedError, UnknownType, Unsupported
#from plexapi import log
# Search Types - Plex uses these to filter specific media types when searching.
SEARCHTYPES = {'movie': 1, 'show': 2, 'season': 3, 'episode': 4,
'artist': 8, 'album': 9, 'track': 10}
SEARCHTYPES = {'movie': 1, 'show': 2, 'season': 3,
'episode': 4, 'artist': 8, 'album': 9, 'track': 10,
'photo': 14}
LIBRARY_TYPES = {}
@ -20,7 +31,7 @@ def register_libtype(cls):
return cls
class NA(object):
class _NA(object):
""" This used to be a simple variable equal to '__NA__'. There has been need to
compare NA against None in some use cases. This object allows the internals
of PlexAPI to distinguish between unfetched values and fetched, but non-existent
@ -31,7 +42,7 @@ class NA(object):
return False
def __eq__(self, other):
return isinstance(other, NA) or other in [None, '__NA__']
return isinstance(other, _NA) or other in [None, '__NA__']
def __nonzero__(self):
return False
@ -40,6 +51,10 @@ class NA(object):
return '__NA__'
# Lets do this for now.
NA = _NA()
class SecretsFilter(logging.Filter):
""" Logging filter to hide secrets. """
def __init__(self, secrets=None):
@ -73,6 +88,7 @@ class PlexPartialObject(object):
self.server = server
self.initpath = initpath
self._loadData(data)
self._reloaded = False
def __eq__(self, other):
return other is not None and self.key == other.key
@ -87,6 +103,7 @@ class PlexPartialObject(object):
# Auto reload self, from the full key (path) when needed.
if attr == 'key' or self.__dict__.get(attr) or self.isFullObject():
return self.__dict__.get(attr, NA)
print('reload because of %s' % attr)
self.reload()
return self.__dict__.get(attr, NA)
@ -95,14 +112,14 @@ class PlexPartialObject(object):
self.__dict__[attr] = value
def _loadData(self, data):
raise Exception('Abstract method not implemented.')
raise NotImplementedError('Abstract method not implemented.')
def isFullObject(self):
""" Retruns True if this is already a full object. A full object means all attributes
were populated from the api path representing only this item. For example, the
search result for a movie often only contain a portion of the attributes a full
object (main url) for that movie contain.
"""
"""
return not self.key or self.key == self.initpath
def isPartialObject(self):
@ -114,6 +131,8 @@ class PlexPartialObject(object):
data = self.server.query(self.key)
self.initpath = self.key
self._loadData(data[0])
self._reloaded = True
return self
class Playable(object):
@ -123,7 +142,7 @@ class Playable(object):
Attributes:
player (:class:`~plexapi.client.PlexClient`): Client object playing this item (for active sessions).
playlistItemID (int): Playlist item ID (only populated for :class:`~plexapi.playlist.Playlist` items).
playlistItemID (int): Playlist item ID (only populated for :class:`~plexapi.playlist.Playlist` items).
sessionKey (int): Active session key.
transcodeSession (:class:`~plexapi.media.TranscodeSession`): Transcode Session object
if item is being transcoded (None otherwise).
@ -170,7 +189,9 @@ class Playable(object):
# remove None values
params = {k: v for k, v in params.items() if v is not None}
streamtype = 'audio' if self.TYPE in ('track', 'album') else 'video'
return self.server.url('/%s/:/transcode/universal/start.m3u8?%s' % (streamtype, urlencode(params)))
# sort the keys since the randomness fucks with my tests..
sorted_params = sorted(params.items(), key=lambda val: val[0])
return self.server.url('/%s/:/transcode/universal/start.m3u8?%s' % (streamtype, urlencode(sorted_params)))
def iterParts(self):
""" Iterates over the parts of this media item. """
@ -186,6 +207,37 @@ class Playable(object):
"""
client.playMedia(self)
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
"""Download a episode. If kwargs are passed your can download a trancoded file.
Args:
savepath (str): Abs path to savefolder
keep_orginal_name (bool): Use the mediafiles orginal name
kwargs:
See getStreamURL docs.
"""
downloaded = []
locs = [i for i in self.iterParts() if i]
for loc in locs:
if keep_orginal_name is False:
name = '%s.%s' % (self._prettyfilename(), loc.container)
else:
name = loc.file
# So this seems to be a alot slower but allows transcode.
if kwargs:
download_url = self.getStreamURL(**kwargs)
else:
download_url = self.server.url('%s?download=1' % loc.key)
dl = download(download_url, filename=name, savepath=savepath, session=self.server.session)
if dl:
downloaded.append(dl)
return downloaded
def buildItem(server, elem, initpath, bytag=False):
""" Factory function to build the objects used within the PlexAPI.
@ -377,7 +429,7 @@ def listChoices(server, path):
def listItems(server, path, libtype=None, watched=None, bytag=False):
""" Returns a list of object built from :func:`~plexapi.utils.buildItem()` found
within the specified path.
within the specified path.
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
@ -401,7 +453,7 @@ def listItems(server, path, libtype=None, watched=None, bytag=False):
return items
def rget(obj, attrstr, default=None, delim='.'):
def rget(obj, attrstr, default=None, delim='.'): # pragma: no cover
""" Returns the value at the specified attrstr location within a nexted tree of
dicts, lists, tuples, functions, classes, etc. The lookup is done recursivley
for each key in attrstr (split by by the delimiter) This function is heavily
@ -466,6 +518,7 @@ def threaded(callback, listargs):
threads[-1].start()
for thread in threads:
thread.join()
return results
@ -482,3 +535,72 @@ def toDatetime(value, format=None):
else:
value = datetime.fromtimestamp(int(value))
return value
def download(url, filename=None, savepath=None, session=None, chunksize=4024, mocked=False):
"""Helper to download a thumb, videofile or something.
Args:
url (str): url where the content be reached
filename (str): Filename of the downloaded file, default None
savepath (str): Defaults to current working dir
chunksize (int): What chunksize read/write at the time
mocked (bool): Helper to do evertything except write the file.
Example:
>>> download(a_episode.getStreamURL(), a_episode.location)
/path/to/file
Returns:
/path/to/file or None
"""
session = session or requests.Session()
print('Mocked download %s' % mocked)
if savepath is None:
savepath = os.getcwd()
else:
# Make sure the user supplied path exists
try:
os.makedirs(savepath)
except OSError:
if not os.path.isdir(savepath): # pragma: no cover
raise
filename = os.path.basename(filename)
fullpath = os.path.join(savepath, filename)
try:
response = session.get(url, stream=True)
# images dont have a extention so we try
# to guess it from content-type
ext = os.path.splitext(fullpath)[-1]
if ext:
ext = ''
else:
cp = response.headers.get('content-type')
if cp:
if 'image' in cp:
ext = '.%s' % cp.split('/')[1]
fullpath = '%s%s' % (fullpath, ext)
if mocked:
return fullpath
with open(fullpath, 'wb') as f:
for chunk in response.iter_content(chunk_size=chunksize):
if chunk:
f.write(chunk)
#log.debug('Downloaded %s to %s from %s' % (filename, fullpath, url))
return fullpath
except Exception as e: # pragma: no cover
print('e %s' % e)
#log.exception('Failed to download %s to %s %s' % (url, fullpath, e))

View file

@ -53,7 +53,7 @@ class Video(PlexPartialObject):
that are useful to knowwhether it's a video file,
a music track, or one of your photos.
"""
self.server.query('/%s/analyze' % self.key)
self.server.query('/%s/analyze' % self.key.lstrip('/'), method=self.server.session.put)
def markWatched(self):
"""Mark a items as watched."""
@ -141,6 +141,39 @@ class Movie(Video, Playable):
def isWatched(self):
return bool(self.viewCount > 0)
@property
def location(self):
""" This does not exist in plex xml response but is added to have a common
interface to get the location of the Movie/Show/Episode
"""
files = [i.file for i in self.iterParts() if i]
if len(files) == 1:
files = files[0]
return files
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
downloaded = []
locs = [i for i in self.iterParts() if i]
for loc in locs:
if keep_orginal_name is False:
name = '%s.%s' % (self.title.replace(' ', '.'), loc.container)
else:
name = loc.file
# So this seems to be a alot slower but allows transcode.
if kwargs:
download_url = self.getStreamURL(**kwargs)
else:
download_url = self.server.url('%s?download=1' % loc.key)
dl = utils.download(download_url, filename=name, savepath=savepath, session=self.server.session)
if dl:
downloaded.append(dl)
return downloaded
@utils.register_libtype
class Show(Video):
@ -153,6 +186,8 @@ class Show(Video):
data (Element): Usually built from server.query
"""
Video._loadData(self, data)
# Incase this was loaded from search etc
self.key = self.key.replace('/children', '')
self.art = data.attrib.get('art', NA)
self.banner = data.attrib.get('banner', NA)
self.childCount = utils.cast(int, data.attrib.get('childCount', NA))
@ -161,7 +196,7 @@ class Show(Video):
self.guid = data.attrib.get('guid', NA)
self.index = data.attrib.get('index', NA)
self.leafCount = utils.cast(int, data.attrib.get('leafCount', NA))
self.location = utils.findLocations(data, single=True)
self.location = utils.findLocations(data, single=True) or NA
self.originallyAvailableAt = utils.toDatetime(
data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
self.rating = utils.cast(float, data.attrib.get('rating', NA))
@ -170,11 +205,11 @@ class Show(Video):
self.viewedLeafCount = utils.cast(
int, data.attrib.get('viewedLeafCount', NA))
self.year = utils.cast(int, data.attrib.get('year', NA))
#if self.isFullObject(): # will be fixed with docs.
self.genres = [media.Genre(self.server, e)
for e in data if e.tag == media.Genre.TYPE]
self.roles = [media.Role(self.server, e)
for e in data if e.tag == media.Role.TYPE]
if self.isFullObject(): # will be fixed with docs.
self.genres = [media.Genre(self.server, e)
for e in data if e.tag == media.Genre.TYPE]
self.roles = [media.Role(self.server, e)
for e in data if e.tag == media.Role.TYPE]
@property
def actors(self):
@ -266,9 +301,23 @@ class Show(Video):
"""
return self.episode(title)
def analyze(self):
""" """
raise 'Cant analyse a show' # fix me
def refresh(self):
"""Refresh the metadata."""
self.server.query('/library/metadata/%s/refresh' % self.ratingKey)
self.server.query('/library/metadata/%s/refresh' % self.ratingKey, method=self.server.session.put)
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
downloaded = []
for ep in self.episodes():
dl = ep.download(savepath=savepath, keep_orginal_name=keep_orginal_name, **kwargs)
if dl:
downloaded.extend(dl)
return downloaded
@utils.register_libtype
@ -282,11 +331,12 @@ class Season(Video):
data (Element): Usually built from server.query
"""
Video._loadData(self, data)
self.key = self.key.replace('/children', '')
self.leafCount = utils.cast(int, data.attrib.get('leafCount', NA))
self.index = utils.cast(int, data.attrib.get('index', NA))
self.parentKey = data.attrib.get('parentKey', NA)
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey', NA))
self.grandparentTitle = data.attrib.get('grandparentTitle', NA)
self.parentTitle = data.attrib.get('parentTitle', NA)
self.viewedLeafCount = utils.cast(
int, data.attrib.get('viewedLeafCount', NA))
@ -296,7 +346,7 @@ class Season(Video):
@property
def seasonNumber(self):
"""Reurns season number."""
"""Returns season number."""
return self.index
def episodes(self, watched=None):
@ -352,7 +402,6 @@ class Season(Video):
else:
raise NotFound('Couldnt find %s.Season %s Episode %s.' % (self.grandparentTitle, self.index. episode))
def get(self, title):
"""Get a episode with a matching title.
@ -380,7 +429,16 @@ class Season(Video):
clsname = self.__class__.__name__
key = self.key.replace('/library/metadata/', '').replace('/children', '') if self.key else 'NA'
title = self.title.replace(' ', '.')[0:20].encode('utf8')
return '<%s:%s:%s:%s>' % (clsname, key, self.grandparentTitle, title)
return '<%s:%s:%s:%s>' % (clsname, key, self.parentTitle, title)
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
downloaded = []
for ep in self.episodes():
dl = ep.download(savepath=savepath, keep_orginal_name=keep_orginal_name, **kwargs)
if dl:
downloaded.extend(dl)
return downloaded
@utils.register_libtype
@ -464,3 +522,19 @@ class Episode(Video, Playable):
def show(self):
"""Return this episodes Show"""
return utils.listItems(self.server, self.grandparentKey)[0]
@property
def location(self):
""" This does not exist in plex xml response but is added to have a common
interface to get the location of the Movie/Show
"""
# Note this should probably belong to some parent.
files = [i.file for i in self.iterParts() if i]
if len(files) == 1:
files = files[0]
return files
def _prettyfilename(self):
return '%s.S%sE%s' % (self.grandparentTitle.replace(' ', '.'), str(self.seasonNumber).zfill(2), str(self.index).zfill(2))

7
requirements_dev.txt Normal file
View file

@ -0,0 +1,7 @@
requests
pytest
pytest-cov
betamax
betamax_serializers
pillow
coveralls

View file

@ -0,0 +1,7 @@
import sys
import os
plexapi_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(0, plexapi_path)

View file

@ -0,0 +1,163 @@
from functools import partial
import os
import betamax
from betamax_serializers import pretty_json
import pytest
import requests
import plexapi
token = os.environ.get('PLEX_TOKEN')
test_token = os.environ.get('PLEX_TEST_TOKEN')
test_username = os.environ.get('PLEX_TEST_USERNAME')
test_password = os.environ.get('PLEX_TEST_PASSWORD')
@pytest.fixture(scope='session')
def pms(request):
from plexapi.server import PlexServer
sess = requests.Session()
"""
CASSETTE_LIBRARY_DIR = 'response/'
betamax.Betamax.register_serializer(pretty_json.PrettyJSONSerializer)
config = betamax.Betamax.configure()
config.define_cassette_placeholder('MASKED', token)
config.define_cassette_placeholder('MASKED', test_token)
recorder = betamax.Betamax(sess, cassette_library_dir=CASSETTE_LIBRARY_DIR)
recorder.use_cassette('http_responses', serialize_with='prettyjson') # record='new_episodes'
recorder.start()
"""
url = 'http://138.68.157.5:32400'
assert test_token
assert url
pms = PlexServer(url, test_token, session=sess)
#request.addfinalizer(recorder.stop)
return pms
@pytest.fixture()
def freshpms():
from plexapi.server import PlexServer
sess = requests.Session()
url = 'http://138.68.157.5:32400'
assert test_token
assert url
pms = PlexServer(url, test_token, session=sess)
return pms
def pytest_addoption(parser):
parser.addoption("--req_client", action="store_true",
help="Run tests that interact with a client")
def pytest_runtest_setup(item):
if 'req_client' in item.keywords and not item.config.getvalue("req_client"):
pytest.skip("need --req_client option to run")
else:
item.config.getvalue("req_client")
@pytest.fixture()
def plex_account():
from plexapi.myplex import MyPlexAccount
username = test_username
password = test_password
assert username and password
account = MyPlexAccount.signin(username, password)
assert account
return account
@pytest.fixture()
def a_movie(pms):
m = pms.library.search('16 blocks')
assert m
return m[0]
@pytest.fixture()
def a_tv_section(pms):
sec = pms.library.section('TV Shows')
assert sec
return sec
@pytest.fixture()
def a_movie_section(pms):
sec = pms.library.section('Movies')
assert sec
return sec
@pytest.fixture()
def a_music_section(pms):
sec = pms.library.section('Music')
assert sec
return sec
@pytest.fixture()
def a_photo_section(pms):
sec = pms.library.section('Photos')
assert sec
return sec
@pytest.fixture()
def a_artist(a_music_section):
sec = a_music_section.get('Infinite State')
assert sec
return sec
@pytest.fixture()
def a_music_album(a_music_section):
sec = a_music_section.get('Infinite State').album('Unmastered Impulses')
assert sec
return sec
@pytest.fixture()
def a_track(a_music_album):
track = a_music_album.track('Holy Moment')
assert track
return track
@pytest.fixture()
def a_show(a_tv_section):
sec = a_tv_section.get('The 100')
assert sec
return sec
@pytest.fixture()
def a_episode(a_show):
ep = a_show.get('Pilot')
assert ep
return ep
@pytest.fixture()
def a_photo_album(pms):
sec = pms.library.section('Photos')
assert sec
album = sec.get('photo_album1')
assert album
return album
@pytest.fixture()
def monkeydownload(request, monkeypatch):
monkeypatch.setattr('plexapi.utils.download', partial(plexapi.utils.download, mocked=True))
yield
monkeypatch.undo()

View file

@ -0,0 +1,20 @@
def test_mark_movie_watched(a_movie):
a_movie.markUnwatched()
print('Marking movie watched: %s' % a_movie)
print('View count: %s' % a_movie.viewCount)
a_movie.markWatched()
print('View count: %s' % a_movie.viewCount)
assert a_movie.viewCount == 1, 'View count 0 after watched.'
a_movie.markUnwatched()
print('View count: %s' % a_movie.viewCount)
assert a_movie.viewCount == 0, 'View count 1 after unwatched.'
def test_refresh_section(pms):
shows = pms.library.section('TV Shows')
#shows.refresh()
def test_refresh_video(pms):
result = pms.search('16 blocks')
#result[0].refresh()

View file

@ -0,0 +1,332 @@
# -*- coding: utf-8 -*-
import pytest
def test_audio_Artist_attr(a_artist):
m = a_artist
m.reload()
assert str(m.addedAt.date()) == '2017-01-17'
assert m.countries == []
assert [i.tag for i in m.genres] == ['Electronic']
assert m.guid == 'com.plexapp.agents.lastfm://Infinite%20State?lang=en'
assert m.index == '1'
assert m.initpath == '/library/metadata/20'
assert m.key == '/library/metadata/20'
assert m.librarySectionID == '3'
assert m.listType == 'audio'
assert m.location == '/media/music/unmastered_impulses'
assert m.ratingKey == 20
assert m.server.baseurl == 'http://138.68.157.5:32400'
assert m.similar == []
assert m.summary == ""
assert m.title == 'Infinite State'
assert m.titleSort == 'Infinite State'
assert m.type == 'artist'
assert str(m.updatedAt.date()) == '2017-01-25'
assert m.viewCount == 0
def test_audio_Artist_get(a_artist, a_music_section):
a_artist == a_music_section.searchArtists(**{'title': 'Infinite State'})[0]
a_artist.title == 'Infinite State'
def test_audio_Artist_track(a_artist):
track = a_artist.track('Holy Moment')
assert track.title == 'Holy Moment'
def test_audio_Artist_tracks(a_artist):
tracks = a_artist.tracks()
assert len(tracks) == 14
def test_audio_Artist_album(a_artist):
album = a_artist.album('Unmastered Impulses')
assert album.title == 'Unmastered Impulses'
def test_audio_Artist_albums(a_artist):
albums = a_artist.albums()
assert len(albums) == 1 and albums[0].title == 'Unmastered Impulses'
def test_audio_Album_attrs(a_music_album):
m = a_music_album
assert str(m.addedAt.date()) == '2017-01-17'
assert [i.tag for i in m.genres] == ['Electronic']
assert m.index == '1'
assert m.initpath == '/library/metadata/21'
assert m.key == '/library/metadata/21'
assert m.librarySectionID == '3'
assert m.listType == 'audio'
assert str(m.originallyAvailableAt.date()) == '2016-01-01'
assert m.parentKey == '/library/metadata/20'
assert m.parentRatingKey == '20'
assert str(m.parentThumb) == '__NA__'
assert m.parentTitle == 'Infinite State'
assert m.ratingKey == 21
assert m.server.baseurl == 'http://138.68.157.5:32400'
assert str(m.studio) == '__NA__'
assert m.summary == ''
assert m.thumb == '/library/metadata/21/thumb/1484693407'
assert m.title == 'Unmastered Impulses'
assert m.titleSort == 'Unmastered Impulses'
assert m.type == 'album'
assert str(m.updatedAt.date()) == '2017-01-17'
assert m.viewCount == 0
assert m.year == 2016
def test_audio_Album_tracks(a_music_album):
tracks = a_music_album.tracks()
assert len(tracks) == 14
assert tracks[0].grandparentKey == '/library/metadata/20'
assert tracks[0].grandparentRatingKey == '20'
assert tracks[0].grandparentTitle == 'Infinite State'
assert tracks[0].index == '1'
assert tracks[0].initpath == '/library/metadata/21/children'
assert tracks[0].key == '/library/metadata/22'
assert tracks[0].listType == 'audio'
assert tracks[0].originalTitle == 'Kenneth Reitz'
assert tracks[0].parentIndex == '1'
assert tracks[0].parentKey == '/library/metadata/21'
assert tracks[0].parentRatingKey == '21'
assert tracks[0].parentThumb == '/library/metadata/21/thumb/1484693407'
assert tracks[0].parentTitle == 'Unmastered Impulses'
assert tracks[0].player is None
assert tracks[0].ratingCount == 9
assert tracks[0].ratingKey == 22
assert tracks[0].server.baseurl == 'http://138.68.157.5:32400'
assert tracks[0].summary == ""
assert tracks[0].thumb == '/library/metadata/21/thumb/1484693407'
assert tracks[0].title == 'Holy Moment'
assert tracks[0].titleSort == 'Holy Moment'
assert tracks[0].transcodeSession is None
assert tracks[0].type == 'track'
assert str(tracks[0].updatedAt.date()) == '2017-01-17'
assert tracks[0].username is None
assert tracks[0].viewCount == 0
assert tracks[0].viewOffset == 0
def test_audio_Album_track(a_music_album):
# this is not reloaded. its not that much info missing.
track = a_music_album.track('Holy Moment')
assert str(track.addedAt.date()) == '2017-01-17'
assert track.duration == 298606
assert track.grandparentKey == '/library/metadata/20'
assert track.grandparentRatingKey == '20'
assert track.grandparentTitle == 'Infinite State'
assert track.index == '1'
assert track.initpath == '/library/metadata/21/children'
assert track.key == '/library/metadata/22'
assert track.listType == 'audio'
# Assign 0 track.media
med0 = track.media[0]
assert track.originalTitle == 'Kenneth Reitz'
assert track.parentIndex == '1'
assert track.parentKey == '/library/metadata/21'
assert track.parentRatingKey == '21'
assert track.parentThumb == '/library/metadata/21/thumb/1484693407'
assert track.parentTitle == 'Unmastered Impulses'
assert track.player is None
assert track.ratingCount == 9
assert track.ratingKey == 22
assert track.server.baseurl == 'http://138.68.157.5:32400'
assert track.summary == ''
assert track.thumb == '/library/metadata/21/thumb/1484693407'
assert track.title == 'Holy Moment'
assert track.titleSort == 'Holy Moment'
assert track.transcodeSession is None
assert track.type == 'track'
assert str(track.updatedAt.date()) == '2017-01-17'
assert track.username is None
assert track.viewCount == 0
assert track.viewOffset == 0
assert med0.aspectRatio is None
assert med0.audioChannels == 2
assert med0.audioCodec == 'mp3'
assert med0.bitrate == 385
assert med0.container == 'mp3'
assert med0.duration == 298606
assert med0.height is None
assert med0.id == 22
assert med0.initpath == '/library/metadata/21/children'
assert med0.optimizedForStreaming is None
# Assign 0 med0.parts
par0 = med0.parts[0]
assert med0.server.baseurl == 'http://138.68.157.5:32400'
assert med0.videoCodec is None
assert med0.videoFrameRate is None
assert med0.videoResolution is None
assert med0.width is None
assert par0.container == 'mp3'
assert par0.duration == 298606
assert par0.file == '/media/music/unmastered_impulses/01-Holy_Moment.mp3'
assert par0.id == 22
assert par0.initpath == '/library/metadata/21/children'
assert par0.key == '/library/parts/22/1484693136/file.mp3'
assert par0.server.baseurl == 'http://138.68.157.5:32400'
assert par0.size == 14360402
def test_audio_Album_get():
""" just a alias for track, lets skip it"""
pass
def test_audio_Album_artist(a_music_album):
artist = a_music_album.artist()
artist.title == 'Infinite State'
def test_audio_Track_attrs(a_music_album):
track = a_music_album.get('Holy Moment')
track.reload()
assert str(track.addedAt.date()) == '2017-01-17'
assert str(track.art) == '__NA__'
assert str(track.chapterSource) == '__NA__'
assert track.duration == 298606
assert str(track.grandparentArt) == '__NA__'
assert track.grandparentKey == '/library/metadata/20'
assert track.grandparentRatingKey == '20'
assert str(track.grandparentThumb) == '__NA__'
assert track.grandparentTitle == 'Infinite State'
assert track.guid == 'local://22'
assert track.index == '1'
assert track.initpath == '/library/metadata/22'
assert track.key == '/library/metadata/22'
assert str(track.lastViewedAt) == '__NA__'
assert track.librarySectionID == '3'
assert track.listType == 'audio'
# Assign 0 track.media
med0 = track.media[0]
assert track.moods == []
assert track.originalTitle == 'Kenneth Reitz'
assert track.parentIndex == '1'
assert track.parentKey == '/library/metadata/21'
assert track.parentRatingKey == '21'
assert track.parentThumb == '/library/metadata/21/thumb/1484693407'
assert track.parentTitle == 'Unmastered Impulses'
assert track.player is None
assert str(track.playlistItemID) == '__NA__'
assert str(track.primaryExtraKey) == '__NA__'
assert track.ratingCount == 9
assert track.ratingKey == 22
assert track.server.baseurl == 'http://138.68.157.5:32400'
assert str(track.sessionKey) == '__NA__'
assert track.summary == ''
assert track.thumb == '/library/metadata/21/thumb/1484693407'
assert track.title == 'Holy Moment'
assert track.titleSort == 'Holy Moment'
assert track.transcodeSession is None
assert track.type == 'track'
assert str(track.updatedAt.date()) == '2017-01-17'
assert track.username is None
assert track.viewCount == 0
assert track.viewOffset == 0
assert str(track.viewedAt) == '__NA__'
assert str(track.year) == '__NA__'
assert med0.aspectRatio is None
assert med0.audioChannels == 2
assert med0.audioCodec == 'mp3'
assert med0.bitrate == 385
assert med0.container == 'mp3'
assert med0.duration == 298606
assert med0.height is None
assert med0.id == 22
assert med0.initpath == '/library/metadata/22'
assert med0.optimizedForStreaming is None
# Assign 0 med0.parts
par0 = med0.parts[0]
assert med0.server.baseurl == 'http://138.68.157.5:32400'
assert med0.videoCodec is None
assert med0.videoFrameRate is None
assert med0.videoResolution is None
assert med0.width is None
assert par0.container == 'mp3'
assert par0.duration == 298606
assert par0.file == '/media/music/unmastered_impulses/01-Holy_Moment.mp3'
assert par0.id == 22
assert par0.initpath == '/library/metadata/22'
assert par0.key == '/library/parts/22/1484693136/file.mp3'
#assert par0.media == <Media:Holy.Moment>
assert par0.server.baseurl == 'http://138.68.157.5:32400'
assert par0.size == 14360402
# Assign 0 par0.streams
str0 = par0.streams[0]
assert str0.audioChannelLayout == 'stereo'
assert str0.bitDepth is None
assert str0.bitrate == 320
assert str0.bitrateMode is None
assert str0.channels == 2
assert str0.codec == 'mp3'
assert str0.codecID is None
assert str0.dialogNorm is None
assert str0.duration is None
assert str0.id == 44
assert str0.index == 0
assert str0.initpath == '/library/metadata/22'
assert str0.language is None
assert str0.languageCode is None
#assert str0.part == <MediaPart:22>
assert str0.samplingRate == 44100
assert str0.selected is True
assert str0.server.baseurl == 'http://138.68.157.5:32400'
assert str0.streamType == 2
assert str0.title is None
assert str0.type == 2
def test_audio_Track_album(a_music_album):
assert a_music_album.tracks()[0].album() == a_music_album
def test_audio_Track_artist(a_music_album, a_artist):
assert a_music_album.tracks()[0].artist() == a_artist
def test_audio_Audio_section(a_artist, a_music_album, a_track):
assert a_artist.section()
assert a_music_album.section()
assert a_track.section()
assert a_track.section() == a_music_album.section() == a_artist.section()
def test_audio_Track_download(monkeydownload, tmpdir, a_track):
f = a_track.download(savepath=str(tmpdir))
assert f
def test_audio_album_download(monkeydownload, a_music_album, tmpdir):
f = a_music_album.download(savepath=str(tmpdir))
assert len(f) == 14
def test_audio_Artist_download(monkeydownload, a_artist, tmpdir):
f = a_artist.download(savepath=str(tmpdir))
assert len(f) == 14

View file

@ -0,0 +1,173 @@
import pytest
@pytest.mark.req_client
def _test_client_PlexClient__loadData(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_connect(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_contextMenu(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_goBack(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_goToHome(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_goToMedia(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_goToMusic(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_headers(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_isPlayingMedia(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_moveDown(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_moveLeft(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_moveRight(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_moveUp(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_nextLetter(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_pageDown(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_pageUp(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_pause(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_play(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_playMedia(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_previousLetter(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_proxyThroughServer(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_query(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_refreshPlayQueue(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_seekTo(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_select(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_sendCommand(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_setAudioStream(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_setParameters(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_setRepeat(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_setShuffle(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_setStreams(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_setSubtitleStream(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_setVideoStream(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_setVolume(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_skipNext(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_skipPrevious(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_skipTo(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_stepBack(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_stepForward(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_stop(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_timeline(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_toggleOSD(pms):
pass
@pytest.mark.req_client
def _test_client_PlexClient_url(pms):
pass

View file

@ -0,0 +1,175 @@
# -*- coding: utf-8 -*-
import pytest
from plexapi.exceptions import NotFound
# func naming should follow:
# tests_file_class_method_some_description
def test_library_Library_section(pms):
sections = pms.library.sections()
assert len(sections) == 4
lfs = 'TV Shows'
section_name = pms.library.section(lfs)
assert section_name.title == lfs
with pytest.raises(NotFound):
assert pms.library.section('gfdsas')
def test_library_Library_sectionByID_is_equal_section(pms, freshpms):
# test that sctionmyID refreshes the section if the key is missing
# this is needed if there isnt any cached sections
assert freshpms.library.sectionByID('1')
assert pms.library.sectionByID('1').uuid == pms.library.section('Movies').uuid
def test_library_sectionByID_with_attrs(pms):
m = pms.library.sectionByID('1')
assert m.agent == 'com.plexapp.agents.imdb'
assert m.allowSync is False
assert m.art == '/:/resources/movie-fanart.jpg'
assert m.composite == '/library/sections/1/composite/1484690696'
assert str(m.createdAt.date()) == '2017-01-17'
assert m.filters == '1'
assert m.initpath == '/library/sections'
assert m.key == '1'
assert m.language == 'en'
assert m.locations == ['/media/movies']
assert m.refreshing is False
assert m.scanner == 'Plex Movie Scanner'
assert m.server.baseurl == 'http://138.68.157.5:32400'
assert m.thumb == '/:/resources/movie.png'
assert m.title == 'Movies'
assert m.type == 'movie'
assert str(m.updatedAt.date()) == '2017-01-17'
assert m.uuid == '2b72d593-3881-43f4-a8b8-db541bd3535a'
def test_library_section_get_movie(pms): # fix me
m = pms.library.section('Movies').get('16 blocks')
assert m
def test_library_getByKey(pms):
m = pms.library.getByKey('1')
assert m.title == '16 Blocks'
def test_library_onDeck(pms):
assert len(list(pms.library.onDeck()))
def test_library_recentlyAdded(pms):
assert len(list(pms.library.recentlyAdded()))
def test_library_get(pms):
m = pms.library.get('16 blocks')
assert m.title == '16 Blocks'
def test_library_Library_cleanBundle(pms):
pms.library.cleanBundles()
def test_library_Library_optimize(pms):
pms.library.optimize()
def test_library_Library_emptyTrash(pms):
pms.library.emptyTrash()
def _test_library_Library_refresh(pms):
pms.library.refresh() # fix mangle and proof the sections attrs
def _test_library_MovieSection_refresh(a_movie_section):
a_movie_section.refresh()
def test_library_MovieSection_onDeck(a_movie_section):
assert len(a_movie_section.onDeck())
def test_library_MovieSection_recentlyAdded(a_movie_section):
assert len(a_movie_section.recentlyAdded())
def test_library_MovieSection_analyze(a_movie_section):
a_movie_section.analyze()
def test_library_ShowSection_searchShows(a_tv_section):
s = a_tv_section.searchShows(**{'title': 'The 100'})
assert s
def test_library_ShowSection_searchEpisodes(a_tv_section):
s = a_tv_section.searchEpisodes(**{'title': 'Pilot'})
assert s
def test_library_ShowSection_recentlyAdded(a_tv_section):
assert len(a_tv_section.recentlyAdded())
def test_library_MusicSection_albums(a_music_section):
assert len(a_music_section.albums())
def test_library_MusicSection_searchTracks(a_music_section):
assert len(a_music_section.searchTracks(**{'title': 'Holy Moment'}))
def test_library_MusicSection_searchAlbums(a_music_section):
assert len(a_music_section.searchAlbums(**{'title': 'Unmastered Impulses'}))
def test_library_PhotoSection_searchAlbums(a_photo_section):
albums = a_photo_section.searchAlbums('photo_album1')
assert len(albums)
print([i.TYPE for i in albums])
def test_library_PhotoSection_searchPhotos(a_photo_section):
assert len(a_photo_section.searchPhotos('lolcat2'))
#### Start on library search
def test_library_and_section_search_for_movie(pms):
find = '16 blocks'
l_search = pms.library.search(find)
s_search = pms.library.section('Movies').search(find)
assert l_search == s_search
def test_search_with_apostrophe(pms):
show_title = "Marvel's Daredevil" # Test ' in show title
result_server = pms.search(show_title)
result_shows = pms.library.section('TV Shows').search(show_title)
assert result_server
assert result_shows
assert result_server == result_shows
def test_crazy_search(pms, a_movie):
movie = a_movie
movies = pms.library.section('Movies')
assert movie in pms.library.search(genre=29, libtype='movie')
assert movie in movies.search(actor=movie.actors[0], sort='titleSort'), 'Unable to search movie by actor.'
assert movie in movies.search(director=movie.directors[0]), 'Unable to search movie by director.'
assert movie in movies.search(year=['2006', '2007']), 'Unable to search movie by year.'
assert movie not in movies.search(year=2007), 'Unable to filter movie by year.'
assert movie in movies.search(actor=movie.actors[0].id)

View file

@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
import pytest
def test_myplex_accounts(plex_account, pms):
account = plex_account
assert account, 'Must specify username, password & resource to run this test.'
print('MyPlexAccount:')
print('username: %s' % account.username)
#print('authenticationToken: %s' % account.authenticationToken)
print('email: %s' % account.email)
print('home: %s' % account.home)
print('queueEmail: %s' % account.queueEmail)
assert account.username, 'Account has no username'
assert account.authenticationToken, 'Account has no authenticationToken'
assert account.email, 'Account has no email'
assert account.home is not None, 'Account has no home'
assert account.queueEmail, 'Account has no queueEmail'
account = pms.account()
print('Local PlexServer.account():')
print('username: %s' % account.username)
print('authToken: %s' % account.authToken)
print('signInState: %s' % account.signInState)
assert account.username, 'Account has no username'
assert account.authToken, 'Account has no authToken'
assert account.signInState, 'Account has no signInState'
def test_myplex_resources(plex_account):
account = plex_account
assert account, 'Must specify username, password & resource to run this test.'
resources = account.resources()
for resource in resources:
name = resource.name or 'Unknown'
connections = [c.uri for c in resource.connections]
connections = ', '.join(connections) if connections else 'None'
print('%s (%s): %s' % (name, resource.product, connections))
assert resources, 'No resources found for account: %s' % account.name
def test_myplex_connect_to_resource(plex_account):
for resource in plex_account.resources():
if resource.name == 'PMS_API_TEST_SERVER':
break
server = resource.connect()
assert 'Ohno' in server.url('Ohno')
assert server
def test_myplex_devices(plex_account):
account = plex_account
devices = account.devices()
for device in devices:
name = device.name or 'Unknown'
connections = ', '.join(device.connections) if device.connections else 'None'
print('%s (%s): %s' % (name, device.product, connections))
assert devices, 'No devices found for account: %s' % account.name
#@pytest.mark.req_client # this need to be recorded?
def _test_myplex_connect_to_device(plex_account):
account = plex_account
devices = account.devices()
for device in devices:
if device.name == 'some client name' and len(device.connections):
break
client = device.connect()
assert client, 'Unable to connect to device'
def test_myplex_users(plex_account):
account = plex_account
users = account.users()
assert users, 'Found no users on account: %s' % account.name
print('Found %s users.' % len(users))
user = account.user('Hellowlol')
print('Found user: %s' % user)
assert user, 'Could not find user Hellowlol'

View file

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
import pytest
def test_navigate_around_show(plex_account, pms):
show = pms.library.section('TV Shows').get('The 100')
seasons = show.seasons()
season = show.season('Season 1')
episodes = show.episodes()
episode = show.episode('Pilot')
assert 'Season 1' in [s.title for s in seasons], 'Unable to list season:'
assert 'Pilot' in [e.title for e in episodes], 'Unable to list episode:'
assert show.season(1) == season
assert show.episode('Pilot') == episode, 'Unable to get show episode:'
assert season.episode('Pilot') == episode, 'Unable to get season episode:'
assert season.show() == show, 'season.show() doesnt match expected show.'
assert episode.show() == show, 'episode.show() doesnt match expected show.'
assert episode.season() == season, 'episode.season() doesnt match expected season.'
def _test_navigate_around_artist(plex_account, pms):
artist = pms.library.section(CONFIG.audio_section).get(CONFIG.audio_artist)
albums = artist.albums()
album = artist.album(CONFIG.audio_album)
tracks = artist.tracks()
track = artist.track(CONFIG.audio_track)
print('Navigating around artist: %s' % artist)
print('Albums: %s...' % albums[:3])
print('Album: %s' % album)
print('Tracks: %s...' % tracks[:3])
print('Track: %s' % track)
assert CONFIG.audio_album in [a.title for a in albums], 'Unable to list album: %s' % CONFIG.audio_album
assert CONFIG.audio_track in [e.title for e in tracks], 'Unable to list track: %s' % CONFIG.audio_track
assert artist.album(CONFIG.audio_album) == album, 'Unable to get artist album: %s' % CONFIG.audio_album
assert artist.track(CONFIG.audio_track) == track, 'Unable to get artist track: %s' % CONFIG.audio_track
assert album.track(CONFIG.audio_track) == track, 'Unable to get album track: %s' % CONFIG.audio_track
assert album.artist() == artist, 'album.artist() doesnt match expected artist.'
assert track.artist() == artist, 'track.artist() doesnt match expected artist.'
assert track.album() == album, 'track.album() doesnt match expected album.'

View file

@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
import time
import pytest
def test_create_playlist(pms, a_show):
# create the playlist
title = 'test_create_playlist_a_show'
#print('Creating playlist %s..' % title)
episodes = a_show.episodes()
playlist = pms.createPlaylist(title, episodes[:3])
try:
items = playlist.items()
#log(4, 'Title: %s' % playlist.title)
#log(4, 'Items: %s' % items)
#log(4, 'Duration: %s min' % int(playlist.duration / 60000.0))
assert playlist.title == title, 'Playlist not created successfully.'
assert len(items) == 3, 'Playlist does not contain 3 items.'
assert items[0].ratingKey == episodes[0].ratingKey, 'Items not in proper order [0a].'
assert items[1].ratingKey == episodes[1].ratingKey, 'Items not in proper order [1a].'
assert items[2].ratingKey == episodes[2].ratingKey, 'Items not in proper order [2a].'
# move items around (b)
#print('Testing move items..')
playlist.moveItem(items[1])
items = playlist.items()
assert items[0].ratingKey == episodes[1].ratingKey, 'Items not in proper order [0b].'
assert items[1].ratingKey == episodes[0].ratingKey, 'Items not in proper order [1b].'
assert items[2].ratingKey == episodes[2].ratingKey, 'Items not in proper order [2b].'
# move items around (c)
playlist.moveItem(items[0], items[1])
items = playlist.items()
assert items[0].ratingKey == episodes[0].ratingKey, 'Items not in proper order [0c].'
assert items[1].ratingKey == episodes[1].ratingKey, 'Items not in proper order [1c].'
assert items[2].ratingKey == episodes[2].ratingKey, 'Items not in proper order [2c].'
# add an item
#print('Testing add item: %s' % episodes[3])
playlist.addItems(episodes[3])
items = playlist.items()
#log(4, '4th Item: %s' % items[3])
assert items[3].ratingKey == episodes[3].ratingKey, 'Missing added item: %s' % episodes[3]
# add two items
#print('Testing add item: %s' % episodes[4:6])
playlist.addItems(episodes[4:6])
items = playlist.items()
#log(4, '5th+ Items: %s' % items[4:])
assert items[4].ratingKey == episodes[4].ratingKey, 'Missing added item: %s' % episodes[4]
assert items[5].ratingKey == episodes[5].ratingKey, 'Missing added item: %s' % episodes[5]
assert len(items) == 6, 'Playlist should have 6 items, %s found' % len(items)
# remove item
toremove = items[3]
#print('Testing remove item: %s' % toremove)
playlist.removeItem(toremove)
items = playlist.items()
assert toremove not in items, 'Removed item still in playlist: %s' % items[3]
assert len(items) == 5, 'Playlist should have 5 items, %s found' % len(items)
finally:
playlist.delete()
@pytest.mark.req_client
def test_playlist_play(pms):
client = getclient(CONFIG.client, CONFIG.client_baseurl, plex)
artist = plex.library.section(CONFIG.audio_section).get(CONFIG.audio_artist)
album = artist.album(CONFIG.audio_album)
pl_name = 'test_play_playlist'
playlist = plex.createPlaylist(pl_name, album)
try:
#print('Playing playlist: %s' % playlist)
client.playMedia(playlist); time.sleep(5)
#print('stop..')
client.stop('music'); time.sleep(1)
finally:
playlist.delete()
assert pl_name not in [i.title for i in pms.playlists()]
def test_playlist_photos(pms, a_photo_album):
album = a_photo_album
photos = album.photos()
pl_name = 'test_playlist_photos'
playlist = pms.createPlaylist(pl_name, photos)
try:
assert len(playlist.items()) == 4
finally:
playlist.delete()
assert pl_name not in [i.title for i in pms.playlists()]
@pytest.mark.req_client
def _test_play_photos(account, plex):
client = getclient('iphone-mike', CONFIG.client_baseurl, plex)
photosection = plex.library.section(CONFIG.photo_section)
album = photosection.get(CONFIG.photo_album)
photos = album.photos()
for photo in photos[:4]:
client.playMedia(photo)
time.sleep(2)
def test_play_queues(pms):
episode = pms.library.section('TV Shows').get('the 100').get('Pilot')
playqueue = pms.createPlayQueue(episode)
assert len(playqueue.items) == 1, 'No items in play queue.'
assert playqueue.items[0].title == episode.title, 'Wrong show queued.'
assert playqueue.playQueueID, 'Play queue ID not set.'

View file

@ -0,0 +1,3 @@
# test search.
# Many more tests is for search later.

View file

@ -0,0 +1,216 @@
import pytest
import os
from plexapi.exceptions import BadRequest, NotFound
from plexapi.utils import download
def test_server_attr(pms):
assert pms.baseurl == 'http://138.68.157.5:32400'
assert pms.friendlyName == 'PMS_API_TEST_SERVER'
assert pms.machineIdentifier == 'e42470b5c527c7e5ebbdc017b5a32c8c683f6f8b'
assert pms.myPlex is True
assert pms.myPlexMappingState == 'mapped'
assert pms.myPlexSigninState == 'ok'
assert pms.myPlexSubscription == '0'
assert pms.myPlexUsername == 'testplexapi@gmail.com'
assert pms.platform == 'Linux'
assert pms.platformVersion == '4.4.0-59-generic (#80-Ubuntu SMP Fri Jan 6 17:47:47 UTC 2017)'
#assert pms.session == <requests.sessions.Session object at 0x029A5E10>
assert pms.token == os.environ.get('PLEX_TEST_TOKEN')
assert pms.transcoderActiveVideoSessions == 0
assert pms.updatedAt == 1484943666
assert pms.version == '1.3.3.3148-b38628e'
@pytest.mark.req_client
def test_server_session():
pass
def test_server_library(pms):
assert pms.library
def test_server_url(pms):
assert 'ohno' in pms.url('ohno')
def test_server_transcodeImage(tmpdir, pms, a_show):
# Ideally we should also test the black white but this has to do for now.
height = 500
width = 500
img_url_resize = pms.transcodeImage(a_show.banner, height, width)
gray = img_url_resize = pms.transcodeImage(a_show.banner, height, width, saturation=0)
resized_image = download(img_url_resize, savepath=str(tmpdir), filename='resize_image')
org_image = download(a_show.server.url(a_show.banner), savepath=str(tmpdir), filename='org_image')
gray_image = download(gray, savepath=str(tmpdir), filename='gray_image')
from PIL import Image, ImageStat
with Image.open(resized_image) as im:
assert width, height == im.size
with Image.open(org_image) as im:
assert width, height != im.size
def detect_color_image(file, thumb_size=150, MSE_cutoff=22, adjust_color_bias=True):
#from http://stackoverflow.com/questions/20068945/detect-if-image-is-color-grayscale-or-black-and-white-with-python-pil
pil_img = Image.open(file)
bands = pil_img.getbands()
if bands == ('R', 'G', 'B') or bands == ('R', 'G', 'B', 'A'):
thumb = pil_img.resize((thumb_size, thumb_size))
SSE, bias = 0, [0, 0, 0]
if adjust_color_bias:
bias = ImageStat.Stat(thumb).mean[:3]
bias = [b - sum(bias) / 3 for b in bias]
for pixel in thumb.getdata():
mu = sum(pixel) / 3
SSE += sum((pixel[i] - mu - bias[i]) * (pixel[i] - mu - bias[i]) for i in [0, 1, 2])
MSE = float(SSE) / (thumb_size * thumb_size)
if MSE <= MSE_cutoff:
return 'grayscale'
else:
return 'color'
elif len(bands) == 1:
return 'blackandwhite'
assert detect_color_image(gray_image, thumb_size=150) == 'grayscale'
def test_server_search(pms):
# basic search. see test_search.py
assert pms.search('16 Blocks')
assert pms.search('16 blocks', mediatype='movie')
def test_server_playlist(pms):
pl = pms.playlist('some_playlist')
assert pl.title == 'some_playlist'
with pytest.raises(NotFound):
pms.playlist('124xxx11y')
def test_server_playlists(pms):
playlists = pms.playlists()
assert len(playlists)
def test_server_history(pms):
history = pms.history()
assert len(history)
def test_server_Server_query(pms):
assert pms.query('/')
from plexapi.server import PlexServer
with pytest.raises(BadRequest):
assert pms.query('/asdasdsada/12123127/aaaa', headers={'random_headers': '1337'})
with pytest.raises(NotFound):
# This is really requests.exceptions.HTTPError:
# 401 Client Error: Unauthorized for url:
PlexServer('http://138.68.157.5:32400', '1234')
def test_server_Server_session():
from requests import Session
from plexapi.server import PlexServer
class MySession(Session):
def __init__(self):
super(self.__class__, self).__init__()
self.plexapi_session_test = True
plex = PlexServer('http://138.68.157.5:32400',
os.environ.get('PLEX_TEST_TOKEN'),
session=MySession())
assert hasattr(plex.session, 'plexapi_session_test')
pl = plex.playlists()
assert hasattr(pl[0].server.session, 'plexapi_session_test')
# check client
# check myplex.
def test_server_token_in_headers(pms):
h = pms.headers()
assert 'X-Plex-Token' in h and len(h['X-Plex-Token'])
def _test_server_createPlayQueue():
# see test_playlists.py
pass
def _test_server_createPlaylist():
# see test_playlists.py
pass
def test_server_client_not_found(pms):
with pytest.raises(NotFound):
pms.client('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
@pytest.mark.req_client
def test_server_client(pms):
assert pms.client('Plex Web (Chrome)')
def test_server_Server_sessions(pms):
assert len(pms.sessions()) == 0
@pytest.mark.req_client
def test_server_clients(pms):
assert len(pms.clients())
m = pms.clients()[0]
assert m.baseurl == 'http://127.0.0.1:32400'
assert m.device is None
assert m.deviceClass == 'pc'
assert m.machineIdentifier == '89hgkrbqxaxmf45o1q2949ru'
assert m.model is None
assert m.platform is None
assert m.platformVersion is None
assert m.product == 'Plex Web'
assert m.protocol == 'plex'
assert m.protocolCapabilities == ['timeline', 'playback', 'navigation', 'mirror', 'playqueues']
assert m.protocolVersion == '1'
assert m.server.baseurl == 'http://138.68.157.5:32400'
#assert m.session == <requests.sessions.Session object at 0x02945E10>
assert m.state is None
assert m.title == 'Plex Web (Chrome)'
assert m.token is None
assert m.vendor is None
assert m.version == '2.12.5'
def test_server_account(pms):
acc = pms.account()
assert acc.authToken
#assert acc.mappingError == 'publisherror' # this is missing from time to time.. why?
assert acc.mappingErrorMessage is None
assert acc.mappingState == 'mapped'
assert acc.privateAddress == '138.68.157.5'
assert acc.privatePort == '32400'
assert acc.publicAddress == '138.68.157.5'
assert acc.publicPort == '32400'
assert acc.signInState == 'ok'
assert acc.subscriptionActive == '0'
assert acc.subscriptionFeatures is None
assert acc.subscriptionState == 'Unknown'
assert acc.username == 'testplexapi@gmail.com'

View file

@ -0,0 +1,100 @@
import pytest
import plexapi.utils as utils
from plexapi.exceptions import NotFound
def test_utils_toDatetime():
assert str(utils.toDatetime('2006-03-03', format='%Y-%m-%d')) == '2006-03-03 00:00:00'
assert str(utils.toDatetime('0'))[:-9] == '1970-01-01'
# should this handle args as '0' # no need element attrs are strings.
def _test_utils_threaded():
pass
def test_utils_searchType():
st = utils.searchType('movie')
assert st == 1
movie = utils.searchType(1)
assert movie == '1'
with pytest.raises(NotFound):
utils.searchType('kekekekeke')
def _test_utils_listItems():
pass
def _test_utils_listChoices(pms):
pass
def test_utils_joinArgs():
test_dict = {'genre': 'action', 'type': 1337}
assert utils.joinArgs(test_dict) == '?genre=action&type=1337'
def test_utils_isInt():
assert utils.isInt(1) is True
assert utils.isInt('got_you') is False
assert utils.isInt('1337') is True
def _test_utils_findUsername():
pass
def _test_utils_findStreams():
pass
def _test_utils_findPlayer():
pass
def _test_utils_findLocations():
pass
def _test_utils_findItem():
pass
def test_utils_findKey(pms):
with pytest.raises(NotFound):
assert utils.findKey(pms, '9999999')
assert utils.findKey(pms, '1')
def test_utils_cast():
t_int_int = utils.cast(int, 1)
t_int_str_int = utils.cast(int, '1')
t_bool_str_int = utils.cast(bool, '1')
t_bool_int = utils.cast(bool, 1)
t_float_int = utils.cast(float, 1)
t_float_float = utils.cast(float, 1)
t_float_str = utils.cast(float, 'kek')
assert t_int_int == 1 and isinstance(t_int_int, int)
assert t_int_str_int == 1 and isinstance(t_int_str_int, int)
assert t_bool_str_int is True
assert t_bool_int is True
assert t_float_float == 1.0 and isinstance(t_float_float, float)
assert t_float_str != t_float_str # nan is never equal
with pytest.raises(ValueError):
t_bool_str = utils.cast(bool, 'kek') # should we catch this in cast?
def test_utils_download(a_episode):
# this files is really getting downloaded..
without_session = utils.download(a_episode.getStreamURL(), filename=a_episode.location, mocked=True)
assert without_session
with_session = utils.download(a_episode.getStreamURL(),
filename=a_episode.location,
session=a_episode.server.session,
mocked=True)
assert with_session
img = utils.download(a_episode.thumbUrl, filename=a_episode.title, mocked=True)
assert img

View file

@ -0,0 +1,571 @@
# -*- coding: utf-8 -*-
#test_the_file_class_method
import os
import pytest
from plexapi.exceptions import NotFound
def test_video_Movie(a_movie_section):
m = a_movie_section.get('Cars')
assert m.title == 'Cars'
def test_video_Movie_getStreamURL(a_movie):
assert a_movie.getStreamURL() == "http://138.68.157.5:32400/video/:/transcode/universal/start.m3u8?X-Plex-Platform=Chrome&copyts=1&mediaIndex=0&offset=0&path=%2Flibrary%2Fmetadata%2F1&X-Plex-Token={0}".format(os.environ.get('PLEX_TEST_TOKEN'))
assert a_movie.getStreamURL(videoResolution='800x600') == "http://138.68.157.5:32400/video/:/transcode/universal/start.m3u8?X-Plex-Platform=Chrome&copyts=1&mediaIndex=0&offset=0&path=%2Flibrary%2Fmetadata%2F1&videoResolution=800x600&X-Plex-Token={0}".format(os.environ.get('PLEX_TEST_TOKEN'))
def test_video_Movie_isFullObject_and_reload(pms):
movie = pms.library.section('Movies').get('16 Blocks')
assert movie.isFullObject() is False
movie.reload()
assert movie.isFullObject() is True
movie_via_search = pms.library.search('16 Blocks')[0]
assert movie_via_search.isFullObject() is False
movie_via_search.reload()
assert movie_via_search.isFullObject() is True
movie_via_section_search = pms.library.section('Movies').search('16 Blocks')[0]
assert movie_via_section_search.isFullObject() is False
movie_via_section_search.reload()
assert movie_via_section_search.isFullObject() is True
# If the verify that the object has been reloaded. xml from search only returns 3 actors.
assert len(movie_via_section_search.roles) > 3
def test_video_Movie_isPartialObject(a_movie):
assert a_movie.isPartialObject()
def test_video_Movie_iterParts(a_movie):
assert len(list(a_movie.iterParts())) == 1
def test_video_Movie_download(monkeydownload, tmpdir, a_movie):
downloaded_movie = a_movie.download(savepath=str(tmpdir))
assert len(downloaded_movie) == 1
downloaded_movie2 = a_movie.download(savepath=str(tmpdir), **{'videoResolution': '500x300'})
assert len(downloaded_movie2) == 1
def test_video_Movie_attrs_as_much_as_possible(a_movie_section):
m = a_movie_section.get('Cars')
assert m.location == '/media/movies/cars/cars.mp4'
assert str(m.addedAt.date()) == '2017-01-17'
assert m.art == '/library/metadata/2/art/1484690715'
assert m.audienceRating == 7.9
assert m.audienceRatingImage == 'rottentomatoes://image.rating.upright'
# Assign 0 m.audioStreams
aud0 = m.audioStreams[0]
assert m.chapterSource == 'agent'
assert m.collections == []
assert m.contentRating == 'G'
#assert m.countries == [<Country:35:USA>]
assert [i.tag for i in m.directors] == ['John Lasseter', 'Joe Ranft']
assert m.duration == 170859
assert m.fields == []
assert [i.tag for i in m.genres] == ['Animation', 'Family', 'Comedy', 'Sport', 'Adventure']
assert m.guid == 'com.plexapp.agents.imdb://tt0317219?lang=en'
assert m.initpath == '/library/metadata/2'
assert m.key == '/library/metadata/2'
assert str(m.lastViewedAt) == '2017-01-30 22:19:38' # fix me
assert m.librarySectionID == '1'
assert m.listType == 'video'
# Assign 0 m.media
med0 = m.media[0]
assert str(m.originalTitle) == '__NA__'
assert str(m.originallyAvailableAt.date()) == '2006-06-09'
assert m.player is None
assert str(m.playlistItemID) == '__NA__'
assert str(m.primaryExtraKey) == '__NA__'
#assert m.producers == [<Producer:130:Darla.K..Anderson>]
assert m.rating == '7.4'
assert m.ratingImage == 'rottentomatoes://image.rating.certified'
assert m.ratingKey == 2
assert [i.tag for i in m.roles] == ['Owen Wilson', 'Paul Newman', 'Bonnie Hunt', 'Larry the Cable Guy', 'Cheech Marin', 'Tony Shalhoub', 'Guido Quaroni', 'Jenifer Lewis', 'Paul Dooley', 'Michael Wallis', 'George Carlin', 'Katherine Helmond', 'John Ratzenberger', 'Michael Keaton', 'Joe Ranft', 'Richard Petty', 'Jeremy Piven', 'Bob Costas', 'Darrell Waltrip', 'Richard Kind', 'Edie McClurg', 'Humpy Wheeler', 'Tom Magliozzi', 'Ray Magliozzi', 'Lynda Petty', 'Andrew Stanton', 'Dale Earnhardt Jr.', 'Michael Schumacher', 'Jay Leno', 'Sarah Clark', 'Mike Nelson', 'Joe Ranft', 'Jonas Rivera', 'Lou Romano', 'Adrian Ochoa', 'E.J. Holowicki', 'Elissa Knight', 'Lindsey Collins', 'Larry Benton', 'Douglas Keever', 'Tom Hanks', 'Tim Allen', 'John Ratzenberger', 'Billy Crystal', 'John Goodman', 'John Ratzenberger', 'Dave Foley', 'John Ratzenberger', 'Vanness Wu']
assert m.server.baseurl == 'http://138.68.157.5:32400'
assert str(m.sessionKey) == '__NA__'
assert m.studio == 'Walt Disney Pictures'
assert m.summary == u"Lightning McQueen, a hotshot rookie race car driven to succeed, discovers that life is about the journey, not the finish line, when he finds himself unexpectedly detoured in the sleepy Route 66 town of Radiator Springs. On route across the country to the big Piston Cup Championship in California to compete against two seasoned pros, McQueen gets to know the town's offbeat characters."
assert m.tagline == "Ahhh... it's got that new movie smell."
assert m.thumb == '/library/metadata/2/thumb/1484690715'
assert m.title == 'Cars'
assert m.titleSort == 'Cars'
assert m.transcodeSession is None
assert m.type == 'movie'
assert str(m.updatedAt.date()) == '2017-01-17'
assert str(m.userRating) == '__NA__'
assert m.username is None
# Assign 0 m.videoStreams
vid0 = m.videoStreams[0]
assert m.viewCount == 0
assert m.viewOffset == 88870
assert str(m.viewedAt) == '__NA__'
assert [i.tag for i in m.writers] == ['Dan Fogelman', 'Joe Ranft', 'John Lasseter', 'Kiel Murray', 'Phil Lorin', 'Jorgen Klubien']
assert m.year == 2006
assert aud0.audioChannelLayout == '5.1'
assert aud0.bitDepth is None
assert aud0.bitrate == 388
assert aud0.bitrateMode is None
assert aud0.channels == 6
assert aud0.codec == 'aac'
assert aud0.codecID is None
assert aud0.dialogNorm is None
assert aud0.duration is None
assert aud0.id == 10
assert aud0.index == 1
assert aud0.initpath == '/library/metadata/2'
assert aud0.language is None
assert aud0.languageCode is None
#assert aud0.part == <MediaPart:2>
assert aud0.samplingRate == 48000
assert aud0.selected is True
assert aud0.server.baseurl == 'http://138.68.157.5:32400'
assert aud0.streamType == 2
assert aud0.title is None
assert aud0.type == 2
assert med0.aspectRatio == 1.78
assert med0.audioChannels == 6
assert med0.audioCodec == 'aac'
assert med0.bitrate == 1474
assert med0.container == 'mp4'
assert med0.duration == 170859
assert med0.height == 720
assert med0.id == 2
assert med0.initpath == '/library/metadata/2'
assert med0.optimizedForStreaming is False
# Assign 0 med0.parts
par0 = med0.parts[0]
assert med0.server.baseurl == 'http://138.68.157.5:32400'
assert med0.video == m
assert med0.videoCodec == 'h264'
assert med0.videoFrameRate == 'PAL'
assert med0.videoResolution == '720'
assert med0.width == 1280
assert vid0.bitDepth == 8
assert vid0.bitrate == 1086
assert vid0.cabac is None
assert vid0.chromaSubsampling == '4:2:0'
assert vid0.codec == 'h264'
assert vid0.codecID is None
assert vid0.colorSpace is None
assert vid0.duration is None
assert vid0.frameRate == 25.0
assert vid0.frameRateMode is None
assert vid0.hasScallingMatrix is None
assert vid0.height == 720
assert vid0.id == 9
assert vid0.index == 0
assert vid0.initpath == '/library/metadata/2'
assert vid0.language is None
assert vid0.languageCode is None
assert vid0.level == 31
#assert vid0.part == <MediaPart:2>
assert vid0.profile == 'main'
assert vid0.refFrames == 1
assert vid0.scanType is None
assert vid0.selected is False
assert vid0.server.baseurl == 'http://138.68.157.5:32400'
assert vid0.streamType == 1
assert vid0.title is None
assert vid0.type == 1
assert vid0.width == 1280
assert par0.container == 'mp4'
assert par0.duration == 170859
assert par0.file == '/media/movies/cars/cars.mp4'
assert par0.id == 2
assert par0.initpath == '/library/metadata/2'
assert par0.key == '/library/parts/2/1484679008/file.mp4'
#assert par0.media == <Media:Cars>
assert par0.server.baseurl == 'http://138.68.157.5:32400'
assert par0.size == 31491130
# Assign 0 par0.streams
str0 = par0.streams[0]
# Assign 1 par0.streams
str1 = par0.streams[1]
assert str0.bitDepth == 8
assert str0.bitrate == 1086
assert str0.cabac is None
assert str0.chromaSubsampling == '4:2:0'
assert str0.codec == 'h264'
assert str0.codecID is None
assert str0.colorSpace is None
assert str0.duration is None
assert str0.frameRate == 25.0
assert str0.frameRateMode is None
assert str0.hasScallingMatrix is None
assert str0.height == 720
assert str0.id == 9
assert str0.index == 0
assert str0.initpath == '/library/metadata/2'
assert str0.language is None
assert str0.languageCode is None
assert str0.level == 31
#assert str0.part == <MediaPart:2>
assert str0.profile == 'main'
assert str0.refFrames == 1
assert str0.scanType is None
assert str0.selected is False
assert str0.server.baseurl == 'http://138.68.157.5:32400'
assert str0.streamType == 1
assert str0.title is None
assert str0.type == 1
assert str0.width == 1280
assert str1.audioChannelLayout == '5.1'
assert str1.bitDepth is None
assert str1.bitrate == 388
assert str1.bitrateMode is None
assert str1.channels == 6
assert str1.codec == 'aac'
assert str1.codecID is None
assert str1.dialogNorm is None
assert str1.duration is None
assert str1.id == 10
assert str1.index == 1
assert str1.initpath == '/library/metadata/2'
assert str1.language is None
assert str1.languageCode is None
#assert str1.part == <MediaPart:2>
assert str1.samplingRate == 48000
assert str1.selected is True
assert str1.server.baseurl == 'http://138.68.157.5:32400'
assert str1.streamType == 2
assert str1.title is None
assert str1.type == 2
def test_video_Show(a_show):
assert a_show.title == 'The 100'
def test_video_Show_attrs(a_show):
m = a_show
assert str(m.addedAt.date()) == '2017-01-17'
assert '/library/metadata/12/art/' in m.art
assert '/library/metadata/12/banner/' in m.banner
assert m.childCount == 2
assert m.contentRating == 'TV-14'
assert m.duration == 2700000
assert m.initpath == '/library/sections/2/all'
# Since we access m.genres the show is forced to reload
assert [i.tag for i in m.genres] == ['Drama', 'Science-Fiction', 'Suspense', 'Thriller']
# So the initkey should have changed because of the reload
assert m.initpath == '/library/metadata/12'
assert m.index == '1'
assert m.key == '/library/metadata/12'
assert str(m.lastViewedAt.date()) == '2017-01-22'
assert m.leafCount == 9
assert m.listType == 'video'
assert m.location == '/media/tvshows/the 100'
assert str(m.originallyAvailableAt.date()) == '2014-03-19'
assert m.rating == 8.1
assert m.ratingKey == 12
assert [i.tag for i in m.roles][:3] == ['Richard Harmon', 'Alycia Debnam-Carey', 'Lindsey Morgan']
assert [i.tag for i in m.actors][:3] == ['Richard Harmon', 'Alycia Debnam-Carey', 'Lindsey Morgan']
assert m.server.baseurl == 'http://138.68.157.5:32400'
assert m.studio == 'The CW'
assert m.summary == u"When nuclear Armageddon destroys civilization on Earth, the only survivors are those on the 12 international space stations in orbit at the time. Three generations later, the 4,000 survivors living on a space ark of linked stations see their resources dwindle and face draconian measures established to ensure humanity's future. Desperately looking for a solution, the ark's leaders send 100 juvenile prisoners back to the planet to test its habitability. Having always lived in space, the exiles find the planet fascinating and terrifying, but with the fate of the human race in their hands, they must forge a path into the unknown."
assert '/library/metadata/12/theme/' in m.theme
assert '/library/metadata/12/thumb/' in m.thumb
assert m.title == 'The 100'
assert m.titleSort == '100'
assert m.type == 'show'
assert str(m.updatedAt.date()) == '2017-01-22'
assert m.viewCount == 1
assert m.viewedLeafCount == 1
assert m.year == 2014
def test_video_Show_watched(a_show):
watched = a_show.watched()
assert len(watched) == 1 and watched[0].title == 'Pilot'
def test_video_Show_unwatched(a_show):
assert len(a_show.unwatched()) == 8
def test_video_Show_location(pms):
# This should be a part of test test_video_Show_attrs
# But is excluded because of https://github.com/mjs7231/python-plexapi/issues/97
s = pms.library.section('TV Shows').get('The 100')
# This will require a reload since the xml from http://138.68.157.5:32400/library/sections/2/all
# Does not contain a location
assert s.location == '/media/tvshows/the 100'
def test_video_Show_reload(pms):
s = pms.library.section('TV Shows').get('Game of Thrones')
assert s.initpath == '/library/sections/2/all'
s.reload()
assert s.initpath == '/library/metadata/6'
assert len(s.roles) > 3
def test_video_Show_episodes(a_show):
inc_watched = a_show.episodes()
ex_watched = a_show.episodes(watched=False)
assert len(inc_watched) == 9
assert len(ex_watched) == 8
def test_video_Show_download(monkeydownload, tmpdir, a_show):
f = a_show.download(savepath=str(tmpdir))
assert len(f) == 9
def test_video_Season_download(monkeydownload, tmpdir, a_show):
sn = a_show.season('Season 1')
f = sn.download(savepath=str(tmpdir))
assert len(f) == 8
def test_video_Episode_download(monkeydownload, tmpdir, a_episode):
f = a_episode.download(savepath=str(tmpdir))
assert len(f) == 1
with_sceen_size = a_episode.download(savepath=str(tmpdir), **{'videoResolution': '500x300'})
assert len(with_sceen_size) == 1
def test_video_Show_thumbUrl(a_show):
assert 'http://138.68.157.5:32400/library/metadata/12/thumb/' in a_show.thumbUrl
@pytest.mark.xfail
def test_video_Show_analyze(a_show):
show = a_show.analyze() # this isnt possble.. should it even be available?
def test_video_Show_markWatched(a_tv_section):
show = a_tv_section.get("Marvel's Daredevil")
show.markWatched()
assert a_tv_section.get("Marvel's Daredevil").isWatched
def test_video_Show_markUnwatched(a_tv_section):
show = a_tv_section.get("Marvel's Daredevil")
show.markUnwatched()
assert not a_tv_section.get("Marvel's Daredevil").isWatched
def test_video_Show_refresh(a_tv_section):
show = a_tv_section.get("Marvel's Daredevil")
show.refresh()
def test_video_Show_get(a_show):
assert a_show.get('Pilot').title == 'Pilot'
def test_video_Show_isWatched(a_show):
assert not a_show.isWatched
@pytest.mark.xfail
def test_video_Show_section(a_show): # BROKEN!
show = a_show.section()
def test_video_Episode(a_show):
pilot = a_show.episode('Pilot')
assert pilot == a_show.episode(season=1, episode=1)
with pytest.raises(TypeError):
a_show.episode()
with pytest.raises(NotFound):
a_show.episode(season=1337, episode=1337)
def test_video_Episode_analyze(a_tv_section):
ep = a_tv_section.get("Marvel's Daredevil").episode(season=1, episode=1)
ep.analyze()
def test_video_Episode_attrs(a_episode):
ep = a_episode
assert str(ep.addedAt.date()) == '2017-01-17'
assert ep.contentRating == 'TV-14'
assert [i.tag for i in ep.directors] == ['Bharat Nalluri']
assert ep.duration == 170859
assert ep.grandparentTitle == 'The 100'
assert ep.index == 1
assert ep.initpath == '/library/metadata/12/allLeaves'
assert ep.key == '/library/metadata/14'
assert ep.listType == 'video'
# Assign 0 ep.media
med0 = ep.media[0]
assert str(ep.originallyAvailableAt.date()) == '2014-03-19'
assert ep.parentIndex == '1'
assert ep.parentKey == '/library/metadata/13'
assert ep.parentRatingKey == 13
assert '/library/metadata/13/thumb/' in ep.parentThumb
#assert ep.parentThumb == '/library/metadata/13/thumb/1485096623'
assert ep.player is None
assert ep.rating == 7.4
assert ep.ratingKey == 14
assert ep.server.baseurl == 'http://138.68.157.5:32400'
assert ep.summary == u'Ninety-seven years ago, nuclear Armageddon decimated planet Earth, destroying civilization. The only survivors were the 400 inhabitants of 12 international space stations that were in orbit at the time. Three generations have been born in space, the survivors now number 4,000, and resources are running out on their dying "Ark." Among the 100 young exiles are Clarke, the bright teenage daughter of the Arks chief medical officer; the daredevil Finn; the brother/sister duo of Bellamy and Octavia, whose illegal sibling status has always led them to flaunt the rules, the lighthearted Jasper and the resourceful Monty. Technologically blind to whats happening on the planet below them, the Arks leaders — Clarkes widowed mother, Abby; Chancellor Jaha; and his shadowy second in command, Kane — are faced with difficult decisions about life, death and the continued existence of the human race.'
assert ep.thumb == '/library/metadata/14/thumb/1485115318'
assert ep.title == 'Pilot'
assert ep.titleSort == 'Pilot'
assert ep.transcodeSession is None
assert ep.type == 'episode'
assert str(ep.updatedAt.date()) == '2017-01-22'
assert ep.username is None
assert ep.viewCount == 1
assert ep.viewOffset == 0
assert [i.tag for i in ep.writers] == ['Jason Rothenberg']
assert ep.year == 2014
assert med0.aspectRatio == 1.78
assert med0.audioChannels == 6
assert med0.audioCodec == 'aac'
assert med0.bitrate == 1474
assert med0.container == 'mp4'
assert med0.duration == 170859
assert med0.height == 720
assert med0.id == 12
assert med0.initpath == '/library/metadata/12/allLeaves'
assert med0.optimizedForStreaming is False
# Assign 0 med0.parts
par0 = med0.parts[0]
assert med0.server.baseurl == 'http://138.68.157.5:32400'
#assert med0.video == <Episode:14:The 100:S1:E1:Pilot>
assert med0.videoCodec == 'h264'
assert med0.videoFrameRate == 'PAL'
assert med0.videoResolution == '720'
assert med0.width == 1280
assert par0.container == 'mp4'
assert par0.duration == 170859
assert par0.file == '/media/tvshows/the 100/season 1/the.100.s01e01.mp4'
assert par0.id == 12
assert par0.initpath == '/library/metadata/12/allLeaves'
assert par0.key == '/library/parts/12/1484679008/file.mp4'
#assert par0.media == <Media:Pilot>
assert par0.server.baseurl == 'http://138.68.157.5:32400'
assert par0.size == 31491130
assert ep.isWatched is True
def test_video_Season(a_show):
seasons = a_show.seasons()
assert len(seasons) == 2
assert ['Season 1', 'Season 2'] == [s.title for s in seasons]
assert a_show.season('Season 1') == seasons[0]
def test_video_Season_attrs(a_show):
m = a_show.season('Season 1')
assert str(m.addedAt.date()) == '2017-01-17'
assert m.index == 1
assert m.initpath == '/library/metadata/12/children'
assert m.key == '/library/metadata/13'
assert str(m.lastViewedAt.date()) == '2017-01-22'
assert m.leafCount == 8
assert m.listType == 'video'
assert m.parentKey == '/library/metadata/12'
assert m.parentRatingKey == 12
assert m.parentTitle == 'The 100'
assert m.ratingKey == 13
assert m.server.baseurl == 'http://138.68.157.5:32400'
assert m.summary == ''
assert '/library/metadata/13/thumb/' in m.thumb
#assert m.thumb == '/library/metadata/13/thumb/1485096623'
assert m.title == 'Season 1'
assert m.titleSort == 'Season 1'
assert m.type == 'season'
assert str(m.updatedAt.date()) == '2017-01-22'
assert m.viewCount == 1
assert m.viewedLeafCount == 1
assert m.seasonNumber == 1
def test_video_Season_show(a_show):
sn = a_show.seasons()[0]
season_by_name = a_show.season('Season 1')
assert a_show.ratingKey == sn.parentRatingKey and season_by_name.parentRatingKey
assert sn.ratingKey == season_by_name.ratingKey
def test_video_Season_watched(a_tv_section):
show = a_tv_section.get("Marvel's Daredevil")
sn = show.season(1)
sne = show.season('Season 1')
assert sn == sne
sn.markWatched()
assert sn.isWatched
def test_video_Season_unwatched(a_tv_section):
sn = a_tv_section.get("Marvel's Daredevil").season(1)
sn.markUnwatched()
assert not sn.isWatched
def test_video_Season_get(a_show):
ep = a_show.season(1).get('Pilot')
assert ep.title == 'Pilot'
def test_video_Season_episode(a_show):
ep = a_show.season(1).get('Pilot')
assert ep.title == 'Pilot'
def test_video_Season_episodes(a_show):
sn_eps = a_show.season(2).episodes()
assert len(sn_eps) == 1
#### MISC
def test_that_reload_return_the_same_object(pms):
# we want to check this that all the urls are correct
movie_library_search = pms.library.section('Movies').search('16 Blocks')[0]
movie_search = pms.search('16 Blocks')[0]
movie_section_get = pms.library.section('Movies').get('16 Blocks')
movie_library_search_key = movie_library_search.key
movie_search_key = movie_search.key
movie_section_get_key = movie_section_get.key
assert movie_library_search_key == movie_library_search.reload().key == movie_search_key == movie_search.reload().key == movie_section_get_key == movie_section_get.reload().key
tvshow_library_search = pms.library.section('TV Shows').search('The 100')[0]
tvshow_search = pms.search('The 100')[0]
tvshow_section_get = pms.library.section('TV Shows').get('The 100')
tvshow_library_search_key = tvshow_library_search.key
tvshow_search_key = tvshow_search.key
tvshow_section_get_key = tvshow_section_get.key
assert tvshow_library_search_key == tvshow_library_search.reload().key == tvshow_search_key == tvshow_search.reload().key == tvshow_section_get_key == tvshow_section_get.reload().key
season_library_search = tvshow_library_search.season(1)
season_search = tvshow_search.season(1)
season_section_get = tvshow_section_get.season(1)
season_library_search_key = season_library_search.key
season_search_key = season_search.key
season_section_get_key = season_section_get.key
assert season_library_search_key == season_library_search.reload().key == season_search_key == season_search.reload().key == season_section_get_key == season_section_get.reload().key
episode_library_search = tvshow_library_search.episode(season=1, episode=1)
episode_search = tvshow_search.episode(season=1, episode=1)
episode_section_get = tvshow_section_get.episode(season=1, episode=1)
episode_library_search_key = episode_library_search.key
episode_search_key = episode_search.key
episode_section_get_key = episode_section_get.key
assert episode_library_search_key == episode_library_search.reload().key == episode_search_key == episode_search.reload().key == episode_section_get_key == episode_section_get.reload().key