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 syntax: glob
*.db *.db
*.egg-info
*.log *.log
*.pyc *.pyc
*.swp
*.sublime-* *.sublime-*
*.swp
*__pycache__* *__pycache__*
dist
build
*.egg-info
.idea/
lib/
bin/
include/
.cache/ .cache/
.idea/
.Python .Python
bin/
build
dist
docs/_build/
include/
lib/
pip-selfcheck.json pip-selfcheck.json
pyvenv.cfg 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, * Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution. 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 may be used to endorse or promote products derived from this software without
specific prior written permission. specific prior written permission.

View file

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

View file

@ -1,4 +1,10 @@
## PlexAPI ## ## 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. Python bindings for the Plex API.
* Navigate local or remote shared libraries. * Navigate local or remote shared libraries.
@ -43,11 +49,10 @@ plex = PlexServer(baseurl, token)
#### Usage Examples #### #### Usage Examples ####
```python ```python
# Example 1: List all unwatched content in library. # Example 1: List all unwatched movies.
for section in plex.library.sections(): movies = plex.library.section('Movies')
print('Unwatched content in %s:' % section.title) for video in movies.search(unwatched=True):
for video in section.unwatched(): print(video.title)
print(' %s' % video.title)
``` ```
```python ```python
# Example 2: Mark all Conan episodes watched. # 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 logging.handlers import RotatingFileHandler
from platform import uname from platform import uname
from plexapi.config import PlexConfig, reset_base_headers from plexapi.config import PlexConfig, reset_base_headers
from plexapi.utils import SecretsFilter
from uuid import getnode from uuid import getnode
# Load User Defined Config # 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) CONFIG = PlexConfig(CONFIG_PATH)
# Core Settings # Core Settings
@ -40,3 +41,6 @@ if logfile:
loghandler.setFormatter(logging.Formatter(logformat)) loghandler.setFormatter(logging.Formatter(logformat))
log.addHandler(loghandler) log.addHandler(loghandler)
log.setLevel(loglevel) log.setLevel(loglevel)
logfilter = SecretsFilter()
if CONFIG.get('logging.show_secrets') != 'true':
log.addFilter(logfilter)

View file

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from plexapi import media, utils from plexapi import media, utils
from plexapi.utils import Playable, PlexPartialObject from plexapi.utils import Playable, PlexPartialObject
@ -7,45 +6,37 @@ NA = utils.NA
class Audio(PlexPartialObject): 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: Parameters:
addedAt (int): int from epoch, datetime.datetime server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional)
index (sting): 1 data (ElementTree): Response from PlexServer used to build this object (optional).
key (str): Fx /library/metadata/102631 initpath (str): Relative path requested when retrieving specified `data` (optional).
lastViewedAt (datetime.datetime): parse int into datetime.datetime.
librarySectionID (int): Attributes:
listType (str): audio addedAt (datetime): Datetime this item was added to the library.
ratingKey (int): Unique key to identify this item index (sting): Index Number (often the track number).
summary (str): Summery of the artist, track, album key (str): API URL (/library/metadata/<ratingkey>).
thumb (str): Url to thumb image lastViewedAt (datetime): Datetime item was last accessed.
title (str): Fx Aerosmith librarySectionID (int): :class:`~plexapi.library.LibrarySection` ID.
titleSort (str): Defaults title if None listType (str): Hardcoded as 'audio' (useful for search filters).
TYPE (str): overwritten by subclass ratingKey (int): Unique key identifying this item.
type (string, NA): Description summary (str): Summary of the artist, track, or album.
updatedAt (datatime.datetime): parse int to datetime.datetime thumb (str): URL to thumbnail image.
viewCount (int): How many time has this item been played 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 TYPE = None
def __init__(self, server, data, initpath): 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) super(Audio, self).__init__(data, initpath, server)
def _loadData(self, data): def _loadData(self, data):
"""Used to set the attributes. """ Load attribute values from Plex XML response. """
Args:
data (Element): XML reponse from PMS as Element
normally built from server.query
"""
self.listType = 'audio' self.listType = 'audio'
self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA)) self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA))
self.index = data.attrib.get('index', NA) self.index = data.attrib.get('index', NA)
@ -63,125 +54,133 @@ class Audio(PlexPartialObject):
@property @property
def thumbUrl(self): def thumbUrl(self):
"""Return url to thumb image.""" """ Returns the URL to this items thumbnail image. """
if self.thumb: if self.thumb:
return self.server.url(self.thumb) return self.server.url(self.thumb)
def refresh(self): 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) self.server.query('%s/refresh' % self.key, method=self.server.session.put)
def section(self): def section(self):
"""Library section.""" """ Returns the :class:`~plexapi.library.LibrarySection` this item belongs to. """
return self.server.library.sectionByID(self.librarySectionID) return self.server.library.sectionByID(self.librarySectionID)
@utils.register_libtype @utils.register_libtype
class Artist(Audio): class Artist(Audio):
"""Artist. """ Represents a single audio artist.
Attributes: Parameters:
art (str): /library/metadata/102631/art/1469310342 server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional)
countries (list): List of media.County fx [<Country:24200:United.States>] data (ElementTree): Response from PlexServer used to build this object (optional).
genres (list): List of media.Genre fx [<Genre:25555:Classic.Rock>] initpath (str): Relative path requested when retrieving specified `data` (optional).
guid (str): Fx guid com.plexapp.agents.plexmusic://gracenote/artist/05517B8701668D28?lang=en
key (str): Fx /library/metadata/102631 Attributes:
location (str): Filepath art (str): Artist artwork (/library/metadata/<ratingkey>/art/<artid>)
similar (list): List of media.Similar fx [<Similar:25220:Guns.N'.Roses>] countries (list): List of :class:`~plexapi.media.Country` objects this artist respresents.
TYPE (str): artist 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' TYPE = 'artist'
def _loadData(self, data): def _loadData(self, data):
"""Used to set the attributes. """ Load attribute values from Plex XML response. """
Args:
data (Element): XML reponse from PMS as Element
normally built from server.query
"""
Audio._loadData(self, data) Audio._loadData(self, data)
self.art = data.attrib.get('art', NA) self.art = data.attrib.get('art', NA)
self.guid = data.attrib.get('guid', NA) self.guid = data.attrib.get('guid', NA)
self.key = self.key.replace('/children', '') # FIX_BUG_50 self.key = self.key.replace('/children', '') # FIX_BUG_50
self.location = utils.findLocations(data, single=True) self.location = utils.findLocations(data, single=True)
if self.isFullObject(): # check if this is needed if self.isFullObject(): # check if this is needed
self.countries = [media.Country(self.server, e) self.countries = [media.Country(self.server, e) for e in data if e.tag == media.Country.TYPE]
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.genres = [media.Genre(self.server, e) self.similar = [media.Similar(self.server, e) for e in data if e.tag == media.Similar.TYPE]
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): 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 path = '%s/children' % self.key
return utils.listItems(self.server, path, Album.TYPE) return utils.listItems(self.server, path, Album.TYPE)
def album(self, title): 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 path = '%s/children' % self.key
return utils.findItem(self.server, path, title) return utils.findItem(self.server, path, title)
def tracks(self, watched=None): def tracks(self):
"""Return all tracks to this artist. """ Returns a list of :class:`~plexapi.audio.Track` objects by this artist. """
Args:
watched(None, False, True): Default to None.
Returns:
List: of Track
"""
path = '%s/allLeaves' % self.key path = '%s/allLeaves' % self.key
return utils.listItems(self.server, path, watched=watched) return utils.listItems(self.server, path)
def track(self, title): def track(self, title):
"""Return a Track that matches title. """ Returns the :class:`~plexapi.audio.Track` that matches the specified title.
Args: Parameters:
title (str): Fx song name title (str): Title of the track to return.
Returns:
Track:
""" """
path = '%s/allLeaves' % self.key path = '%s/allLeaves' % self.key
return utils.findItem(self.server, path, title) return utils.findItem(self.server, path, title)
def get(self, title): def get(self, title):
"""Alias. See track.""" """ Alias of :func:`~plexapi.audio.Artist.track`. """
return self.track(title) 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 @utils.register_libtype
class Album(Audio): class Album(Audio):
"""Album. """ Represents a single audio album.
Attributes: Parameters:
art (str): Fx /library/metadata/102631/art/1469310342 server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional)
genres (list): List of media.Genre data (ElementTree): Response from PlexServer used to build this object (optional).
key (str): Fx /library/metadata/102632 initpath (str): Relative path requested when retrieving specified `data` (optional).
originallyAvailableAt (TYPE): Description
parentKey (str): /library/metadata/102631 Attributes:
parentRatingKey (int): Fx 1337 art (str): Album artwork (/library/metadata/<ratingkey>/art/<artid>)
parentThumb (TYPE): Relative url to parent thumb image genres (list): List of :class:`~plexapi.media.Genre` objects this album respresents.
parentTitle (str): Aerosmith key (str): API URL (/library/metadata/<ratingkey>).
studio (str): originallyAvailableAt (datetime): Datetime this album was released.
TYPE (str): album parentKey (str): API URL of this artist.
year (int): 1999 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' TYPE = 'album'
def _loadData(self, data): def _loadData(self, data):
"""Used to set the attributes. """ Load attribute values from Plex XML response. """
Args:
data (Element): XML reponse from PMS as Element
normally built from server.query
"""
Audio._loadData(self, data) Audio._loadData(self, data)
self.art = data.attrib.get('art', NA) 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.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
self.parentKey = data.attrib.get('parentKey', NA) self.parentKey = data.attrib.get('parentKey', NA)
self.parentRatingKey = data.attrib.get('parentRatingKey', NA) self.parentRatingKey = data.attrib.get('parentRatingKey', NA)
@ -192,88 +191,91 @@ class Album(Audio):
if self.isFullObject(): if self.isFullObject():
self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE] self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE]
def tracks(self, watched=None): def tracks(self):
"""Return all tracks to this album. """ Returns a list of :class:`~plexapi.audio.Track` objects in this album. """
Args:
watched(None, False, True): Default to None.
Returns:
List: of Track
"""
path = '%s/children' % self.key path = '%s/children' % self.key
return utils.listItems(self.server, path, watched=watched) return utils.listItems(self.server, path)
def track(self, title): def track(self, title):
"""Return a Track that matches title. """ Returns the :class:`~plexapi.audio.Track` that matches the specified title.
Args: Parameters:
title (str): Fx song name title (str): Title of the track to return.
Returns:
Track:
""" """
path = '%s/children' % self.key path = '%s/children' % self.key
return utils.findItem(self.server, path, title) return utils.findItem(self.server, path, title)
def get(self, title): def get(self, title):
"""Alias. See track.""" """ Alias of :func:`~plexapi.audio.Album.track`. """
return self.track(title) return self.track(title)
def artist(self): def artist(self):
"""Return Artist of this album.""" """ Return :func:`~plexapi.audio.Artist` of this album. """
return utils.listItems(self.server, self.parentKey)[0] return utils.listItems(self.server, self.parentKey)[0]
def watched(self): def download(self, savepath=None, keep_orginal_name=False, **kwargs):
"""Return Track that is lisson on.""" """ Downloads all tracks for this artist to the specified location.
return self.tracks(watched=True)
def unwatched(self): Parameters:
"""Return Track that is not lisson on.""" savepath (str): Title of the track to return.
return self.tracks(watched=False) 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)
return downloaded
@utils.register_libtype @utils.register_libtype
class Track(Audio, Playable): class Track(Audio, Playable):
"""Track. """ Represents a single audio track.
Attributes: Parameters:
art (str): Relative path fx /library/metadata/102631/art/1469310342 server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional)
chapterSource (TYPE): Description data (ElementTree): XML response from PlexServer used to build this object (optional).
duration (TYPE): Description initpath (str): Relative path requested when retrieving specified `data` (optional).
grandparentArt (str): Relative path
grandparentKey (str): Relative path Fx /library/metadata/102631 Attributes:
grandparentRatingKey (TYPE): Description art (str): Track artwork (/library/metadata/<ratingkey>/art/<artid>)
grandparentThumb (str): Relative path to Artist thumb img chapterSource (TYPE): Unknown
grandparentTitle (str): Aerosmith duration (int): Length of this album in seconds.
guid (TYPE): Description grandparentArt (str): Artist artowrk.
media (list): List of media.Media grandparentKey (str): Artist API URL.
moods (list): List of media.Moods grandparentRatingKey (str): Unique key identifying artist.
originalTitle (str): Some track title grandparentThumb (str): URL to artist thumbnail image.
parentIndex (int): 1 grandparentTitle (str): Name of the artist for this track.
parentKey (str): Relative path Fx /library/metadata/102632 guid (str): Unknown (unique ID).
parentRatingKey (int): 1337 media (list): List of :class:`~plexapi.media.Media` objects for this track.
parentThumb (str): Relative path to Album thumb moods (list): List of :class:`~plexapi.media.Mood` objects for this track.
parentTitle (str): Album title originalTitle (str): Original track title (if translated).
player (None): #TODO parentIndex (int): Album index.
primaryExtraKey (TYPE): #TODO parentKey (str): Album API URL.
ratingCount (int): 10 parentRatingKey (int): Unique key identifying album.
sessionKey (int): Description parentThumb (str): URL to album thumbnail image.
transcodeSession (None): parentTitle (str): Name of the album for this track.
TYPE (str): track primaryExtraKey (str): Unknown
username (str): username@mail.com ratingCount (int): Rating of this track (1-10?)
viewOffset (int): 100 viewOffset (int): Unknown
year (int): 1999 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' TYPE = 'track'
def _loadData(self, data): def _loadData(self, data):
"""Used to set the attributes """ Load attribute values from Plex XML response. """
Args:
data (Element): Usually built from server.query
"""
Audio._loadData(self, data) Audio._loadData(self, data)
Playable._loadData(self, data) Playable._loadData(self, data)
self.art = data.attrib.get('art', NA) 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.ratingCount = utils.cast(int, data.attrib.get('ratingCount', NA))
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
self.year = utils.cast(int, data.attrib.get('year', NA)) 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 if self.isFullObject(): # check me
self.moods = [media.Mood(self.server, e) self.moods = [media.Mood(self.server, e) for e in data if e.tag == media.Mood.TYPE]
for e in data if e.tag == media.Mood.TYPE] #self.media = [media.Media(self.server, e, self.initpath, self)
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]
# data for active sessions and history # data for active sessions and history
self.sessionKey = utils.cast(int, data.attrib.get('sessionKey', NA)) self.sessionKey = utils.cast(int, data.attrib.get('sessionKey', NA))
self.username = utils.findUsername(data) self.username = utils.findUsername(data)
@ -308,14 +312,18 @@ class Track(Audio, Playable):
@property @property
def thumbUrl(self): def thumbUrl(self):
"""Return url to thumb image.""" """ Returns the URL thumbnail image for this track's album. """
if self.parentThumb: if self.parentThumb:
return self.server.url(self.parentThumb) return self.server.url(self.parentThumb)
def album(self): def album(self):
"""Return this track's Album.""" """ Return this track's :class:`~plexapi.audio.Album`. """
return utils.listItems(self.server, self.parentKey)[0] return utils.listItems(self.server, self.parentKey)[0]
def artist(self): def artist(self):
"""Return this track's Artist.""" """ Return this track's :class:`~plexapi.audio.Artist`. """
return utils.listItems(self.server, self.grandparentKey)[0] 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 -*- # -*- 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 import requests
from requests.status_codes import _codes as codes from requests.status_codes import _codes as codes
from plexapi import BASE_HEADERS, TIMEOUT, log, utils from plexapi import BASE_HEADERS, TIMEOUT, log, utils
@ -13,65 +7,64 @@ from xml.etree import ElementTree
class PlexClient(object): 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: Parameters:
baseurl (str): http adress for the client baseurl (str): HTTP URL to connect dirrectly to this client.
device (None): Description token (str): X-Plex-Token used for authenication (optional).
deviceClass (sting): pc, phone session (:class:`~requests.Session`): requests.Session object if you want more control (optional).
machineIdentifier (str): uuid fx 5471D9EA-1467-4051-9BE7-FCBDF490ACE3 server (:class:`~plexapi.server.PlexServer`): PlexServer this client is connected to (optional)
model (TYPE): Description data (ElementTree): Response from PlexServer used to build this object (optional).
platform (TYPE): Description
platformVersion (TYPE): Description Attributes:
product (str): plex for ios baseurl (str): HTTP address of the client
protocol (str): plex device (str): Best guess on the type of device this is (PS, iPhone, Linux, etc).
protocolCapabilities (list): List of what client can do deviceClass (str): Device class (pc, phone, etc).
protocolVersion (str): 1 machineIdentifier (str): Unique ID for this device.
server (plexapi.server.Plexserver): PMS your connected to model (str): Unknown
session (None or requests.Session): Add your own session object to cache stuff platform (str): Unknown
state (None): Description platformVersion (str): Description
title (str): fx Johns Iphone product (str): Client Product (Plex for iOS, etc).
token (str): X-Plex-Token, using for authenication with PMS protocol (str): Always seems ot be 'plex'.
vendor (str): Description protocolCapabilities (list<str>): List of client capabilities (navigation, playback,
version (str): fx. 4.6 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): 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.baseurl = baseurl.strip('/')
self.token = token self.token = token
self.session = session or requests.Session()
self.server = server 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._loadData(data) if data is not None else self.connect()
self._proxyThroughServer = False self._proxyThroughServer = False
self._commandId = 0 self._commandId = 0
def _loadData(self, data): def _loadData(self, data):
"""Sets attrs to the class. """ Load attribute values from Plex XML response. """
Args:
data (Element): XML response from PMS as a Element
"""
self.deviceClass = data.attrib.get('deviceClass') self.deviceClass = data.attrib.get('deviceClass')
self.machineIdentifier = data.attrib.get('machineIdentifier') self.machineIdentifier = data.attrib.get('machineIdentifier')
self.product = data.attrib.get('product') self.product = data.attrib.get('product')
self.protocol = data.attrib.get('protocol') self.protocol = data.attrib.get('protocol')
self.protocolCapabilities = data.attrib.get( self.protocolCapabilities = data.attrib.get('protocolCapabilities', '').split(',')
'protocolCapabilities', '').split(',')
self.protocolVersion = data.attrib.get('protocolVersion') self.protocolVersion = data.attrib.get('protocolVersion')
self.platform = data.attrib.get('platform') self.platform = data.attrib.get('platform')
self.platformVersion = data.attrib.get('platformVersion') self.platformVersion = data.attrib.get('platformVersion')
self.title = data.attrib.get('title') or data.attrib.get('name') self.title = data.attrib.get('title') or data.attrib.get('name')
# active session details # Active session details
self.device = data.attrib.get('device') self.device = data.attrib.get('device')
self.model = data.attrib.get('model') self.model = data.attrib.get('model')
self.state = data.attrib.get('state') self.state = data.attrib.get('state')
@ -79,7 +72,11 @@ class PlexClient(object):
self.version = data.attrib.get('version') self.version = data.attrib.get('version')
def connect(self): 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: try:
data = self.query('/resources')[0] data = self.query('/resources')[0]
self._loadData(data) self._loadData(data)
@ -88,43 +85,38 @@ class PlexClient(object):
raise NotFound('No client found at: %s' % self.baseurl) raise NotFound('No client found at: %s' % self.baseurl)
def headers(self): def headers(self):
"""Default headers """ Returns a dict of all default headers for Client requests. """
Returns:
dict: default headers
"""
headers = BASE_HEADERS headers = BASE_HEADERS
if self.token: if self.token:
headers['X-Plex-Token'] = self.token headers['X-Plex-Token'] = self.token
return headers return headers
def proxyThroughServer(self, value=True): 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: Parameters:
value (bool, optional): Description value (bool): Enable or disable proxying (optional, default True).
Raises: Raises:
Unsupported: Cannot use client proxy with unknown server. :class:`~plexapi.exceptions.Unsupported`: Cannot use client proxy with unknown server.
""" """
if value is True and not self.server: if value is True and not self.server:
raise Unsupported('Cannot use client proxy with unknown server.') raise Unsupported('Cannot use client proxy with unknown server.')
self._proxyThroughServer = value self._proxyThroughServer = value
def query(self, path, method=None, headers=None, **kwargs): 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: Parameters:
path (str): Relative path path (str): Relative path to query.
method (None, optional): requests.post etc method (func): `self.session.get` or `self.session.post`
headers (None, optional): Set headers manually headers (dict): Additional headers to include or override in the request.
**kwargs (TYPE): Passord to the http request used for filter, sorting. **kwargs (TYPE): Additional arguments to inclde in the request.<method> call.
Returns: Raises:
Element :class:`~plexapi.exceptions.BadRequest`: When the response is not in [200, 201]
Raises:
BadRequest: Http error and code
""" """
url = self.url(path) url = self.url(path)
method = method or self.session.get method = method or self.session.get
@ -138,24 +130,23 @@ class PlexClient(object):
return ElementTree.fromstring(data) if data else None return ElementTree.fromstring(data) if data else None
def sendCommand(self, command, proxy=None, **params): 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: Parameters:
command (str): See the commands listed below command (str): Command to be sent in for format '<controller>/<command>'.
proxy (None, optional): Description proxy (bool): Set True to proxy this command through the PlexServer.
**params (dict): Description **params (dict): Additional GET parameters to include with the command.
Returns: Raises:
Element :class:`~plexapi.exceptions.Unsupported`: When we detect the client doesn't support this capability.
Raises:
Unsupported: Unsupported clients
""" """
command = command.strip('/') command = command.strip('/')
controller = command.split('/')[0] controller = command.split('/')[0]
if controller not in self.protocolCapabilities: if controller not in self.protocolCapabilities:
raise Unsupported( raise Unsupported('Client %s does not support the %s controller.' %
'Client %s does not support the %s controller.' % (self.title, controller)) (self.title, controller))
path = '/player/%s%s' % (command, utils.joinArgs(params)) path = '/player/%s%s' % (command, utils.joinArgs(params))
headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier} headers = {'X-Plex-Target-Client-Identifier': self.machineIdentifier}
self._commandId += 1 self._commandId += 1
@ -167,82 +158,87 @@ class PlexClient(object):
return self.query(path, headers=headers) return self.query(path, headers=headers)
def url(self, path): def url(self, path):
"""Return a full url """ Given a path, this retuns the full PlexClient the PlexServer URL to request.
Args: Parameters:
path (str): Relative path path (str): Relative path to be converted.
Returns:
string: full path to PMS
""" """
if self.token: if self.token:
delim = '&' if '?' in path else '?' delim = '&' if '?' in path else '?'
return '%s%s%sX-Plex-Token=%s' % (self.baseurl, path, delim, self.token) return '%s%s%sX-Plex-Token=%s' % (self.baseurl, path, delim, self.token)
return '%s%s' % (self.baseurl, path) return '%s%s' % (self.baseurl, path)
#---------------------
# Navigation Commands # Navigation Commands
# These commands navigate around the user-interface. # These commands navigate around the user-interface.
def contextMenu(self): def contextMenu(self):
"""Open the context menu on the client.""" """ Open the context menu on the client. """
self.sendCommand('navigation/contextMenu') self.sendCommand('navigation/contextMenu')
def goBack(self): def goBack(self):
"""One step back""" """ Navigate back one position. """
self.sendCommand('navigation/back') self.sendCommand('navigation/back')
def goToHome(self): def goToHome(self):
"""Jump to home screen.""" """ Go directly to the home screen. """
self.sendCommand('navigation/home') self.sendCommand('navigation/home')
def goToMusic(self): def goToMusic(self):
"""Jump to music.""" """ Go directly to the playing music panel. """
self.sendCommand('navigation/music') self.sendCommand('navigation/music')
def moveDown(self): def moveDown(self):
"""One step down.""" """ Move selection down a position. """
self.sendCommand('navigation/moveDown') self.sendCommand('navigation/moveDown')
def moveLeft(self): def moveLeft(self):
""" Move selection left a position. """
self.sendCommand('navigation/moveLeft') self.sendCommand('navigation/moveLeft')
def moveRight(self): def moveRight(self):
""" Move selection right a position. """
self.sendCommand('navigation/moveRight') self.sendCommand('navigation/moveRight')
def moveUp(self): def moveUp(self):
""" Move selection up a position. """
self.sendCommand('navigation/moveUp') self.sendCommand('navigation/moveUp')
def nextLetter(self): def nextLetter(self):
"""Jump to the next letter in the alphabeth.""" """ Jump to next letter in the alphabet. """
self.sendCommand('navigation/nextLetter') self.sendCommand('navigation/nextLetter')
def pageDown(self): def pageDown(self):
""" Move selection down a full page. """
self.sendCommand('navigation/pageDown') self.sendCommand('navigation/pageDown')
def pageUp(self): def pageUp(self):
""" Move selection up a full page. """
self.sendCommand('navigation/pageUp') self.sendCommand('navigation/pageUp')
def previousLetter(self): def previousLetter(self):
""" Jump to previous letter in the alphabet. """
self.sendCommand('navigation/previousLetter') self.sendCommand('navigation/previousLetter')
def select(self): def select(self):
""" Select element at the current position. """
self.sendCommand('navigation/select') self.sendCommand('navigation/select')
def toggleOSD(self): def toggleOSD(self):
""" Toggle the on screen display during playback. """
self.sendCommand('navigation/toggleOSD') self.sendCommand('navigation/toggleOSD')
def goToMedia(self, media, **params): def goToMedia(self, media, **params):
"""Go to a media on the client. """ Navigate directly to the specified media page.
Args: Parameters:
media (str): movie, music, photo media (:class:`~plexapi.media.Media`): Media object to navigate to.
**params (TYPE): Description # todo **params (dict): Additional GET parameters to include with the command.
Raises: Raises:
Unsupported: Description :class:`~plexapi.exceptions.Unsupported`: When no PlexServer specified in this object.
""" """
if not self.server: if not self.server:
raise Unsupported( raise Unsupported('A server must be specified before using this command.')
'A server must be specified before using this command.')
server_url = media.server.baseurl.split(':') server_url = media.server.baseurl.split(':')
self.sendCommand('mirror/details', **dict({ self.sendCommand('mirror/details', **dict({
'machineIdentifier': self.server.machineIdentifier, 'machineIdentifier': self.server.machineIdentifier,
@ -251,192 +247,184 @@ class PlexClient(object):
'key': media.key, 'key': media.key,
}, **params)) }, **params))
#-------------------
# Playback Commands # Playback Commands
# Most of the playback commands take a mandatory mtype {'music','photo','video'} argument, # 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 # 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 # is in case there are multiple things happening (e.g. music in the background, photo
# slideshow in the foreground). # slideshow in the foreground).
def pause(self, mtype): def pause(self, mtype):
"""Pause playback """ Pause the currently playing media type.
Args: Parameters:
mtype (str): music, photo, video mtype (str): Media type to take action against (music, photo, video).
""" """
self.sendCommand('playback/pause', type=mtype) self.sendCommand('playback/pause', type=mtype)
def play(self, mtype): def play(self, mtype):
"""Start playback """ Start playback for the specified media type.
Args: Parameters:
mtype (str): music, photo, video mtype (str): Media type to take action against (music, photo, video).
""" """
self.sendCommand('playback/play', type=mtype) self.sendCommand('playback/play', type=mtype)
def refreshPlayQueue(self, playQueueID, mtype=None): def refreshPlayQueue(self, playQueueID, mtype=None):
"""Summary """ Refresh the specified Playqueue.
Args:
playQueueID (TYPE): Description
mtype (None, optional): photo, video, music
Parameters:
playQueueID (str): Playqueue ID.
mtype (str): Media type to take action against (music, photo, video).
""" """
self.sendCommand( self.sendCommand(
'playback/refreshPlayQueue', playQueueID=playQueueID, type=mtype) 'playback/refreshPlayQueue', playQueueID=playQueueID, type=mtype)
def seekTo(self, offset, mtype=None): def seekTo(self, offset, mtype=None):
"""Seek to a time in a plaback. """ Seek to the specified offset (ms) during playback.
Args:
offset (int): in milliseconds
mtype (None, optional): photo, video, music
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) self.sendCommand('playback/seekTo', offset=offset, type=mtype)
def skipNext(self, mtype=None): def skipNext(self, mtype=None):
"""Skip to next """ Skip to the next playback item.
Args: Parameters:
mtype (None, string, optional): photo, video, music mtype (str): Media type to take action against (music, photo, video).
""" """
self.sendCommand('playback/skipNext', type=mtype) self.sendCommand('playback/skipNext', type=mtype)
def skipPrevious(self, mtype=None): def skipPrevious(self, mtype=None):
"""Skip to previous """ Skip to previous playback item.
Args: Parameters:
mtype (None, optional): Description mtype (str): Media type to take action against (music, photo, video).
""" """
self.sendCommand('playback/skipPrevious', type=mtype) self.sendCommand('playback/skipPrevious', type=mtype)
def skipTo(self, key, mtype=None): def skipTo(self, key, mtype=None):
"""Jump to """ Skip to the playback item with the specified key.
Args: Parameters:
key (TYPE): # what is this key (str): Key of the media item to skip to.
mtype (None, optional): photo, video, music mtype (str): Media type to take action against (music, photo, video).
Returns:
TYPE: Description
""" """
# skips to item with matching key
self.sendCommand('playback/skipTo', key=key, type=mtype) self.sendCommand('playback/skipTo', key=key, type=mtype)
def stepBack(self, mtype=None): def stepBack(self, mtype=None):
""" """ Step backward a chunk of time in the current playback item.
Args: Parameters:
mtype (None, optional): photo, video, music mtype (str): Media type to take action against (music, photo, video).
""" """
self.sendCommand('playback/stepBack', type=mtype) self.sendCommand('playback/stepBack', type=mtype)
def stepForward(self, mtype): def stepForward(self, mtype):
"""Summary """ Step forward a chunk of time in the current playback item.
Args: Parameters:
mtype (TYPE): Description mtype (str): Media type to take action against (music, photo, video).
Returns:
TYPE: Description
""" """
self.sendCommand('playback/stepForward', type=mtype) self.sendCommand('playback/stepForward', type=mtype)
def stop(self, mtype): def stop(self, mtype):
"""Stop playback """ Stop the currently playing item.
Args:
mtype (str): video, music, photo
Parameters:
mtype (str): Media type to take action against (music, photo, video).
""" """
self.sendCommand('playback/stop', type=mtype) self.sendCommand('playback/stop', type=mtype)
def setRepeat(self, repeat, mtype): def setRepeat(self, repeat, mtype):
"""Summary """ Enable repeat for the specified playback items.
Args: Parameters:
repeat (int): 0=off, 1=repeatone, 2=repeatall repeat (int): Repeat mode (0=off, 1=repeatone, 2=repeatall).
mtype (TYPE): video, music, photo mtype (str): Media type to take action against (music, photo, video).
""" """
self.setParameters(repeat=repeat, mtype=mtype) self.setParameters(repeat=repeat, mtype=mtype)
def setShuffle(self, shuffle, mtype): def setShuffle(self, shuffle, mtype):
"""Set shuffle """ Enable shuffle for the specified playback items.
Args: Parameters:
shuffle (int): 0=off, 1=on shuffle (int): Shuffle mode (0=off, 1=on)
mtype (TYPE): Description mtype (str): Media type to take action against (music, photo, video).
""" """
self.setParameters(shuffle=shuffle, mtype=mtype) self.setParameters(shuffle=shuffle, mtype=mtype)
def setVolume(self, volume, mtype): def setVolume(self, volume, mtype):
"""Change volume """ Enable volume for the current playback item.
Args: Parameters:
volume (int): 0-100 volume (int): Volume level (0-100).
mtype (TYPE): Description mtype (str): Media type to take action against (music, photo, video).
""" """
self.setParameters(volume=volume, mtype=mtype) self.setParameters(volume=volume, mtype=mtype)
def setAudioStream(self, audioStreamID, mtype): def setAudioStream(self, audioStreamID, mtype):
"""Select a audio stream """ Select the audio stream for the current playback item (only video).
Args: Parameters:
audioStreamID (TYPE): Description audioStreamID (str): ID of the audio stream from the media object.
mtype (str): video, music, photo mtype (str): Media type to take action against (music, photo, video).
""" """
self.setStreams(audioStreamID=audioStreamID, mtype=mtype) self.setStreams(audioStreamID=audioStreamID, mtype=mtype)
def setSubtitleStream(self, subtitleStreamID, mtype): def setSubtitleStream(self, subtitleStreamID, mtype):
"""Select a subtitle """ Select the subtitle stream for the current playback item (only video).
Args: Parameters:
subtitleStreamID (TYPE): Description subtitleStreamID (str): ID of the subtitle stream from the media object.
mtype (str): video, music, photo mtype (str): Media type to take action against (music, photo, video).
""" """
self.setStreams(subtitleStreamID=subtitleStreamID, mtype=mtype) self.setStreams(subtitleStreamID=subtitleStreamID, mtype=mtype)
def setVideoStream(self, videoStreamID, mtype): def setVideoStream(self, videoStreamID, mtype):
"""Summary """ Select the video stream for the current playback item (only video).
Args:
videoStreamID (TYPE): Description
mtype (str): video, music, photo
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) self.setStreams(videoStreamID=videoStreamID, mtype=mtype)
def playMedia(self, media, **params): def playMedia(self, media, offset=0, **params):
"""Start playback on a media item. """ Start playback of the specified media item. See also:
Args: Parameters:
media (str): movie, music, photo media (:class:`~plexapi.media.Media`): Media item to be played back (movie, music, photo).
**params (TYPE): Description 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
Raises: Raises:
Unsupported: Description :class:`~plexapi.exceptions.Unsupported`: When no PlexServer specified in this object.
""" """
if not self.server: if not self.server:
raise Unsupported( raise Unsupported('A server must be specified before using this command.')
'A server must be specified before using this command.')
server_url = media.server.baseurl.split(':') server_url = media.server.baseurl.split(':')
playqueue = self.server.createPlayQueue(media) playqueue = self.server.createPlayQueue(media)
self.sendCommand('playback/playMedia', **dict({ self.sendCommand('playback/playMedia', **dict({
'machineIdentifier': self.server.machineIdentifier, 'machineIdentifier': self.server.machineIdentifier,
'address': server_url[1].strip('/'), 'address': server_url[1].strip('/'),
'port': server_url[-1], 'port': server_url[-1],
'offset': offset,
'key': media.key, 'key': media.key,
'containerKey': '/playQueues/%s?window=100&own=1' % playqueue.playQueueID, 'containerKey': '/playQueues/%s?window=100&own=1' % playqueue.playQueueID,
}, **params)) }, **params))
def setParameters(self, volume=None, shuffle=None, repeat=None, mtype=None): def setParameters(self, volume=None, shuffle=None, repeat=None, mtype=None):
"""Set params for the client """ Set multiple playback parameters at once.
Args: Parameters:
volume (None, optional): 0-100 volume (int): Volume level (0-100; optional).
shuffle (None, optional): 0=off, 1=on shuffle (int): Shuffle mode (0=off, 1=on; optional).
repeat (None, optional): 0=off, 1=repeatone, 2=repeatall repeat (int): Repeat mode (0=off, 1=repeatone, 2=repeatall; optional).
mtype (None, optional): music,photo,video mtype (str): Media type to take action against (optional music, photo, video).
""" """
params = {} params = {}
if repeat is not None: if repeat is not None:
@ -449,15 +437,14 @@ class PlexClient(object):
params['type'] = mtype params['type'] = mtype
self.sendCommand('playback/setParameters', **params) self.sendCommand('playback/setParameters', **params)
def setStreams(self, audioStreamID=None, subtitleStreamID=None, def setStreams(self, audioStreamID=None, subtitleStreamID=None, videoStreamID=None, mtype=None):
videoStreamID=None, mtype=None): """ Select multiple playback streams at once.
"""Select streams.
Args: Parameters:
audioStreamID (None, optional): Description audioStreamID (str): ID of the audio stream from the media object.
subtitleStreamID (None, optional): Description subtitleStreamID (str): ID of the subtitle stream from the media object.
videoStreamID (None, optional): Description videoStreamID (str): ID of the video stream from the media object.
mtype (None, optional): music,photo,video mtype (str): Media type to take action against (optional music, photo, video).
""" """
params = {} params = {}
if audioStreamID is not None: if audioStreamID is not None:
@ -470,19 +457,18 @@ class PlexClient(object):
params['type'] = mtype params['type'] = mtype
self.sendCommand('playback/setStreams', **params) self.sendCommand('playback/setStreams', **params)
#-------------------
# Timeline Commands # Timeline Commands
def timeline(self): def timeline(self):
"""Timeline""" """ Poll the current timeline and return the XML response. """
return self.sendCommand('timeline/poll', **{'wait': 1, 'commandID': 4}) return self.sendCommand('timeline/poll', **{'wait': 1, 'commandID': 4})
def isPlayingMedia(self, includePaused=False): def isPlayingMedia(self, includePaused=False):
"""Check timeline if anything is playing """ Returns True if any media is currently playing.
Args: Parameters:
includePaused (bool, optional): Should paused be included includePaused (bool): Set True to treat currently paused items
as playing (optional; default True).
Returns:
bool
""" """
for mediatype in self.timeline(): for mediatype in self.timeline():
if mediatype.get('state') == 'playing': if mediatype.get('state') == 'playing':

View file

@ -1,9 +1,11 @@
# -*- coding: utf-8 -*- # -*- 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: try:
from urllib.parse import urlencode from urllib.parse import urlencode
@ -20,8 +22,13 @@ try:
except ImportError: except ImportError:
from urllib import unquote from urllib import unquote
try: try:
from configparser import ConfigParser from configparser import ConfigParser
except ImportError: except ImportError:
from ConfigParser import ConfigParser 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 -*- # -*- 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 from collections import defaultdict
try: from plexapi.compat import ConfigParser
from ConfigParser import ConfigParser # Python2
except ImportError:
from configparser import ConfigParser # Python3
class PlexConfig(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): def __init__(self, path):
ConfigParser.__init__(self) ConfigParser.__init__(self)
self.read(path) self.read(path)
self.data = self._asDict() 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): 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: try:
section, name = key.split('.') section, name = key.split('.')
value = self.data.get(section.lower(), {}).get(name.lower(), default) value = self.data.get(section.lower(), {}).get(name.lower(), default)
@ -27,6 +41,7 @@ class PlexConfig(ConfigParser):
return default return default
def _asDict(self): def _asDict(self):
""" Returns all configuration values as a dictionary. """
config = defaultdict(dict) config = defaultdict(dict)
for section in self._sections: for section in self._sections:
for name, value in self._sections[section].items(): for name, value in self._sections[section].items():
@ -36,6 +51,7 @@ class PlexConfig(ConfigParser):
def reset_base_headers(): def reset_base_headers():
""" Convenience function returns a dict of all base X-Plex-* headers for session requests. """
import plexapi import plexapi
return { return {
'X-Plex-Platform': plexapi.X_PLEX_PLATFORM, 'X-Plex-Platform': plexapi.X_PLEX_PLATFORM,

View file

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

View file

@ -1,14 +1,24 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import logging
from plexapi import log, utils import logging
from plexapi import X_PLEX_CONTAINER_SIZE from plexapi import X_PLEX_CONTAINER_SIZE, log, utils
from plexapi.compat import unquote from plexapi.compat import unquote
from plexapi.media import MediaTag, Genre, Role, Director from plexapi.media import MediaTag, Genre, Role, Director
from plexapi.exceptions import BadRequest, NotFound from plexapi.exceptions import BadRequest, NotFound
from plexapi.media import MediaTag
class Library(object): 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): def __init__(self, server, data):
self.identifier = data.attrib.get('identifier') self.identifier = data.attrib.get('identifier')
@ -16,12 +26,16 @@ class Library(object):
self.server = server self.server = server
self.title1 = data.attrib.get('title1') self.title1 = data.attrib.get('title1')
self.title2 = data.attrib.get('title2') self.title2 = data.attrib.get('title2')
self._sectionsByID = {} # cached section UUIDs self._sectionsByID = {} # cached Section UUIDs
def __repr__(self): def __repr__(self):
return '<Library:%s>' % self.title1.encode('utf8') return '<Library:%s>' % self.title1.encode('utf8')
def sections(self): 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 = [] items = []
SECTION_TYPES = { SECTION_TYPES = {
MovieSection.TYPE: MovieSection, MovieSection.TYPE: MovieSection,
@ -40,40 +54,69 @@ class Library(object):
return items return items
def section(self, title=None): 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(): for item in self.sections():
if item.title == title: if item.title == title:
return item return item
raise NotFound('Invalid library section: %s' % title) raise NotFound('Invalid library section: %s' % title)
def sectionByID(self, sectionID): 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() self.sections()
return self._sectionsByID[sectionID] return self._sectionsByID[sectionID]
def all(self): def all(self):
return [item for section in self.sections() """ Returns a list of all media from all library sections.
for item in section.all()] This may be a very large dataset to retrieve.
"""
return [item for section in self.sections() for item in section.all()]
def onDeck(self): def onDeck(self):
""" Returns a list of all media items on deck. """
return utils.listItems(self.server, '/library/onDeck') return utils.listItems(self.server, '/library/onDeck')
def recentlyAdded(self): def recentlyAdded(self):
""" Returns a list of all media items recently added. """
return utils.listItems(self.server, '/library/recentlyAdded') return utils.listItems(self.server, '/library/recentlyAdded')
def get(self, title): def get(self, title): # this should use hub search when its merged
return utils.findItem(self.server, '/library/all', title) """ 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): 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) return utils.findKey(self.server, key)
def search(self, title=None, libtype=None, **kwargs): def search(self, title=None, libtype=None, **kwargs):
""" Searching within a library section is much more powerful. It seems certain attributes on the media """ Searching within a library section is much more powerful. It seems certain
objects can be targeted to filter this search down a bit, but I havent found the documentation for attributes on the media objects can be targeted to filter this search down
it. a bit, but I havent found the documentation for it.
Example: "studio=Comedy%20Central" or "year=1999" "title=Kung Fu" all work. Other items 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. 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. TLDR: This is untested but seems to work. Use library section search when you can.
""" """
args = {} args = {}
if title: if title:
@ -86,16 +129,30 @@ class Library(object):
return utils.listItems(self.server, query) return utils.listItems(self.server, query)
def cleanBundles(self): 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') self.server.query('/library/clean/bundles')
def emptyTrash(self): def emptyTrash(self):
""" If a library has items in the Library Trash, use this option to empty the Trash. """
for section in self.sections(): for section in self.sections():
section.emptyTrash() section.emptyTrash()
def optimize(self): 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') self.server.query('/library/optimize')
def refresh(self): 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') self.server.query('/library/sections/all/refresh')
def __len__(self): def __len__(self):
@ -103,6 +160,33 @@ class Library(object):
class LibrarySection(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_FILTERS = ()
ALLOWED_SORT = () ALLOWED_SORT = ()
BOOLEAN_FILTERS = ('unwatched', 'duplicate') BOOLEAN_FILTERS = ('unwatched', 'duplicate')
@ -118,7 +202,6 @@ class LibrarySection(object):
self.filters = data.attrib.get('filters') self.filters = data.attrib.get('filters')
self.key = data.attrib.get('key') self.key = data.attrib.get('key')
self.language = data.attrib.get('language') self.language = data.attrib.get('language')
self.language = data.attrib.get('language')
self.locations = utils.findLocations(data) self.locations = utils.findLocations(data)
self.refreshing = utils.cast(bool, data.attrib.get('refreshing')) self.refreshing = utils.cast(bool, data.attrib.get('refreshing'))
self.scanner = data.attrib.get('scanner') self.scanner = data.attrib.get('scanner')
@ -133,42 +216,66 @@ class LibrarySection(object):
return '<%s:%s>' % (self.__class__.__name__, title.encode('utf8')) return '<%s:%s>' % (self.__class__.__name__, title.encode('utf8'))
def get(self, title): 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 path = '/library/sections/%s/all' % self.key
return utils.findItem(self.server, path, title) return utils.findItem(self.server, path, title)
def all(self): def all(self):
""" Returns a list of media from this library section. """
return utils.listItems(self.server, '/library/sections/%s/all' % self.key) return utils.listItems(self.server, '/library/sections/%s/all' % self.key)
def onDeck(self): 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) return utils.listItems(self.server, '/library/sections/%s/onDeck' % self.key)
def recentlyAdded(self, maxresults=50): 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) return self.search(sort='addedAt:desc', maxresults=maxresults)
def analyze(self): 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): 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) self.server.query('/library/sections/%s/emptyTrash' % self.key)
def refresh(self): 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) self.server.query('/library/sections/%s/refresh' % self.key)
def listChoices(self, category, libtype=None, **kwargs): def listChoices(self, category, libtype=None, **kwargs):
""" List choices for the specified filter category. kwargs can be any of the same """ Returns a list of :class:`~plexapi.library.FilterChoice` objects for the
kwargs in self.search() to help narrow down the choices to only those that specified category and libtype. kwargs can be any of the same kwargs in
matter in your current context. :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: if category in kwargs:
raise BadRequest( raise BadRequest('Cannot include kwarg equal to specified category: %s' % category)
'Cannot include kwarg equal to specified category: %s' % category)
args = {} args = {}
for subcategory, value in kwargs.items(): for subcategory, value in kwargs.items():
args[category] = self._cleanSearchFilter(subcategory, value) args[category] = self._cleanSearchFilter(subcategory, value)
if libtype is not None: if libtype is not None:
args['type'] = utils.searchType(libtype) args['type'] = utils.searchType(libtype)
query = '/library/sections/%s/%s%s' % ( query = '/library/sections/%s/%s%s' % (self.key, category, utils.joinArgs(args))
self.key, category, utils.joinArgs(args))
return utils.listItems(self.server, query, bytag=True) return utils.listItems(self.server, query, bytag=True)
def search(self, title=None, sort=None, maxresults=999999, libtype=None, **kwargs): 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 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. doesn't iterate over all results on the server.
Args: Parameters:
title (string, optional): General string query to search for. title (str): General string query to search for (optional).
sort (string): column:dir; column can be any of {addedAt, originallyAvailableAt, lastViewedAt, sort (str): column:dir; column can be any of {addedAt, originallyAvailableAt, lastViewedAt,
titleSort, rating, mediaHeight, duration}. dir can be asc or desc. titleSort, rating, mediaHeight, duration}. dir can be asc or desc (optional).
maxresults (int): Only return the specified number of results maxresults (int): Only return the specified number of results (optional).
libtype (string): Filter results to a spcifiec libtype {movie, show, episode, artist, album, track} libtype (str): Filter results to a spcifiec libtype (movie, show, episode, artist, album, track; optional).
kwargs: Any of the available filters for the current library section. Partial string **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 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. available options and a warning logged if the option does not appear valid.
'unwatched': Display or hide unwatched content (True, False). [all] * unwatched: Display or hide unwatched content (True, False). [all]
'duplicate': Display or hide duplicate items (True, False). [movie] * duplicate: Display or hide duplicate items (True, False). [movie]
'actor': List of actors to search ([actor_or_id, ...]). [movie] * actor: List of actors to search ([actor_or_id, ...]). [movie]
'collection': List of collections to search within ([collection_or_id, ...]). [all] * collection: List of collections to search within ([collection_or_id, ...]). [all]
'contentRating': List of content ratings to search within ([rating_or_key, ...]). [movie,tv] * 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] * country: List of countries to search within ([country_or_key, ...]). [movie,music]
'decade': List of decades to search within ([yyy0, ...]). [movie] * decade: List of decades to search within ([yyy0, ...]). [movie]
'director': List of directors to search ([director_or_id, ...]). [movie] * director: List of directors to search ([director_or_id, ...]). [movie]
'genre': List Genres to search within ([genere_or_id, ...]). [all] * genre: List Genres to search within ([genere_or_id, ...]). [all]
'network': List of TV networks to search within ([resolution_or_key, ...]). [tv] * network: List of TV networks to search within ([resolution_or_key, ...]). [tv]
'resolution': List of video resolutions to search within ([resolution_or_key, ...]). [movie] * resolution: List of video resolutions to search within ([resolution_or_key, ...]). [movie]
'studio': List of studios to search within ([studio_or_key, ...]). [music] * studio: List of studios to search within ([studio_or_key, ...]). [music]
'year': List of years to search within ([yyyy, ...]). [all] * year: List of years to search within ([yyyy, ...]). [all]
""" """
# Cleanup the core arguments # Cleanup the core arguments
args = {} args = {}
@ -211,12 +318,10 @@ class LibrarySection(object):
args['sort'] = self._cleanSearchSort(sort) args['sort'] = self._cleanSearchSort(sort)
if libtype is not None: if libtype is not None:
args['type'] = utils.searchType(libtype) args['type'] = utils.searchType(libtype)
# Iterate over the results # Iterate over the results
results, subresults = [], '_init' results, subresults = [], '_init'
args['X-Plex-Container-Start'] = 0 args['X-Plex-Container-Start'] = 0
args['X-Plex-Container-Size'] = min(X_PLEX_CONTAINER_SIZE, maxresults) args['X-Plex-Container-Size'] = min(X_PLEX_CONTAINER_SIZE, maxresults)
while subresults and maxresults > len(results): while subresults and maxresults > len(results):
query = '/library/sections/%s/all%s' % ( query = '/library/sections/%s/all%s' % (
self.key, utils.joinArgs(args)) self.key, utils.joinArgs(args))
@ -233,13 +338,11 @@ class LibrarySection(object):
return '1' if value else '0' return '1' if value else '0'
if not isinstance(value, (list, tuple)): if not isinstance(value, (list, tuple)):
value = [value] value = [value]
# convert list of values to list of keys or ids # convert list of values to list of keys or ids
result = set() result = set()
choices = self.listChoices(category, libtype) choices = self.listChoices(category, libtype)
lookup = {c.title.lower(): unquote(unquote(c.key)) for c in choices} lookup = {c.title.lower(): unquote(unquote(c.key)) for c in choices}
allowed = set(c.key for c in choices) allowed = set(c.key for c in choices)
for item in value: for item in value:
item = str(item.id if isinstance(item, MediaTag) else item).lower() item = str(item.id if isinstance(item, MediaTag) else item).lower()
# find most logical choice(s) to use in url # find most logical choice(s) to use in url
@ -254,8 +357,7 @@ class LibrarySection(object):
map(result.add, matches) map(result.add, matches)
continue continue
# nothing matched; use raw item value # nothing matched; use raw item value
log.warning( log.warning('Filter value not listed, using raw item value: %s' % item)
'Filter value not listed, using raw item value: %s' % item)
result.add(item) result.add(item)
return ','.join(result) return ','.join(result)
@ -271,58 +373,108 @@ class LibrarySection(object):
class MovieSection(LibrarySection): class MovieSection(LibrarySection):
ALLOWED_FILTERS = ('unwatched', 'duplicate', 'year', 'decade', 'genre', 'contentRating', 'collection', """ Represents a :class:`~plexapi.library.LibrarySection` section containing movies.
'director', 'actor', 'country', 'studio', 'resolution')
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', ALLOWED_SORT = ('addedAt', 'originallyAvailableAt', 'lastViewedAt', 'titleSort', 'rating',
'mediaHeight', 'duration') 'mediaHeight', 'duration')
TYPE = 'movie' TYPE = 'movie'
class ShowSection(LibrarySection): class ShowSection(LibrarySection):
ALLOWED_FILTERS = ('unwatched', 'year', 'genre', """ Represents a :class:`~plexapi.library.LibrarySection` section containing tv shows.
'contentRating', 'network', 'collection')
ALLOWED_SORT = ('addedAt', 'lastViewedAt', Attributes:
'originallyAvailableAt', 'titleSort', 'rating', 'unwatched') 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' TYPE = 'show'
def searchShows(self, **kwargs): def searchShows(self, **kwargs):
""" Search for a show. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
return self.search(libtype='show', **kwargs) return self.search(libtype='show', **kwargs)
def searchEpisodes(self, **kwargs): def searchEpisodes(self, **kwargs):
""" Search for an episode. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
return self.search(libtype='episode', **kwargs) return self.search(libtype='episode', **kwargs)
def recentlyAdded(self, libtype='episode', maxresults=50): 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) return self.search(sort='addedAt:desc', libtype=libtype, maxresults=maxresults)
class MusicSection(LibrarySection): class MusicSection(LibrarySection):
""" Represents a :class:`~plexapi.library.LibrarySection` section containing music artists.
Attributes:
ALLOWED_FILTERS (list<str>): List of allowed search filters. ('genre',
'country', 'collection')
ALLOWED_SORT (list<str>): List of allowed sorting keys. ('addedAt',
'lastViewedAt', 'viewCount', 'titleSort')
TYPE (str): 'artist'
"""
ALLOWED_FILTERS = ('genre', 'country', 'collection') ALLOWED_FILTERS = ('genre', 'country', 'collection')
ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'viewCount', 'titleSort') ALLOWED_SORT = ('addedAt', 'lastViewedAt', 'viewCount', 'titleSort')
TYPE = 'artist' TYPE = 'artist'
def albums(self): 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) return utils.listItems(self.server, '/library/sections/%s/albums' % self.key)
def searchArtists(self, **kwargs): def searchArtists(self, **kwargs):
""" Search for an artist. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
return self.search(libtype='artist', **kwargs) return self.search(libtype='artist', **kwargs)
def searchAlbums(self, **kwargs): def searchAlbums(self, **kwargs):
""" Search for an album. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
return self.search(libtype='album', **kwargs) return self.search(libtype='album', **kwargs)
def searchTracks(self, **kwargs): def searchTracks(self, **kwargs):
""" Search for a track. See :func:`~plexapi.library.LibrarySection.search()` for usage. """
return self.search(libtype='track', **kwargs) return self.search(libtype='track', **kwargs)
class PhotoSection(LibrarySection): 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 = () ALLOWED_SORT = ()
TYPE = 'photo' TYPE = 'photo'
def searchAlbums(self, **kwargs): def searchAlbums(self, title, **kwargs): # lets use this for now.
return self.search(libtype='photo', **kwargs) """ 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): def searchPhotos(self, title, **kwargs):
return self.search(libtype='photo', **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 @utils.register_libtype
@ -363,6 +515,20 @@ class Hub(object):
@utils.register_libtype @utils.register_libtype
class FilterChoice(object): 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' TYPE = 'Directory'
def __init__(self, server, data, initpath): def __init__(self, server, data, initpath):

View file

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

View file

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

View file

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

View file

@ -1,7 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""
PlexPlaylist
"""
from plexapi import utils from plexapi import utils
from plexapi.exceptions import BadRequest from plexapi.exceptions import BadRequest
from plexapi.utils import cast, toDatetime from plexapi.utils import cast, toDatetime
@ -60,7 +57,7 @@ class Playlist(PlexPartialObject, Playable):
for item in items: for item in items:
if item.listType != self.playlistType: if item.listType != self.playlistType:
raise BadRequest('Can not mix media types when building a playlist: %s and %s' % (self.playlistType, item.listType)) 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 uuid = items[0].section().uuid
ratingKeys = ','.join(ratingKeys) ratingKeys = ','.join(ratingKeys)
path = '%s/items%s' % (self.key, utils.joinArgs({ path = '%s/items%s' % (self.key, utils.joinArgs({

View file

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

View file

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

View file

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import requests import requests
from plexapi import utils from plexapi import utils
from plexapi.exceptions import NotFound from plexapi.exceptions import NotFound

View file

@ -1,192 +1,166 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import logging, os, re, requests
import re
from datetime import datetime from datetime import datetime
from plexapi.compat import quote, urlencode
from plexapi.exceptions import NotFound, UnknownType, Unsupported
from threading import Thread 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. # Search Types - Plex uses these to filter specific media types when searching.
SEARCHTYPES = {'movie': 1, 'show': 2, 'season': 3, SEARCHTYPES = {'movie': 1, 'show': 2, 'season': 3, 'episode': 4,
'episode': 4, 'artist': 8, 'album': 9, 'track': 10} 'artist': 8, 'album': 9, 'track': 10, 'photo': 14}
LIBRARY_TYPES = {} LIBRARY_TYPES = {}
def register_libtype(cls): def register_libtype(cls):
"""Registry of library types we may come across when parsing XML. """ Registry of library types we may come across when parsing XML. This allows us to
This allows us to define a few helper functions to dynamically convery define a few helper functions to dynamically convery the XML into objects. See
the XML into objects. See buildItem() below for an example. buildItem() below for an example.
""" """
LIBRARY_TYPES[cls.TYPE] = cls LIBRARY_TYPES[cls.TYPE] = cls
return cls return cls
class _NA(object): class _NA(object):
"""This used to be a simple variable equal to '__NA__'. """ This used to be a simple variable equal to '__NA__'. There has been need to
However, there has been need to compare NA against None in some use cases. compare NA against None in some use cases. This object allows the internals
This object allows the internals of PlexAPI to distinguish between unfetched of PlexAPI to distinguish between unfetched values and fetched, but non-existent
values and fetched, but non-existent values. values. (NA == None results to True; NA is None results to False)
(NA == None results to True; NA is None results to False)
""" """
def __bool__(self): def __bool__(self):
"""Make sure Na always is False.
Returns:
bool: False
"""
return False return False
def __eq__(self, other): def __eq__(self, other):
"""Check eq.
Args:
other (str): Description
Returns:
bool: True is equal
"""
return isinstance(other, _NA) or other in [None, '__NA__'] return isinstance(other, _NA) or other in [None, '__NA__']
def __nonzero__(self): def __nonzero__(self):
return False return False
def __repr__(self): def __repr__(self):
"""Pretty print."""
return '__NA__' 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): class PlexPartialObject(object):
"""Not all objects in the Plex listings return the complete list of elements """ 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, 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 and if the specified value you request is None it will fetch the full object
automatically and update itself. automatically and update itself.
Attributes: Attributes:
initpath (str): Relative url to PMS data (ElementTree): Response from PlexServer used to build this object (optional).
server (): Description 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): 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.server = server
self.initpath = initpath self.initpath = initpath
self._loadData(data) self._loadData(data)
self._reloaded = False
def __eq__(self, other): def __eq__(self, other):
"""Summary
Args:
other (TYPE): Description
Returns:
TYPE: Description
"""
return other is not None and self.key == other.key return other is not None and self.key == other.key
def __repr__(self): def __repr__(self):
"""Pretty repr."""
clsname = self.__class__.__name__ clsname = self.__class__.__name__
key = self.key.replace('/library/metadata/', '') if self.key else 'NA' key = self.key.replace('/library/metadata/', '') if self.key else 'NA'
title = self.title.replace(' ', '.')[0:20].encode('utf8') title = self.title.replace(' ', '.')[0:20].encode('utf8')
return '<%s:%s:%s>' % (clsname, key, title) return '<%s:%s:%s>' % (clsname, key, title)
def __getattr__(self, attr): def __getattr__(self, attr):
"""Auto reload self, if the attribute is NA # Auto reload self, from the full key (path) when needed.
Args:
attr (str): fx key
"""
if attr == 'key' or self.__dict__.get(attr) or self.isFullObject(): if attr == 'key' or self.__dict__.get(attr) or self.isFullObject():
return self.__dict__.get(attr, NA) return self.__dict__.get(attr, NA)
print('reload because of %s' % attr)
self.reload() self.reload()
return self.__dict__.get(attr, NA) return self.__dict__.get(attr, NA)
def __setattr__(self, attr, value): def __setattr__(self, attr, value):
"""Set attribute
Args:
attr (str): fx key
value (TYPE): Description
"""
if value != NA or self.isFullObject(): if value != NA or self.isFullObject():
self.__dict__[attr] = value self.__dict__[attr] = value
def _loadData(self, data): def _loadData(self, data):
"""Uses a element to set a attrs. raise NotImplementedError('Abstract method not implemented.')
Args:
data (Element): Used by attrs
"""
raise Exception('Abstract method not implemented.')
def isFullObject(self): 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 return not self.key or self.key == self.initpath
def isPartialObject(self): def isPartialObject(self):
""" Returns True if this is NOT a full object. """
return not self.isFullObject() return not self.isFullObject()
def reload(self): 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) data = self.server.query(self.key)
self.initpath = self.key self.initpath = self.key
self._loadData(data[0]) self._loadData(data[0])
self._reloaded = True
return self
class Playable(object): class Playable(object):
"""This is a general place to store functions specific to media that is 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, Things were getting mixed up a bit when dealing with Shows, Season, Artists,
Artists, Albums which are all not playable. Albums which are all not playable.
Attributes: # todo Attributes:
player (Plexclient): Player player (:class:`~plexapi.client.PlexClient`): Client object playing this item (for active sessions).
playlistItemID (int): Playlist item id playlistItemID (int): Playlist item ID (only populated for :class:`~plexapi.playlist.Playlist` items).
sessionKey (int): 1223 sessionKey (int): Active session key.
transcodeSession (str): 12312312 transcodeSession (:class:`~plexapi.media.TranscodeSession`): Transcode Session object
username (str): Fx Hellowlol if item is being transcoded (None otherwise).
viewedAt (datetime): viewed at. username (str): Username of the person playing this item (for active sessions).
viewedAt (datetime): Datetime item was last viewed (history).
""" """
def _loadData(self, data): def _loadData(self, data):
"""Set the class attributes # Load data for active sessions (/status/sessions)
Args:
data (xml.etree.ElementTree.Element): usually from server.query
"""
# data for active sessions (/status/sessions)
self.sessionKey = cast(int, data.attrib.get('sessionKey', NA)) self.sessionKey = cast(int, data.attrib.get('sessionKey', NA))
self.username = findUsername(data) self.username = findUsername(data)
self.player = findPlayer(self.server, data) self.player = findPlayer(self.server, data)
self.transcodeSession = findTranscodeSession(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)) 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)) self.playlistItemID = cast(int, data.attrib.get('playlistItemID', NA))
def getStreamURL(self, **params): 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: Parameters:
**params (dict): Description **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: Raises:
string: '' Unsupported: When the item doesn't support fetching a stream URL.
Raises:
Unsupported: Raises a error is the type is wrong.
""" """
if self.TYPE not in ('movie', 'episode', 'track'): if self.TYPE not in ('movie', 'episode', 'track'):
raise Unsupported( raise Unsupported('Fetching stream URL for %s is unsupported.' % self.TYPE)
'Fetching stream URL for %s is unsupported.' % self.TYPE)
mvb = params.get('maxVideoBitrate') mvb = params.get('maxVideoBitrate')
vr = params.get('videoResolution', '') vr = params.get('videoResolution', '')
params = { params = {
@ -202,35 +176,68 @@ class Playable(object):
# remove None values # remove None values
params = {k: v for k, v in params.items() if v is not None} params = {k: v for k, v in params.items() if v is not None}
streamtype = 'audio' if self.TYPE in ('track', 'album') else 'video' 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): def iterParts(self):
"""Yield parts.""" """ Iterates over the parts of this media item. """
for item in self.media: for item in self.media:
for part in item.parts: for part in item.parts:
yield part yield part
def play(self, client): def play(self, client):
"""Start playback on a client. """ Start playback on the specified client.
Args: Parameters:
client (PlexClient): The client to start playing on. client (:class:`~plexapi.client.PlexClient`): Client to start playing on.
""" """
client.playMedia(self) 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): def buildItem(server, elem, initpath, bytag=False):
"""Build classes used by the plexapi. """ Factory function to build the objects used within the PlexAPI.
Args: Parameters:
server (Plexserver): Your connected to. server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
elem (xml.etree.ElementTree.Element): xml from PMS elem (ElementTree): XML data needed to build the object.
initpath (str): Relative path initpath (str): Relative path requested when retrieving specified `data` (optional).
bytag (bool, optional): Description # figure out what this do 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
Raises:
UnknownType: Unknown library type.
""" """
libtype = elem.tag if bytag else elem.attrib.get('type') libtype = elem.tag if bytag else elem.attrib.get('type')
if libtype == 'photo' and elem.tag == 'Directory': if libtype == 'photo' and elem.tag == 'Directory':
@ -242,19 +249,11 @@ def buildItem(server, elem, initpath, bytag=False):
def cast(func, value): def cast(func, value):
"""Helper to change to the correct type. """ Cast the specified value to the specified type (returned by func).
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
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: if not value:
return return
@ -272,14 +271,14 @@ def cast(func, value):
def findKey(server, key): def findKey(server, key):
"""Finds and builds a object based on ratingKey. """ Finds and builds a object based on ratingKey.
Args: Parameters:
server (Plexserver): PMS your connected to server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
key (int): key to look for key (int): ratingKey to find and return.
Raises: Raises:
NotFound: Unable to find key. Key NotFound: Unable to find key
""" """
path = '/library/metadata/{0}'.format(key) path = '/library/metadata/{0}'.format(key)
try: try:
@ -291,15 +290,15 @@ def findKey(server, key):
def findItem(server, path, title): def findItem(server, path, title):
"""Finds and builds a object based on title. """ Finds and builds a object based on title.
Args: Parameters:
server (Plexserver): Description server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
path (str): Relative path path (str): API path that returns item to search title for.
title (str): Fx 16 blocks title (str): Title of the item to find and return.
Raises: Raises:
NotFound: Unable to find item: title NotFound: Unable to find item.
""" """
for elem in server.query(path): for elem in server.query(path):
if elem.attrib.get('title').lower() == title.lower(): if elem.attrib.get('title').lower() == title.lower():
@ -308,14 +307,12 @@ def findItem(server, path, title):
def findLocations(data, single=False): def findLocations(data, single=False):
"""Extract the path from a location tag """ Returns a list of filepaths from a location tag.
Args: Parameters:
data (xml.etree.ElementTree.Element): xml from PMS as Element data (ElementTree): XML object to search for locations in.
single (bool, optional): Only return one single (bool): Set True to only return the first location found.
Return type will be a string if this is set to True.
Returns:
filepath string if single is True else list of filepaths
""" """
locations = [] locations = []
for elem in data: for elem in data:
@ -327,33 +324,26 @@ def findLocations(data, single=False):
def findPlayer(server, data): def findPlayer(server, data):
"""Find a player in a elementthee """ Returns the :class:`~plexapi.client.PlexClient` object found in the specified data.
Args: Parameters:
server (Plexserver): PMS your connected to server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
data (xml.etree.ElementTree.Element): xml from pms as a element data (ElementTree): XML data to find Player in.
Returns:
PlexClient or None
""" """
elem = data.find('Player') elem = data.find('Player')
if elem is not None: if elem is not None:
from plexapi.client import PlexClient from plexapi.client import PlexClient
baseurl = 'http://%s:%s' % (elem.attrib.get('address'), baseurl = 'http://%s:%s' % (elem.attrib.get('address'), elem.attrib.get('port'))
elem.attrib.get('port'))
return PlexClient(baseurl, server=server, data=elem) return PlexClient(baseurl, server=server, data=elem)
return None return None
def findStreams(media, streamtype): def findStreams(media, streamtype):
"""Find streams. """ Returns a list of streams (str) found in media that match the specified streamtype.
Args: Parameters:
media (Show, Movie, Episode): A item where find streams media (:class:`~plexapi.utils.Playable`): Item to search for streams (show, movie, episode).
streamtype (str): Possible options [movie, show, episode] # is this correct? streamtype (str): Streamtype to return (videostream, audiostream, subtitlestream).
Returns:
list: of streams
""" """
streams = [] streams = []
for mediaitem in media: for mediaitem in media:
@ -365,14 +355,12 @@ def findStreams(media, streamtype):
def findTranscodeSession(server, data): def findTranscodeSession(server, data):
"""Find transcode session. """ Returns a :class:`~plexapi.media.TranscodeSession` object if found within the specified
XML data.
Args: Parameters:
server (Plexserver): PMS your connected to server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
data (xml.etree.ElementTree.Element): XML response from PMS as Element data (ElementTree): XML data to find TranscodeSession in.
Returns:
media.TranscodeSession or None
""" """
elem = data.find('TranscodeSession') elem = data.find('TranscodeSession')
@ -383,13 +371,10 @@ def findTranscodeSession(server, data):
def findUsername(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: Parameters:
data (xml.etree.ElementTree.Element): XML from PMS as a Element data (ElementTree): XML data to find username in.
Returns:
username or None
""" """
elem = data.find('User') elem = data.find('User')
if elem is not None: if elem is not None:
@ -398,7 +383,7 @@ def findUsername(data):
def isInt(str): def isInt(str):
"""Check of a string is a int""" """ Returns True if the specified string passes as an int. """
try: try:
int(str) int(str)
return True return True
@ -407,14 +392,11 @@ def isInt(str):
def joinArgs(args): def joinArgs(args):
"""Builds a query string where only """ Returns a query string (uses for HTTP URLs) where only the value is URL encoded.
the value is quoted. Example return value: '?genre=action&type=1337'.
Args: Parameters:
args (dict): ex {'genre': 'action', 'type': 1337} args (dict): Arguments to include in query string.
Returns:
string: ?genre=action&type=1337
""" """
if not args: if not args:
return '' return ''
@ -426,30 +408,25 @@ def joinArgs(args):
def listChoices(server, path): def listChoices(server, path):
"""ListChoices is by _cleanSort etc. """ Returns a dict of {title:key} for all simple choices in a search filter.
Args: Parameters:
server (Plexserver): Server your connected to server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
path (str): Relative path to PMS path (str): Relative path to request XML data from.
Returns:
dict: title:key
""" """
return {c.attrib['title']: c.attrib['key'] for c in server.query(path)} return {c.attrib['title']: c.attrib['key'] for c in server.query(path)}
def listItems(server, path, libtype=None, watched=None, bytag=False): 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: Parameters:
server (Plexserver): PMS your connected to. server (:class:`~plexapi.server.PlexServer`): PlexServer object this is from.
path (str): Relative path to PMS path (str): Relative path to request XML data from.
libtype (None or string, optional): [movie, show, episode, music] # check me libtype (str): Optionally return only the specified library type.
watched (None, True, False, optional): Skip or include watched items watched (bool): Optionally return only watched or unwatched items.
bytag (bool, optional): Dunno wtf this is used for # todo bytag (bool): Set true if libtype is found in the XML tag (and not the 'type' attribute).
Returns:
list: of buildItem
""" """
items = [] items = []
for elem in server.query(path): for elem in server.query(path):
@ -466,7 +443,18 @@ def listItems(server, path, libtype=None, watched=None, bytag=False):
return items 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: try:
parts = attrstr.split(delim, 1) parts = attrstr.split(delim, 1)
attr = parts[0] attr = parts[0]
@ -487,19 +475,15 @@ def rget(obj, attrstr, default=None, delim='.'):
def searchType(libtype): def searchType(libtype):
"""Map search type name to int using SEACHTYPES """ Returns the integer value of the library string type.
Used when querying PMS.
Args: Parameters:
libtype (str): Possible options see SEARCHTYPES libtype (str): Library type to lookup (movie, show, season, episode,
artist, album, track)
Returns: Raises:
int: fx 1 NotFound: Unknown libtype
Raises:
NotFound: Unknown libtype: libtype
""" """
libtype = str(libtype) libtype = str(libtype)
if libtype in [str(v) for v in SEARCHTYPES.values()]: if libtype in [str(v) for v in SEARCHTYPES.values()]:
return libtype return libtype
@ -509,12 +493,12 @@ def searchType(libtype):
def threaded(callback, listargs): def threaded(callback, listargs):
"""Run some function in threads. """ Returns the result of <callback> for each set of \*args in listargs. Each call
to <callback. is called concurrently in their own separate threads.
Args:
callback (function): funcion to run in thread
listargs (list): args parssed to the callback
Parameters:
callback (func): Callback function to apply to each set of \*args.
listargs (list): List of lists; \*args to pass each thread.
""" """
threads, results = [], [] threads, results = [], []
for args in listargs: for args in listargs:
@ -524,18 +508,16 @@ def threaded(callback, listargs):
threads[-1].start() threads[-1].start()
for thread in threads: for thread in threads:
thread.join() thread.join()
return results return results
def toDatetime(value, format=None): def toDatetime(value, format=None):
"""Helper for datetime. """ Returns a datetime object from the specified value.
Args: Parameters:
value (str): value to use to make datetime value (str): value to return as a datetime
format (None, optional): string as strptime. format (str): Format to pass strftime (optional; if value is a str).
Returns:
datetime
""" """
if value and value != NA: if value and value != NA:
if format: if format:
@ -543,3 +525,57 @@ def toDatetime(value, format=None):
else: else:
value = datetime.fromtimestamp(int(value)) value = datetime.fromtimestamp(int(value))
return 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 -*- # -*- coding: utf-8 -*-
from plexapi import media, utils from plexapi import media, utils
from plexapi.exceptions import NotFound
from plexapi.utils import Playable, PlexPartialObject from plexapi.utils import Playable, PlexPartialObject
NA = utils.NA NA = utils.NA
@ -29,8 +29,7 @@ class Video(PlexPartialObject):
self.listType = 'video' self.listType = 'video'
self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA)) self.addedAt = utils.toDatetime(data.attrib.get('addedAt', NA))
self.key = data.attrib.get('key', NA) self.key = data.attrib.get('key', NA)
self.lastViewedAt = utils.toDatetime( self.lastViewedAt = utils.toDatetime(data.attrib.get('lastViewedAt', NA))
data.attrib.get('lastViewedAt', NA))
self.librarySectionID = data.attrib.get('librarySectionID', NA) self.librarySectionID = data.attrib.get('librarySectionID', NA)
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey', NA)) self.ratingKey = utils.cast(int, data.attrib.get('ratingKey', NA))
self.summary = data.attrib.get('summary', 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, that are useful to knowwhether it's a video file,
a music track, or one of your photos. 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): def markWatched(self):
"""Mark a items as watched.""" """Mark a items as watched."""
@ -110,24 +109,15 @@ class Movie(Video, Playable):
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
self.year = utils.cast(int, data.attrib.get('year', NA)) self.year = utils.cast(int, data.attrib.get('year', NA))
if self.isFullObject(): # check this if self.isFullObject(): # check this
self.collections = [media.Collection( self.collections = [media.Collection(self.server, e) for e in data if e.tag == media.Collection.TYPE]
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.countries = [media.Country(self.server, e) self.directors = [media.Director(self.server, e) for e in data if e.tag == media.Director.TYPE]
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.directors = [media.Director( self.media = [media.Media(self.server, e, self.initpath, self) for e in data if e.tag == media.Media.TYPE]
self.server, e) for e in data if e.tag == media.Director.TYPE] self.producers = [media.Producer(self.server, e) for e in data if e.tag == media.Producer.TYPE]
self.genres = [media.Genre(self.server, e) self.roles = [media.Role(self.server, e) for e in data if e.tag == media.Role.TYPE]
for e in data if e.tag == media.Genre.TYPE] self.writers = [media.Writer(self.server, e) for e in data if e.tag == media.Writer.TYPE]
self.media = [media.Media(self.server, e, self.initpath, self) self.fields = [media.Field(e) for e in data if e.tag == media.Field.TYPE]
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.videoStreams = utils.findStreams(self.media, 'videostream')
self.audioStreams = utils.findStreams(self.media, 'audiostream') self.audioStreams = utils.findStreams(self.media, 'audiostream')
self.subtitleStreams = utils.findStreams( self.subtitleStreams = utils.findStreams(
@ -141,6 +131,35 @@ class Movie(Video, Playable):
def isWatched(self): def isWatched(self):
return bool(self.viewCount > 0) 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 @utils.register_libtype
class Show(Video): class Show(Video):
@ -153,6 +172,8 @@ class Show(Video):
data (Element): Usually built from server.query data (Element): Usually built from server.query
""" """
Video._loadData(self, data) 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.art = data.attrib.get('art', NA)
self.banner = data.attrib.get('banner', NA) self.banner = data.attrib.get('banner', NA)
self.childCount = utils.cast(int, data.attrib.get('childCount', 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.guid = data.attrib.get('guid', NA)
self.index = data.attrib.get('index', NA) self.index = data.attrib.get('index', NA)
self.leafCount = utils.cast(int, data.attrib.get('leafCount', 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( self.originallyAvailableAt = utils.toDatetime(
data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d') data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
self.rating = utils.cast(float, data.attrib.get('rating', NA)) self.rating = utils.cast(float, data.attrib.get('rating', NA))
@ -170,11 +191,9 @@ class Show(Video):
self.viewedLeafCount = utils.cast( self.viewedLeafCount = utils.cast(
int, data.attrib.get('viewedLeafCount', NA)) int, data.attrib.get('viewedLeafCount', NA))
self.year = utils.cast(int, data.attrib.get('year', NA)) self.year = utils.cast(int, data.attrib.get('year', NA))
#if self.isFullObject(): # will be fixed with docs. if self.isFullObject(): # will be fixed with docs.
self.genres = [media.Genre(self.server, e) self.genres = [media.Genre(self.server, e) for e in data if e.tag == media.Genre.TYPE]
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]
self.roles = [media.Role(self.server, e)
for e in data if e.tag == media.Role.TYPE]
@property @property
def actors(self): def actors(self):
@ -189,12 +208,15 @@ class Show(Video):
path = '/library/metadata/%s/children' % self.ratingKey path = '/library/metadata/%s/children' % self.ratingKey
return utils.listItems(self.server, path, Season.TYPE) return utils.listItems(self.server, path, Season.TYPE)
def season(self, title): def season(self, title=None):
"""Returns a Season """Returns a Season
Args: 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 path = '/library/metadata/%s/children' % self.ratingKey
return utils.findItem(self.server, path, title) return utils.findItem(self.server, path, title)
@ -207,9 +229,45 @@ class Show(Video):
leavesKey = '/library/metadata/%s/allLeaves' % self.ratingKey leavesKey = '/library/metadata/%s/allLeaves' % self.ratingKey
return utils.listItems(self.server, leavesKey, watched=watched) return utils.listItems(self.server, leavesKey, watched=watched)
def episode(self, title): def episode(self, title=None, season=None, episode=None):
path = '/library/metadata/%s/allLeaves' % self.ratingKey """Find a episode using a title or season and episode.
return utils.findItem(self.server, path, title)
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): def watched(self):
"""Return a list of watched episodes""" """Return a list of watched episodes"""
@ -227,9 +285,21 @@ class Show(Video):
""" """
return self.episode(title) return self.episode(title)
def analyze(self):
""" """
raise 'Cant analyse a show' # fix me
def refresh(self): def refresh(self):
"""Refresh the metadata.""" """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 @utils.register_libtype
@ -243,10 +313,12 @@ class Season(Video):
data (Element): Usually built from server.query data (Element): Usually built from server.query
""" """
Video._loadData(self, data) Video._loadData(self, data)
self.key = self.key.replace('/children', '')
self.leafCount = utils.cast(int, data.attrib.get('leafCount', NA)) 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.parentKey = data.attrib.get('parentKey', NA)
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey', NA)) self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey', NA))
self.parentTitle = data.attrib.get('parentTitle', NA)
self.viewedLeafCount = utils.cast( self.viewedLeafCount = utils.cast(
int, data.attrib.get('viewedLeafCount', NA)) int, data.attrib.get('viewedLeafCount', NA))
@ -256,7 +328,7 @@ class Season(Video):
@property @property
def seasonNumber(self): def seasonNumber(self):
"""Reurns season number.""" """Returns season number."""
return self.index return self.index
def episodes(self, watched=None): def episodes(self, watched=None):
@ -273,20 +345,43 @@ class Season(Video):
childrenKey = '/library/metadata/%s/children' % self.ratingKey childrenKey = '/library/metadata/%s/children' % self.ratingKey
return utils.listItems(self.server, childrenKey, watched=watched) return utils.listItems(self.server, childrenKey, watched=watched)
def episode(self, title): def episode(self, title=None, episode=None):
"""Find a episode with a matching title. """Find a episode using a title or season and episode.
Args: Note:
title (sting): Fx 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 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 if not title and not episode:
return utils.findItem(self.server, path, title) 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): def get(self, title):
"""Get a episode witha matching title """Get a episode with a matching title.
Args: Args:
title (str): fx Secret santa title (str): fx Secret santa
@ -308,6 +403,20 @@ class Season(Video):
"""Returns a list of unwatched Episode""" """Returns a list of unwatched Episode"""
return self.episodes(watched=False) 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 @utils.register_libtype
class Episode(Video, Playable): class Episode(Video, Playable):
@ -332,9 +441,8 @@ class Episode(Video, Playable):
self.grandparentThumb = data.attrib.get('grandparentThumb', NA) self.grandparentThumb = data.attrib.get('grandparentThumb', NA)
self.grandparentTitle = data.attrib.get('grandparentTitle', NA) self.grandparentTitle = data.attrib.get('grandparentTitle', NA)
self.guid = data.attrib.get('guid', NA) self.guid = data.attrib.get('guid', NA)
self.index = data.attrib.get('index', NA) self.index = utils.cast(int, data.attrib.get('index', NA))
self.originallyAvailableAt = utils.toDatetime( self.originallyAvailableAt = utils.toDatetime(data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
data.attrib.get('originallyAvailableAt', NA), '%Y-%m-%d')
self.parentIndex = data.attrib.get('parentIndex', NA) self.parentIndex = data.attrib.get('parentIndex', NA)
self.parentKey = data.attrib.get('parentKey', NA) self.parentKey = data.attrib.get('parentKey', NA)
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey', 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.rating = utils.cast(float, data.attrib.get('rating', NA))
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0)) self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
self.year = utils.cast(int, data.attrib.get('year', NA)) self.year = utils.cast(int, data.attrib.get('year', NA))
self.directors = [media.Director(self.server, e) self.directors = [media.Director(self.server, e) for e in data if e.tag == media.Director.TYPE]
for e in data if e.tag == media.Director.TYPE] self.media = [media.Media(self.server, e, self.initpath, self) for e in data if e.tag == media.Media.TYPE]
self.media = [media.Media(self.server, e, self.initpath, self) self.writers = [media.Writer(self.server, e) for e in data if e.tag == media.Writer.TYPE]
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.videoStreams = utils.findStreams(self.media, 'videostream')
self.audioStreams = utils.findStreams(self.media, 'audiostream') self.audioStreams = utils.findStreams(self.media, 'audiostream')
self.subtitleStreams = utils.findStreams(self.media, 'subtitlestream') self.subtitleStreams = utils.findStreams(self.media, 'subtitlestream')
@ -359,6 +464,12 @@ class Episode(Video, Playable):
# Cached season number # Cached season number
self._seasonNumber = None 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 @property
def isWatched(self): def isWatched(self):
"""Returns True if watched, False if not.""" """Returns True if watched, False if not."""
@ -369,7 +480,7 @@ class Episode(Video, Playable):
"""Return this episode seasonnumber.""" """Return this episode seasonnumber."""
if self._seasonNumber is None: if self._seasonNumber is None:
self._seasonNumber = self.parentIndex if self.parentIndex else self.season().seasonNumber self._seasonNumber = self.parentIndex if self.parentIndex else self.season().seasonNumber
return self._seasonNumber return utils.cast(int, self._seasonNumber)
@property @property
def thumbUrl(self): def thumbUrl(self):
@ -384,3 +495,18 @@ class Episode(Video, Playable):
def show(self): def show(self):
"""Return this episodes Show""" """Return this episodes Show"""
return utils.listItems(self.server, self.grandparentKey)[0] 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 # PlexAPI Requirements
# pip install -r requirments.txt
#--------------------------------------------------------- #---------------------------------------------------------
requests 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 import re
from distutils.core import setup from distutils.core import setup
from setuptools import find_packages
# Convert markdown readme to rst # Convert markdown readme to rst
try: try:
@ -29,7 +28,7 @@ setup(
author='Michael Shepanski', author='Michael Shepanski',
author_email='mjs7231@gmail.com', author_email='mjs7231@gmail.com',
url='https://github.com/mjs7231/plexapi', url='https://github.com/mjs7231/plexapi',
packages=find_packages(), packages=['plexapi'],
install_requires=['requests'], install_requires=['requests'],
long_description=read_md('README.md'), long_description=read_md('README.md'),
keywords=['plex', 'api'], 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'])