Merge branch 'master' into hub

This commit is contained in:
Michael Shepanski 2017-02-02 22:23:46 -05:00 committed by GitHub
commit f479b8453b
74 changed files with 5017 additions and 2028 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__.:

22
.gitignore vendored
View file

@ -1,20 +1,22 @@
syntax: glob
*.db
*.egg-info
*.log
*.pyc
*.swp
*.sublime-*
*.swp
*__pycache__*
dist
build
*.egg-info
.idea/
lib/
bin/
include/
.cache/
.idea/
.Python
bin/
build
dist
docs/_build/
include/
lib/
pip-selfcheck.json
pyvenv.cfg
htmlcov
.coverage
*.orig

28
.travis.yml Normal file
View file

@ -0,0 +1,28 @@
#---------------------------------------------------------
# Travis CI Build Environment
# https://docs.travis-ci.com/user/customizing-the-build
#---------------------------------------------------------
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 -r requirements_dev.txt
script:
- py.test tests --cov-config .coveragerc --cov=plexapi --cov-report=html
after_success:
- coveralls
matrix:
fast_finish: true

View file

@ -9,7 +9,7 @@ are permitted provided that the following conditions are met:
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name conky-pkmeter nor the names of its contributors
* Neither the name python-plexapi nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.

View file

@ -1,2 +1,2 @@
include README.md
include requirements.pip
include requirements.txt

View file

@ -1,4 +1,10 @@
## PlexAPI ##
<a href="https://badge.fury.io/py/PlexAPI">
<img align="right" src="https://badge.fury.io/py/PlexAPI.svg"/></a>
<a href='https://coveralls.io/github/mjs7231/python-plexapi'>
<img align="right" src='https://coveralls.io/repos/github/mjs7231/python-plexapi/badge.svg' alt='Coverage Status' /></a>
<a href="https://travis-ci.org/mjs7231/python-plexapi">
<img align="right" src="https://travis-ci.org/mjs7231/python-plexapi.svg?branch=master"/></a>
Python bindings for the Plex API.
* Navigate local or remote shared libraries.
@ -43,11 +49,10 @@ plex = PlexServer(baseurl, token)
#### Usage Examples ####
```python
# Example 1: List all unwatched content in library.
for section in plex.library.sections():
print('Unwatched content in %s:' % section.title)
for video in section.unwatched():
print(' %s' % video.title)
# Example 1: List all unwatched movies.
movies = plex.library.section('Movies')
for video in movies.search(unwatched=True):
print(video.title)
```
```python
# Example 2: Mark all Conan episodes watched.

225
docs/Makefile Normal file
View file

@ -0,0 +1,225 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " epub3 to make an epub3"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
@echo " dummy to check syntax errors of document sources"
.PHONY: clean
clean:
rm -rf $(BUILDDIR)/*
.PHONY: html
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: dirhtml
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
.PHONY: singlehtml
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
.PHONY: pickle
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
.PHONY: json
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
.PHONY: htmlhelp
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
.PHONY: qthelp
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PythonPlexAPI.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PythonPlexAPI.qhc"
.PHONY: applehelp
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
.PHONY: devhelp
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/PythonPlexAPI"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PythonPlexAPI"
@echo "# devhelp"
.PHONY: epub
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
.PHONY: epub3
epub3:
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
@echo
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
.PHONY: latex
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
.PHONY: latexpdf
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: latexpdfja
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: text
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
.PHONY: man
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
.PHONY: texinfo
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
.PHONY: info
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
.PHONY: gettext
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
.PHONY: changes
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
.PHONY: linkcheck
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
.PHONY: doctest
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
.PHONY: coverage
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
.PHONY: xml
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
.PHONY: pseudoxml
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
.PHONY: dummy
dummy:
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
@echo
@echo "Build finished. Dummy builder generates no files."

314
docs/conf.py Normal file
View file

@ -0,0 +1,314 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Python PlexAPI documentation build configuration file, created by
# sphinx-quickstart on Sun Jan 8 23:50:18 2017.
#
# This file is execfile()d with the current directory set to its
# containing dir. Note that not all possible configuration values are
# present in this autogenerated file. All configuration values have a
# default; values that are commented out serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
import copy, sys
import sphinx_rtd_theme
from os.path import abspath, dirname, join
from recommonmark.parser import CommonMarkParser
sys.path.insert(0, join(dirname(abspath('.')), 'plexapi'))
import plexapi
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.viewcode',
'sphinx.ext.githubpages',
'sphinxcontrib.napoleon',
]
# -- Monkey-patch docstring to not auto-link :ivars ------------------------
from sphinx.domains.python import PythonDomain
print('Monkey-patching PythonDomain.resolve_xref()')
old_resolve_xref = copy.deepcopy(PythonDomain.resolve_xref)
def new_resolve_xref(*args):
if '.' not in args[5]: # target
return None
return old_resolve_xref(*args)
PythonDomain.resolve_xref = new_resolve_xref
# -- Napoleon Settings -----------------------------------------------------
napoleon_google_docstring = True
napoleon_numpy_docstring = False
napoleon_include_init_with_doc = False
napoleon_include_private_with_doc = False
napoleon_include_special_with_doc = False
napoleon_use_admonition_for_examples = False
napoleon_use_admonition_for_notes = False
napoleon_use_admonition_for_references = False
napoleon_use_ivar = True
napoleon_use_param = True
napoleon_use_rtype = True
napoleon_use_keyword = True
autodoc_member_order = 'bysource'
# -- General Configuration ------------------------------------------------
# Add any paths that contain templates here, relative to this directory.
templates_path = ['../']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
# source_suffix = '.rst'
source_parsers = {'.md': CommonMarkParser}
source_suffix = ['.rst', '.md']
# The encoding of source files.
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'Python PlexAPI'
copyright = '2017, M.Shepanski'
author = 'M.Shepanski'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
# The short X.Y version.
version = plexapi.VERSION
# The full version, including alpha/beta/rc tags.
#release = '2.0.2'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
# today = ''
#
# Else, today_fmt is used as the format for a strftime call.
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The reST default role (used for this markup: `text`) to use for all
# documents.
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
# html_theme = 'alabaster'
html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
html_theme_options = {
'collapse_navigation': False,
'display_version': False,
#'navigation_depth': 3,
}
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
# The name for this set of Sphinx documents.
# "<project> v<release> documentation" by default.
# html_title = 'Python PlexAPI v2'
# A shorter title for the navigation bar. Default is the same as html_title.
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
# html_logo = None
# The name of an image file (relative to this directory) to use as a favicon of
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
# html_static_path = ['static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
# html_extra_path = []
# If not None, a 'Last updated on:' timestamp is inserted at every page
# bottom, using the given strftime format.
# The empty string is equivalent to '%b %d, %Y'.
# html_last_updated_fmt = None
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
# html_additional_pages = {}
# If false, no module index is generated.
# html_domain_indices = True
# If false, no index is generated.
# html_use_index = True
# If true, the index is split into individual pages for each letter.
# html_split_index = False
# If true, links to the reST sources are added to the pages.
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh'
# html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# 'ja' uses this config value.
# 'zh' user can custom change `jieba` dictionary path.
# html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
# html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'PythonPlexAPIdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
# Latex figure (float) alignment
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'PythonPlexAPI.tex', 'Python PlexAPI Documentation',
'M.Shepanski', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
# latex_use_parts = False
# If true, show page references after internal links.
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
# latex_appendices = []
# If false, no module index is generated.
# latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'pythonplexapi', 'Python PlexAPI Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
# man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'PythonPlexAPI', 'Python PlexAPI Documentation',
author, 'PythonPlexAPI', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
# texinfo_appendices = []
# If false, no module index is generated.
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
# texinfo_no_detailmenu = False

4
docs/configuration.rst Normal file
View file

@ -0,0 +1,4 @@
Configuration
=============
dasfasd

10
docs/index.rst Normal file
View file

@ -0,0 +1,10 @@
Python PlexAPI
==============
.. include:: toc.rst
.. automodule:: myplex
Usage & Contributions
---------------------
* Source is available on the `Github Project Page <https://github.com/mjs7231/python-plexapi>`_.
* Contributors to python-plexapi own their own contributions and may distribute that code under
the `BSD license <https://github.com/mjs7231/python-plexapi/blob/master/LICENSE.txt>`_.

145
docs/introduction.rst Normal file
View file

@ -0,0 +1,145 @@
Getting Started
===============
.. |br| raw:: html
<br />
Python bindings for the Plex API.
* Navigate local or remote shared libraries.
* Mark shows watched or unwatched.
* Request rescan, analyze, empty trash.
* Play media on connected clients.
* Get URL to stream stream h264/aac video (playable in VLC,MPV,etc).
* Plex Sync Support.
* Plex Audio Support.
Installation
------------
.. code-block:: python
pip install plexapi
Getting a PlexServer Instance
-----------------------------
There are two types of authentication. If you are running on a separate network
or using Plex Users you can log into MyPlex to get a PlexServer instance. An
example of this is below. NOTE: Servername below is the name of the server (not
the hostname and port). If logged into Plex Web you can see the server name in
the top left above your available libraries.
.. code-block:: python
from plexapi.myplex import MyPlexAccount
account = MyPlexAccount.signin('<USERNAME>', '<PASSWORD>')
plex = account.resource('<SERVERNAME>').connect() # returns a PlexServer instance
If you want to avoid logging into MyPlex and you already know your auth token
string, you can use the PlexServer object directly as above, but passing in
the baseurl and auth token directly.
.. code-block:: python
from plexapi.server import PlexServer
baseurl = 'http://plexserver:32400'
token = '2ffLuB84dqLswk9skLos'
plex = PlexServer(baseurl, token)
Usage Examples
--------------
.. code-block:: python
# Example 1: List all unwatched movies.
movies = plex.library.section('Movies')
for video in movies.search(unwatched=True):
print(video.title)
.. code-block:: python
# Example 2: Mark all Conan episodes watched.
plex.library.get('Conan (2010)').markWatched()
.. code-block:: python
# Example 3: List all clients connected to the Server.
for client in plex.clients():
print(client.title)
.. code-block:: python
# Example 4: Play the movie Avatar on another client.
# Note: Client must be on same network as server.
avatar = plex.library.section('Movies').get('Avatar')
client = plex.client("Michael's iPhone")
client.playMedia(avatar)
.. code-block:: python
# Example 5: List all content with the word 'Game' in the title.
for video in plex.search('Game'):
print('%s (%s)' % (video.title, video.TYPE))
.. code-block:: python
# Example 6: List all movies directed by the same person as Jurassic Park.
movies = plex.library.section('Movies')
jurassic_park = movies.get('Jurassic Park')
director = jurassic_park.directors[0]
for movie in movies.search(None, director=director):
print(movie.title)
.. code-block:: python
# Example 7: List files for the latest episode of Friends.
thelastone = plex.library.get('Friends').episodes()[-1]
for part in thelastone.iterParts():
print(part.file)
.. code-block:: python
# Example 8: Get a URL to stream a movie or show in another client
jurassic_park = plex.library.section('Movies').get('Jurassic Park')
print 'Run running the following command to play in VLC:'
print 'vlc "%s"' % jurassic_park.getStreamUrl(videoResolution='800x600')
.. code-block:: python
# Example 9: Get audio/video/all playlists
for playlist in self.plex.playlists():
print(playlist.title)
FAQs
----
**Q. Why are you using camelCase and not following PEP8 guidelines?** |br|
A. This API reads XML documents provided by MyPlex and the Plex Server.
We decided to conform to their style so that the API variable names directly
match with the provided XML documents.
**Q. Why don't you offer feature XYZ?** |br|
A. This library is meant to be a wrapper around the XML pages the Plex
server provides. If we are not providing an API that is offerered in the
XML pages, please let us know! -- Adding additional features beyond that
should be done outside the scope of this library.
**Q. What are some helpful links if trying to understand the raw Plex API?** |br|
https://github.com/plexinc/plex-media-player/wiki/Remote-control-API |br|
https://forums.plex.tv/discussion/104353/pms-web-api-documentation |br|
https://github.com/Arcanemagus/plex-api/wiki |br|

5
docs/modules/audio.rst Normal file
View file

@ -0,0 +1,5 @@
Audio (plexapi.audio)
---------------------
.. automodule:: plexapi.audio
:members:
:show-inheritance:

5
docs/modules/client.rst Normal file
View file

@ -0,0 +1,5 @@
Client (plexapi.client)
-----------------------
.. automodule:: plexapi.client
:members:
:show-inheritance:

5
docs/modules/config.rst Normal file
View file

@ -0,0 +1,5 @@
Config (plexapi.config)
-----------------------
.. automodule:: plexapi.config
:members:
:show-inheritance:

View file

@ -0,0 +1,5 @@
Exceptions (plexapi.exceptions)
-------------------------------
.. automodule:: plexapi.exceptions
:members:
:show-inheritance:

5
docs/modules/library.rst Normal file
View file

@ -0,0 +1,5 @@
Library (plexapi.library)
-------------------------
.. automodule:: plexapi.library
:members:
:show-inheritance:

5
docs/modules/media.rst Normal file
View file

@ -0,0 +1,5 @@
Media (plexapi.media)
---------------------
.. automodule:: plexapi.media
:members:
:show-inheritance:

5
docs/modules/myplex.rst Normal file
View file

@ -0,0 +1,5 @@
MyPlex (plexapi.myplex)
-----------------------
.. automodule:: plexapi.myplex
:members:
:show-inheritance:

5
docs/modules/photo.rst Normal file
View file

@ -0,0 +1,5 @@
Photo (plexapi.photo)
---------------------
.. automodule:: plexapi.photo
:members:
:show-inheritance:

View file

@ -0,0 +1,5 @@
Playlist (plexapi.playlist)
---------------------------
.. automodule:: plexapi.playlist
:members:
:show-inheritance:

View file

@ -0,0 +1,5 @@
Playqueue (plexapi.playqueue)
-----------------------------
.. automodule:: plexapi.playqueue
:members:
:show-inheritance:

5
docs/modules/server.rst Normal file
View file

@ -0,0 +1,5 @@
Server (plexapi.server)
-----------------------
.. automodule:: plexapi.server
:members:
:show-inheritance:

5
docs/modules/sync.rst Normal file
View file

@ -0,0 +1,5 @@
Sync (plexapi.sync)
-------------------
.. automodule:: plexapi.sync
:members:
:show-inheritance:

5
docs/modules/utils.rst Normal file
View file

@ -0,0 +1,5 @@
Utils (plexapi.utils)
---------------------
.. automodule:: plexapi.utils
:members:
:show-inheritance:

5
docs/modules/video.rst Normal file
View file

@ -0,0 +1,5 @@
Video (plexapi.video)
-----------------------
.. automodule:: plexapi.video
:members:
:show-inheritance:

27
docs/toc.rst Normal file
View file

@ -0,0 +1,27 @@
.. toctree::
:maxdepth: 1
:caption: Table of Contents
:titlesonly:
self
introduction
configuration
.. toctree::
:maxdepth: 1
:caption: Modules
modules/audio
modules/client
modules/config
modules/exceptions
modules/library
modules/media
modules/myplex
modules/photo
modules/playlist
modules/playqueue
modules/server
modules/sync
modules/utils
modules/video

View file

@ -3,11 +3,12 @@ import logging, os
from logging.handlers import RotatingFileHandler
from platform import uname
from plexapi.config import PlexConfig, reset_base_headers
from plexapi.utils import SecretsFilter
from uuid import getnode
# Load User Defined Config
CONFIG_PATH = os.path.expanduser('~/.config/plexapi/config.ini')
DEFAULT_CONFIG_PATH = os.path.expanduser('~/.config/plexapi/config.ini')
CONFIG_PATH = os.environ.get('PLEX_CONFIG_PATH', DEFAULT_CONFIG_PATH)
CONFIG = PlexConfig(CONFIG_PATH)
# Core Settings
@ -40,3 +41,6 @@ if logfile:
loghandler.setFormatter(logging.Formatter(logformat))
log.addHandler(loghandler)
log.setLevel(loglevel)
logfilter = SecretsFilter()
if CONFIG.get('logging.show_secrets') != 'true':
log.addFilter(logfilter)

View file

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
from plexapi import media, utils
from plexapi.utils import Playable, PlexPartialObject
@ -7,45 +6,37 @@ NA = utils.NA
class Audio(PlexPartialObject):
"""Base class for audio.
""" Base class for audio :class:`~plexapi.audio.Artist`, :class:`~plexapi.audio.Album`
and :class:`~plexapi.audio.Track` objects.
Attributes:
addedAt (int): int from epoch, datetime.datetime
index (sting): 1
key (str): Fx /library/metadata/102631
lastViewedAt (datetime.datetime): parse int into datetime.datetime.
librarySectionID (int):
listType (str): audio
ratingKey (int): Unique key to identify this item
summary (str): Summery of the artist, track, album
thumb (str): Url to thumb image
title (str): Fx Aerosmith
titleSort (str): Defaults title if None
TYPE (str): overwritten by subclass
type (string, NA): Description
updatedAt (datatime.datetime): parse int to datetime.datetime
viewCount (int): How many time has this item been played
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional)
data (ElementTree): Response from PlexServer used to build this object (optional).
initpath (str): Relative path requested when retrieving specified `data` (optional).
Attributes:
addedAt (datetime): Datetime this item was added to the library.
index (sting): Index Number (often the track number).
key (str): API URL (/library/metadata/<ratingkey>).
lastViewedAt (datetime): Datetime item was last accessed.
librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID.
listType (str): Hardcoded as 'audio' (useful for search filters).
ratingKey (int): Unique key identifying this item.
summary (str): Summary of the artist, track, or album.
thumb (str): URL to thumbnail image.
title (str): Artist, Album or Track title. (Jason Mraz, We Sing, Lucky, etc.)
titleSort (str): Title to use when sorting (defaults to title).
type (str): 'artist', 'album', or 'track'.
updatedAt (datatime): Datetime this item was updated.
viewCount (int): Count of times this item was accessed.
"""
TYPE = None
def __init__(self, server, data, initpath):
"""Used to set the attributes.
Args:
server (Plexserver): PMS your connected to
data (Element): XML reponse from PMS as Element
normally built from server.query
initpath (str): Fx /library/sections/7/all
"""
super(Audio, self).__init__(data, initpath, server)
def _loadData(self, data):
"""Used to set the attributes.
Args:
data (Element): XML reponse from PMS as Element
normally built from server.query
"""
""" Load attribute values from Plex XML response. """
self.listType = 'audio'
self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA))
self.index = data.attrib.get('index', NA)
@ -63,125 +54,133 @@ class Audio(PlexPartialObject):
@property
def thumbUrl(self):
"""Return url to thumb image."""
""" Returns the URL to this items thumbnail image. """
if self.thumb:
return self.server.url(self.thumb)
def refresh(self):
"""Refresh the metadata."""
""" Tells Plex to refresh the metadata for this and all subitems. """
self.server.query('%s/refresh' % self.key, method=self.server.session.put)
def section(self):
"""Library section."""
""" Returns the :class:`~plexapi.library.LibrarySection` this item belongs to. """
return self.server.library.sectionByID(self.librarySectionID)
@utils.register_libtype
class Artist(Audio):
"""Artist.
""" Represents a single audio artist.
Attributes:
art (str): /library/metadata/102631/art/1469310342
countries (list): List of media.County fx [<Country:24200:United.States>]
genres (list): List of media.Genre fx [<Genre:25555:Classic.Rock>]
guid (str): Fx guid com.plexapp.agents.plexmusic://gracenote/artist/05517B8701668D28?lang=en
key (str): Fx /library/metadata/102631
location (str): Filepath
similar (list): List of media.Similar fx [<Similar:25220:Guns.N'.Roses>]
TYPE (str): artist
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional)
data (ElementTree): Response from PlexServer used to build this object (optional).
initpath (str): Relative path requested when retrieving specified `data` (optional).
Attributes:
art (str): Artist artwork (/library/metadata/<ratingkey>/art/<artid>)
countries (list): List of :class:`~plexapi.media.Country` objects this artist respresents.
genres (list): List of :class:`~plexapi.media.Genre` objects this artist respresents.
guid (str): Unknown (unique ID; com.plexapp.agents.plexmusic://gracenote/artist/05517B8701668D28?lang=en)
key (str): API URL (/library/metadata/<ratingkey>).
location (str): Filepath this artist is found on disk.
similar (list): List of :class:`~plexapi.media.Similar` artists.
"""
TYPE = 'artist'
def _loadData(self, data):
"""Used to set the attributes.
Args:
data (Element): XML reponse from PMS as Element
normally built from server.query
"""
""" Load attribute values from Plex XML response. """
Audio._loadData(self, data)
self.art = data.attrib.get('art', NA)
self.guid = data.attrib.get('guid', NA)
self.key = self.key.replace('/children', '') # FIX_BUG_50
self.location = utils.findLocations(data, single=True)
if self.isFullObject(): # check if this is needed
self.countries = [media.Country(self.server, e)
for e in data if e.tag == media.Country.TYPE]
self.genres = [media.Genre(self.server, e)
for e in data if e.tag == media.Genre.TYPE]
self.similar = [media.Similar(self.server, e)
for e in data if e.tag == media.Similar.TYPE]
self.countries = [media.Country(self.server, e) for e in data if e.tag == media.Country.TYPE]
self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE]
self.similar = [media.Similar(self.server, e) for e in data if e.tag == media.Similar.TYPE]
def albums(self):
"""Return a list of Albums by thus artist."""
""" Returns a list of :class:`~plexapi.audio.Album` objects by this artist. """
path = '%s/children' % self.key
return utils.listItems(self.server, path, Album.TYPE)
def album(self, title):
"""Return a album from this artist that match title."""
""" Returns the :class:`~plexapi.audio.Album` that matches the specified title.
Parameters:
title (str): Title of the album to return.
"""
path = '%s/children' % self.key
return utils.findItem(self.server, path, title)
def tracks(self, watched=None):
"""Return all tracks to this artist.
Args:
watched(None, False, True): Default to None.
Returns:
List: of Track
"""
def tracks(self):
""" Returns a list of :class:`~plexapi.audio.Track` objects by this artist. """
path = '%s/allLeaves' % self.key
return utils.listItems(self.server, path, watched=watched)
return utils.listItems(self.server, path)
def track(self, title):
"""Return a Track that matches title.
""" Returns the :class:`~plexapi.audio.Track` that matches the specified title.
Args:
title (str): Fx song name
Returns:
Track:
Parameters:
title (str): Title of the track to return.
"""
path = '%s/allLeaves' % self.key
return utils.findItem(self.server, path, title)
def get(self, title):
"""Alias. See track."""
""" Alias of :func:`~plexapi.audio.Artist.track`. """
return self.track(title)
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
""" Downloads all tracks for this artist to the specified location.
Parameters:
savepath (str): Title of the track to return.
keep_orginal_name (bool): Set True to keep the original filename as stored in
the Plex server. False will create a new filename with the format
"<Atrist> - <Album> <Track>".
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL()` will
be returned and the additional arguments passed in will be sent to that
function. If kwargs is not specified, the media items will be downloaded
and saved to disk.
"""
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):
"""Album.
""" Represents a single audio album.
Attributes:
art (str): Fx /library/metadata/102631/art/1469310342
genres (list): List of media.Genre
key (str): Fx /library/metadata/102632
originallyAvailableAt (TYPE): Description
parentKey (str): /library/metadata/102631
parentRatingKey (int): Fx 1337
parentThumb (TYPE): Relative url to parent thumb image
parentTitle (str): Aerosmith
studio (str):
TYPE (str): album
year (int): 1999
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional)
data (ElementTree): Response from PlexServer used to build this object (optional).
initpath (str): Relative path requested when retrieving specified `data` (optional).
Attributes:
art (str): Album artwork (/library/metadata/<ratingkey>/art/<artid>)
genres (list): List of :class:`~plexapi.media.Genre` objects this album respresents.
key (str): API URL (/library/metadata/<ratingkey>).
originallyAvailableAt (datetime): Datetime this album was released.
parentKey (str): API URL of this artist.
parentRatingKey (int): Unique key identifying artist.
parentThumb (str): URL to artist thumbnail image.
parentTitle (str): Name of the artist for this album.
studio (str): Studio that released this album.
year (int): Year this album was released.
"""
TYPE = 'album'
def _loadData(self, data):
"""Used to set the attributes.
Args:
data (Element): XML reponse from PMS as Element
normally built from server.query
"""
""" Load attribute values from Plex XML response. """
Audio._loadData(self, data)
self.art = data.attrib.get('art', NA)
self.key = self.key.replace('/children', '') # FIX_BUG_50
self.key = self.key.replace('/children', '') # fixes bug #50
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
self.parentKey = data.attrib.get('parentKey', NA)
self.parentRatingKey = data.attrib.get('parentRatingKey', NA)
@ -192,88 +191,91 @@ class Album(Audio):
if self.isFullObject():
self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE]
def tracks(self, watched=None):
"""Return all tracks to this album.
Args:
watched(None, False, True): Default to None.
Returns:
List: of Track
"""
def tracks(self):
""" Returns a list of :class:`~plexapi.audio.Track` objects in this album. """
path = '%s/children' % self.key
return utils.listItems(self.server, path, watched=watched)
return utils.listItems(self.server, path)
def track(self, title):
"""Return a Track that matches title.
""" Returns the :class:`~plexapi.audio.Track` that matches the specified title.
Args:
title (str): Fx song name
Returns:
Track:
Parameters:
title (str): Title of the track to return.
"""
path = '%s/children' % self.key
return utils.findItem(self.server, path, title)
def get(self, title):
"""Alias. See track."""
""" Alias of :func:`~plexapi.audio.Album.track`. """
return self.track(title)
def artist(self):
"""Return Artist of this album."""
""" Return :func:`~plexapi.audio.Artist` of this album. """
return utils.listItems(self.server, self.parentKey)[0]
def watched(self):
"""Return Track that is lisson on."""
return self.tracks(watched=True)
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
""" Downloads all tracks for this artist to the specified location.
Parameters:
savepath (str): Title of the track to return.
keep_orginal_name (bool): Set True to keep the original filename as stored in
the Plex server. False will create a new filename with the format
"<Atrist> - <Album> <Track>".
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL()` will
be returned and the additional arguments passed in will be sent to that
function. If kwargs is not specified, the media items will be downloaded
and saved to disk.
"""
downloaded = []
for ep in self.tracks():
dl = ep.download(savepath=savepath, keep_orginal_name=keep_orginal_name, **kwargs)
if dl:
downloaded.extend(dl)
def unwatched(self):
"""Return Track that is not lisson on."""
return self.tracks(watched=False)
return downloaded
@utils.register_libtype
class Track(Audio, Playable):
"""Track.
""" Represents a single audio track.
Attributes:
art (str): Relative path fx /library/metadata/102631/art/1469310342
chapterSource (TYPE): Description
duration (TYPE): Description
grandparentArt (str): Relative path
grandparentKey (str): Relative path Fx /library/metadata/102631
grandparentRatingKey (TYPE): Description
grandparentThumb (str): Relative path to Artist thumb img
grandparentTitle (str): Aerosmith
guid (TYPE): Description
media (list): List of media.Media
moods (list): List of media.Moods
originalTitle (str): Some track title
parentIndex (int): 1
parentKey (str): Relative path Fx /library/metadata/102632
parentRatingKey (int): 1337
parentThumb (str): Relative path to Album thumb
parentTitle (str): Album title
player (None): #TODO
primaryExtraKey (TYPE): #TODO
ratingCount (int): 10
sessionKey (int): Description
transcodeSession (None):
TYPE (str): track
username (str): username@mail.com
viewOffset (int): 100
year (int): 1999
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional)
data (ElementTree): XML response from PlexServer used to build this object (optional).
initpath (str): Relative path requested when retrieving specified `data` (optional).
Attributes:
art (str): Track artwork (/library/metadata/<ratingkey>/art/<artid>)
chapterSource (TYPE): Unknown
duration (int): Length of this album in seconds.
grandparentArt (str): Artist artowrk.
grandparentKey (str): Artist API URL.
grandparentRatingKey (str): Unique key identifying artist.
grandparentThumb (str): URL to artist thumbnail image.
grandparentTitle (str): Name of the artist for this track.
guid (str): Unknown (unique ID).
media (list): List of :class:`~plexapi.media.Media` objects for this track.
moods (list): List of :class:`~plexapi.media.Mood` objects for this track.
originalTitle (str): Original track title (if translated).
parentIndex (int): Album index.
parentKey (str): Album API URL.
parentRatingKey (int): Unique key identifying album.
parentThumb (str): URL to album thumbnail image.
parentTitle (str): Name of the album for this track.
primaryExtraKey (str): Unknown
ratingCount (int): Rating of this track (1-10?)
viewOffset (int): Unknown
year (int): Year this track was released.
sessionKey (int): Session Key (active sessions only).
username (str): Username of person playing this track (active sessions only).
player (str): :class:`~plexapi.client.PlexClient` for playing track (active sessions only).
transcodeSession (None): :class:`~plexapi.media.TranscodeSession` for playing
track (active sessions only).
"""
TYPE = 'track'
def _loadData(self, data):
"""Used to set the attributes
Args:
data (Element): Usually built from server.query
"""
""" Load attribute values from Plex XML response. """
Audio._loadData(self, data)
Playable._loadData(self, data)
self.art = data.attrib.get('art', NA)
@ -295,11 +297,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.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]
# data for active sessions and history
self.sessionKey = utils.cast(int, data.attrib.get('sessionKey', NA))
self.username = utils.findUsername(data)
@ -308,14 +312,18 @@ class Track(Audio, Playable):
@property
def thumbUrl(self):
"""Return url to thumb image."""
""" Returns the URL thumbnail image for this track's album. """
if self.parentThumb:
return self.server.url(self.parentThumb)
def album(self):
"""Return this track's Album."""
""" Return this track's :class:`~plexapi.audio.Album`. """
return utils.listItems(self.server, self.parentKey)[0]
def artist(self):
"""Return this track's Artist."""
""" Return this track's :class:`~plexapi.audio.Artist`. """
return utils.listItems(self.server, self.grandparentKey)[0]
def _prettyfilename(self):
""" Returns a filename for use in download. """
return '%s - %s %s' % (self.grandparentTitle, self.parentTitle, self.title)

View file

@ -1,10 +1,4 @@
# -*- 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 BASE_HEADERS, TIMEOUT, log, utils
@ -13,65 +7,64 @@ from xml.etree import ElementTree
class PlexClient(object):
"""Main class for interacting with a client.
""" Main class for interacting with a Plex client. This class can connect
directly to the client and control it or proxy commands through your
Plex Server. To better understand the Plex client API's read this page:
https://github.com/plexinc/plex-media-player/wiki/Remote-control-API
Attributes:
baseurl (str): http adress for the client
device (None): Description
deviceClass (sting): pc, phone
machineIdentifier (str): uuid fx 5471D9EA-1467-4051-9BE7-FCBDF490ACE3
model (TYPE): Description
platform (TYPE): Description
platformVersion (TYPE): Description
product (str): plex for ios
protocol (str): plex
protocolCapabilities (list): List of what client can do
protocolVersion (str): 1
server (plexapi.server.Plexserver): PMS your connected to
session (None or requests.Session): Add your own session object to cache stuff
state (None): Description
title (str): fx Johns Iphone
token (str): X-Plex-Token, using for authenication with PMS
vendor (str): Description
version (str): fx. 4.6
Parameters:
baseurl (str): HTTP URL to connect dirrectly to this client.
token (str): X-Plex-Token used for authenication (optional).
session (:class:`~requests.Session`): requests.Session object if you want more control (optional).
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional)
data (ElementTree): Response from PlexServer used to build this object (optional).
Attributes:
baseurl (str): HTTP address of the client
device (str): Best guess on the type of device this is (PS, iPhone, Linux, etc).
deviceClass (str): Device class (pc, phone, etc).
machineIdentifier (str): Unique ID for this device.
model (str): Unknown
platform (str): Unknown
platformVersion (str): Description
product (str): Client Product (Plex for iOS, etc).
protocol (str): Always seems ot be 'plex'.
protocolCapabilities (list<str>): List of client capabilities (navigation, playback,
timeline, mirror, playqueues).
protocolVersion (str): Protocol version (1, future proofing?)
server (:class:`~plexapi.server.PlexServer`): Server this client is connected to.
session (:class:`~requests.Session`): Session object used for connection.
state (str): Unknown
title (str): Name of this client (Johns iPhone, etc).
token (str): X-Plex-Token used for authenication
vendor (str): Unknown
version (str): Device version (4.6.1, etc).
_proxyThroughServer (bool): Set to True after calling
:func:`~plexapi.client.PlexClient.proxyThroughServer()` (default False).
"""
def __init__(self, baseurl, token=None, session=None, server=None, data=None):
"""Kick shit off.
Args:
baseurl (sting): fx http://10.0.0.99:1111222
token (None, optional): X-Plex-Token, using for authenication with PMS
session (None, optional): requests.Session() or your own session
server (None, optional): PlexServer
data (None, optional): XML response from PMS as Element
or uses connect to get it
"""
self.baseurl = baseurl.strip('/')
self.token = token
self.session = session or requests.Session()
self.server = server
# session > server.session > requests.Session
_server_session = server.session if server else None
self.session = session or _server_session or requests.Session()
self._loadData(data) if data is not None else self.connect()
self._proxyThroughServer = False
self._commandId = 0
def _loadData(self, data):
"""Sets attrs to the class.
Args:
data (Element): XML response from PMS as a Element
"""
""" Load attribute values from Plex XML response. """
self.deviceClass = data.attrib.get('deviceClass')
self.machineIdentifier = data.attrib.get('machineIdentifier')
self.product = data.attrib.get('product')
self.protocol = data.attrib.get('protocol')
self.protocolCapabilities = data.attrib.get(
'protocolCapabilities', '').split(',')
self.protocolCapabilities = data.attrib.get('protocolCapabilities', '').split(',')
self.protocolVersion = data.attrib.get('protocolVersion')
self.platform = data.attrib.get('platform')
self.platformVersion = data.attrib.get('platformVersion')
self.title = data.attrib.get('title') or data.attrib.get('name')
# active session details
# Active session details
self.device = data.attrib.get('device')
self.model = data.attrib.get('model')
self.state = data.attrib.get('state')
@ -79,7 +72,11 @@ class PlexClient(object):
self.version = data.attrib.get('version')
def connect(self):
"""Connect"""
""" Connects to the client and reloads all class attributes.
Raises:
:class:`~plexapi.exceptions.NotFound`: No client found at the specified url.
"""
try:
data = self.query('/resources')[0]
self._loadData(data)
@ -88,43 +85,38 @@ class PlexClient(object):
raise NotFound('No client found at: %s' % self.baseurl)
def headers(self):
"""Default headers
Returns:
dict: default headers
"""
""" Returns a dict of all default headers for Client requests. """
headers = BASE_HEADERS
if self.token:
headers['X-Plex-Token'] = self.token
return headers
def proxyThroughServer(self, value=True):
"""Connect to the client via the server.
""" Tells this PlexClient instance to proxy all future commands through the PlexServer.
Useful if you do not wish to connect directly to the Client device itself.
Args:
value (bool, optional): Description
Parameters:
value (bool): Enable or disable proxying (optional, default True).
Raises:
Unsupported: Cannot use client proxy with unknown server.
Raises:
:class:`~plexapi.exceptions.Unsupported`: Cannot use client proxy with unknown server.
"""
if value is True and not self.server:
raise Unsupported('Cannot use client proxy with unknown server.')
self._proxyThroughServer = value
def query(self, path, method=None, headers=None, **kwargs):
"""Used to fetch relative paths to pms.
""" Returns an ElementTree object containing the response
from the specified request path.
Args:
path (str): Relative path
method (None, optional): requests.post etc
headers (None, optional): Set headers manually
**kwargs (TYPE): Passord to the http request used for filter, sorting.
Parameters:
path (str): Relative path to query.
method (func): `self.session.get` or `self.session.post`
headers (dict): Additional headers to include or override in the request.
**kwargs (TYPE): Additional arguments to inclde in the request.<method> call.
Returns:
Element
Raises:
BadRequest: Http error and code
Raises:
:class:`~plexapi.exceptions.BadRequest`: When the response is not in [200, 201]
"""
url = self.url(path)
method = method or self.session.get
@ -138,24 +130,23 @@ class PlexClient(object):
return ElementTree.fromstring(data) if data else None
def sendCommand(self, command, proxy=None, **params):
"""Send a command to the client
""" Convenience wrapper around :func:`~plexapi.client.PlexClient.query()` to more easily
send simple commands to the client. Returns an ElementTree object containing
the response.
Args:
command (str): See the commands listed below
proxy (None, optional): Description
**params (dict): Description
Parameters:
command (str): Command to be sent in for format '<controller>/<command>'.
proxy (bool): Set True to proxy this command through the PlexServer.
**params (dict): Additional GET parameters to include with the command.
Returns:
Element
Raises:
Unsupported: Unsupported clients
Raises:
:class:`~plexapi.exceptions.Unsupported`: When we detect the client doesn't support this capability.
"""
command = command.strip('/')
controller = command.split('/')[0]
if controller not in self.protocolCapabilities:
raise Unsupported(
'Client %s does not support the %s controller.' % (self.title, controller))
raise Unsupported('Client %s does not support the %s controller.' %
(self.title, controller))
path = '/player/%s%s' % (command, utils.joinArgs(params))
headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier}
self._commandId += 1
@ -167,82 +158,87 @@ class PlexClient(object):
return self.query(path, headers=headers)
def url(self, path):
"""Return a full url
""" Given a path, this retuns the full PlexClient the PlexServer URL to request.
Args:
path (str): Relative path
Returns:
string: full path to PMS
Parameters:
path (str): Relative path to be converted.
"""
if self.token:
delim = '&' if '?' in path else '?'
return '%s%s%sX-Plex-Token=%s' % (self.baseurl, path, delim, self.token)
return '%s%s' % (self.baseurl, path)
#---------------------
# Navigation Commands
# These commands navigate around the user-interface.
def contextMenu(self):
"""Open the context menu on the client."""
""" Open the context menu on the client. """
self.sendCommand('navigation/contextMenu')
def goBack(self):
"""One step back"""
""" Navigate back one position. """
self.sendCommand('navigation/back')
def goToHome(self):
"""Jump to home screen."""
""" Go directly to the home screen. """
self.sendCommand('navigation/home')
def goToMusic(self):
"""Jump to music."""
""" Go directly to the playing music panel. """
self.sendCommand('navigation/music')
def moveDown(self):
"""One step down."""
""" Move selection down a position. """
self.sendCommand('navigation/moveDown')
def moveLeft(self):
""" Move selection left a position. """
self.sendCommand('navigation/moveLeft')
def moveRight(self):
""" Move selection right a position. """
self.sendCommand('navigation/moveRight')
def moveUp(self):
""" Move selection up a position. """
self.sendCommand('navigation/moveUp')
def nextLetter(self):
"""Jump to the next letter in the alphabeth."""
""" Jump to next letter in the alphabet. """
self.sendCommand('navigation/nextLetter')
def pageDown(self):
""" Move selection down a full page. """
self.sendCommand('navigation/pageDown')
def pageUp(self):
""" Move selection up a full page. """
self.sendCommand('navigation/pageUp')
def previousLetter(self):
""" Jump to previous letter in the alphabet. """
self.sendCommand('navigation/previousLetter')
def select(self):
""" Select element at the current position. """
self.sendCommand('navigation/select')
def toggleOSD(self):
""" Toggle the on screen display during playback. """
self.sendCommand('navigation/toggleOSD')
def goToMedia(self, media, **params):
"""Go to a media on the client.
""" Navigate directly to the specified media page.
Args:
media (str): movie, music, photo
**params (TYPE): Description # todo
Parameters:
media (:class:`~plexapi.media.Media`): Media object to navigate to.
**params (dict): Additional GET parameters to include with the command.
Raises:
Unsupported: Description
Raises:
:class:`~plexapi.exceptions.Unsupported`: When no PlexServer specified in this object.
"""
if not self.server:
raise Unsupported(
'A server must be specified before using this command.')
raise Unsupported('A server must be specified before using this command.')
server_url = media.server.baseurl.split(':')
self.sendCommand('mirror/details', **dict({
'machineIdentifier': self.server.machineIdentifier,
@ -251,192 +247,184 @@ class PlexClient(object):
'key': media.key,
}, **params))
#-------------------
# Playback Commands
# 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):
"""Pause playback
""" Pause the currently playing media type.
Args:
mtype (str): music, photo, video
Parameters:
mtype (str): Media type to take action against (music, photo, video).
"""
self.sendCommand('playback/pause', type=mtype)
def play(self, mtype):
"""Start playback
""" Start playback for the specified media type.
Args:
mtype (str): music, photo, video
Parameters:
mtype (str): Media type to take action against (music, photo, video).
"""
self.sendCommand('playback/play', type=mtype)
def refreshPlayQueue(self, playQueueID, mtype=None):
"""Summary
Args:
playQueueID (TYPE): Description
mtype (None, optional): photo, video, music
""" Refresh the specified Playqueue.
Parameters:
playQueueID (str): Playqueue ID.
mtype (str): Media type to take action against (music, photo, video).
"""
self.sendCommand(
'playback/refreshPlayQueue', playQueueID=playQueueID, type=mtype)
def seekTo(self, offset, mtype=None):
"""Seek to a time in a plaback.
Args:
offset (int): in milliseconds
mtype (None, optional): photo, video, music
""" Seek to the specified offset (ms) during playback.
Parameters:
offset (int): Position to seek to (milliseconds).
mtype (str): Media type to take action against (music, photo, video).
"""
self.sendCommand('playback/seekTo', offset=offset, type=mtype)
def skipNext(self, mtype=None):
"""Skip to next
""" Skip to the next playback item.
Args:
mtype (None, string, optional): photo, video, music
Parameters:
mtype (str): Media type to take action against (music, photo, video).
"""
self.sendCommand('playback/skipNext', type=mtype)
def skipPrevious(self, mtype=None):
"""Skip to previous
""" Skip to previous playback item.
Args:
mtype (None, optional): Description
Parameters:
mtype (str): Media type to take action against (music, photo, video).
"""
self.sendCommand('playback/skipPrevious', type=mtype)
def skipTo(self, key, mtype=None):
"""Jump to
""" Skip to the playback item with the specified key.
Args:
key (TYPE): # what is this
mtype (None, optional): photo, video, music
Returns:
TYPE: Description
Parameters:
key (str): Key of the media item to skip to.
mtype (str): Media type to take action against (music, photo, video).
"""
# skips to item with matching key
self.sendCommand('playback/skipTo', key=key, type=mtype)
def stepBack(self, mtype=None):
"""
""" Step backward a chunk of time in the current playback item.
Args:
mtype (None, optional): photo, video, music
Parameters:
mtype (str): Media type to take action against (music, photo, video).
"""
self.sendCommand('playback/stepBack', type=mtype)
def stepForward(self, mtype):
"""Summary
""" Step forward a chunk of time in the current playback item.
Args:
mtype (TYPE): Description
Returns:
TYPE: Description
Parameters:
mtype (str): Media type to take action against (music, photo, video).
"""
self.sendCommand('playback/stepForward', type=mtype)
def stop(self, mtype):
"""Stop playback
Args:
mtype (str): video, music, photo
""" Stop the currently playing item.
Parameters:
mtype (str): Media type to take action against (music, photo, video).
"""
self.sendCommand('playback/stop', type=mtype)
def setRepeat(self, repeat, mtype):
"""Summary
""" Enable repeat for the specified playback items.
Args:
repeat (int): 0=off, 1=repeatone, 2=repeatall
mtype (TYPE): video, music, photo
Parameters:
repeat (int): Repeat mode (0=off, 1=repeatone, 2=repeatall).
mtype (str): Media type to take action against (music, photo, video).
"""
self.setParameters(repeat=repeat, mtype=mtype)
def setShuffle(self, shuffle, mtype):
"""Set shuffle
""" Enable shuffle for the specified playback items.
Args:
shuffle (int): 0=off, 1=on
mtype (TYPE): Description
Parameters:
shuffle (int): Shuffle mode (0=off, 1=on)
mtype (str): Media type to take action against (music, photo, video).
"""
self.setParameters(shuffle=shuffle, mtype=mtype)
def setVolume(self, volume, mtype):
"""Change volume
""" Enable volume for the current playback item.
Args:
volume (int): 0-100
mtype (TYPE): Description
Parameters:
volume (int): Volume level (0-100).
mtype (str): Media type to take action against (music, photo, video).
"""
self.setParameters(volume=volume, mtype=mtype)
def setAudioStream(self, audioStreamID, mtype):
"""Select a audio stream
""" Select the audio stream for the current playback item (only video).
Args:
audioStreamID (TYPE): Description
mtype (str): video, music, photo
Parameters:
audioStreamID (str): ID of the audio stream from the media object.
mtype (str): Media type to take action against (music, photo, video).
"""
self.setStreams(audioStreamID=audioStreamID, mtype=mtype)
def setSubtitleStream(self, subtitleStreamID, mtype):
"""Select a subtitle
""" Select the subtitle stream for the current playback item (only video).
Args:
subtitleStreamID (TYPE): Description
mtype (str): video, music, photo
Parameters:
subtitleStreamID (str): ID of the subtitle stream from the media object.
mtype (str): Media type to take action against (music, photo, video).
"""
self.setStreams(subtitleStreamID=subtitleStreamID, mtype=mtype)
def setVideoStream(self, videoStreamID, mtype):
"""Summary
Args:
videoStreamID (TYPE): Description
mtype (str): video, music, photo
""" Select the video stream for the current playback item (only video).
Parameters:
videoStreamID (str): ID of the video stream from the media object.
mtype (str): Media type to take action against (music, photo, video).
"""
self.setStreams(videoStreamID=videoStreamID, mtype=mtype)
def playMedia(self, media, **params):
"""Start playback on a media item.
def playMedia(self, media, offset=0, **params):
""" Start playback of the specified media item. See also:
Parameters:
media (:class:`~plexapi.media.Media`): Media item to be played back (movie, music, photo).
offset (int): Number of milliseconds at which to start playing with zero representing
the beginning (default 0).
**params (dict): Optional additional parameters to include in the playback request. See
also: https://github.com/plexinc/plex-media-player/wiki/Remote-control-API#modified-commands
Args:
media (str): movie, music, photo
**params (TYPE): Description
Raises:
Unsupported: Description
Raises:
:class:`~plexapi.exceptions.Unsupported`: When no PlexServer specified in this object.
"""
if not self.server:
raise Unsupported(
'A server must be specified before using this command.')
raise Unsupported('A server must be specified before using this command.')
server_url = media.server.baseurl.split(':')
playqueue = self.server.createPlayQueue(media)
self.sendCommand('playback/playMedia', **dict({
'machineIdentifier': self.server.machineIdentifier,
'address': server_url[1].strip('/'),
'port': server_url[-1],
'offset': offset,
'key': media.key,
'containerKey': '/playQueues/%s?window=100&own=1' % playqueue.playQueueID,
}, **params))
def setParameters(self, volume=None, shuffle=None, repeat=None, mtype=None):
"""Set params for the client
""" Set multiple playback parameters at once.
Args:
volume (None, optional): 0-100
shuffle (None, optional): 0=off, 1=on
repeat (None, optional): 0=off, 1=repeatone, 2=repeatall
mtype (None, optional): music,photo,video
Parameters:
volume (int): Volume level (0-100; optional).
shuffle (int): Shuffle mode (0=off, 1=on; optional).
repeat (int): Repeat mode (0=off, 1=repeatone, 2=repeatall; optional).
mtype (str): Media type to take action against (optional music, photo, video).
"""
params = {}
if repeat is not None:
@ -449,15 +437,14 @@ class PlexClient(object):
params['type'] = mtype
self.sendCommand('playback/setParameters', **params)
def setStreams(self, audioStreamID=None, subtitleStreamID=None,
videoStreamID=None, mtype=None):
"""Select streams.
def setStreams(self, audioStreamID=None, subtitleStreamID=None, videoStreamID=None, mtype=None):
""" Select multiple playback streams at once.
Args:
audioStreamID (None, optional): Description
subtitleStreamID (None, optional): Description
videoStreamID (None, optional): Description
mtype (None, optional): music,photo,video
Parameters:
audioStreamID (str): ID of the audio stream from the media object.
subtitleStreamID (str): ID of the subtitle stream from the media object.
videoStreamID (str): ID of the video stream from the media object.
mtype (str): Media type to take action against (optional music, photo, video).
"""
params = {}
if audioStreamID is not None:
@ -470,19 +457,18 @@ class PlexClient(object):
params['type'] = mtype
self.sendCommand('playback/setStreams', **params)
#-------------------
# Timeline Commands
def timeline(self):
"""Timeline"""
""" Poll the current timeline and return the XML response. """
return self.sendCommand('timeline/poll', **{'wait': 1, 'commandID': 4})
def isPlayingMedia(self, includePaused=False):
"""Check timeline if anything is playing
""" Returns True if any media is currently playing.
Args:
includePaused (bool, optional): Should paused be included
Returns:
bool
Parameters:
includePaused (bool): Set True to treat currently paused items
as playing (optional; default True).
"""
for mediatype in self.timeline():
if mediatype.get('state') == 'playing':

View file

@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-
# flake8:noqa
"""
Python 2/3 compatability
Always try Py3 first
"""
# Python 2/3 compatability
# Always try Py3 first
try:
string_type = basestring
except NameError:
string_type = str
try:
from urllib.parse import urlencode
@ -20,8 +22,13 @@ try:
except ImportError:
from urllib import unquote
try:
from configparser import ConfigParser
except ImportError:
from ConfigParser import ConfigParser
try:
from xml.etree import cElementTree as ElementTree
except ImportError:
from xml.etree import ElementTree

View file

@ -1,24 +1,38 @@
# -*- coding: utf-8 -*-
"""
PlexConfig
Settings are stored in an INI file and can be overridden after import
plexapi by simply setting the value.
"""
from collections import defaultdict
try:
from ConfigParser import ConfigParser # Python2
except ImportError:
from configparser import ConfigParser # Python3
from plexapi.compat import ConfigParser
class PlexConfig(ConfigParser):
""" PlexAPI configuration object. Settings are stored in an INI file within the
user's home directory and can be overridden after importing plexapi by simply
setting the value. See the documentation section 'Configuration' for more
details on available options.
Parameters:
path (str): Path of the configuration file to load.
"""
def __init__(self, path):
ConfigParser.__init__(self)
self.read(path)
self.data = self._asDict()
def __getattr__(self, attr):
if attr not in ('get', '_asDict', 'data'):
for section in self._sections:
for name, value in self._sections[section].items():
if name == attr:
return value
raise Exception('Config attr not found: %s' % attr)
def get(self, key, default=None, cast=None):
""" Returns the specified configuration value or <default> if not found.
Parameters:
key (str): Configuration variable to load in the format '<section>.<variable>'.
default: Default value to use if key not found.
cast (func): Cast the value to the specified type before returning.
"""
try:
section, name = key.split('.')
value = self.data.get(section.lower(), {}).get(name.lower(), default)
@ -27,6 +41,7 @@ class PlexConfig(ConfigParser):
return default
def _asDict(self):
""" Returns all configuration values as a dictionary. """
config = defaultdict(dict)
for section in self._sections:
for name, value in self._sections[section].items():
@ -36,6 +51,7 @@ class PlexConfig(ConfigParser):
def reset_base_headers():
""" Convenience function returns a dict of all base X-Plex-* headers for session requests. """
import plexapi
return {
'X-Plex-Platform': plexapi.X_PLEX_PLATFORM,

View file

@ -1,23 +1,29 @@
# -*- coding: utf-8 -*-
# flake8:noqa
"""
PlexAPI Exceptions
"""
class PlexApiException(Exception):
""" Base class for all PlexAPI exceptions. """
pass
class BadRequest(PlexApiException):
""" An invalid request, generally a user error. """
pass
class NotFound(PlexApiException):
""" Request media item or device is not found. """
pass
class UnknownType(PlexApiException):
""" Unknown library type. """
pass
class Unsupported(PlexApiException):
""" Unsupported client request. """
pass
class Unauthorized(PlexApiException):
""" Invalid username or password. """
pass
class NotImplementedError(PlexApiException):
""" Feature is not yet implemented. """
pass

View file

@ -1,14 +1,24 @@
# -*- coding: utf-8 -*-
import logging
from plexapi import log, utils
from plexapi import X_PLEX_CONTAINER_SIZE
import logging
from plexapi import X_PLEX_CONTAINER_SIZE, log, utils
from plexapi.compat import unquote
from plexapi.media import MediaTag, Genre, Role, Director
from plexapi.exceptions import BadRequest, NotFound
from plexapi.media import MediaTag
class Library(object):
""" Represents a PlexServer library. This contains all sections of media defined
in your Plex server including video, shows and audio.
Attributes:
identifier (str): Unknown ('com.plexapp.plugins.library').
mediaTagVersion (str): Unknown (/system/bundle/media/flags/)
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to.
title1 (str): 'Plex Library' (not sure how useful this is).
title2 (str): Second title (this is blank on my setup).
"""
def __init__(self, server, data):
self.identifier = data.attrib.get('identifier')
@ -16,12 +26,16 @@ class Library(object):
self.server = server
self.title1 = data.attrib.get('title1')
self.title2 = data.attrib.get('title2')
self._sectionsByID = {} # cached section UUIDs
self._sectionsByID = {} # cached Section UUIDs
def __repr__(self):
return '<Library:%s>' % self.title1.encode('utf8')
def sections(self):
""" Returns a list of all media sections in this library. Library sections may be any of
:class:`~plexapi.library.MovieSection`, :class:`~plexapi.library.ShowSection`,
:class:`~plexapi.library.MusicSection`, :class:`~plexapi.library.PhotoSection`.
"""
items = []
SECTION_TYPES = {
MovieSection.TYPE: MovieSection,
@ -40,40 +54,69 @@ class Library(object):
return items
def section(self, title=None):
""" Returns the :class:`~plexapi.library.LibrarySection` that matches the specified title.
Parameters:
title (str): Title of the section to return.
Raises:
:class:`~plexapi.exceptions.NotFound`: Invalid library section title.
"""
for item in self.sections():
if item.title == title:
return item
raise NotFound('Invalid library section: %s' % title)
def sectionByID(self, sectionID):
if not self._sectionsByID:
""" Returns the :class:`~plexapi.library.LibrarySection` that matches the specified sectionID.
Parameters:
sectionID (int): ID of the section to return.
"""
if not self._sectionsByID or sectionID not in self._sectionsByID:
self.sections()
return self._sectionsByID[sectionID]
def all(self):
return [item for section in self.sections()
for item in section.all()]
""" Returns a list of all media from all library sections.
This may be a very large dataset to retrieve.
"""
return [item for section in self.sections() for item in section.all()]
def onDeck(self):
""" Returns a list of all media items on deck. """
return utils.listItems(self.server, '/library/onDeck')
def recentlyAdded(self):
""" Returns a list of all media items recently added. """
return utils.listItems(self.server, '/library/recentlyAdded')
def get(self, title):
return utils.findItem(self.server, '/library/all', 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.
"""
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.
Parameters:
key (str): Key of the item to return.
"""
return utils.findKey(self.server, key)
def search(self, title=None, libtype=None, **kwargs):
""" Searching within a library section is much more powerful. It seems certain attributes on the media
objects can be targeted to filter this search down a bit, but I havent found the documentation for
it.
""" Searching within a library section is much more powerful. It seems certain
attributes on the media objects can be targeted to filter this search down
a bit, but I havent found the documentation for it.
Example: "studio=Comedy%20Central" or "year=1999" "title=Kung Fu" all work. Other items
such as actor=<id> seem to work, but require you already know the id of the actor.
TLDR: This is untested but seems to work. Use library section search when you can.
such as actor=<id> seem to work, but require you already know the id of the actor.
TLDR: This is untested but seems to work. Use library section search when you can.
"""
args = {}
if title:
@ -86,16 +129,30 @@ class Library(object):
return utils.listItems(self.server, query)
def cleanBundles(self):
""" Poster images and other metadata for items in your library are kept in "bundle"
packages. When you remove items from your library, these bundles aren't immediately
removed. Removing these old bundles can reduce the size of your install. By default, your
server will automatically clean up old bundles once a week as part of Scheduled Tasks.
"""
# TODO: Should this check the response for success or the correct mediaprefix?
self.server.query('/library/clean/bundles')
def emptyTrash(self):
""" If a library has items in the Library Trash, use this option to empty the Trash. """
for section in self.sections():
section.emptyTrash()
def optimize(self):
""" The Optimize option cleans up the server database from unused or fragmented data.
For example, if you have deleted or added an entire library or many items in a
library, you may like to optimize the database.
"""
self.server.query('/library/optimize')
def refresh(self):
""" Refresh the metadata for the entire library. This will fetch fresh metadata for
all contents in the library, including items that already have metadata.
"""
self.server.query('/library/sections/all/refresh')
def __len__(self):
@ -103,6 +160,33 @@ class Library(object):
class LibrarySection(object):
""" Base class for a single library section.
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer object this library section is from.
data (ElementTree): Response from PlexServer used to build this object (optional).
initpath (str): Relative path requested when retrieving specified `data` (optional).
Attributes:
server (:class:`~plexapi.server.PlexServer`): Server this client is connected to.
initpath (str): Path requested when building this object.
agent (str): Unknown (com.plexapp.agents.imdb, etc)
allowSync (bool): True if you allow syncing content from this section.
art (str): Wallpaper artwork used to respresent this section.
composite (str): Composit image used to represent this section.
createdAt (datetime): Datetime this library section was created.
filters (str): Unknown
key (str): Key (or ID) of this library section.
language (str): Language represented in this section (en, xn, etc).
locations (str): Paths on disk where section content is stored.
refreshing (str): True if this section is currently being refreshed.
scanner (str): Internal scanner used to find media (Plex Movie Scanner, Plex Premium Music Scanner, etc.)
thumb (str): Thumbnail image used to represent this section.
title (str): Title of this section.
type (str): Type of content section represents (movie, artist, photo, show).
updatedAt (datetime): Datetime this library section was last updated.
uuid (str): Unique id for this section (32258d7c-3e6c-4ac5-98ad-bad7a3b78c63)
"""
ALLOWED_FILTERS = ()
ALLOWED_SORT = ()
BOOLEAN_FILTERS = ('unwatched', 'duplicate')
@ -118,7 +202,6 @@ class LibrarySection(object):
self.filters = data.attrib.get('filters')
self.key = data.attrib.get('key')
self.language = data.attrib.get('language')
self.language = data.attrib.get('language')
self.locations = utils.findLocations(data)
self.refreshing = utils.cast(bool, data.attrib.get('refreshing'))
self.scanner = data.attrib.get('scanner')
@ -133,42 +216,66 @@ class LibrarySection(object):
return '<%s:%s>' % (self.__class__.__name__, title.encode('utf8'))
def get(self, title):
""" Returns the media item with the specified title.
Parameters:
title (str): Title of the item to return.
"""
path = '/library/sections/%s/all' % self.key
return utils.findItem(self.server, path, title)
def all(self):
""" Returns a list of media from this library section. """
return utils.listItems(self.server, '/library/sections/%s/all' % self.key)
def onDeck(self):
""" Returns a list of media items on deck from this library section. """
return utils.listItems(self.server, '/library/sections/%s/onDeck' % self.key)
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).
"""
return self.search(sort='addedAt:desc', maxresults=maxresults)
def analyze(self):
self.server.query('/library/sections/%s/analyze' % self.key)
""" Run an analysis on all of the items in this library section. """
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. """
self.server.query('/library/sections/%s/emptyTrash' % self.key)
def refresh(self):
""" Refresh the metadata for this library section. This will fetch fresh metadata for
all contents in the section, including items that already have metadata.
"""
self.server.query('/library/sections/%s/refresh' % self.key)
def listChoices(self, category, libtype=None, **kwargs):
""" List choices for the specified filter category. kwargs can be any of the same
kwargs in self.search() to help narrow down the choices to only those that
matter in your current context.
""" Returns a list of :class:`~plexapi.library.FilterChoice` objects for the
specified category and libtype. kwargs can be any of the same kwargs in
:func:`plexapi.library.LibraySection.search()` to help narrow down the choices
to only those that matter in your current context.
Parameters:
category (str): Category to list choices for (genre, contentRating, etc).
libtype (int): Library type of item filter.
**kwargs (dict): Additional kwargs to narrow down the choices.
Raises:
:class:`~plexapi.exceptions.BadRequest`: Cannot include kwarg equal to specified category.
"""
if category in kwargs:
raise BadRequest(
'Cannot include kwarg equal to specified category: %s' % category)
raise BadRequest('Cannot include kwarg equal to specified category: %s' % category)
args = {}
for subcategory, value in kwargs.items():
args[category] = self._cleanSearchFilter(subcategory, value)
if libtype is not None:
args['type'] = utils.searchType(libtype)
query = '/library/sections/%s/%s%s' % (
self.key, category, utils.joinArgs(args))
query = '/library/sections/%s/%s%s' % (self.key, category, utils.joinArgs(args))
return utils.listItems(self.server, query, bytag=True)
def search(self, title=None, sort=None, maxresults=999999, libtype=None, **kwargs):
@ -177,29 +284,29 @@ class LibrarySection(object):
results, it would be wise to set the maxresults option to that amount so this functions
doesn't iterate over all results on the server.
Args:
title (string, optional): General string query to search for.
sort (string): column:dir; column can be any of {addedAt, originallyAvailableAt, lastViewedAt,
titleSort, rating, mediaHeight, duration}. dir can be asc or desc.
maxresults (int): Only return the specified number of results
libtype (string): Filter results to a spcifiec libtype {movie, show, episode, artist, album, track}
kwargs: Any of the available filters for the current library section. Partial string
Parameters:
title (str): General string query to search for (optional).
sort (str): column:dir; column can be any of {addedAt, originallyAvailableAt, lastViewedAt,
titleSort, rating, mediaHeight, duration}. dir can be asc or desc (optional).
maxresults (int): Only return the specified number of results (optional).
libtype (str): Filter results to a spcifiec libtype (movie, show, episode, artist, album, track; optional).
**kwargs (dict): Any of the available filters for the current library section. Partial string
matches allowed. Multiple matches OR together. All inputs will be compared with the
available options and a warning logged if the option does not appear valid.
'unwatched': Display or hide unwatched content (True, False). [all]
'duplicate': Display or hide duplicate items (True, False). [movie]
'actor': List of actors to search ([actor_or_id, ...]). [movie]
'collection': List of collections to search within ([collection_or_id, ...]). [all]
'contentRating': List of content ratings to search within ([rating_or_key, ...]). [movie,tv]
'country': List of countries to search within ([country_or_key, ...]). [movie,music]
'decade': List of decades to search within ([yyy0, ...]). [movie]
'director': List of directors to search ([director_or_id, ...]). [movie]
'genre': List Genres to search within ([genere_or_id, ...]). [all]
'network': List of TV networks to search within ([resolution_or_key, ...]). [tv]
'resolution': List of video resolutions to search within ([resolution_or_key, ...]). [movie]
'studio': List of studios to search within ([studio_or_key, ...]). [music]
'year': List of years to search within ([yyyy, ...]). [all]
* unwatched: Display or hide unwatched content (True, False). [all]
* duplicate: Display or hide duplicate items (True, False). [movie]
* actor: List of actors to search ([actor_or_id, ...]). [movie]
* collection: List of collections to search within ([collection_or_id, ...]). [all]
* contentRating: List of content ratings to search within ([rating_or_key, ...]). [movie,tv]
* country: List of countries to search within ([country_or_key, ...]). [movie,music]
* decade: List of decades to search within ([yyy0, ...]). [movie]
* director: List of directors to search ([director_or_id, ...]). [movie]
* genre: List Genres to search within ([genere_or_id, ...]). [all]
* network: List of TV networks to search within ([resolution_or_key, ...]). [tv]
* resolution: List of video resolutions to search within ([resolution_or_key, ...]). [movie]
* studio: List of studios to search within ([studio_or_key, ...]). [music]
* year: List of years to search within ([yyyy, ...]). [all]
"""
# Cleanup the core arguments
args = {}
@ -211,12 +318,10 @@ class LibrarySection(object):
args['sort'] = self._cleanSearchSort(sort)
if libtype is not None:
args['type'] = utils.searchType(libtype)
# Iterate over the results
results, subresults = [], '_init'
args['X-Plex-Container-Start'] = 0
args['X-Plex-Container-Size'] = min(X_PLEX_CONTAINER_SIZE, maxresults)
while subresults and maxresults > len(results):
query = '/library/sections/%s/all%s' % (
self.key, utils.joinArgs(args))
@ -233,13 +338,11 @@ class LibrarySection(object):
return '1' if value else '0'
if not isinstance(value, (list, tuple)):
value = [value]
# convert list of values to list of keys or ids
result = set()
choices = self.listChoices(category, libtype)
lookup = {c.title.lower(): unquote(unquote(c.key)) for c in choices}
allowed = set(c.key for c in choices)
for item in value:
item = str(item.id if isinstance(item, MediaTag) else item).lower()
# find most logical choice(s) to use in url
@ -254,8 +357,7 @@ class LibrarySection(object):
map(result.add, matches)
continue
# nothing matched; use raw item value
log.warning(
'Filter value not listed, using raw item value: %s' % item)
log.warning('Filter value not listed, using raw item value: %s' % item)
result.add(item)
return ','.join(result)
@ -271,58 +373,108 @@ class LibrarySection(object):
class MovieSection(LibrarySection):
ALLOWED_FILTERS = ('unwatched', 'duplicate', 'year', 'decade', 'genre', 'contentRating', 'collection',
'director', 'actor', 'country', 'studio', 'resolution')
""" 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',
'director', 'actor', 'country', 'studio', 'resolution')
ALLOWED_SORT (list<str>): List of allowed sorting keys. ('addedAt',
'originallyAvailableAt', 'lastViewedAt', 'titleSort', 'rating',
'mediaHeight', 'duration')
TYPE (str): 'movie'
"""
ALLOWED_FILTERS = ('unwatched', 'duplicate', 'year', 'decade', 'genre', 'contentRating',
'collection', 'director', 'actor', 'country', 'studio', 'resolution')
ALLOWED_SORT = ('addedAt', 'originallyAvailableAt', 'lastViewedAt', 'titleSort', 'rating',
'mediaHeight', 'duration')
'mediaHeight', 'duration')
TYPE = 'movie'
class ShowSection(LibrarySection):
ALLOWED_FILTERS = ('unwatched', 'year', 'genre',
'contentRating', 'network', 'collection')
ALLOWED_SORT = ('addedAt', 'lastViewedAt',
'originallyAvailableAt', 'titleSort', 'rating', 'unwatched')
""" 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')
ALLOWED_SORT (list<str>): List of allowed sorting keys. ('addedAt', 'lastViewedAt',
'originallyAvailableAt', 'titleSort', 'rating', 'unwatched')
TYPE (str): 'show'
"""
ALLOWED_FILTERS = ('unwatched', 'year', 'genre', 'contentRating', 'network', 'collection')
ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'originallyAvailableAt', 'titleSort',
'rating', 'unwatched')
TYPE = 'show'
def searchShows(self, **kwargs):
""" Search for a show. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
return self.search(libtype='show', **kwargs)
def searchEpisodes(self, **kwargs):
""" Search for an episode. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
return self.search(libtype='episode', **kwargs)
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).
"""
return self.search(sort='addedAt:desc', libtype=libtype, maxresults=maxresults)
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')
ALLOWED_SORT (list<str>): List of allowed sorting keys. ('addedAt',
'lastViewedAt', 'viewCount', 'titleSort')
TYPE (str): 'artist'
"""
ALLOWED_FILTERS = ('genre', 'country', 'collection')
ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'viewCount', 'titleSort')
TYPE = 'artist'
def albums(self):
""" Returns a list of :class:`~plexapi.audio.Album` objects in this section. """
return utils.listItems(self.server, '/library/sections/%s/albums' % self.key)
def searchArtists(self, **kwargs):
""" Search for an artist. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
return self.search(libtype='artist', **kwargs)
def searchAlbums(self, **kwargs):
""" Search for an album. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
return self.search(libtype='album', **kwargs)
def searchTracks(self, **kwargs):
""" Search for a track. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
return self.search(libtype='track', **kwargs)
class PhotoSection(LibrarySection):
ALLOWED_FILTERS = ()
""" 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 = ('all', 'iso', 'make', 'lens', 'aperture', 'exposure')
ALLOWED_SORT = ()
TYPE = 'photo'
def searchAlbums(self, **kwargs):
return self.search(libtype='photo', **kwargs)
def searchAlbums(self, title, **kwargs): # lets use this for now.
""" Search for an album. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
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):
return self.search(libtype='photo', **kwargs)
def searchPhotos(self, title, **kwargs):
""" Search for a photo. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
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
@ -363,6 +515,20 @@ class Hub(object):
@utils.register_libtype
class FilterChoice(object):
""" Represents a single filter choice. These objects are gathered when using filters
while searching for library items and is the object returned in the result set of
:func:`~plexapi.library.LibrarySection.listChoices()`.
Attributes:
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to.
initpath (str): Relative path requested when retrieving specified `data` (optional).
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.
title (str): Human readable name for this filter option.
type (str): Filter type (genre, contentRating, etc).
"""
TYPE = 'Directory'
def __init__(self, server, data, initpath):

View file

@ -1,7 +1,4 @@
# -*- coding: utf-8 -*-
"""
PlexAPI Media
"""
from plexapi.utils import cast
@ -177,15 +174,49 @@ class MediaTag(object):
return '<%s:%s:%s>' % (self.__class__.__name__, self.id, tag)
class Collection(MediaTag): TYPE = 'Collection'; FILTER = 'collection'
class Country(MediaTag): TYPE = 'Country'; FILTER = 'country'
class Director(MediaTag): TYPE = 'Director'; FILTER = 'director'
class Genre(MediaTag): TYPE = 'Genre'; FILTER = 'genre'
class Mood(MediaTag): TYPE = 'Mood'; FILTER = 'mood'
class Producer(MediaTag): TYPE = 'Producer'; FILTER = 'producer'
class Role(MediaTag): TYPE = 'Role'; FILTER = 'role'
class Similar(MediaTag): TYPE = 'Similar'; FILTER = 'similar'
class Writer(MediaTag): TYPE = 'Writer'; FILTER = 'writer'
class Collection(MediaTag):
TYPE = 'Collection'
FILTER = 'collection'
class Country(MediaTag):
TYPE = 'Country'
FILTER = 'country'
class Director(MediaTag):
TYPE = 'Director'
FILTER = 'director'
class Genre(MediaTag):
TYPE = 'Genre'
FILTER = 'genre'
class Mood(MediaTag):
TYPE = 'Mood'
FILTER = 'mood'
class Producer(MediaTag):
TYPE = 'Producer'
FILTER = 'producer'
class Role(MediaTag):
TYPE = 'Role'
FILTER = 'role'
class Similar(MediaTag):
TYPE = 'Similar'
FILTER = 'similar'
class Writer(MediaTag):
TYPE = 'Writer'
FILTER = 'writer'
class Field(object):

View file

@ -1,69 +1,56 @@
# -*- coding: utf-8 -*-
import sys
if sys.version_info <= (3, 3):
try:
from xml.etree import cElementTree as ElementTree
except ImportError:
from xml.etree import ElementTree
else:
# py 3.3 and above selects the fastest automatically
from xml.etree import ElementTree
from requests.status_codes import _codes as codes
import plexapi
import requests
from plexapi import TIMEOUT, log, utils
import plexapi, requests
from plexapi import TIMEOUT, log, logfilter, utils
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
from plexapi.client import PlexClient
from plexapi.compat import ElementTree
from plexapi.server import PlexServer
from requests.status_codes import _codes as codes
CONFIG = plexapi.CONFIG
class MyPlexAccount(object):
"""Your personal MyPlex account and profile information
""" MyPlex account and profile information. The easiest way to build
this object is by calling the staticmethod :func:`~plexapi.myplex.MyPlexAccount.signin`
with your username and password. This object represents the data found Account on
the myplex.tv servers at the url https://plex.tv/users/account.
Attributes:
authenticationToken (TYPE): Description
BASEURL (str): Description
certificateVersion (TYPE): Description
cloudSyncDevice (TYPE): Description
email (TYPE): Description
entitlements (TYPE): Description
guest (TYPE): Description
home (TYPE): Description
homeSize (TYPE): Description
id (TYPE): Description
locale (TYPE): Description
mailing_list_status (TYPE): Description
maxHomeSize (TYPE): Description
queueEmail (TYPE): Description
queueUid (TYPE): Description
restricted (TYPE): Description
roles (TYPE): Description
scrobbleTypes (TYPE): Description
secure (TYPE): Description
SIGNIN (str): Description
subscriptionActive (TYPE): Description
subscriptionFeatures (TYPE): Description
subscriptionPlan (TYPE): Description
subscriptionStatus (TYPE): Description
thumb (TYPE): Description
title (TYPE): Description
username (TYPE): Description
uuid (TYPE): Description
Attributes:
authenticationToken (str): <Unknown>
certificateVersion (str): <Unknown>
cloudSyncDevice (str):
email (str): Your current Plex email address.
entitlements (List<str>): List of devices your allowed to use with this account.
guest (bool): <Unknown>
home (bool): <Unknown>
homeSize (int): <Unknown>
id (str): Your Plex account ID.
locale (str): Your Plex locale
mailing_list_status (str): Your current mailing list status.
maxHomeSize (int): <Unknown>
queueEmail (str): Email address to add items to your `Watch Later` queue.
queueUid (str): <Unknown>
restricted (bool): <Unknown>
roles: (List<str>) Lit of account roles. Plexpass membership listed here.
scrobbleTypes (str): Description
secure (bool): Description
subscriptionActive (bool): True if your subsctiption is active.
subscriptionFeatures: (List<str>) List of features allowed on your subscription.
subscriptionPlan (str): Name of subscription plan.
subscriptionStatus (str): String representation of `subscriptionActive`.
thumb (str): URL of your account thumbnail.
title (str): <Unknown> - Looks like an alias for `username`.
username (str): Your account username.
uuid (str): <Unknown>
"""
BASEURL = 'https://plex.tv/users/account'
SIGNIN = 'https://my.plexapp.com/users/sign_in.xml'
def __init__(self, data, initpath=None):
"""Sets the attrs.
Args:
data (Element): XML response from PMS as a Element
initpath (string, optional): relative path.
"""
def __init__(self, data=None, initpath=None, session=None):
self._session = session or requests.Session()
self.authenticationToken = data.attrib.get('authenticationToken')
if self.authenticationToken:
logfilter.add_secret(self.authenticationToken)
self.certificateVersion = data.attrib.get('certificateVersion')
self.cloudSyncDevice = data.attrib.get('cloudSyncDevice')
self.email = data.attrib.get('email')
@ -83,143 +70,114 @@ class MyPlexAccount(object):
self.title = data.attrib.get('title')
self.username = data.attrib.get('username')
self.uuid = data.attrib.get('uuid')
# TODO: Complete these items!
self.subscriptionActive = None # renamed on server
self.subscriptionStatus = None # renamed on server
self.subscriptionPlan = None # renmaed on server
self.subscriptionFeatures = None # renamed on server
# TODO: Fetch missing MyPlexAccount attributes
self.subscriptionActive = None # renamed on server
self.subscriptionStatus = None # renamed on server
self.subscriptionPlan = None # renmaed on server
self.subscriptionFeatures = None # renamed on server
self.roles = None
self.entitlements = None
def __repr__(self):
"""Pretty print."""
return '<%s:%s:%s>' % (self.__class__.__name__, self.id, self.username.encode('utf8'))
def devices(self):
"""Return a all devices connected to the plex account.
Returns:
list: of MyPlexDevice
"""
return _listItems(MyPlexDevice.BASEURL, self.authenticationToken, MyPlexDevice)
def device(self, name):
"""Return a device wth a matching name.
""" Returns the :class:`~plexapi.myplex.MyPlexDevice` that matches the name specified.
Args:
name (str): Name to match against.
Returns:
class: MyPlexDevice
Parameters:
name (str): Name to match against.
"""
return _findItem(self.devices(), name)
def resources(self):
"""Resources.
def devices(self):
""" Returns a list of all :class:`~plexapi.myplex.MyPlexDevice` objects connected to the server. """
return _listItems(MyPlexDevice.BASEURL, self.authenticationToken, MyPlexDevice)
Returns:
List: of MyPlexResource
"""
def resources(self):
""" Returns a list of all :class:`~plexapi.myplex.MyPlexResource` objects connected to the server. """
return _listItems(MyPlexResource.BASEURL, self.authenticationToken, MyPlexResource)
def resource(self, name):
"""Find resource ny name.
""" Returns the :class:`~plexapi.myplex.MyPlexResource` that matches the name specified.
Args:
name (str): to find
Returns:
class: MyPlexResource
Parameters:
name (str): Name to match against.
"""
return _findItem(self.resources(), name)
def users(self):
"""List of users.
Returns:
List: of MyPlexuser
"""
""" Returns a list of all :class:`~plexapi.myplex.MyPlexUser` objects connected to your account. """
return _listItems(MyPlexUser.BASEURL, self.authenticationToken, MyPlexUser)
def user(self, email):
"""Find a user by email.
""" Returns the :class:`~myplex.MyPlexUser` that matches the email or username specified.
Args:
email (str): Username to match against.
Returns:
class: User
Parameters:
email (str): Username or email to match against.
"""
return _findItem(self.users(), email, ['username', 'email'])
@classmethod
def signin(cls, username, password):
"""Summary
def signin(cls, username=None, password=None, 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.
Args:
username (str): username
password (str): password
Parameters:
username (str): Your MyPlex.tv username. If not specified, it will check the config.ini file.
password (str): Your MyPlex.tv password. If not specified, it will check the config.ini file.
Returns:
class: MyPlexAccount
Raises:
BadRequest: (HTTPCODE) http codename
Unauthorized: (HTTPCODE) http codename
Raises:
:class:`~plexapi.exceptions.Unauthorized`: (401) If the username or password are invalid.
:class:`~plexapi.exceptions.BadRequest`: If any other errors occured not allowing us to log into MyPlex.tv.
"""
if 'X-Plex-Token' in plexapi.BASE_HEADERS:
del plexapi.BASE_HEADERS['X-Plex-Token']
username = username or CONFIG.get('authentication.username')
password = password or CONFIG.get('authentication.password')
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]
if response.status_code == 401:
raise Unauthorized('(%s) %s' %
(response.status_code, codename))
raise Unauthorized('(%s) %s' % (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)
# Not to be confused with the MyPlexAccount, this represents
# non-signed in users such as friends and linked accounts.
class MyPlexUser(object):
"""Class to other users.
""" This object represents non-signed in users such as friends and linked
accounts. NOTE: This should not be confused with the :class:`~myplex.MyPlexAccount`
which is your specific account. The raw xml for the data presented here
can be found at: https://plex.tv/api/users/
Attributes:
allowCameraUpload (bool): True if this user can upload images
allowChannels (bool): True if this user has access to channels
allowSync (bool): True if this user can sync
BASEURL (str): Description
email (str): user@gmail.com
filterAll (str): Description
filterMovies (str): Description
filterMusic (str): Description
filterPhotos (str): Description
filterTelevision (str): Description
home (bool):
id (int): 1337
protected (False): Is this if ssl? check it
recommendationsPlaylistId (str): Description
restricted (str): fx 0
thumb (str): Link to the users avatar
title (str): Hellowlol
username (str): Hellowlol
Attributes:
allowCameraUpload (bool): True if this user can upload images
allowChannels (bool): True if this user has access to channels
allowSync (bool): True if this user can sync
email (str): User's email address (user@gmail.com)
filterAll (str): Unknown
filterMovies (str): Unknown
filterMusic (str): Unknown
filterPhotos (str): Unknown
filterTelevision (str): Unknown
home (bool): Unknown
id (int): User's Plex account ID.
protected (False): Unknown (possibly SSL enabled?)
recommendationsPlaylistId (str): Unknown
restricted (str): Unknown
thumb (str): Link to the users avatar
title (str): Seems to be an aliad for username
username (str): User's username
"""
BASEURL = 'https://plex.tv/api/users/'
def __init__(self, data, initpath=None):
"""Summary
Args:
data (Element): XML repsonse as Element
initpath (None, optional): Relative url str
"""
self.allowCameraUpload = utils.cast(
bool, data.attrib.get('allowCameraUpload'))
self.allowCameraUpload = utils.cast(bool, data.attrib.get('allowCameraUpload'))
self.allowChannels = utils.cast(bool, data.attrib.get('allowChannels'))
self.allowSync = utils.cast(bool, data.attrib.get('allowSync'))
self.email = data.attrib.get('email')
@ -231,50 +189,48 @@ class MyPlexUser(object):
self.home = utils.cast(bool, data.attrib.get('home'))
self.id = utils.cast(int, data.attrib.get('id'))
self.protected = utils.cast(bool, data.attrib.get('protected'))
self.recommendationsPlaylistId = data.attrib.get(
'recommendationsPlaylistId')
self.recommendationsPlaylistId = data.attrib.get('recommendationsPlaylistId')
self.restricted = data.attrib.get('restricted')
self.thumb = data.attrib.get('thumb')
self.title = data.attrib.get('title')
self.username = data.attrib.get('username')
def __repr__(self):
"""Pretty repr."""
return '<%s:%s:%s>' % (self.__class__.__name__, self.id, self.username)
class MyPlexResource(object):
"""Summary
""" This object represents resources connected to your Plex server that can provide
content such as Plex Media Servers, iPhone or Android clients, etc. The raw xml
for the data presented here can be found at: https://plex.tv/api/resources?includeHttps=1
Attributes:
accessToken (str): This resource accesstoken.
BASEURL (TYPE): Description
clientIdentifier (str): 1f2fe128794fd...
connections (list): of ResourceConnection
createdAt (datetime): Description
device (str): pc
home (None): Dunno wtf this can me
lastSeenAt (datetime): Description
name (str): Pretty name fx S-PC
owned (bool): True if this is your own.
platform (str): Windows
platformVersion (str): fx. 6.1 (Build 7601)
presence (bool): True if online
product (str): Plex Media Server
productVersion (str): 1.3.3.3148-b38628e
provides (str): fx server
synced (bool): Description
Attributes:
accessToken (str): This resources accesstoken.
clientIdentifier (str): Unique ID for this resource.
connections (list): List of :class:`~myplex.ResourceConnection` objects
for this resource.
createdAt (datetime): Timestamp this resource first connected to your server.
device (str): Best guess on the type of device this is (PS, iPhone, Linux, etc).
home (bool): Unknown
lastSeenAt (datetime): Timestamp this resource last connected.
name (str): Descriptive name of this resource.
owned (bool): True if this resource is one of your own (you logged into it).
platform (str): OS the resource is running (Linux, Windows, Chrome, etc.)
platformVersion (str): Version of the platform.
presence (bool): True if the resource is online
product (str): Plex product (Plex Media Server, Plex for iOS, Plex Web, etc.)
productVersion (str): Version of the product.
provides (str): List of services this resource provides (client, server,
player, pubsub-player, etc.)
synced (bool): Unknown (possibly True if the resource has synced content?)
"""
BASEURL = 'https://plex.tv/api/resources?includeHttps=1'
def __init__(self, data):
"""Summary
Args:
data (Element): XML response as Element
"""
self.name = data.attrib.get('name')
self.accessToken = data.attrib.get('accessToken')
if self.accessToken:
logfilter.add_secret(self.accessToken)
self.product = data.attrib.get('product')
self.productVersion = data.attrib.get('productVersion')
self.platform = data.attrib.get('platform')
@ -288,34 +244,36 @@ class MyPlexResource(object):
self.home = utils.cast(bool, data.attrib.get('home'))
self.synced = utils.cast(bool, data.attrib.get('synced'))
self.presence = utils.cast(bool, data.attrib.get('presence'))
self.connections = [ResourceConnection(
elem) for elem in data if elem.tag == 'Connection']
self.connections = [ResourceConnection(elem) for elem in data if elem.tag == 'Connection']
def __repr__(self):
"""Pretty repr."""
return '<%s:%s>' % (self.__class__.__name__, self.name.encode('utf8'))
def connect(self, ssl=None):
"""Connect.
""" Returns a new :class:`~server.PlexServer` object. Often times there is more than
one address specified for a server or client. This function will prioritize local
connections before remote and HTTPS before HTTP. After trying to connect to all
available addresses for this resource and assuming at least one connection was
successful, the PlexServer object is built and returned.
Args:
ssl (None, optional): Use ssl.
Parameters:
ssl (optional): Set True to only connect to HTTPS connections. Set False to
only connect to HTTP connections. Set None (default) to connect to any
HTTP or HTTPS connection.
Returns:
class: Plexserver
Raises:
NotFound: Unable to connect to resource: name
Raises:
:class:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this resource.
"""
# Sort connections from (https, local) to (http, remote)
# Only check non-local connections unless we own the resource
forcelocal = lambda c: self.owned or c.local
connections = sorted(
self.connections, key=lambda c: c.local, reverse=True)
connections = sorted(self.connections, key=lambda c: c.local, reverse=True)
https = [c.uri for c in self.connections if forcelocal(c)]
http = [c.httpuri for c in self.connections if forcelocal(c)]
connections = https + http
# Force ssl, no ssl, or any (default)
if ssl is True: connections = https
elif ssl is False: connections = http
else: connections = https + http
# Try connecting to all known resource connections in parellel, but
# only return the first server (in order) that provides a response.
listargs = [[c] for c in connections]
@ -325,52 +283,33 @@ class MyPlexResource(object):
# established.
for url, token, result in results:
okerr = 'OK' if result else 'ERR'
log.info(
'Testing resource connection: %s?X-Plex-Token=%s %s', url, token, okerr)
results = [r[2] for r in results if r and r is not None]
log.info('Testing resource connection: %s?X-Plex-Token=%s %s', url, token, okerr)
results = [r[2] for r in results if r and r[2] is not None]
if not results:
raise NotFound('Unable to connect to resource: %s' % self.name)
log.info('Connecting to server: %s?X-Plex-Token=%s',
results[0].baseurl, results[0].token)
log.info('Connecting to server: %s?X-Plex-Token=%s', results[0].baseurl, results[0].token)
return results[0]
def _connect(self, url, results, i):
"""Connect.
Args:
url (str): url to the resource
results (TYPE): Description
i (TYPE): Description
Returns:
TYPE: Description
"""
try:
results[i] = (url, self.accessToken,
PlexServer(url, self.accessToken))
results[i] = (url, self.accessToken, PlexServer(url, self.accessToken))
except NotFound:
results[i] = (url, self.accessToken, None)
class ResourceConnection(object):
"""ResourceConnection.
""" Represents a Resource Connection object found within the
:class:`~myplex.MyPlexResource` objects.
Attributes:
address (str): Local ip adress
httpuri (str): Full local address
local (bool): True if local
port (int): 32400
protocol (str): http or https
uri (str): External adress
Attributes:
address (str): Local IP address
httpuri (str): Full local address
local (bool): True if local
port (int): 32400
protocol (str): HTTP or HTTPS
uri (str): External address
"""
def __init__(self, data):
"""Set attrs.
Args:
data (Element): XML response as Element from PMS.
"""
self.protocol = data.attrib.get('protocol')
self.address = data.attrib.get('address')
self.port = utils.cast(int, data.attrib.get('port'))
@ -379,42 +318,38 @@ class ResourceConnection(object):
self.httpuri = 'http://%s:%s' % (self.address, self.port)
def __repr__(self):
"""Pretty repr."""
return '<%s:%s>' % (self.__class__.__name__, self.uri.encode('utf8'))
class MyPlexDevice(object):
"""Device connected.
""" This object represents resources connected to your Plex server that provide
playback ability from your Plex Server, iPhone or Android clients, Plex Web,
this API, etc. The raw xml for the data presented here can be found at:
https://plex.tv/devices.xml
Attributes:
BASEURL (str): Plex.tv XML device url
clientIdentifier (str): 0x685d43d...
connections (list):
device (str): fx Windows
id (str): 123
model (str):
name (str): fx Computername
platform (str): Windows
platformVersion (str): Fx 8
product (str): Fx PlexAPI
productVersion (string): 2.0.2
provides (str): fx controller
publicAddress (str): Public ip address
screenDensity (str): Description
screenResolution (str): Description
token (str): Auth token
vendor (str): Description
version (str): fx 2.0.2
Attributes:
clientIdentifier (str): Unique ID for this resource.
connections (list): List of connection URIs for the device.
device (str): Best guess on the type of device this is (Linux, iPad, AFTB, etc).
id (str): MyPlex ID of the device.
model (str): Model of the device (bueller, Linux, x86_64, etc.)
name (str): Hostname of the device.
platform (str): OS the resource is running (Linux, Windows, Chrome, etc.)
platformVersion (str): Version of the platform.
product (str): Plex product (Plex Media Server, Plex for iOS, Plex Web, etc.)
productVersion (string): Version of the product.
provides (str): List of services this resource provides (client, controller,
sync-target, player, pubsub-player).
publicAddress (str): Public IP address.
screenDensity (str): Unknown
screenResolution (str): Screen resolution (750x1334, 1242x2208, etc.)
token (str): Plex authentication token for the device.
vendor (str): Device vendor (ubuntu, etc).
version (str): Unknown (1, 2, 1.3.3.3148-b38628e, 1.3.15, etc.)
"""
BASEURL = 'https://plex.tv/devices.xml'
def __init__(self, data):
"""Set attrs
Args:
data (Element): XML response as Element from PMS
"""
self.name = data.attrib.get('name')
self.publicAddress = data.attrib.get('publicAddress')
self.product = data.attrib.get('product')
@ -429,26 +364,23 @@ class MyPlexDevice(object):
self.version = data.attrib.get('version')
self.id = data.attrib.get('id')
self.token = data.attrib.get('token')
if self.token:
logfilter.add_secret(self.token)
self.screenResolution = data.attrib.get('screenResolution')
self.screenDensity = data.attrib.get('screenDensity')
self.connections = [connection.attrib.get(
'uri') for connection in data.iter('Connection')]
self.connections = [connection.attrib.get('uri') for connection in data.iter('Connection')]
def __repr__(self):
"""Pretty repr."""
return '<%s:%s:%s>' % (self.__class__.__name__, self.name.encode('utf8'), self.product.encode('utf8'))
def connect(self, ssl=None):
"""Connect to the first server.
def connect(self):
""" Returns a new :class:`~plexapi.client.PlexClient` object. Sometimes there is more than
one address specified for a server or client. After trying to connect to all
available addresses for this resource and assuming at least one connection was
successful, the PlexClient object is built and returned.
Args:
ssl (None, optional): Use SSL?
Returns:
TYPE: Plexserver
Raises:
NotFound: Unable to connect to resource: name
Raises:
:class:`~plexapi.exceptions.NotFound`: When unable to connect to any addresses for this device.
"""
# Try connecting to all known resource connections in parellel, but
# only return the first server (in order) that provides a response.
@ -459,46 +391,23 @@ class MyPlexDevice(object):
# established.
for url, token, result in results:
okerr = 'OK' if result else 'ERR'
log.info('Testing device connection: %s?X-Plex-Token=%s %s',
url, token, okerr)
log.info('Testing device connection: %s?X-Plex-Token=%s %s', url, token, okerr)
results = [r[2] for r in results if r and r[2] is not None]
if not results:
raise NotFound('Unable to connect to resource: %s' % self.name)
log.info('Connecting to server: %s?X-Plex-Token=%s',
results[0].baseurl, results[0].token)
log.info('Connecting to server: %s?X-Plex-Token=%s', results[0].baseurl, results[0].token)
return results[0]
def _connect(self, url, results, i):
"""Summary
Args:
url (TYPE): Description
results (TYPE): Description
i (TYPE): Description
Returns:
TYPE: Description
"""
try:
results[i] = (url, self.token, PlexClient(url, self.token))
except NotFound as err:
except NotFound:
results[i] = (url, self.token, None)
def _findItem(items, value, attrs=None):
"""Simple helper to find something using attrs
Args:
items (cls): list of Object to get the attrs from
value (str): value to match against
attrs (None, optional): attr to match against value.
Returns:
TYPE: Description
Raises:
NotFound: Description
""" This will return the first item in the list of items where value is
found in any of the specified attributes.
"""
attrs = attrs or ['name']
for item in items:
@ -509,16 +418,7 @@ def _findItem(items, value, attrs=None):
def _listItems(url, token, cls):
"""Helper that builds list of classes from a XML response.
Args:
url (str): Description
token (str): Description
cls (class): Class to initate
Returns:
List: of classes
"""
""" Builds list of classes from a XML response. """
headers = plexapi.BASE_HEADERS
headers['X-Plex-Token'] = token
log.info('GET %s?X-Plex-Token=%s', url, token)

View file

@ -1,10 +1,4 @@
# -*- coding: utf-8 -*-
"""
PlexPhoto
Attributes:
NA (TYPE): Description
"""
from plexapi import media, utils
from plexapi.utils import PlexPartialObject
NA = utils.NA
@ -12,46 +6,36 @@ NA = utils.NA
@utils.register_libtype
class Photoalbum(PlexPartialObject):
"""Summary
""" Represents a photoalbum (collection of photos).
Attributes:
addedAt (TYPE): Description
art (TYPE): Description
composite (TYPE): Description
guid (TYPE): Description
index (TYPE): Description
key (TYPE): Description
librarySectionID (TYPE): Description
listType (str): Description
ratingKey (TYPE): Description
summary (TYPE): Description
thumb (TYPE): Description
title (TYPE): Description
TYPE (str): Description
type (TYPE): Description
updatedAt (TYPE): Description
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional)
data (ElementTree): Response from PlexServer used to build this object (optional).
initpath (str): Relative path requested when retrieving specified `data` (optional).
Attributes:
addedAt (datetime): Datetime this item was added to the library.
art (str): Photo art (/library/metadata/<ratingkey>/art/<artid>)
composite (str): Unknown
guid (str): Unknown (unique ID)
index (sting): Index number of this album.
key (str): API URL (/library/metadata/<ratingkey>).
librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID.
listType (str): Hardcoded as 'photo' (useful for search filters).
ratingKey (int): Unique key identifying this item.
summary (str): Summary of the photoalbum.
thumb (str): URL to thumbnail image.
title (str): Photoalbum title. (Trip to Disney World)
type (str): Unknown
updatedAt (datatime): Datetime this item was updated.
"""
TYPE = 'photoalbum'
def __init__(self, server, data, initpath):
"""Summary
Args:
server (TYPE): Description
data (TYPE): Description
initpath (TYPE): Description
"""
super(Photoalbum, self).__init__(data, initpath, server)
def _loadData(self, data):
"""Summary
Args:
data (TYPE): Description
Returns:
TYPE: Description
"""
""" Load attribute values from Plex XML response. """
self.listType = 'photo'
self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA))
self.art = data.attrib.get('art', NA)
@ -68,78 +52,53 @@ class Photoalbum(PlexPartialObject):
self.updatedAt = utils.toDatetime(data.attrib.get('updatedAt', NA))
def photos(self):
"""Summary
Returns:
TYPE: Description
"""
""" Returns a list of :class:`~plexapi.photo.Photo` objects in this album. """
path = '/library/metadata/%s/children' % self.ratingKey
return utils.listItems(self.server, path, Photo.TYPE)
def photo(self, title):
"""Summary
Args:
title (TYPE): Description
Returns:
TYPE: Description
"""
""" Returns the :class:`~plexapi.photo.Photo` that matches the specified title. """
path = '/library/metadata/%s/children' % self.ratingKey
return utils.findItem(self.server, path, title)
def section(self):
"""Summary
Returns:
TYPE: Description
"""
""" Returns the :class:`~plexapi.library.LibrarySection` this item belongs to. """
return self.server.library.sectionByID(self.librarySectionID)
@utils.register_libtype
class Photo(PlexPartialObject):
"""Summary
""" Represents a single photo.
Attributes:
addedAt (TYPE): Description
index (TYPE): Description
key (TYPE): Description
listType (str): Description
media (TYPE): Description
originallyAvailableAt (TYPE): Description
parentKey (TYPE): Description
parentRatingKey (TYPE): Description
ratingKey (TYPE): Description
summary (TYPE): Description
thumb (TYPE): Description
title (TYPE): Description
TYPE (str): Description
type (TYPE): Description
updatedAt (TYPE): Description
year (TYPE): Description
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional)
data (ElementTree): Response from PlexServer used to build this object (optional).
initpath (str): Relative path requested when retrieving specified `data` (optional).
Attributes:
addedAt (datetime): Datetime this item was added to the library.
index (sting): Index number of this photo.
key (str): API URL (/library/metadata/<ratingkey>).
listType (str): Hardcoded as 'photo' (useful for search filters).
media (TYPE): Unknown
originallyAvailableAt (datetime): Datetime this photo was added to Plex.
parentKey (str): Photoalbum API URL.
parentRatingKey (int): Unique key identifying the photoalbum.
ratingKey (int): Unique key identifying this item.
summary (str): Summary of the photo.
thumb (str): URL to thumbnail image.
title (str): Photo title.
type (str): Unknown
updatedAt (datatime): Datetime this item was updated.
year (int): Year this photo was taken.
"""
TYPE = 'photo'
def __init__(self, server, data, initpath):
"""Summary
Args:
server (TYPE): Description
data (TYPE): Description
initpath (TYPE): Description
"""
super(Photo, self).__init__(data, initpath, server)
def _loadData(self, data):
"""Summary
Args:
data (TYPE): Description
Returns:
TYPE: Description
"""
""" Load attribute values from Plex XML response. """
self.listType = 'photo'
self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA))
self.index = utils.cast(int, data.attrib.get('index', NA))
@ -157,20 +116,12 @@ class Photo(PlexPartialObject):
self.year = utils.cast(int, data.attrib.get('year', NA))
if self.isFullObject():
self.media = [media.Media(self.server, e, self.initpath, self)
for e in data if e.tag == media.Media.TYPE]
for e in data if e.tag == media.Media.TYPE]
def photoalbum(self):
"""Summary
Returns:
TYPE: Description
"""
""" Return this photo's :class:`~plexapi.photo.Photoalbum`. """
return utils.listItems(self.server, self.parentKey)[0]
def section(self):
"""Summary
Returns:
TYPE: Description
"""
""" Returns the :class:`~plexapi.library.LibrarySection` this item belongs to. """
return self.server.library.sectionByID(self.photoalbum().librarySectionID)

View file

@ -1,7 +1,4 @@
# -*- coding: utf-8 -*-
"""
PlexPlaylist
"""
from plexapi import utils
from plexapi.exceptions import BadRequest
from plexapi.utils import cast, toDatetime
@ -60,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

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
import plexapi
import requests
from plexapi import utils
@ -36,10 +34,8 @@ class PlayQueue(object):
self.mediaTagPrefix = data.attrib.get('mediaTagPrefix')
self.mediaTagVersion = data.attrib.get('mediaTagVersion')
self.playQueueID = data.attrib.get('playQueueID')
self.playQueueSelectedItemID = data.attrib.get(
'playQueueSelectedItemID')
self.playQueueSelectedItemOffset = data.attrib.get(
'playQueueSelectedItemOffset')
self.playQueueSelectedItemID = data.attrib.get('playQueueSelectedItemID')
self.playQueueSelectedItemOffset = data.attrib.get('playQueueSelectedItemOffset')
self.playQueueTotalCount = data.attrib.get('playQueueTotalCount')
self.playQueueVersion = data.attrib.get('playQueueVersion')
self.items = [utils.buildItem(server, elem, initpath) for elem in data]

View file

@ -13,8 +13,8 @@ else:
import requests
from requests.status_codes import _codes as codes
from plexapi import BASE_HEADERS, TIMEOUT
from plexapi import log, utils
from plexapi import BASE_HEADERS, CONFIG, TIMEOUT
from plexapi import log, logfilter, utils
from plexapi import audio, video, photo, playlist # noqa; required # why is this needed?
from plexapi.client import PlexClient
from plexapi.compat import quote, urlencode
@ -62,8 +62,10 @@ class PlexServer(object):
session (requests.Session, optional): Use your own session object if you want
to cache the http responses from PMS
"""
self.baseurl = baseurl
self.token = token
self.baseurl = baseurl or CONFIG.get('authentication.baseurl')
self.token = token or CONFIG.get('authentication.token')
if self.token:
logfilter.add_secret(self.token)
self.session = session or requests.Session()
data = self._connect()
self.friendlyName = data.attrib.get('friendlyName')
@ -113,8 +115,7 @@ class PlexServer(object):
"""
items = []
for elem in self.query('/clients'):
baseurl = 'http://%s:%s' % (elem.attrib['address'],
elem.attrib['port'])
baseurl = 'http://%s:%s' % (elem.attrib['host'], elem.attrib['port'])
items.append(PlexClient(baseurl, server=self, data=elem))
return items
@ -133,8 +134,7 @@ class PlexServer(object):
"""
for elem in self.query('/clients'):
if elem.attrib.get('name').lower() == name.lower():
baseurl = 'http://%s:%s' % (
elem.attrib['address'], elem.attrib['port'])
baseurl = 'http://%s:%s' % (elem.attrib['host'], elem.attrib['port'])
return PlexClient(baseurl, server=self, data=elem)
raise NotFound('Unknown client name: %s' % name)
@ -204,9 +204,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,5 +1,4 @@
# -*- coding: utf-8 -*-
import requests
from plexapi import utils
from plexapi.exceptions import NotFound

View file

@ -1,192 +1,166 @@
# -*- coding: utf-8 -*-
import re
import logging, os, re, requests
from datetime import datetime
from plexapi.compat import quote, urlencode
from plexapi.exceptions import NotFound, UnknownType, Unsupported
from threading import Thread
from plexapi import log
from plexapi.compat import quote, string_type, urlencode
from plexapi.exceptions import NotFound, NotImplementedError, UnknownType, Unsupported
# 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 = {}
def register_libtype(cls):
"""Registry of library types we may come across when parsing XML.
This allows us to define a few helper functions to dynamically convery
the XML into objects. See buildItem() below for an example.
""" Registry of library types we may come across when parsing XML. This allows us to
define a few helper functions to dynamically convery the XML into objects. See
buildItem() below for an example.
"""
LIBRARY_TYPES[cls.TYPE] = cls
return cls
class _NA(object):
"""This used to be a simple variable equal to '__NA__'.
However, 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 values.
(NA == None results to True; NA is None results to False)
""" 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
values. (NA == None results to True; NA is None results to False)
"""
def __bool__(self):
"""Make sure Na always is False.
Returns:
bool: False
"""
return False
def __eq__(self, other):
"""Check eq.
Args:
other (str): Description
Returns:
bool: True is equal
"""
return isinstance(other, _NA) or other in [None, '__NA__']
def __nonzero__(self):
return False
def __repr__(self):
"""Pretty print."""
return '__NA__'
NA = _NA()
NA = _NA() # Keep this for now.
class SecretsFilter(logging.Filter):
""" Logging filter to hide secrets. """
def __init__(self, secrets=None):
self.secrets = secrets or set()
def add_secret(self, secret):
self.secrets.add(secret)
def filter(self, record):
cleanargs = list(record.args)
for i in range(len(cleanargs)):
if isinstance(cleanargs[i], string_type):
for secret in self.secrets:
cleanargs[i] = cleanargs[i].replace(secret, '<hidden>')
record.args = tuple(cleanargs)
return True
class PlexPartialObject(object):
"""Not all objects in the Plex listings return the complete list of elements
for the object.This object will allow you to assume each object is complete,
and if the specified value you request is None it will fetch the full object
automatically and update itself.
""" Not all objects in the Plex listings return the complete list of elements
for the object. This object will allow you to assume each object is complete,
and if the specified value you request is None it will fetch the full object
automatically and update itself.
Attributes:
initpath (str): Relative url to PMS
server (): Description
Attributes:
data (ElementTree): Response from PlexServer used to build this object (optional).
initpath (str): Relative path requested when retrieving specified `data` (optional).
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
"""
def __init__(self, data, initpath, server=None):
"""
Args:
data (xml.etree.ElementTree.Element): passed from server.query
initpath (str): Relative path
server (None or Plexserver, optional): PMS class your connected to
"""
self.server = server
self.initpath = initpath
self._loadData(data)
self._reloaded = False
def __eq__(self, other):
"""Summary
Args:
other (TYPE): Description
Returns:
TYPE: Description
"""
return other is not None and self.key == other.key
def __repr__(self):
"""Pretty repr."""
clsname = self.__class__.__name__
key = self.key.replace('/library/metadata/', '') if self.key else 'NA'
title = self.title.replace(' ', '.')[0:20].encode('utf8')
return '<%s:%s:%s>' % (clsname, key, title)
def __getattr__(self, attr):
"""Auto reload self, if the attribute is NA
Args:
attr (str): fx key
"""
# 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)
def __setattr__(self, attr, value):
"""Set attribute
Args:
attr (str): fx key
value (TYPE): Description
"""
if value != NA or self.isFullObject():
self.__dict__[attr] = value
def _loadData(self, data):
"""Uses a element to set a attrs.
Args:
data (Element): Used by attrs
"""
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):
""" Returns True if this is NOT a full object. """
return not self.isFullObject()
def reload(self):
"""Reload the data for this object from PlexServer XML."""
""" Reload the data for this object from PlexServer XML. """
data = self.server.query(self.key)
self.initpath = self.key
self._loadData(data[0])
self._reloaded = True
return self
class Playable(object):
"""This is a general place to store functions specific to media that is Playable.
Things were getting mixed up a bit when dealing with Shows, Season,
Artists, Albums which are all not playable.
""" This is a general place to store functions specific to media that is Playable.
Things were getting mixed up a bit when dealing with Shows, Season, Artists,
Albums which are all not playable.
Attributes: # todo
player (Plexclient): Player
playlistItemID (int): Playlist item id
sessionKey (int): 1223
transcodeSession (str): 12312312
username (str): Fx Hellowlol
viewedAt (datetime): viewed at.
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).
sessionKey (int): Active session key.
transcodeSession (:class:`~plexapi.media.TranscodeSession`): Transcode Session object
if item is being transcoded (None otherwise).
username (str): Username of the person playing this item (for active sessions).
viewedAt (datetime): Datetime item was last viewed (history).
"""
def _loadData(self, data):
"""Set the class attributes
Args:
data (xml.etree.ElementTree.Element): usually from server.query
"""
# data for active sessions (/status/sessions)
# Load data for active sessions (/status/sessions)
self.sessionKey = cast(int, data.attrib.get('sessionKey', NA))
self.username = findUsername(data)
self.player = findPlayer(self.server, data)
self.transcodeSession = findTranscodeSession(self.server, data)
# data for history details (/status/sessions/history/all)
# Load data for history details (/status/sessions/history/all)
self.viewedAt = toDatetime(data.attrib.get('viewedAt', NA))
# data for playlist items
# Load data for playlist items
self.playlistItemID = cast(int, data.attrib.get('playlistItemID', NA))
def getStreamURL(self, **params):
"""Make a stream url that can be used by vlc.
""" Returns a stream url that may be used by external applications such as VLC.
Args:
**params (dict): Description
Parameters:
**params (dict): optional parameters to manipulate the playback when accessing
the stream. A few known parameters include: maxVideoBitrate, videoResolution
offset, copyts, protocol, mediaIndex, platform.
Returns:
string: ''
Raises:
Unsupported: Raises a error is the type is wrong.
Raises:
Unsupported: When the item doesn't support fetching a stream URL.
"""
if self.TYPE not in ('movie', 'episode', 'track'):
raise Unsupported(
'Fetching stream URL for %s is unsupported.' % self.TYPE)
raise Unsupported('Fetching stream URL for %s is unsupported.' % self.TYPE)
mvb = params.get('maxVideoBitrate')
vr = params.get('videoResolution', '')
params = {
@ -202,35 +176,68 @@ 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):
"""Yield parts."""
""" Iterates over the parts of this media item. """
for item in self.media:
for part in item.parts:
yield part
def play(self, client):
"""Start playback on a client.
""" Start playback on the specified client.
Args:
client (PlexClient): The client to start playing on.
Parameters:
client (:class:`~plexapi.client.PlexClient`): Client to start playing on.
"""
client.playMedia(self)
def download(self, savepath=None, keep_orginal_name=False, **kwargs):
""" Downloads this items media to the specified location. Returns a list of
filepaths that have been saved to disk.
Parameters:
savepath (str): Title of the track to return.
keep_orginal_name (bool): Set True to keep the original filename as stored in
the Plex server. False will create a new filename with the format
"<Atrist> - <Album> <Track>".
kwargs (dict): If specified, a :func:`~plexapi.audio.Track.getStreamURL()` will
be returned and the additional arguments passed in will be sent to that
function. If kwargs is not specified, the media items will be downloaded
and saved to disk.
"""
filepaths = []
locations = [i for i in self.iterParts() if i]
for location in locations:
filename = location.file
if keep_orginal_name is False:
filename = '%s.%s' % (self._prettyfilename(), location.container)
# 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' % location.key)
filepath = download(download_url, filename=filename, savepath=savepath,
session=self.server.session)
if filepath:
filepaths.append(filepath)
return filepaths
def buildItem(server, elem, initpath, bytag=False):
"""Build classes used by the plexapi.
""" Factory function to build the objects used within the PlexAPI.
Args:
server (Plexserver): Your connected to.
elem (xml.etree.ElementTree.Element): xml from PMS
initpath (str): Relative path
bytag (bool, optional): Description # figure out what this do
Raises:
UnknownType: Unknown library type libtype
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
elem (ElementTree): XML data needed to build the object.
initpath (str): Relative path requested when retrieving specified `data` (optional).
bytag (bool): Creates the object from the name specified by the tag instead of the
default which builds the object specified by the type attribute. <tag type='foo' />
Raises:
UnknownType: Unknown library type.
"""
libtype = elem.tag if bytag else elem.attrib.get('type')
if libtype == 'photo' and elem.tag == 'Directory':
@ -242,19 +249,11 @@ def buildItem(server, elem, initpath, bytag=False):
def cast(func, value):
"""Helper to change to the correct type.
Args:
func (function): function to used [int, bool float]
value (string, int, float): value to cast
Returns:
None, nan, int, bool, or float
Raises:
TypeError: cast only allows int, float and bool
as func argument
""" Cast the specified value to the specified type (returned by func).
Parameters:
func (func): Calback function to used cast to type (int, bool, float, etc).
value (any): value to be cast and returned.
"""
if not value:
return
@ -272,14 +271,14 @@ def cast(func, value):
def findKey(server, key):
"""Finds and builds a object based on ratingKey.
""" Finds and builds a object based on ratingKey.
Args:
server (Plexserver): PMS your connected to
key (int): key to look for
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
key (int): ratingKey to find and return.
Raises:
NotFound: Unable to find key. Key
Raises:
NotFound: Unable to find key
"""
path = '/library/metadata/{0}'.format(key)
try:
@ -291,15 +290,15 @@ def findKey(server, key):
def findItem(server, path, title):
"""Finds and builds a object based on title.
""" Finds and builds a object based on title.
Args:
server (Plexserver): Description
path (str): Relative path
title (str): Fx 16 blocks
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
path (str): API path that returns item to search title for.
title (str): Title of the item to find and return.
Raises:
NotFound: Unable to find item: title
Raises:
NotFound: Unable to find item.
"""
for elem in server.query(path):
if elem.attrib.get('title').lower() == title.lower():
@ -308,14 +307,12 @@ def findItem(server, path, title):
def findLocations(data, single=False):
"""Extract the path from a location tag
""" Returns a list of filepaths from a location tag.
Args:
data (xml.etree.ElementTree.Element): xml from PMS as Element
single (bool, optional): Only return one
Returns:
filepath string if single is True else list of filepaths
Parameters:
data (ElementTree): XML object to search for locations in.
single (bool): Set True to only return the first location found.
Return type will be a string if this is set to True.
"""
locations = []
for elem in data:
@ -327,33 +324,26 @@ def findLocations(data, single=False):
def findPlayer(server, data):
"""Find a player in a elementthee
""" Returns the :class:`~plexapi.client.PlexClient` object found in the specified data.
Args:
server (Plexserver): PMS your connected to
data (xml.etree.ElementTree.Element): xml from pms as a element
Returns:
PlexClient or None
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
data (ElementTree): XML data to find Player in.
"""
elem = data.find('Player')
if elem is not None:
from plexapi.client import PlexClient
baseurl = 'http://%s:%s' % (elem.attrib.get('address'),
elem.attrib.get('port'))
baseurl = 'http://%s:%s' % (elem.attrib.get('address'), elem.attrib.get('port'))
return PlexClient(baseurl, server=server, data=elem)
return None
def findStreams(media, streamtype):
"""Find streams.
""" Returns a list of streams (str) found in media that match the specified streamtype.
Args:
media (Show, Movie, Episode): A item where find streams
streamtype (str): Possible options [movie, show, episode] # is this correct?
Returns:
list: of streams
Parameters:
media (:class:`~plexapi.utils.Playable`): Item to search for streams (show, movie, episode).
streamtype (str): Streamtype to return (videostream, audiostream, subtitlestream).
"""
streams = []
for mediaitem in media:
@ -365,14 +355,12 @@ def findStreams(media, streamtype):
def findTranscodeSession(server, data):
"""Find transcode session.
""" Returns a :class:`~plexapi.media.TranscodeSession` object if found within the specified
XML data.
Args:
server (Plexserver): PMS your connected to
data (xml.etree.ElementTree.Element): XML response from PMS as Element
Returns:
media.TranscodeSession or None
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
data (ElementTree): XML data to find TranscodeSession in.
"""
elem = data.find('TranscodeSession')
@ -383,13 +371,10 @@ def findTranscodeSession(server, data):
def findUsername(data):
"""Find a username in a Element
""" Returns the username if found in the specified XML data. Returns None if not found.
Args:
data (xml.etree.ElementTree.Element): XML from PMS as a Element
Returns:
username or None
Parameters:
data (ElementTree): XML data to find username in.
"""
elem = data.find('User')
if elem is not None:
@ -398,7 +383,7 @@ def findUsername(data):
def isInt(str):
"""Check of a string is a int"""
""" Returns True if the specified string passes as an int. """
try:
int(str)
return True
@ -407,14 +392,11 @@ def isInt(str):
def joinArgs(args):
"""Builds a query string where only
the value is quoted.
""" Returns a query string (uses for HTTP URLs) where only the value is URL encoded.
Example return value: '?genre=action&type=1337'.
Args:
args (dict): ex {'genre': 'action', 'type': 1337}
Returns:
string: ?genre=action&type=1337
Parameters:
args (dict): Arguments to include in query string.
"""
if not args:
return ''
@ -426,30 +408,25 @@ def joinArgs(args):
def listChoices(server, path):
"""ListChoices is by _cleanSort etc.
""" Returns a dict of {title:key} for all simple choices in a search filter.
Args:
server (Plexserver): Server your connected to
path (str): Relative path to PMS
Returns:
dict: title:key
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
path (str): Relative path to request XML data from.
"""
return {c.attrib['title']: c.attrib['key'] for c in server.query(path)}
def listItems(server, path, libtype=None, watched=None, bytag=False):
"""Return a list buildItem. See buildItem doc.
""" Returns a list of object built from :func:`~plexapi.utils.buildItem()` found
within the specified path.
Args:
server (Plexserver): PMS your connected to.
path (str): Relative path to PMS
libtype (None or string, optional): [movie, show, episode, music] # check me
watched (None, True, False, optional): Skip or include watched items
bytag (bool, optional): Dunno wtf this is used for # todo
Returns:
list: of buildItem
Parameters:
server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
path (str): Relative path to request XML data from.
libtype (str): Optionally return only the specified library type.
watched (bool): Optionally return only watched or unwatched items.
bytag (bool): Set true if libtype is found in the XML tag (and not the 'type' attribute).
"""
items = []
for elem in server.query(path):
@ -466,7 +443,18 @@ 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
influenced by the lookups used in Django templates.
Parameters:
obj (any): Object to start the lookup in (dict, obj, list, tuple, etc).
attrstr (str): String to lookup (ex: 'foo.bar.baz.value')
default (any): Default value to return if not found.
delim (str): Delimiter separating keys in attrstr.
"""
try:
parts = attrstr.split(delim, 1)
attr = parts[0]
@ -487,19 +475,15 @@ def rget(obj, attrstr, default=None, delim='.'):
def searchType(libtype):
"""Map search type name to int using SEACHTYPES
Used when querying PMS.
""" Returns the integer value of the library string type.
Args:
libtype (str): Possible options see SEARCHTYPES
Parameters:
libtype (str): Library type to lookup (movie, show, season, episode,
artist, album, track)
Returns:
int: fx 1
Raises:
NotFound: Unknown libtype: libtype
Raises:
NotFound: Unknown libtype
"""
libtype = str(libtype)
if libtype in [str(v) for v in SEARCHTYPES.values()]:
return libtype
@ -509,12 +493,12 @@ def searchType(libtype):
def threaded(callback, listargs):
"""Run some function in threads.
Args:
callback (function): funcion to run in thread
listargs (list): args parssed to the callback
""" Returns the result of <callback> for each set of \*args in listargs. Each call
to <callback. is called concurrently in their own separate threads.
Parameters:
callback (func): Callback function to apply to each set of \*args.
listargs (list): List of lists; \*args to pass each thread.
"""
threads, results = [], []
for args in listargs:
@ -524,18 +508,16 @@ def threaded(callback, listargs):
threads[-1].start()
for thread in threads:
thread.join()
return results
def toDatetime(value, format=None):
"""Helper for datetime.
""" Returns a datetime object from the specified value.
Args:
value (str): value to use to make datetime
format (None, optional): string as strptime.
Returns:
datetime
Parameters:
value (str): value to return as a datetime
format (str): Format to pass strftime (optional; if value is a str).
"""
if value and value != NA:
if format:
@ -543,3 +525,57 @@ 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 other media item. Returns the local
path to the downloaded file.
Parameters:
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
"""
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 err: # pragma: no cover
print('Error downloading file: %s' % err)
#log.exception('Failed to download %s to %s %s' % (url, fullpath, e))

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
from plexapi import media, utils
from plexapi.exceptions import NotFound
from plexapi.utils import Playable, PlexPartialObject
NA = utils.NA
@ -29,8 +29,7 @@ class Video(PlexPartialObject):
self.listType = 'video'
self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA))
self.key = data.attrib.get('key', NA)
self.lastViewedAt = utils.toDatetime(
data.attrib.get('lastViewedAt', NA))
self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt', NA))
self.librarySectionID = data.attrib.get('librarySectionID', NA)
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey', NA))
self.summary = data.attrib.get('summary', NA)
@ -53,7 +52,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."""
@ -110,24 +109,15 @@ class Movie(Video, Playable):
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
self.year = utils.cast(int, data.attrib.get('year', NA))
if self.isFullObject(): # check this
self.collections = [media.Collection(
self.server, e) for e in data if e.tag == media.Collection.TYPE]
self.countries = [media.Country(self.server, e)
for e in data if e.tag == media.Country.TYPE]
self.directors = [media.Director(
self.server, e) for e in data if e.tag == media.Director.TYPE]
self.genres = [media.Genre(self.server, e)
for e in data if e.tag == media.Genre.TYPE]
self.media = [media.Media(self.server, e, self.initpath, self)
for e in data if e.tag == media.Media.TYPE]
self.producers = [media.Producer(
self.server, e) for e in data if e.tag == media.Producer.TYPE]
self.roles = [media.Role(self.server, e)
for e in data if e.tag == media.Role.TYPE]
self.writers = [media.Writer(self.server, e)
for e in data if e.tag == media.Writer.TYPE]
self.fields = [media.Field(e)
for e in data if e.tag == media.Field.TYPE]
self.collections = [media.Collection(self.server, e) for e in data if e.tag == media.Collection.TYPE]
self.countries = [media.Country(self.server, e) for e in data if e.tag == media.Country.TYPE]
self.directors = [media.Director(self.server, e) for e in data if e.tag == media.Director.TYPE]
self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE]
self.media = [media.Media(self.server, e, self.initpath, self) for e in data if e.tag == media.Media.TYPE]
self.producers = [media.Producer(self.server, e) for e in data if e.tag == media.Producer.TYPE]
self.roles = [media.Role(self.server, e) for e in data if e.tag == media.Role.TYPE]
self.writers = [media.Writer(self.server, e) for e in data if e.tag == media.Writer.TYPE]
self.fields = [media.Field(e) for e in data if e.tag == media.Field.TYPE]
self.videoStreams = utils.findStreams(self.media, 'videostream')
self.audioStreams = utils.findStreams(self.media, 'audiostream')
self.subtitleStreams = utils.findStreams(
@ -141,6 +131,35 @@ 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 +172,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 +182,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 +191,9 @@ 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):
@ -189,12 +208,15 @@ class Show(Video):
path = '/library/metadata/%s/children' % self.ratingKey
return utils.listItems(self.server, path, Season.TYPE)
def season(self, title):
def season(self, title=None):
"""Returns a Season
Args:
title (str): fx Season1
title (str, int): fx Season 1
"""
if isinstance(title, int):
title = 'Season %s' % title
path = '/library/metadata/%s/children' % self.ratingKey
return utils.findItem(self.server, path, title)
@ -207,9 +229,45 @@ class Show(Video):
leavesKey = '/library/metadata/%s/allLeaves' % self.ratingKey
return utils.listItems(self.server, leavesKey, watched=watched)
def episode(self, title):
path = '/library/metadata/%s/allLeaves' % self.ratingKey
return utils.findItem(self.server, path, title)
def episode(self, title=None, season=None, episode=None):
"""Find a episode using a title or season and episode.
Note:
Both season and episode is required if title is missing.
Args:
title (str): Default None
season (int): Season number, default None
episode (int): Episode number, default None
Raises:
ValueError: If season and episode is missing.
NotFound: If the episode is missing.
Returns:
Episode
Examples:
>>> plex.search('The blacklist')[0].episode(season=1, episode=1)
<Episode:116263:The.Freelancer>
>>> plex.search('The blacklist')[0].episode('The Freelancer')
<Episode:116263:The.Freelancer>
"""
if not title and (not season or not episode):
raise TypeError('Missing argument: title or season and episode are required')
if title:
path = '/library/metadata/%s/allLeaves' % self.ratingKey
return utils.findItem(self.server, path, title)
elif season and episode:
results = [i for i in self.episodes()
if i.seasonNumber == season and i.index == episode]
if results:
return results[0]
else:
raise NotFound('Couldnt find %s S%s E%s' % (self.title, season, episode))
def watched(self):
"""Return a list of watched episodes"""
@ -227,9 +285,21 @@ 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
@ -243,10 +313,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 = data.attrib.get('index', 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.parentTitle = data.attrib.get('parentTitle', NA)
self.viewedLeafCount = utils.cast(
int, data.attrib.get('viewedLeafCount', NA))
@ -256,7 +328,7 @@ class Season(Video):
@property
def seasonNumber(self):
"""Reurns season number."""
"""Returns season number."""
return self.index
def episodes(self, watched=None):
@ -268,25 +340,48 @@ class Season(Video):
Returns:
list: of Episode
"""
childrenKey = '/library/metadata/%s/children' % self.ratingKey
return utils.listItems(self.server, childrenKey, watched=watched)
def episode(self, title):
"""Find a episode with a matching title.
def episode(self, title=None, episode=None):
"""Find a episode using a title or season and episode.
Args:
title (sting): Fx
Note:
episode is required if title is missing.
Returns:
Args:
title (str): Default None
episode (int): Episode number, default None
Raises:
TypeError: If title and episode is missing.
NotFound: If that episode cant be found.
Returns:
Episode
Examples:
>>> plex.search('The blacklist').season(1).episode(episode=1)
<Episode:116263:The.Freelancer>
>>> plex.search('The blacklist').season(1).episode('The Freelancer')
<Episode:116263:The.Freelancer>
"""
path = '/library/metadata/%s/children' % self.ratingKey
return utils.findItem(self.server, path, title)
if not title and not episode:
raise TypeError('Missing argument, you need to use title or episode.')
if title:
path = '/library/metadata/%s/children' % self.ratingKey
return utils.findItem(self.server, path, title)
elif episode:
results = [i for i in self.episodes() if i.seasonNumber == self.index and i.index == episode]
if results:
return results[0]
raise NotFound('Couldnt find %s.Season %s Episode %s.' % (self.grandparentTitle, self.index. episode))
def get(self, title):
"""Get a episode witha matching title
"""Get a episode with a matching title.
Args:
title (str): fx Secret santa
@ -308,6 +403,20 @@ class Season(Video):
"""Returns a list of unwatched Episode"""
return self.episodes(watched=False)
def __repr__(self):
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.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
class Episode(Video, Playable):
@ -332,9 +441,8 @@ class Episode(Video, Playable):
self.grandparentThumb = data.attrib.get('grandparentThumb', NA)
self.grandparentTitle = data.attrib.get('grandparentTitle', NA)
self.guid = data.attrib.get('guid', NA)
self.index = data.attrib.get('index', NA)
self.originallyAvailableAt = utils.toDatetime(
data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
self.index = utils.cast(int, data.attrib.get('index', NA))
self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
self.parentIndex = data.attrib.get('parentIndex', NA)
self.parentKey = data.attrib.get('parentKey', NA)
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey', NA))
@ -342,12 +450,9 @@ class Episode(Video, Playable):
self.rating = utils.cast(float, data.attrib.get('rating', NA))
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
self.year = utils.cast(int, data.attrib.get('year', NA))
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.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 = utils.findStreams(self.media, 'videostream')
self.audioStreams = utils.findStreams(self.media, 'audiostream')
self.subtitleStreams = utils.findStreams(self.media, 'subtitlestream')
@ -359,6 +464,12 @@ class Episode(Video, Playable):
# Cached season number
self._seasonNumber = None
def __repr__(self):
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%s:E%s:%s>' % (clsname, key, self.grandparentTitle, self.seasonNumber, self.index, title)
@property
def isWatched(self):
"""Returns True if watched, False if not."""
@ -369,7 +480,7 @@ class Episode(Video, Playable):
"""Return this episode seasonnumber."""
if self._seasonNumber is None:
self._seasonNumber = self.parentIndex if self.parentIndex else self.season().seasonNumber
return self._seasonNumber
return utils.cast(int, self._seasonNumber)
@property
def thumbUrl(self):
@ -384,3 +495,18 @@ 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))

View file

@ -1,4 +1,5 @@
#---------------------------------------------------------
# PlexAPI Requirements
# pip install -r requirments.txt
#---------------------------------------------------------
requests

7
requirements_dev.txt Normal file
View file

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

View file

@ -5,7 +5,6 @@ Install PlexAPI
"""
import re
from distutils.core import setup
from setuptools import find_packages
# Convert markdown readme to rst
try:
@ -29,7 +28,7 @@ setup(
author='Michael Shepanski',
author_email='mjs7231@gmail.com',
url='https://github.com/mjs7231/plexapi',
packages=find_packages(),
packages=['plexapi'],
install_requires=['requests'],
long_description=read_md('README.md'),
keywords=['plex', 'api'],

0
tests-old/__init__.py Normal file
View file

76
tests-old/runtests.py Executable file
View file

@ -0,0 +1,76 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
You can run this test suite with the following command:
>> python tests.py -u <USERNAME> -p <PASSWORD> -s <SERVERNAME>
"""
import argparse, pkgutil, sys, time, traceback
from os.path import dirname, abspath
sys.path.append(dirname(dirname(abspath(__file__))))
from plexapi import CONFIG
from plexapi.server import PlexServer
from plexapi.myplex import MyPlexAccount
from utils import log, itertests
def runtests(args):
# Get username and password from environment
username = args.username or CONFIG.get('authentication.username')
password = args.password or CONFIG.get('authentication.password')
resource = args.resource or CONFIG.get('authentication.resource')
# Register known tests
for loader, name, ispkg in pkgutil.iter_modules([dirname(abspath(__file__))]):
if name.startswith('test_'):
log(0, 'Registering tests from %s.py' % name)
loader.find_module(name).load_module(name)
# Create Account and Plex objects
log(0, 'Logging into MyPlex as %s' % username)
account = MyPlexAccount.signin(username, password)
log(0, 'Signed into MyPlex as %s (%s)' % (account.username, account.email))
if resource:
plex = account.resource(resource).connect()
log(0, 'Connected to PlexServer resource %s' % plex.friendlyName)
else:
plex = PlexServer(args.baseurl, args.token)
log(0, 'Connected to PlexServer %s' % plex.friendlyName)
log(0, '')
# Run all specified tests
tests = {'passed':0, 'failed':0}
for test in itertests(args.query):
starttime = time.time()
log(0, test['name'], 'green')
try:
test['func'](account, plex)
runtime = time.time() - starttime
log(2, 'PASS! (runtime: %.3fs)' % runtime, 'blue')
tests['passed'] += 1
except Exception as err:
errstr = str(err)
errstr += '\n%s' % traceback.format_exc() if args.verbose else ''
log(2, 'FAIL: %s' % errstr, 'red')
tests['failed'] += 1
log(0, '')
# Log a final report
log(0, 'Tests Run: %s' % sum(tests.values()))
log(0, 'Tests Passed: %s' % tests['passed'])
if tests['failed']:
log(0, 'Tests Failed: %s' % tests['failed'], 'red')
if not tests['failed']:
log(0, '')
log(0, 'EVERYTHING OK!! :)')
raise SystemExit(tests['failed'])
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Run PlexAPI tests.')
# Auth Method 1: Pass --username, --password, and optionally --resource.
parser.add_argument('-u', '--username', help='Username for your MyPlex account.')
parser.add_argument('-p', '--password', help='Password for your MyPlex account.')
parser.add_argument('-r', '--resource', help='Name of the Plex resource (requires user/pass).')
# Auth Method 2: Pass --baseurl and --token.
parser.add_argument('-b', '--baseurl', help='Baseurl needed for auth token authentication')
parser.add_argument('-t', '--token', help='Auth token (instead of user/pass)')
# Misc options
parser.add_argument('-q', '--query', help='Only run the specified tests.')
parser.add_argument('-v', '--verbose', default=False, action='store_true', help='Print verbose logging.')
runtests(parser.parse_args())

29
tests-old/test_actions.py Normal file
View file

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from utils import log, register
from plexapi import CONFIG
@register()
def test_mark_movie_watched(account, plex):
movie = plex.library.section(CONFIG.movie_section).get(CONFIG.movie_title)
movie.markUnwatched()
log(2, 'Marking movie watched: %s' % movie)
log(2, 'View count: %s' % movie.viewCount)
movie.markWatched()
log(2, 'View count: %s' % movie.viewCount)
assert movie.viewCount == 1, 'View count 0 after watched.'
movie.markUnwatched()
log(2, 'View count: %s' % movie.viewCount)
assert movie.viewCount == 0, 'View count 1 after unwatched.'
@register()
def test_refresh_section(account, plex):
shows = plex.library.section(CONFIG.movie_section)
shows.refresh()
@register()
def test_refresh_video(account, plex):
result = plex.search(CONFIG.movie_title)
result[0].refresh()

132
tests-old/test_client.py Normal file
View file

@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
import time
from utils import log, register, getclient
from plexapi import CONFIG
@register()
def test_list_clients(account, plex):
clients = [c.title for c in plex.clients()]
log(2, 'Clients: %s' % ', '.join(clients or []))
assert clients, 'Server is not listing any clients.'
@register()
def test_client_navigation(account, plex):
client = getclient(CONFIG.client, CONFIG.client_baseurl, plex)
_navigate(plex, client)
@register()
def test_client_navigation_via_proxy(account, plex):
client = getclient(CONFIG.client, CONFIG.client_baseurl, plex)
client.proxyThroughServer()
_navigate(plex, client)
def _navigate(plex, client):
episode = plex.library.section(CONFIG.show_section).get(CONFIG.show_title).get(CONFIG.show_episode)
artist = plex.library.section(CONFIG.audio_section).get(CONFIG.audio_artist)
log(2, 'Client: %s (%s)' % (client.title, 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()
def test_video_playback(account, plex):
client = getclient(CONFIG.client, CONFIG.client_baseurl, plex)
_video_playback(plex, client)
@register()
def test_video_playback_via_proxy(account, plex):
client = getclient(CONFIG.client, CONFIG.client_baseurl, plex)
client.proxyThroughServer()
_video_playback(plex, client)
def _video_playback(plex, client):
try:
mtype = 'video'
movie = plex.library.section(CONFIG.movie_section).get(CONFIG.movie_title)
subs = [s for s in movie.subtitleStreams if s.language == 'English']
log(2, 'Client: %s (%s)' % (client.title, 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)
finally:
log(2, 'Cleanup: Marking %s watched.' % movie.title)
movie.markWatched()
@register()
def test_client_timeline(account, plex):
client = getclient(CONFIG.client, CONFIG.client_baseurl, plex)
_test_timeline(plex, client)
@register()
def test_client_timeline_via_proxy(account, plex):
client = getclient(CONFIG.client, CONFIG.client_baseurl, plex)
client.proxyThroughServer()
_test_timeline(plex, client)
def _test_timeline(plex, client):
try:
mtype = 'video'
client = getclient(CONFIG.client, CONFIG.client_baseurl, plex)
movie = plex.library.section(CONFIG.movie_section).get(CONFIG.movie_title)
time.sleep(30) # previous test may have played media..
playing = client.isPlayingMedia()
log(2, 'Playing Media: %s' % playing)
assert playing is False, 'isPlayingMedia() should have returned False.'
client.playMedia(movie); time.sleep(30)
playing = client.isPlayingMedia()
log(2, 'Playing Media: %s' % playing)
assert playing is True, 'isPlayingMedia() should have returned True.'
client.stop(mtype); time.sleep(30)
playing = client.isPlayingMedia()
log(2, 'Playing Media: %s' % playing)
assert playing is False, 'isPlayingMedia() should have returned False.'
finally:
log(2, 'Cleanup: Marking %s watched.' % movie.title)
movie.markWatched()

52
tests-old/test_core.py Normal file
View file

@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
import time
from utils import log, register, getclient
from plexapi import CONFIG
@register()
def test_server(account, plex):
log(2, 'Username: %s' % plex.myPlexUsername)
log(2, 'Platform: %s' % plex.platform)
log(2, 'Version: %s' % plex.version)
assert plex.myPlexUsername is not None, 'Unknown username.'
assert plex.platform is not None, 'Unknown platform.'
assert plex.version is not None, 'Unknown version.'
@register()
def test_list_sections(account, plex):
sections = [s.title for s in plex.library.sections()]
log(2, 'Sections: %s' % sections)
assert CONFIG.show_section in sections, '%s not a library section.' % CONFIG.show_section
assert CONFIG.movie_section in sections, '%s not a library section.' % CONFIG.movie_section
plex.library.section(CONFIG.show_section)
plex.library.section(CONFIG.movie_section)
@register()
def test_history(account, plex):
history = plex.history()
for item in history[:20]:
log(2, "%s: %s played %s '%s'" % (item.viewedAt, item.username, item.TYPE, item.title))
assert len(history), 'No history items have been found.'
@register()
def test_sessions(account, plex):
client, movie = None, None
try:
mtype = 'video'
movie = plex.library.section(CONFIG.movie_section).get(CONFIG.movie_title)
client = getclient(CONFIG.client, CONFIG.client_baseurl, plex)
log(2, 'Playing %s..' % movie.title)
client.playMedia(movie); time.sleep(5)
sessions = plex.sessions()
for item in sessions[:20]:
log(2, "%s is playing %s '%s' on %s" % (item.username, item.TYPE, item.title, item.player.platform))
assert len(sessions), 'No session items have been found.'
finally:
log(2, 'Stop..')
if client: client.stop(mtype); time.sleep(1)
log(2, 'Cleanup: Marking %s watched.' % movie.title)
if movie: movie.markWatched()

144
tests-old/test_metadata.py Normal file
View file

@ -0,0 +1,144 @@
# -*- coding: utf-8 -*-
from os.path import basename
from utils import log, register
from plexapi import CONFIG
@register()
def test_partial_video(account, plex):
result = plex.search(CONFIG.movie_foreign)
log(2, 'Title: %s' % result[0].title)
log(2, 'Original Title: %s' % result[0].originalTitle)
assert(result[0].originalTitle != None)
@register()
def test_list_media_files(account, plex):
# Fetch file names from the tv show
episode_files = []
episode = plex.library.section(CONFIG.show_section).get(CONFIG.show_title).episodes()[-1]
log(2, 'Episode Files: %s' % episode)
for media in episode.media:
for part in media.parts:
log(4, part.file)
episode_files.append(part.file)
assert filter(None, episode_files), 'No show files have been listed.'
# Fetch file names from the movie
movie_files = []
movie = plex.library.section(CONFIG.movie_section).get(CONFIG.movie_title)
log(2, 'Movie Files: %s' % movie)
for media in movie.media:
for part in media.parts:
log(4, part.file)
movie_files.append(part.file)
assert filter(None, movie_files), 'No movie files have been listed.'
@register()
def test_list_video_tags(account, plex):
movies = plex.library.section(CONFIG.movie_section)
movie = movies.get(CONFIG.movie_title)
log(2, 'Countries: %s' % movie.countries[0:3])
log(2, 'Directors: %s' % movie.directors[0:3])
log(2, 'Genres: %s' % movie.genres[0:3])
log(2, 'Producers: %s' % movie.producers[0:3])
log(2, 'Actors: %s' % movie.actors[0:3])
log(2, 'Writers: %s' % movie.writers[0:3])
assert filter(None, movie.countries), 'No countries listed for movie.'
assert filter(None, movie.directors), 'No directors listed for movie.'
assert filter(None, movie.genres), 'No genres listed for movie.'
assert filter(None, movie.producers), 'No producers listed for movie.'
assert filter(None, movie.actors), 'No actors listed for movie.'
assert filter(None, movie.writers), 'No writers listed for movie.'
log(2, 'List movies with same director: %s' % movie.directors[0])
related = movies.search(None, director=movie.directors[0])
log(4, related[0:3])
assert movie in related, 'Movie was not found in related directors search.'
@register()
def test_list_video_streams(account, plex):
movie = plex.library.section(CONFIG.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()
def test_list_audio_tags(account, plex):
section = plex.library.section(CONFIG.audio_section)
artist = section.get(CONFIG.audio_artist)
track = artist.get(CONFIG.audio_track)
log(2, 'Countries: %s' % artist.countries[0:3])
log(2, 'Genres: %s' % artist.genres[0:3])
log(2, 'Similar: %s' % artist.similar[0:3])
log(2, 'Album Genres: %s' % track.album().genres[0:3])
log(2, 'Moods: %s' % track.moods[0:3])
log(2, 'Media: %s' % track.media[0:3])
assert filter(None, artist.countries), 'No countries listed for artist.'
assert filter(None, artist.genres), 'No genres listed for artist.'
assert filter(None, artist.similar), 'No similar artists listed.'
assert filter(None, track.album().genres), 'No genres listed for album.'
assert filter(None, track.moods), 'No moods listed for track.'
assert filter(None, track.media), 'No media listed for track.'
@register()
def test_is_watched(account, plex):
show = plex.library.section(CONFIG.show_section).get(CONFIG.show_title)
episode = show.get(CONFIG.show_episode)
log(2, '%s isWatched: %s' % (episode.title, episode.isWatched))
movie = plex.library.section(CONFIG.movie_section).get(CONFIG.movie_title)
log(2, '%s isWatched: %s' % (movie.title, movie.isWatched))
@register()
def test_fetch_details_not_in_search_result(account, plex):
# Search results only contain 3 actors per movie. This text checks there
# are more than 3 results in the actor list (meaning it fetched the detailed
# information behind the scenes).
result = plex.search(CONFIG.movie_title)[0]
actors = result.actors
assert len(actors) >= 4, 'Unable to fetch detailed movie information'
log(2, '%s actors found.' % len(actors))
@register()
def test_stream_url(account, plex):
movie = plex.library.section(CONFIG.movie_section).get(CONFIG.movie_title)
episode = plex.library.section(CONFIG.show_section).get(CONFIG.show_title).episodes()[-1]
track = plex.library.section(CONFIG.audio_section).get(CONFIG.audio_artist).get(CONFIG.audio_track)
log(2, 'Movie: vlc "%s"' % movie.getStreamURL())
log(2, 'Episode: vlc "%s"' % episode.getStreamURL())
log(2, 'Track: cvlc "%s"' % track.getStreamURL())
@register()
def test_list_audioalbums(account, plex):
music = plex.library.section(CONFIG.audio_section)
albums = music.albums()
for album in albums[:10]:
log(2, '%s - %s [%s]' % (album.artist().title, album.title, album.year))
@register()
def test_list_photoalbums(account, plex):
photosection = plex.library.section(CONFIG.photo_section)
photoalbums = photosection.all()
log(2, 'Listing albums..')
for album in photoalbums[:10]:
log(4, '%s' % album.title)
assert len(photoalbums), 'No photoalbums found.'
album = photosection.get(CONFIG.photo_album)
photos = album.photos()
for photo in photos[:10]:
filename = basename(photo.media[0].parts[0].file)
width, height = photo.media[0].width, photo.media[0].height
log(4, '%s (%sx%s)' % (filename, width, height))
assert len(photoalbums), 'No photos found.'

72
tests-old/test_myplex.py Normal file
View file

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
from utils import log, register
from plexapi import CONFIG
@register()
def test_myplex_accounts(account, plex):
assert account, 'Must specify username, password & resource to run this test.'
log(2, 'MyPlexAccount:')
log(4, 'username: %s' % account.username)
log(4, 'authenticationToken: %s' % account.authenticationToken)
log(4, 'email: %s' % account.email)
log(4, 'home: %s' % account.home)
log(4, '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 = plex.account()
log(2, 'Local PlexServer.account():')
log(4, 'username: %s' % account.username)
log(4, 'authToken: %s' % account.authToken)
log(4, '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'
@register()
def test_myplex_resources(account, plex):
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'
log(2, '%s (%s): %s' % (name, resource.product, connections))
assert resources, 'No resources found for account: %s' % account.name
@register()
def test_myplex_devices(account, plex):
assert account, 'Must specify username, password & resource to run this test.'
devices = account.devices()
for device in devices:
name = device.name or 'Unknown'
connections = ', '.join(device.connections) if device.connections else 'None'
log(2, '%s (%s): %s' % (name, device.product, connections))
assert devices, 'No devices found for account: %s' % account.name
@register()
def test_myplex_users(account, plex):
users = account.users()
assert users, 'Found no users on account: %s' % account.name
log(2, 'Found %s users.' % len(users))
user = account.user('sdfsdfplex')
log(2, 'Found user: %s' % user)
assert users, 'Could not find user sdfsdfplex'
@register()
def test_myplex_connect_to_device(account, plex):
assert account, 'Must specify username, password & resource to run this test.'
devices = account.devices()
for device in devices:
if device.name == CONFIG.client and len(device.connections):
break
client = device.connect()
log(2, 'Connected to client: %s (%s)' % (client.title, client.product))
assert client, 'Unable to connect to device'

View file

@ -0,0 +1,70 @@
# -*- coding: utf-8 -*-
from utils import log, register
from plexapi import CONFIG
# TODO: test_navigation/test_navigate_to_movie
# FAIL: (500) internal_server_error
# @register()
def test_navigate_to_movie(account, plex):
result_library = plex.library.get(CONFIG.movie_title)
result_movies = plex.library.section(CONFIG.movie_section).get(CONFIG.movie_title)
log(2, 'Navigating to: %s' % CONFIG.movie_title)
log(2, 'Result Library: %s' % result_library)
log(2, 'Result Movies: %s' % result_movies)
assert result_movies, 'Movie navigation not working.'
assert result_library == result_movies, 'Movie navigation not consistent.'
@register()
def test_navigate_to_show(account, plex):
result_shows = plex.library.section(CONFIG.show_section).get(CONFIG.show_title)
log(2, 'Navigating to: %s' % CONFIG.show_title)
log(2, 'Result Shows: %s' % result_shows)
assert result_shows, 'Show navigation not working.'
# TODO: Fix test_navigation/test_navigate_around_show
# FAIL: Unable to list season: Season 1
# @register()
def test_navigate_around_show(account, plex):
show = plex.library.section(CONFIG.show_section).get(CONFIG.show_title)
seasons = show.seasons()
season = show.season(CONFIG.show_season)
episodes = show.episodes()
episode = show.episode(CONFIG.show_episode)
log(2, 'Navigating around show: %s' % show)
log(2, 'Seasons: %s...' % seasons[:3])
log(2, 'Season: %s' % season)
log(2, 'Episodes: %s...' % episodes[:3])
log(2, 'Episode: %s' % episode)
assert CONFIG.show_season in [s.title for s in seasons], 'Unable to list season: %s' % CONFIG.show_season
assert CONFIG.show_episode in [e.title for e in episodes], 'Unable to list episode: %s' % CONFIG.show_episode
assert show.season(CONFIG.show_season) == season, 'Unable to get show season: %s' % CONFIG.show_season
assert show.episode(CONFIG.show_episode) == episode, 'Unable to get show episode: %s' % CONFIG.show_episode
assert season.episode(CONFIG.show_episode) == episode, 'Unable to get season episode: %s' % CONFIG.show_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.'
@register()
def test_navigate_around_artist(account, plex):
artist = plex.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)
log(2, 'Navigating around artist: %s' % artist)
log(2, 'Albums: %s...' % albums[:3])
log(2, 'Album: %s' % album)
log(2, 'Tracks: %s...' % tracks[:3])
log(2, '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.'

119
tests-old/test_playlists.py Normal file
View file

@ -0,0 +1,119 @@
# -*- coding: utf-8 -*-
import time
from utils import log, register, getclient
from plexapi import CONFIG
@register()
def test_list_playlists(account, plex):
playlists = plex.playlists()
for playlist in playlists:
log(2, playlist.title)
# TODO: Fix test_playlists/test_create_playlist
# FAIL: (500) internal_server_error
# @register()
def test_create_playlist(account, plex):
# create the playlist
title = 'test_create_playlist'
log(2, 'Creating playlist %s..' % title)
episodes = plex.library.section(CONFIG.show_section).get(CONFIG.show_title).episodes()
playlist = plex.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)
log(2, '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
log(2, '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
log(2, '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]
log(2, '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()
@register()
def test_playlist(account, plex):
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)
playlist = plex.createPlaylist('test_play_playlist', album)
try:
log(2, 'Playing playlist: %s' % playlist)
client.playMedia(playlist); time.sleep(5)
log(2, 'stop..')
client.stop('music'); time.sleep(1)
finally:
playlist.delete()
@register()
def test_playlist_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()
playlist = plex.createPlaylist('test_play_playlist2', photos)
try:
client.playMedia(playlist)
for i in range(3):
time.sleep(2)
client.skipNext(mtype='photo')
finally:
playlist.delete()
@register()
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)
@register()
def test_play_queues(account, plex):
episode = plex.library.section(CONFIG.show_section).get(CONFIG.show_title).get(CONFIG.show_episode)
playqueue = plex.createPlayQueue(episode)
assert len(playqueue.items) == 1, 'No items in play queue.'
assert playqueue.items[0].title == CONFIG.show_episode, 'Wrong show queued.'
assert playqueue.playQueueID, 'Play queue ID not set.'

95
tests-old/test_search.py Normal file
View file

@ -0,0 +1,95 @@
# -*- coding: utf-8 -*-
from utils import log, register
from plexapi import CONFIG
@register()
def test_search_show(account, plex):
result_server = plex.search(CONFIG.show_title)
result_shows = plex.library.section(CONFIG.show_section).search(CONFIG.show_title)
result_movies = plex.library.section(CONFIG.movie_section).search(CONFIG.show_title)
log(2, 'Searching for: %s' % CONFIG.show_title)
log(4, 'Result Server: %s' % result_server)
log(4, 'Result Shows: %s' % result_shows)
log(4, 'Result Movies: %s' % result_movies)
assert result_server, 'Show not found.'
assert result_server == result_shows, 'Show searches not consistent.'
assert not result_movies, 'Movie search returned show title.'
@register()
def test_search_with_apostrophe(account, plex):
show_title = "Marvel's Daredevil" # Test ' in show title
result_server = plex.search(show_title)
result_shows = plex.library.section(CONFIG.show_section).search(show_title)
log(2, 'Searching for: %s' % CONFIG.show_title)
log(4, 'Result Server: %s' % result_server)
log(4, 'Result Shows: %s' % result_shows)
assert result_server, 'Show not found.'
assert result_server == result_shows, 'Show searches not consistent.'
@register()
def test_search_movie(account, plex):
result_server = plex.search(CONFIG.movie_title)
result_library = plex.library.search(CONFIG.movie_title)
result_shows = plex.library.section(CONFIG.show_section).search(CONFIG.movie_title)
result_movies = plex.library.section(CONFIG.movie_section).search(CONFIG.movie_title)
log(2, 'Searching for: %s' % CONFIG.movie_title)
log(4, 'Result Server: %s' % result_server)
log(4, 'Result Library: %s' % result_library)
log(4, 'Result Shows: %s' % result_shows)
log(4, 'Result Movies: %s' % result_movies)
assert result_server, 'Movie not found.'
assert result_server == result_library == result_movies, 'Movie searches not consistent.'
assert not result_shows, 'Show search returned show title.'
@register()
def test_search_audio(account, plex):
result_server = plex.search(CONFIG.audio_artist)
result_library = plex.library.search(CONFIG.audio_artist)
result_music = plex.library.section(CONFIG.audio_section).search(CONFIG.audio_artist)
log(2, 'Searching for: %s' % CONFIG.audio_artist)
log(4, 'Result Server: %s' % result_server)
log(4, 'Result Library: %s' % result_library)
log(4, 'Result Music: %s' % result_music)
assert result_server, 'Artist not found.'
assert result_server == result_library == result_music, 'Audio searches not consistent.'
@register()
def test_search_related(account, plex):
movies = plex.library.section(CONFIG.movie_section)
movie = movies.get(CONFIG.movie_title)
related_by_actors = movies.search(actor=movie.actors, maxresults=3)
log(2, u'Actors: %s..' % movie.actors)
log(2, u'Related by Actors: %s..' % related_by_actors)
assert related_by_actors, 'No related movies found by actor.'
related_by_genre = movies.search(genre=movie.genres, maxresults=3)
log(2, u'Genres: %s..' % movie.genres)
log(2, u'Related by Genre: %s..' % related_by_genre)
assert related_by_genre, 'No related movies found by genre.'
related_by_director = movies.search(director=movie.directors, maxresults=3)
log(2, 'Directors: %s..' % movie.directors)
log(2, 'Related by Director: %s..' % related_by_director)
assert related_by_director, 'No related movies found by director.'
# TODO: Fix test_search/test_crazy_search
# FAIL: Unable to search movie by director.
# @register()
def test_crazy_search(account, plex):
movies = plex.library.section(CONFIG.movie_section)
movie = movies.get('Jurassic World')
log(2, u'Search by Actor: "Chris Pratt"')
assert movie in movies.search(actor='Chris Pratt'), 'Unable to search movie by actor.'
log(2, u'Search by Director: ["Trevorrow"]')
assert movie in movies.search(director=['Trevorrow']), 'Unable to search movie by director.'
log(2, u'Search by Year: ["2014", "2015"]')
assert movie in movies.search(year=['2014', '2015']), 'Unable to search movie by year.'
log(2, u'Filter by Year: 2014')
assert movie not in movies.search(year=2014), 'Unable to filter movie by year.'
judy = [a for a in movie.actors if 'Judy' in a.tag][0]
log(2, u'Search by Unpopular Actor: %s' % judy)
assert movie in movies.search(actor=judy.id), 'Unable to filter movie by year.'

19
tests-old/test_sync.py Normal file
View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from utils import log, register
# TODO: Fix test_sync/test_sync_items
# I don't know if this ever worked. It was contributed by the guy that added sync support.
# @register()
def test_sync_items(account, plex):
device = account.getDevice('device-uuid')
# fetch the sync items via the device sync list
for item in device.sync_items():
# fetch the media object associated with the sync item
for video in item.get_media():
# fetch the media parts (actual video/audio streams) associated with the media
for part in video.iterParts():
log(2, 'Found media to download!')
# make the relevant sync id (media part) as downloaded
# this tells the server that this device has successfully downloaded this media part of this sync item
item.mark_as_done(part.sync_id)

42
tests-old/utils.py Normal file
View file

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
"""
Test Library Functions
"""
import datetime, sys
from plexapi.client import PlexClient
from plexapi.exceptions import NotFound
REGISTERED = []
COLORS = {'blue':'\033[94m', 'green':'\033[92m', 'red':'\033[91m', 'yellow':'\033[93m', 'end':'\033[0m'}
def register():
def wrapper(func):
name = '%s/%s' % (func.__module__, func.__name__)
REGISTERED.append({'func':func, 'name':name})
return lambda plex, account: func(plex, account)
return wrapper
def itertests(query):
for test in REGISTERED:
if not query:
yield test
elif query in test['name']:
yield test
def log(indent, message, color=None):
dt = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
if color:
return sys.stdout.write('%s: %s%s%s%s\n' % (dt, ' '*indent, COLORS[color], message, COLORS['end']))
return sys.stdout.write('%s: %s%s\n' % (dt, ' '*indent, message))
def getclient(name, baseurl, server):
try:
return server.client(name)
except NotFound as err:
log(2, 'Warning: %s' % err)
return PlexClient(baseurl, server=server)

View file

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
import sys
from os.path import dirname, abspath
# Make sure plexapi is in the systempath
sys.path.insert(0, dirname(dirname(abspath(__file__))))

148
tests/conftest.py Normal file
View file

@ -0,0 +1,148 @@
# -*- coding: utf-8 -*-
import betamax, os, plexapi
import pytest, requests
from betamax_serializers import pretty_json
from functools import partial
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()

22
tests/test_actions.py Normal file
View file

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
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()

307
tests/test_audio.py Normal file
View file

@ -0,0 +1,307 @@
# -*- coding: utf-8 -*-
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-02-02'
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(); 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

217
tests/test_client.py Normal file
View file

@ -0,0 +1,217 @@
# -*- coding: utf-8 -*-
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

161
tests/test_library.py Normal file
View file

@ -0,0 +1,161 @@
# -*- coding: utf-8 -*-
import pytest
from plexapi.exceptions import NotFound
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)

75
tests/test_myplex.py Normal file
View file

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
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('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'

37
tests/test_navigation.py Normal file
View file

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
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('Music').get('Infinite State')
albums = artist.albums()
album = artist.album('Unmastered Impulses')
tracks = artist.tracks()
track = artist.track('Mantra')
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 'Unmastered Impulses' in [a.title for a in albums], 'Unable to list album.'
assert 'Mantra' in [e.title for e in tracks], 'Unable to list track.'
assert artist.album('Unmastered Impulses') == album, 'Unable to get artist album.'
assert artist.track('Mantra') == track, 'Unable to get artist track.'
assert album.track('Mantra') == track, 'Unable to get album 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.'

102
tests/test_playlist.py Normal file
View file

@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
import pytest, time
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:
client.playMedia(playlist); time.sleep(5)
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.'

2
tests/test_search.py Normal file
View file

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
# TODO: Many more tests is for search later.

196
tests/test_server.py Normal file
View file

@ -0,0 +1,196 @@
# -*- coding: utf-8 -*-
import os, pytest
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.
from PIL import Image
width, height = 500, 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')
with Image.open(resized_image) as im:
assert width, height == im.size
with Image.open(org_image) as im:
assert width, height != im.size
assert _detect_color_image(gray_image, thumb_size=150) == 'grayscale'
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
from PIL import Image, ImageStat
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'
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')
# TODO: Check client in test_server_Server_session.
# TODO: Check myplex in test_server_Server_session.
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('<This-client-should-not-be-found>')
@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.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
# TODO: Figure out why this is missing from time to time.
#assert acc.mappingError == 'publisherror'
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'

107
tests/test_utils.py Normal file
View file

@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
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():
# TODO: Implement 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():
# TODO: Implement test_utils_listItems
pass
def _test_utils_listChoices(pms):
# TODO: Implement test_utils_listChoices
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():
# TODO: Implement test_utils_findUsername
pass
def _test_utils_findStreams():
# TODO: Implement test_utils_findStreams
pass
def _test_utils_findPlayer():
# TODO: Implement test_utils_findPlayer
pass
def _test_utils_findLocations():
# TODO: Implement test_utils_findLocations
pass
def _test_utils_findItem():
# TODO: Implement 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

543
tests/test_video.py Normal file
View file

@ -0,0 +1,543 @@
# -*- coding: utf-8 -*-
import os, 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
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

View file

@ -1,772 +0,0 @@
# -*- coding: utf-8 -*-
"""
Test Library Functions
As of Plex version 0.9.11 I noticed that you must be logged in
to browse even the plex server locatewd at localhost. You can
run this test suite with the following command:
>> python tests.py -u <USERNAME> -p <PASSWORD> -s <SERVERNAME>
"""
import argparse, sys, time
from os.path import basename, dirname, abspath
sys.path.append(dirname(dirname(abspath(__file__))))
from utils import log, register, safe_client, run_tests
from plexapi.utils import NA
SHOW_SECTION = 'TV Shows'
SHOW_TITLE = 'Game of Thrones'
SHOW_SEASON = 'Season 1'
SHOW_EPISODE = 'Winter Is Coming'
MOVIE_SECTION = 'Movies'
MOVIE_TITLE = 'Jurassic World'
AUDIO_SECTION = 'Music'
AUDIO_ARTIST = 'Beastie Boys'
AUDIO_ALBUM = 'Licensed To Ill'
AUDIO_TRACK = 'Brass Monkey'
PHOTO_SECTION = 'Photos'
PHOTO_ALBUM = '2015-12-12 - Family Photo for Christmas card'
CLIENT = 'pkkid-home'
CLIENT_BASEURL = 'http://192.168.1.131:3005'
#-----------------------
# Core
#-----------------------
@register('core,server')
def test_server(plex, account=None):
log(2, 'Username: %s' % plex.myPlexUsername)
log(2, 'Platform: %s' % plex.platform)
log(2, 'Version: %s' % plex.version)
assert plex.myPlexUsername is not None, 'Unknown username.'
assert plex.platform is not None, 'Unknown platform.'
assert plex.version is not None, 'Unknown version.'
@register('core')
def test_list_sections(plex, account=None):
sections = [s.title for s in plex.library.sections()]
log(2, 'Sections: %s' % sections)
assert SHOW_SECTION in sections, '%s not a library section.' % SHOW_SECTION
assert MOVIE_SECTION in sections, '%s not a library section.' % MOVIE_SECTION
plex.library.section(SHOW_SECTION)
plex.library.section(MOVIE_SECTION)
@register('core')
def test_history(plex, account=None):
history = plex.history()
for item in history[:20]:
log(2, "%s: %s played %s '%s'" % (item.viewedAt, item.username, item.TYPE, item.title))
assert len(history), 'No history items have been found.'
@register('core')
def test_sessions(plex, account=None):
try:
mtype = 'video'
movie = plex.library.section(MOVIE_SECTION).get(MOVIE_TITLE)
client = safe_client(CLIENT, CLIENT_BASEURL, plex)
log(2, 'Playing %s..' % movie.title)
client.playMedia(movie); time.sleep(5)
sessions = plex.sessions()
for item in sessions[:20]:
log(2, "%s is playing %s '%s' on %s" % (item.username, item.TYPE, item.title, item.player.platform))
assert len(sessions), 'No session items have been found.'
finally:
log(2, 'Stop..')
client.stop(mtype); time.sleep(1)
log(2, 'Cleanup: Marking %s watched.' % movie.title)
movie.markWatched()
#-----------------------
# Search
#-----------------------
@register('search,show')
def test_search_show(plex, account=None):
result_server = plex.search(SHOW_TITLE)
result_shows = plex.library.section(SHOW_SECTION).search(SHOW_TITLE)
result_movies = plex.library.section(MOVIE_SECTION).search(SHOW_TITLE)
log(2, 'Searching for: %s' % SHOW_TITLE)
log(4, 'Result Server: %s' % result_server)
log(4, 'Result Shows: %s' % result_shows)
log(4, 'Result Movies: %s' % result_movies)
assert result_server, 'Show not found.'
assert result_server == result_shows, 'Show searches not consistent.'
assert not result_movies, 'Movie search returned show title.'
@register('search,show')
def test_search_with_apostrophe(plex, account=None):
show_title = "Marvel's Daredevil" # Test ' in show title
result_server = plex.search(show_title)
result_shows = plex.library.section(SHOW_SECTION).search(show_title)
log(2, 'Searching for: %s' % SHOW_TITLE)
log(4, 'Result Server: %s' % result_server)
log(4, 'Result Shows: %s' % result_shows)
assert result_server, 'Show not found.'
assert result_server == result_shows, 'Show searches not consistent.'
@register('search,movie')
def test_search_movie(plex, account=None):
result_server = plex.search(MOVIE_TITLE)
result_library = plex.library.search(MOVIE_TITLE)
result_shows = plex.library.section(SHOW_SECTION).search(MOVIE_TITLE)
result_movies = plex.library.section(MOVIE_SECTION).search(MOVIE_TITLE)
log(2, 'Searching for: %s' % MOVIE_TITLE)
log(4, 'Result Server: %s' % result_server)
log(4, 'Result Library: %s' % result_library)
log(4, 'Result Shows: %s' % result_shows)
log(4, 'Result Movies: %s' % result_movies)
assert result_server, 'Movie not found.'
assert result_server == result_library == result_movies, 'Movie searches not consistent.'
assert not result_shows, 'Show search returned show title.'
@register('search,audio')
def test_search_audio(plex, account=None):
result_server = plex.search(AUDIO_ARTIST)
result_library = plex.library.search(AUDIO_ARTIST)
result_music = plex.library.section(AUDIO_SECTION).search(AUDIO_ARTIST)
log(2, 'Searching for: %s' % AUDIO_ARTIST)
log(4, 'Result Server: %s' % result_server)
log(4, 'Result Library: %s' % result_library)
log(4, 'Result Music: %s' % result_music)
assert result_server, 'Artist not found.'
assert result_server == result_library == result_music, 'Audio searches not consistent.'
@register('search,audio')
def test_search_related(plex, account=None):
movies = plex.library.section(MOVIE_SECTION)
movie = movies.get(MOVIE_TITLE)
related_by_actors = movies.search(actor=movie.actors, maxresults=3)
log(2, u'Actors: %s..' % movie.actors)
log(2, u'Related by Actors: %s..' % related_by_actors)
assert related_by_actors, 'No related movies found by actor.'
related_by_genre = movies.search(genre=movie.genres, maxresults=3)
log(2, u'Genres: %s..' % movie.genres)
log(2, u'Related by Genre: %s..' % related_by_genre)
assert related_by_genre, 'No related movies found by genre.'
related_by_director = movies.search(director=movie.directors, maxresults=3)
log(2, 'Directors: %s..' % movie.directors)
log(2, 'Related by Director: %s..' % related_by_director)
assert related_by_director, 'No related movies found by director.'
@register('search,show')
def test_crazy_search(plex, account=None):
movies = plex.library.section(MOVIE_SECTION)
movie = movies.get('Jurassic World')
log(2, u'Search by Actor: "Chris Pratt"')
assert movie in movies.search(actor='Chris Pratt'), 'Unable to search movie by actor.'
log(2, u'Search by Director: ["Trevorrow"]')
assert movie in movies.search(director=['Trevorrow']), 'Unable to search movie by director.'
log(2, u'Search by Year: ["2014", "2015"]')
assert movie in movies.search(year=['2014', '2015']), 'Unable to search movie by year.'
log(2, u'Filter by Year: 2014')
assert movie not in movies.search(year=2014), 'Unable to filter movie by year.'
judy = [a for a in movie.actors if 'Judy' in a.tag][0]
log(2, u'Search by Unpopular Actor: %s' % judy)
assert movie in movies.search(actor=judy.id), 'Unable to filter movie by year.'
#-----------------------
# Library Navigation
#-----------------------
@register('navigate,movie,show')
def test_navigate_to_movie(plex, account=None):
result_library = plex.library.get(MOVIE_TITLE)
result_movies = plex.library.section(MOVIE_SECTION).get(MOVIE_TITLE)
log(2, 'Navigating to: %s' % MOVIE_TITLE)
log(2, 'Result Library: %s' % result_library)
log(2, 'Result Movies: %s' % result_movies)
assert result_movies, 'Movie navigation not working.'
assert result_library == result_movies, 'Movie navigation not consistent.'
@register('navigate,movie,show')
def test_navigate_to_show(plex, account=None):
result_shows = plex.library.section(SHOW_SECTION).get(SHOW_TITLE)
log(2, 'Navigating to: %s' % SHOW_TITLE)
log(2, 'Result Shows: %s' % result_shows)
assert result_shows, 'Show navigation not working.'
@register('navigate,show')
def test_navigate_around_show(plex, account=None):
show = plex.library.section(SHOW_SECTION).get(SHOW_TITLE)
seasons = show.seasons()
season = show.season(SHOW_SEASON)
episodes = show.episodes()
episode = show.episode(SHOW_EPISODE)
log(2, 'Navigating around show: %s' % show)
log(2, 'Seasons: %s...' % seasons[:3])
log(2, 'Season: %s' % season)
log(2, 'Episodes: %s...' % episodes[:3])
log(2, 'Episode: %s' % episode)
assert SHOW_SEASON in [s.title for s in seasons], 'Unable to list season: %s' % SHOW_SEASON
assert SHOW_EPISODE in [e.title for e in episodes], 'Unable to list episode: %s' % SHOW_EPISODE
assert show.season(SHOW_SEASON) == season, 'Unable to get show season: %s' % SHOW_SEASON
assert show.episode(SHOW_EPISODE) == episode, 'Unable to get show episode: %s' % SHOW_EPISODE
assert season.episode(SHOW_EPISODE) == episode, 'Unable to get season episode: %s' % SHOW_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.'
@register('navigate,audio')
def test_navigate_around_artist(plex, account=None):
artist = plex.library.section(AUDIO_SECTION).get(AUDIO_ARTIST)
albums = artist.albums()
album = artist.album(AUDIO_ALBUM)
tracks = artist.tracks()
track = artist.track(AUDIO_TRACK)
log(2, 'Navigating around artist: %s' % artist)
log(2, 'Albums: %s...' % albums[:3])
log(2, 'Album: %s' % album)
log(2, 'Tracks: %s...' % tracks[:3])
log(2, 'Track: %s' % track)
assert AUDIO_ALBUM in [a.title for a in albums], 'Unable to list album: %s' % AUDIO_ALBUM
assert AUDIO_TRACK in [e.title for e in tracks], 'Unable to list track: %s' % AUDIO_TRACK
assert artist.album(AUDIO_ALBUM) == album, 'Unable to get artist album: %s' % AUDIO_ALBUM
assert artist.track(AUDIO_TRACK) == track, 'Unable to get artist track: %s' % AUDIO_TRACK
assert album.track(AUDIO_TRACK) == track, 'Unable to get album track: %s' % 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.'
#-----------------------
# Library Actions
#-----------------------
@register('action,movie')
def test_mark_movie_watched(plex, account=None):
movie = plex.library.section(MOVIE_SECTION).get(MOVIE_TITLE)
movie.markUnwatched()
log(2, 'Marking movie watched: %s' % movie)
log(2, 'View count: %s' % movie.viewCount)
movie.markWatched()
log(2, 'View count: %s' % movie.viewCount)
assert movie.viewCount == 1, 'View count 0 after watched.'
movie.markUnwatched()
log(2, 'View count: %s' % movie.viewCount)
assert movie.viewCount == 0, 'View count 1 after unwatched.'
@register('action')
def test_refresh_section(plex, account=None):
shows = plex.library.section(MOVIE_SECTION)
shows.refresh()
@register('action,movie')
def test_refresh_video(plex, account=None):
result = plex.search(MOVIE_TITLE)
result[0].refresh()
#-----------------------
# Playlists
#-----------------------
@register('playlist')
def test_list_playlists(plex, account=None):
playlists = plex.playlists()
for playlist in playlists:
log(2, playlist.title)
@register('playlist')
def test_create_playlist(plex, account=None):
# create the playlist
title = 'test_create_playlist'
log(2, 'Creating playlist %s..' % title)
episodes = plex.library.section(SHOW_SECTION).get(SHOW_TITLE).episodes()
playlist = plex.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)
log(2, '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
log(2, '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
log(2, '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]
log(2, '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()
@register('playlist,client')
def test_playlist(plex, account=None):
client = safe_client(CLIENT, CLIENT_BASEURL, plex)
artist = plex.library.section(AUDIO_SECTION).get(AUDIO_ARTIST)
album = artist.album(AUDIO_ALBUM)
playlist = plex.createPlaylist('test_play_playlist', album)
try:
log(2, 'Playing playlist: %s' % playlist)
client.playMedia(playlist); time.sleep(5)
log(2, 'stop..')
client.stop('music'); time.sleep(1)
finally:
playlist.delete()
@register('playlist,photos')
def test_playlist_photos(plex, account=None):
client = safe_client('iphone-mike', CLIENT_BASEURL, plex)
photosection = plex.library.section(PHOTO_SECTION)
album = photosection.get(PHOTO_ALBUM)
photos = album.photos()
playlist = plex.createPlaylist('test_play_playlist2', photos)
try:
client.playMedia(playlist)
for i in range(3):
time.sleep(2)
client.skipNext(mtype='photo')
finally:
playlist.delete()
@register('playlist,photos')
def test_play_photos(plex, account=None):
client = safe_client('iphone-mike', CLIENT_BASEURL, plex)
photosection = plex.library.section(PHOTO_SECTION)
album = photosection.get(PHOTO_ALBUM)
photos = album.photos()
for photo in photos[:4]:
client.playMedia(photo)
time.sleep(2)
#-----------------------
# Metadata
#-----------------------
@register('meta,movie')
def test_partial_video(plex, account=None):
movie_title = 'Bedside Detective'
result = plex.search(movie_title)
log(2, 'Title: %s' % result[0].title)
log(2, 'Original Title: %s' % result[0].originalTitle)
assert(result[0].originalTitle != NA)
@register('meta,movie,show')
def test_list_media_files(plex, account=None):
# Fetch file names from the tv show
episode_files = []
episode = plex.library.section(SHOW_SECTION).get(SHOW_TITLE).episodes()[-1]
log(2, 'Episode Files: %s' % episode)
for media in episode.media:
for part in media.parts:
log(4, part.file)
episode_files.append(part.file)
assert filter(None, episode_files), 'No show files have been listed.'
# Fetch file names from the movie
movie_files = []
movie = plex.library.section(MOVIE_SECTION).get(MOVIE_TITLE)
log(2, 'Movie Files: %s' % movie)
for media in movie.media:
for part in media.parts:
log(4, part.file)
movie_files.append(part.file)
assert filter(None, movie_files), 'No movie files have been listed.'
@register('meta,movie')
def test_list_video_tags(plex, account=None):
movies = plex.library.section(MOVIE_SECTION)
movie = movies.get(MOVIE_TITLE)
log(2, 'Countries: %s' % movie.countries[0:3])
log(2, 'Directors: %s' % movie.directors[0:3])
log(2, 'Genres: %s' % movie.genres[0:3])
log(2, 'Producers: %s' % movie.producers[0:3])
log(2, 'Actors: %s' % movie.actors[0:3])
log(2, 'Writers: %s' % movie.writers[0:3])
assert filter(None, movie.countries), 'No countries listed for movie.'
assert filter(None, movie.directors), 'No directors listed for movie.'
assert filter(None, movie.genres), 'No genres listed for movie.'
assert filter(None, movie.producers), 'No producers listed for movie.'
assert filter(None, movie.actors), 'No actors listed for movie.'
assert filter(None, movie.writers), 'No writers listed for movie.'
log(2, 'List movies with same director: %s' % movie.directors[0])
related = movies.search(None, director=movie.directors[0])
log(4, related[0:3])
assert movie in related, 'Movie was not found in related directors search.'
@register('client')
def test_list_video_streams(plex, account=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, account=None):
section = plex.library.section(AUDIO_SECTION)
artist = section.get(AUDIO_ARTIST)
track = artist.get(AUDIO_TRACK)
log(2, 'Countries: %s' % artist.countries[0:3])
log(2, 'Genres: %s' % artist.genres[0:3])
log(2, 'Similar: %s' % artist.similar[0:3])
log(2, 'Album Genres: %s' % track.album().genres[0:3])
log(2, 'Moods: %s' % track.moods[0:3])
log(2, 'Media: %s' % track.media[0:3])
assert filter(None, artist.countries), 'No countries listed for artist.'
assert filter(None, artist.genres), 'No genres listed for artist.'
assert filter(None, artist.similar), 'No similar artists listed.'
assert filter(None, track.album().genres), 'No genres listed for album.'
assert filter(None, track.moods), 'No moods listed for track.'
assert filter(None, track.media), 'No media listed for track.'
@register('meta,show')
def test_is_watched(plex, account=None):
show = plex.library.section(SHOW_SECTION).get(SHOW_TITLE)
episode = show.get(SHOW_EPISODE)
log(2, '%s isWatched: %s' % (episode.title, episode.isWatched))
movie = plex.library.section(MOVIE_SECTION).get(MOVIE_TITLE)
log(2, '%s isWatched: %s' % (movie.title, movie.isWatched))
@register('meta,movie')
def test_fetch_details_not_in_search_result(plex, account=None):
# Search results only contain 3 actors per movie. This text checks there
# are more than 3 results in the actor list (meaning it fetched the detailed
# information behind the scenes).
result = plex.search(MOVIE_TITLE)[0]
actors = result.actors
assert len(actors) >= 4, 'Unable to fetch detailed movie information'
log(2, '%s actors found.' % len(actors))
@register('movie,audio')
def test_stream_url(plex, account=None):
movie = plex.library.section(MOVIE_SECTION).get(MOVIE_TITLE)
episode = plex.library.section(SHOW_SECTION).get(SHOW_TITLE).episodes()[-1]
track = plex.library.section(AUDIO_SECTION).get(AUDIO_ARTIST).get(AUDIO_TRACK)
log(2, 'Movie: vlc "%s"' % movie.getStreamURL())
log(2, 'Episode: vlc "%s"' % episode.getStreamURL())
log(2, 'Track: cvlc "%s"' % track.getStreamURL())
@register('audio')
def test_list_audioalbums(plex, account=None):
music = plex.library.section(AUDIO_SECTION)
albums = music.albums()
for album in albums[:10]:
log(2, '%s - %s [%s]' % (album.artist().title, album.title, album.year))
@register('photo')
def test_list_photoalbums(plex, account=None):
photosection = plex.library.section(PHOTO_SECTION)
photoalbums = photosection.all()
log(2, 'Listing albums..')
for album in photoalbums[:10]:
log(4, '%s' % album.title)
assert len(photoalbums), 'No photoalbums found.'
album = photosection.get(PHOTO_ALBUM)
photos = album.photos()
for photo in photos[:10]:
log(4, '%s (%sx%s)' % (basename(photo.media[0].parts[0].file), photo.media[0].width, photo.media[0].height))
assert len(photoalbums), 'No photos found.'
#-----------------------
# Play Queue
#-----------------------
@register('queue')
def test_play_queues(plex, account=None):
episode = plex.library.section(SHOW_SECTION).get(SHOW_TITLE).get(SHOW_EPISODE)
playqueue = plex.createPlayQueue(episode)
assert len(playqueue.items) == 1, 'No items in play queue.'
assert playqueue.items[0].title == SHOW_EPISODE, 'Wrong show queued.'
assert playqueue.playQueueID, 'Play queue ID not set.'
#-----------------------
# Client
#-----------------------
@register('client')
def test_list_clients(plex, account=None):
clients = [c.title for c in plex.clients()]
log(2, 'Clients: %s' % ', '.join(clients or []))
assert clients, 'Server is not listing any clients.'
@register('client')
def test_client_navigation(plex, account=None):
client = safe_client(CLIENT, CLIENT_BASEURL, plex)
_navigate(plex, client)
@register('client,proxy')
def test_client_navigation_via_proxy(plex, account=None):
client = safe_client(CLIENT, CLIENT_BASEURL, plex)
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.title, 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, account=None):
client = safe_client(CLIENT, CLIENT_BASEURL, plex)
_video_playback(plex, client)
@register('client,proxy')
def test_video_playback_via_proxy(plex, account=None):
client = safe_client(CLIENT, CLIENT_BASEURL, plex)
client.proxyThroughServer()
_video_playback(plex, client)
def _video_playback(plex, client):
try:
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.title, 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)
finally:
log(2, 'Cleanup: Marking %s watched.' % movie.title)
movie.markWatched()
@register('client')
def test_client_timeline(plex, account=None):
client = safe_client(CLIENT, CLIENT_BASEURL, plex)
_test_timeline(plex, client)
@register('client,proxy')
def test_client_timeline_via_proxy(plex, account=None):
client = safe_client(CLIENT, CLIENT_BASEURL, plex)
client.proxyThroughServer()
_test_timeline(plex, client)
def _test_timeline(plex, client):
try:
mtype = 'video'
client = safe_client(CLIENT, CLIENT_BASEURL, plex)
movie = plex.library.section(MOVIE_SECTION).get(MOVIE_TITLE)
time.sleep(30) # previous test may have played media..
playing = client.isPlayingMedia()
log(2, 'Playing Media: %s' % playing)
assert playing is False, 'isPlayingMedia() should have returned False.'
client.playMedia(movie); time.sleep(30)
playing = client.isPlayingMedia()
log(2, 'Playing Media: %s' % playing)
assert playing is True, 'isPlayingMedia() should have returned True.'
client.stop(mtype); time.sleep(30)
playing = client.isPlayingMedia()
log(2, 'Playing Media: %s' % playing)
assert playing is False, 'isPlayingMedia() should have returned False.'
finally:
log(2, 'Cleanup: Marking %s watched.' % movie.title)
movie.markWatched()
# TODO: MAKE THIS WORK..
# @register('client')
def test_sync_items(plex, account=None):
device = account.getDevice('device-uuid')
# fetch the sync items via the device sync list
for item in device.sync_items():
# fetch the media object associated with the sync item
for video in item.get_media():
# fetch the media parts (actual video/audio streams) associated with the media
for part in video.iterParts():
print('Found media to download!')
# make the relevant sync id (media part) as downloaded
# this tells the server that this device has successfully downloaded this media part of this sync item
item.mark_as_done(part.sync_id)
#-----------------------
# MyPlex Resources
#-----------------------
@register('myplex')
def test_myplex_accounts(plex, account=None):
assert account, 'Must specify username, password & resource to run this test.'
log(2, 'MyPlexAccount:')
log(4, 'username: %s' % account.username)
log(4, 'authenticationToken: %s' % account.authenticationToken)
log(4, 'email: %s' % account.email)
log(4, 'home: %s' % account.home)
log(4, '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 = plex.account()
log(2, 'Local PlexServer.account():')
log(4, 'username: %s' % account.username)
log(4, 'authToken: %s' % account.authToken)
log(4, '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'
@register('myplex,resource')
def test_myplex_resources(plex, account=None):
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'
log(2, '%s (%s): %s' % (name, resource.product, connections))
assert resources, 'No resources found for account: %s' % account.name
@register('myplex,devices')
def test_myplex_devices(plex, account=None):
assert account, 'Must specify username, password & resource to run this test.'
devices = account.devices()
for device in devices:
name = device.name or 'Unknown'
connections = ', '.join(device.connections) if device.connections else 'None'
log(2, '%s (%s): %s' % (name, device.product, connections))
assert devices, 'No devices found for account: %s' % account.name
@register('myplex')
def test_myplex_users(plex, account=None):
users = account.users()
assert users, 'Found no users on account: %s' % account.name
log(2, 'Found %s users.' % len(users))
user = account.user('sdfsdfplex')
log(2, 'Found user: %s' % user)
assert users, 'Could not find user sdfsdfplex'
@register('myplex,devices')
def test_myplex_connect_to_device(plex, account=None):
assert account, 'Must specify username, password & resource to run this test.'
devices = account.devices()
for device in devices:
if device.name == CLIENT and len(device.connections):
break
client = device.connect()
log(2, 'Connected to client: %s (%s)' % (client.title, client.product))
assert client, 'Unable to connect to device'
if __name__ == '__main__':
# There are three ways to authenticate:
# 1. If the server is running on localhost, just run without any auth.
# 2. Pass in --username, --password, and --resource.
# 3. Pass in --baseurl, --token
parser = argparse.ArgumentParser(description='Run PlexAPI tests.')
parser.add_argument('-u', '--username', help='Username for your MyPlex account.')
parser.add_argument('-p', '--password', help='Password for your MyPlex account.')
parser.add_argument('-r', '--resource', help='Name of the Plex resource (requires user/pass).')
parser.add_argument('-b', '--baseurl', help='Baseurl needed for auth token authentication')
parser.add_argument('-t', '--token', help='Auth token (instead of user/pass)')
parser.add_argument('-q', '--query', help='Only run the specified tests.')
parser.add_argument('-v', '--verbose', default=False, action='store_true', help='Print verbose logging.')
args = parser.parse_args()
run_tests(__name__, args)

View file

@ -1,88 +0,0 @@
# -*- coding: utf-8 -*-
"""
Test Library Functions
"""
import sys, traceback
import datetime, time
from plexapi import server
from plexapi.client import PlexClient
from plexapi.exceptions import NotFound
from plexapi.myplex import MyPlexAccount
COLORS = {'blue':'\033[94m', 'green':'\033[92m', 'red':'\033[91m', 'yellow':'\033[93m', 'end':'\033[0m'}
registered = []
def register(tags=''):
def wrap2(func):
registered.append({'name':func.__name__, 'tags':tags.split(','), 'func':func})
def wrap1(*args, **kwargs): # noqa
func(*args, **kwargs)
return wrap1
return wrap2
def log(indent, message, color=None):
dt = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
if color:
return sys.stdout.write('%s: %s%s%s%s\n' % (dt, ' '*indent, COLORS[color], message, COLORS['end']))
return sys.stdout.write('%s: %s%s\n' % (dt, ' '*indent, message))
def fetch_server(args):
if args.resource and args.username and args.password:
log(0, 'Signing in as MyPlex account %s..' % args.username)
account = MyPlexAccount.signin(args.username, args.password)
log(0, 'Connecting to Plex server %s..' % args.resource)
return account.resource(args.resource).connect(), account
elif args.baseurl and args.token:
log(0, 'Connecting to Plex server %s..' % args.baseurl)
return server.PlexServer(args.baseurl, args.token), None
return server.PlexServer(), None
def safe_client(name, baseurl, server):
try:
return server.client(name)
except NotFound as err:
log(2, 'Warning: %s' % err)
return PlexClient(baseurl, server=server)
def iter_tests(query):
tags = query[5:].split(',') if query and query.startswith('tags:') else ''
for test in registered:
if not query:
yield test
elif tags:
matching_tags = [t for t in tags if t in test['tags']]
if matching_tags: yield test
elif query == test['name']:
yield test
def run_tests(module, args):
plex, account = fetch_server(args)
tests = {'passed':0, 'failed':0}
for test in iter_tests(args.query):
starttime = time.time()
log(0, '%s (%s)' % (test['name'], ','.join(test['tags'])))
try:
test['func'](plex, account)
runtime = time.time() - starttime
log(2, 'PASS! (runtime: %.3fs)' % runtime, 'blue')
tests['passed'] += 1
except Exception as err:
errstr = str(err)
errstr += '\n%s' % traceback.format_exc() if args.verbose else ''
log(2, 'FAIL!: %s' % errstr, 'red')
tests['failed'] += 1
log(0, '')
log(0, 'Tests Run: %s' % sum(tests.values()))
log(0, 'Tests Passed: %s' % tests['passed'])
if tests['failed']:
log(0, 'Tests Failed: %s' % tests['failed'], 'red')
if not tests['failed']:
log(0, '')
log(0, 'EVERYTHING OK!! :)')
raise SystemExit(tests['failed'])