Merge branch 'master' into intro_marker

This commit is contained in:
blacktwin 2020-05-27 21:48:41 -04:00 committed by GitHub
commit 1d8d76ef56
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 1884 additions and 937 deletions

View file

@ -1,3 +1,8 @@
[run]
omit = */site-packages/plexapi/*
[report] [report]
exclude_lines = exclude_lines =
pragma: no cover pragma: no cover

21
.readthedocs.yml Normal file
View file

@ -0,0 +1,21 @@
# .readthedocs.yml
version: 2
build:
image: latest
sphinx:
configuration: docs/conf.py
formats: all
python:
version: 3.7
install:
- requirements: requirements_dev.txt
- method: pip
path: .
system_packages: true

View file

@ -42,6 +42,10 @@ after_success:
after_script: after_script:
- '[ -z "${PLEXAPI_AUTH_MYPLEX_USERNAME}" ] || PYTHONPATH="$PWD:$PYTHONPATH" python -u tools/plex-teardowntest.py' - '[ -z "${PLEXAPI_AUTH_MYPLEX_USERNAME}" ] || PYTHONPATH="$PWD:$PYTHONPATH" python -u tools/plex-teardowntest.py'
notifications:
webhooks: https://coveralls.io/webhook
jobs: jobs:
include: include:
- python: 3.6 - python: 3.6
@ -72,8 +76,8 @@ jobs:
- PLEX_CONTAINER_TAG=latest - PLEX_CONTAINER_TAG=latest
deploy: deploy:
provider: pypi provider: pypi
user: mjs7231 user: hellowlol
password: password:
secure: UhuEN9GAp9zMEXdVTxSrbhfYf4HjTcj47l093Qh1HYKmZACxJM/+JkQCm7+oHPJpo7YDLk2we9oEsQ41maZBr9WgZI1lwR6m590M12vPhPI7NCVzINxJqebc0uZhCFsAFFKA3kzpRQbDfsBUG4yL/AzeMcvJMgIg3m07KRVhBywnnRhQ77trbBI0Io5MBzfW9PYDeGJqlNDBM7SbB4tK0udGZQT9wmFwvIoJODPDnM15Ry4vpkVNww/vVgyHklmnYlPzQgvhSMOXk0+MWlYtaKmu6uuLAiRccT1Fsmi1POKuFEq8S0Z7w4LmwxCVRaCvsZdNW5eXWgPDhZXNcLrKMwjgJt9Vj3VcD+NCywux/C1hTq7tecBocA13kzbgg4fd2sATOjQT5iaRPGrDtKm8e00hxr125n0StDxXdYGl2W5sH0LCkZE6Vq1GjXYjKFXZeTk3Fzav/3N8IxHBX3CliJB/vbloJ2mpz1kXL4UTORl9pghPyGOOq2yJPYSSWly/RsAD7UDrL1/lezaPSJGKbZJ0CMyfA83kd82/hgZflOuBuTcPHCZSU3zMCs0fsImZZxr6Qm1tbff+iyNS/ufoYgeVfsWhlEl9FoLv1g4HG6oA+uDHz+jKz9uSRHcGqD6P4JJK+H+yy0PeYfo7b6eSqFxgt8q8QfifUaCrVoCiY+c= secure: cwMf16s+PxIUjt6/pKE7KeQPsWg4Atf9JvipkLTjx1VrVBilSjrZ7fkSDDjglkz4sCynw5/fzQRkBWqLdyqPigwKICYbpc6QWwvR+WWQLfe04zl+d4AmDljN2rtNkThlpH2qKNaFX7Up3AAcTf+GtQ7weAAyjMyJaiWBTFcgc2eBMDgEkS3bhiF4qdbfdAYbHVurJwCWXfHjkIiBJxDHA15cQRhJpkqQGIYzAst6ZwEr/Aw9FwqfC3lvFM3uIQ7sfFa/UdIesQZ50IT//roI1bvTg2T4gAMRYMs09jFm1E5mnPn4qxvK2hzsiNNesw3wSXhCehJFym8cO5jX+EYnfNJuWJtappIZeJGKldVC2g2v3PNWhmqbbKnyc446rkPtjVQqrSSUjPNOhTG61n872JVnizopo8ISDAtceSoC/mySItzjRQnRDrelkhHdV33WSyOsJKTw0H2LzjZDQRxxTqABmmCwzn7h+ycQ2Xk2INu9gt0hgIOco6Lt6VeoDMXhD6wAGDAaD7XBQwD/JSbGZns3VIgoLMxyHda9S6UeDmwwoAWhHM0mbzt0L618R+7CK0E+3vQ2k2Ee23tKIOq7DvZMpmLLjUE3wlckoKCPDrgP7Tuf5nUoz6kWv3Hxsb0wZCuVO0npSCFx+JOSQ6/7ONU4eh3hvjnnb6vqAgKqQ80=
on: on:
tags: true tags: true

View file

@ -6,6 +6,12 @@ Python-PlexAPI
:target: https://travis-ci.org/pkkid/python-plexapi :target: https://travis-ci.org/pkkid/python-plexapi
.. image:: https://coveralls.io/repos/github/pkkid/python-plexapi/badge.svg?branch=master .. image:: https://coveralls.io/repos/github/pkkid/python-plexapi/badge.svg?branch=master
:target: https://coveralls.io/github/pkkid/python-plexapi?branch=master :target: https://coveralls.io/github/pkkid/python-plexapi?branch=master
.. image:: https://img.shields.io/github/tag/pkkid/python-plexapi.svg?label=github+release
:target: https://github.com/pkkid/python-plexapi/releases
.. image:: https://badge.fury.io/py/PlexAPI.svg
:target: https://badge.fury.io/py/PlexAPI
.. image:: https://img.shields.io/github/last-commit/pkkid/python-plexapi.svg
:target: https://img.shields.io/github/last-commit/pkkid/python-plexapi.svg
Overview Overview
@ -15,7 +21,7 @@ Plex Web Client. A few of the many features we currently support are:
* Navigate local or remote shared libraries. * Navigate local or remote shared libraries.
* Perform library actions such as scan, analyze, empty trash. * Perform library actions such as scan, analyze, empty trash.
* Remote control and play media on connected clients. * Remote control and play media on connected clients, including `Controlling Sonos speakers`_
* Listen in on all Plex Server notifications. * Listen in on all Plex Server notifications.
@ -129,35 +135,65 @@ Usage Examples
plex.library.section('TV Shows').get('The 100').rate(8.0) plex.library.section('TV Shows').get('The 100').rate(8.0)
Controlling Sonos speakers
--------------------------
To control Sonos speakers directly using Plex APIs, the following requirements must be met:
1. Active Plex Pass subscription
2. Sonos account linked to Plex account
3. Plex remote access enabled
Due to the design of Sonos music services, the API calls to control Sonos speakers route through https://sonos.plex.tv
and back via the Plex server's remote access. Actual media playback is local unless networking restrictions prevent the
Sonos speakers from connecting to the Plex server directly.
.. code-block:: python
from plexapi.myplex import MyPlexAccount
from plexapi.server import PlexServer
baseurl = 'http://plexserver:32400'
token = '2ffLuB84dqLswk9skLos'
account = MyPlexAccount(token)
server = PlexServer(baseurl, token)
# List available speakers/groups
for speaker in account.sonos_speakers():
print(speaker.title)
# Obtain PlexSonosPlayer instance
speaker = account.sonos_speaker("Kitchen")
album = server.library.section('Music').get('Stevie Wonder').album('Innervisions')
# Speaker control examples
speaker.playMedia(album)
speaker.pause()
speaker.setVolume(10)
speaker.skipNext()
Running tests over PlexAPI Running tests over PlexAPI
-------------------------- --------------------------
In order to test the PlexAPI library you have to prepare a Plex Server instance with following libraries: Use:
1. Movies section (agent `com.plexapp.agents.imdb`) containing both movies: .. code-block:: bash
* Sintel - https://durian.blender.org/
* Elephants Dream - https://orange.blender.org/
* Sita Sings the Blues - http://www.sitasingstheblues.com/
* Big Buck Bunny - https://peach.blender.org/
2. TV Show section (agent `com.plexapp.agents.thetvdb`) containing the shows:
* Game of Thrones (Season 1 and 2)
* The 100 (Seasons 1 and 2)
* (or symlink the above movies with proper names)
3. Music section (agent `com.plexapp.agents.lastfm`) containing the albums:
* Infinite State - Unmastered Impulses - https://github.com/kennethreitz/unmastered-impulses
* Broke For Free - Layers - http://freemusicarchive.org/music/broke_for_free/Layers/
4. A Photos section (any agent) containing the photoalbums (photoalbum is just a folder on your disk):
* `Cats`
* Within `Cats` album you need to place 3 photos (cute cat photos, of course)
* Within `Cats` album you should place 3 more photoalbums (one of them should be named `Cats in bed`,
names of others doesn't matter)
* Within `Cats in bed` you need to place 7 photos
* Within other 2 albums you should place 1 photo in each
Instead of manual creation of the library you could use a script `tools/plex-boostraptest.py` with appropriate tools/plex-boostraptest.py
with appropriate
arguments and add this new server to a shared user which username is defined in environment veriable `SHARED_USERNAME`. arguments and add this new server to a shared user which username is defined in environment veriable `SHARED_USERNAME`.
It uses `official docker image`_ to create a proper instance. It uses `official docker image`_ to create a proper instance.
For skipping the docker and reuse a existing server use
.. code-block:: bash
python plex-boostraptest.py --no-docker -username USERNAME --password PASSWORD --server-name NAME-OF-YOUR-SEVER
Also in order to run most of the tests you have to provide some environment variables: Also in order to run most of the tests you have to provide some environment variables:
* `PLEXAPI_AUTH_SERVER_BASEURL` containing an URL to your Plex instance, e.g. `http://127.0.0.1:32400` (without trailing * `PLEXAPI_AUTH_SERVER_BASEURL` containing an URL to your Plex instance, e.g. `http://127.0.0.1:32400` (without trailing

7
docs/modules/gdm.rst Normal file
View file

@ -0,0 +1,7 @@
.. include:: ../global.rst
Gdm :modname:`plexapi.gdm`
--------------------------------
.. automodule:: plexapi.gdm
:members:
:show-inheritance:

7
docs/modules/sonos.rst Normal file
View file

@ -0,0 +1,7 @@
.. include:: ../global.rst
Sonos :modname:`plexapi.sonos`
--------------------------------
.. automodule:: plexapi.sonos
:members:
:show-inheritance:

View file

@ -17,6 +17,7 @@
modules/client modules/client
modules/config modules/config
modules/exceptions modules/exceptions
modules/gdm
modules/library modules/library
modules/media modules/media
modules/myplex modules/myplex
@ -25,6 +26,8 @@
modules/playqueue modules/playqueue
modules/server modules/server
modules/settings modules/settings
modules/sonos
modules/sync modules/sync
modules/utils modules/utils
modules/video modules/video

View file

@ -3,9 +3,10 @@ import logging
import os import os
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
from platform import uname from platform import uname
from uuid import getnode
from plexapi.config import PlexConfig, reset_base_headers from plexapi.config import PlexConfig, reset_base_headers
from plexapi.utils import SecretsFilter from plexapi.utils import SecretsFilter
from uuid import getnode
# Load User Defined Config # Load User Defined Config
DEFAULT_CONFIG_PATH = os.path.expanduser('~/.config/plexapi/config.ini') DEFAULT_CONFIG_PATH = os.path.expanduser('~/.config/plexapi/config.ini')
@ -14,7 +15,7 @@ CONFIG = PlexConfig(CONFIG_PATH)
# PlexAPI Settings # PlexAPI Settings
PROJECT = 'PlexAPI' PROJECT = 'PlexAPI'
VERSION = '3.4.0' VERSION = '3.6.0'
TIMEOUT = CONFIG.get('plexapi.timeout', 30, int) TIMEOUT = CONFIG.get('plexapi.timeout', 30, int)
X_PLEX_CONTAINER_SIZE = CONFIG.get('plexapi.container_size', 100, int) X_PLEX_CONTAINER_SIZE = CONFIG.get('plexapi.container_size', 100, int)
X_PLEX_ENABLE_FAST_CONNECT = CONFIG.get('plexapi.enable_fast_connect', False, bool) X_PLEX_ENABLE_FAST_CONNECT = CONFIG.get('plexapi.enable_fast_connect', False, bool)

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import json import json
import threading import threading
import websocket
from plexapi import log from plexapi import log
@ -40,6 +40,11 @@ class AlertListener(threading.Thread):
self._ws = None self._ws = None
def run(self): def run(self):
try:
import websocket
except ImportError:
log.warning("Can't use the AlertListener without websocket")
return
# create the websocket connection # create the websocket connection
url = self._server.url(self.key, includeToken=True).replace('http', 'ws') url = self._server.url(self.key, includeToken=True).replace('http', 'ws')
log.info('Starting AlertListener: %s', url) log.info('Starting AlertListener: %s', url)

View file

@ -1,7 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from urllib.parse import quote_plus
from plexapi import media, utils from plexapi import media, utils
from plexapi.base import Playable, PlexPartialObject from plexapi.base import Playable, PlexPartialObject
from plexapi.compat import quote_plus
class Audio(PlexPartialObject): class Audio(PlexPartialObject):

View file

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import re import re
from urllib.parse import quote_plus, urlencode
from plexapi import log, utils from plexapi import log, utils
from plexapi.compat import quote_plus, urlencode
from plexapi.exceptions import BadRequest, NotFound, UnknownType, Unsupported from plexapi.exceptions import BadRequest, NotFound, UnknownType, Unsupported
from plexapi.utils import tag_helper from plexapi.utils import tag_helper
@ -142,19 +142,21 @@ class PlexObject(object):
clsname = cls.__name__ if cls else 'None' clsname = cls.__name__ if cls else 'None'
raise NotFound('Unable to find elem: cls=%s, attrs=%s' % (clsname, kwargs)) raise NotFound('Unable to find elem: cls=%s, attrs=%s' % (clsname, kwargs))
def fetchItems(self, ekey, cls=None, **kwargs): def fetchItems(self, ekey, cls=None, container_start=None, container_size=None, **kwargs):
""" Load the specified key to find and build all items with the specified tag """ Load the specified key to find and build all items with the specified tag
and attrs. See :func:`~plexapi.base.PlexObject.fetchItem` for more details and attrs. See :func:`~plexapi.base.PlexObject.fetchItem` for more details
on how this is used. on how this is used.
Use container_start and container_size for pagination. Parameters:
container_start (None, int): offset to get a subset of the data
container_size (None, int): How many items in data
""" """
url_kw = {} url_kw = {}
for key, value in dict(kwargs).items(): if container_start is not None:
if key == "container_start": url_kw["X-Plex-Container-Start"] = container_start
url_kw["X-Plex-Container-Start"] = kwargs.pop(key) if container_size is not None:
if key == "container_size": url_kw["X-Plex-Container-Size"] = container_size
url_kw["X-Plex-Container-Size"] = kwargs.pop(key)
if ekey is None: if ekey is None:
raise BadRequest('ekey was not provided') raise BadRequest('ekey was not provided')
@ -519,29 +521,33 @@ class PlexPartialObject(PlexObject):
key = '/library/metadata/%s/matches' % self.ratingKey key = '/library/metadata/%s/matches' % self.ratingKey
params = {'manual': 1} params = {'manual': 1}
if any([agent, title, year, language]): if agent and not any([title, year, language]):
if title is None: params['language'] = self.section().language
params['title'] = self.title params['agent'] = utils.getAgentIdentifier(self.section(), agent)
else: else:
params['title'] = title if any(x is not None for x in [agent, title, year, language]):
if title is None:
params['title'] = self.title
else:
params['title'] = title
if year is None: if year is None:
params['year'] = self.year params['year'] = self.year
else: else:
params['year'] = year params['year'] = year
params['language'] = language or self.section().language params['language'] = language or self.section().language
if agent is None: if agent is None:
params['agent'] = self.section().agent params['agent'] = self.section().agent
else: else:
params['agent'] = utils.getAgentIdentifier(self.section(), agent) params['agent'] = utils.getAgentIdentifier(self.section(), agent)
key = key + '?' + urlencode(params) key = key + '?' + urlencode(params)
data = self._server.query(key, method=self._server._session.get) data = self._server.query(key, method=self._server._session.get)
return self.findItems(data) return self.findItems(data, initpath=key)
def fixMatch(self, searchResult=None, auto=False): def fixMatch(self, searchResult=None, auto=False, agent=None):
""" Use match result to update show metadata. """ Use match result to update show metadata.
Parameters: Parameters:
@ -549,10 +555,15 @@ class PlexPartialObject(PlexObject):
False allows user to provide the match False allows user to provide the match
searchResult (:class:`~plexapi.media.SearchResult`): Search result from searchResult (:class:`~plexapi.media.SearchResult`): Search result from
~plexapi.base.matches() ~plexapi.base.matches()
agent (str): Agent name to be used (imdb, thetvdb, themoviedb, etc.)
""" """
key = '/library/metadata/%s/match' % self.ratingKey key = '/library/metadata/%s/match' % self.ratingKey
if auto: if auto:
searchResult = self.matches()[0] autoMatch = self.matches(agent=agent)
if autoMatch:
searchResult = autoMatch[0]
else:
raise NotFound('No matches found using this agent: (%s:%s)' % (agent, autoMatch))
elif not searchResult: elif not searchResult:
raise NotFound('fixMatch() requires either auto=True or ' raise NotFound('fixMatch() requires either auto=True or '
'searchResult=:class:`~plexapi.media.SearchResult`.') 'searchResult=:class:`~plexapi.media.SearchResult`.')
@ -651,6 +662,14 @@ class Playable(object):
key = '%s/split' % self.key key = '%s/split' % self.key
return self._server.query(key, method=self._server._session.put) return self._server.query(key, method=self._server._session.put)
def merge(self, ratingKeys):
"""Merge duplicate items."""
if not isinstance(ratingKeys, list):
ratingKeys = str(ratingKeys).split(",")
key = '%s/merge?ids=%s' % (self.key, ','.join(ratingKeys))
return self._server.query(key, method=self._server._session.put)
def unmatch(self): def unmatch(self):
"""Unmatch a media file.""" """Unmatch a media file."""
key = '%s/unmatch' % self.key key = '%s/unmatch' % self.key

View file

@ -1,10 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import time import time
from xml.etree import ElementTree
import requests import requests
from plexapi import BASE_HEADERS, CONFIG, TIMEOUT, log, logfilter, utils from plexapi import BASE_HEADERS, CONFIG, TIMEOUT, log, logfilter, utils
from plexapi.base import PlexObject from plexapi.base import PlexObject
from plexapi.compat import ElementTree
from plexapi.exceptions import BadRequest, NotFound, Unauthorized, Unsupported from plexapi.exceptions import BadRequest, NotFound, Unauthorized, Unsupported
from plexapi.playqueue import PlayQueue from plexapi.playqueue import PlayQueue
from requests.status_codes import _codes as codes from requests.status_codes import _codes as codes
@ -157,7 +157,7 @@ class PlexClient(PlexObject):
log.debug('%s %s', method.__name__.upper(), url) log.debug('%s %s', method.__name__.upper(), url)
headers = self._headers(**headers or {}) headers = self._headers(**headers or {})
response = method(url, headers=headers, timeout=timeout, **kwargs) response = method(url, headers=headers, timeout=timeout, **kwargs)
if response.status_code not in (200, 201): if response.status_code not in (200, 201, 204):
codename = codes.get(response.status_code)[0] codename = codes.get(response.status_code)[0]
errtext = response.text.replace('\n', ' ') errtext = response.text.replace('\n', ' ')
message = '(%s) %s; %s %s' % (response.status_code, codename, response.url, errtext) message = '(%s) %s; %s %s' % (response.status_code, codename, response.url, errtext)

View file

@ -1,123 +0,0 @@
# -*- coding: utf-8 -*-
# Python 2/3 compatability
# Always try Py3 first
import os
import sys
from sys import version_info
ustr = str
if version_info < (3,):
ustr = unicode
try:
string_type = basestring
except NameError:
string_type = str
try:
from urllib.parse import urlencode
except ImportError:
from urllib import urlencode
try:
from urllib.parse import quote
except ImportError:
from urllib import quote
try:
from urllib.parse import quote_plus, quote
except ImportError:
from urllib import quote_plus, quote
try:
from urllib.parse import unquote
except ImportError:
from urllib import unquote
try:
from configparser import ConfigParser
except ImportError:
from ConfigParser import ConfigParser
try:
from xml.etree import cElementTree as ElementTree
except ImportError:
from xml.etree import ElementTree
try:
from unittest.mock import patch, MagicMock
except ImportError:
from mock import patch, MagicMock
def makedirs(name, mode=0o777, exist_ok=False):
""" Mimicks os.makedirs() from Python 3. """
try:
os.makedirs(name, mode)
except OSError:
if not os.path.isdir(name) or not exist_ok:
raise
def which(cmd, mode=os.F_OK | os.X_OK, path=None):
"""Given a command, mode, and a PATH string, return the path which
conforms to the given mode on the PATH, or None if there is no such
file.
`mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result
of os.environ.get("PATH"), or can be overridden with a custom search
path.
Copied from https://hg.python.org/cpython/file/default/Lib/shutil.py
"""
# Check that a given file can be accessed with the correct mode.
# Additionally check that `file` is not a directory, as on Windows
# directories pass the os.access check.
def _access_check(fn, mode):
return (os.path.exists(fn) and os.access(fn, mode)
and not os.path.isdir(fn))
# If we're given a path with a directory part, look it up directly rather
# than referring to PATH directories. This includes checking relative to the
# current directory, e.g. ./script
if os.path.dirname(cmd):
if _access_check(cmd, mode):
return cmd
return None
if path is None:
path = os.environ.get("PATH", os.defpath)
if not path:
return None
path = path.split(os.pathsep)
if sys.platform == "win32":
# The current directory takes precedence on Windows.
if not os.curdir in path:
path.insert(0, os.curdir)
# PATHEXT is necessary to check on Windows.
pathext = os.environ.get("PATHEXT", "").split(os.pathsep)
# See if the given file matches any of the expected path extensions.
# This will allow us to short circuit when given "python.exe".
# If it does match, only test that one, otherwise we have to try
# others.
if any(cmd.lower().endswith(ext.lower()) for ext in pathext):
files = [cmd]
else:
files = [cmd + ext for ext in pathext]
else:
# On other platforms you don't have things like PATHEXT to tell you
# what file suffixes are executable, so just pass on cmd as-is.
files = [cmd]
seen = set()
for dir in path:
normdir = os.path.normcase(dir)
if not normdir in seen:
seen.add(normdir)
for thefile in files:
name = os.path.join(dir, thefile)
if _access_check(name, mode):
return name
return None

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
from collections import defaultdict from collections import defaultdict
from plexapi.compat import ConfigParser from configparser import ConfigParser
class PlexConfig(ConfigParser): class PlexConfig(ConfigParser):
@ -13,6 +13,7 @@ class PlexConfig(ConfigParser):
Parameters: Parameters:
path (str): Path of the configuration file to load. 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)

View file

@ -4,9 +4,8 @@ Support for discovery using GDM (Good Day Mate), multicast protocol by Plex.
# Licensed Apache 2.0 # Licensed Apache 2.0
# From https://github.com/home-assistant/netdisco/netdisco/gdm.py # From https://github.com/home-assistant/netdisco/netdisco/gdm.py
Inspired by Inspired by:
hippojay's plexGDM: hippojay's plexGDM: https://github.com/hippojay/script.plexbmc.helper/resources/lib/plexgdm.py
https://github.com/hippojay/script.plexbmc.helper/resources/lib/plexgdm.py
iBaa's PlexConnect: https://github.com/iBaa/PlexConnect/PlexAPI.py iBaa's PlexConnect: https://github.com/iBaa/PlexConnect/PlexAPI.py
""" """
import socket import socket
@ -50,31 +49,33 @@ class GDM:
Examples of the dict list assigned to self.entries by this function: Examples of the dict list assigned to self.entries by this function:
Server: Server:
[{'data': {
'Content-Type': 'plex/media-server',
'Host': '53f4b5b6023d41182fe88a99b0e714ba.plex.direct',
'Name': 'myfirstplexserver',
'Port': '32400',
'Resource-Identifier': '646ab0aa8a01c543e94ba975f6fd6efadc36b7',
'Updated-At': '1585769946',
'Version': '1.18.8.2527-740d4c206',
},
'from': ('10.10.10.100', 32414)}]
Clients: [{'data': {
[{'data': {'Content-Type': 'plex/media-player', 'Content-Type': 'plex/media-server',
'Device-Class': 'stb', 'Host': '53f4b5b6023d41182fe88a99b0e714ba.plex.direct',
'Name': 'plexamp', 'Name': 'myfirstplexserver',
'Port': '36000', 'Port': '32400',
'Product': 'Plexamp', 'Resource-Identifier': '646ab0aa8a01c543e94ba975f6fd6efadc36b7',
'Protocol': 'plex', 'Updated-At': '1585769946',
'Protocol-Capabilities': 'timeline,playback,playqueues,playqueues-creation', 'Version': '1.18.8.2527-740d4c206',
'Protocol-Version': '1', },
'Resource-Identifier': 'b6e57a3f-e0f8-494f-8884-f4b58501467e', 'from': ('10.10.10.100', 32414)}]
'Version': '1.1.0',
}, Clients:
'from': ('10.10.10.101', 32412)}]
[{'data': {'Content-Type': 'plex/media-player',
'Device-Class': 'stb',
'Name': 'plexamp',
'Port': '36000',
'Product': 'Plexamp',
'Protocol': 'plex',
'Protocol-Capabilities': 'timeline,playback,playqueues,playqueues-creation',
'Protocol-Version': '1',
'Resource-Identifier': 'b6e57a3f-e0f8-494f-8884-f4b58501467e',
'Version': '1.1.0',
},
'from': ('10.10.10.101', 32412)}]
""" """
gdm_msg = 'M-SEARCH * HTTP/1.0'.encode('ascii') gdm_msg = 'M-SEARCH * HTTP/1.0'.encode('ascii')

View file

@ -1,7 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from urllib.parse import quote, quote_plus, unquote, urlencode
from plexapi import X_PLEX_CONTAINER_SIZE, log, utils from plexapi import X_PLEX_CONTAINER_SIZE, log, utils
from plexapi.base import PlexObject from plexapi.base import PlexObject
from plexapi.compat import quote_plus, unquote, urlencode
from plexapi.exceptions import BadRequest, NotFound from plexapi.exceptions import BadRequest, NotFound
from plexapi.media import MediaTag from plexapi.media import MediaTag
from plexapi.settings import Setting from plexapi.settings import Setting
@ -360,6 +361,40 @@ class LibrarySection(PlexObject):
# Private attrs as we dont want a reload. # Private attrs as we dont want a reload.
self._total_size = None self._total_size = None
def fetchItems(self, ekey, cls=None, container_start=None, container_size=None, **kwargs):
""" Load the specified key to find and build all items with the specified tag
and attrs. See :func:`~plexapi.base.PlexObject.fetchItem` for more details
on how this is used.
Parameters:
container_start (None, int): offset to get a subset of the data
container_size (None, int): How many items in data
"""
url_kw = {}
if container_start is not None:
url_kw["X-Plex-Container-Start"] = container_start
if container_size is not None:
url_kw["X-Plex-Container-Size"] = container_size
if ekey is None:
raise BadRequest('ekey was not provided')
data = self._server.query(ekey, params=url_kw)
if '/all' in ekey:
# totalSize is only included in the xml response
# if container size is used.
total_size = data.attrib.get("totalSize") or data.attrib.get("size")
self._total_size = utils.cast(int, total_size)
items = self.findItems(data, cls, ekey, **kwargs)
librarySectionID = data.attrib.get('librarySectionID')
if librarySectionID:
for item in items:
item.librarySectionID = librarySectionID
return items
@property @property
def totalSize(self): def totalSize(self):
if self._total_size is None: if self._total_size is None:
@ -404,7 +439,7 @@ class LibrarySection(PlexObject):
Parameters: Parameters:
title (str): Title of the item to return. title (str): Title of the item to return.
""" """
key = '/library/sections/%s/all?title=%s' % (self.key, title) key = '/library/sections/%s/all?title=%s' % (self.key, quote(title, safe=''))
return self.fetchItem(key, title__iexact=title) return self.fetchItem(key, title__iexact=title)
def all(self, sort=None, **kwargs): def all(self, sort=None, **kwargs):
@ -505,9 +540,9 @@ class LibrarySection(PlexObject):
key = '/library/sections/%s/%s%s' % (self.key, category, utils.joinArgs(args)) key = '/library/sections/%s/%s%s' % (self.key, category, utils.joinArgs(args))
return self.fetchItems(key, cls=FilterChoice) return self.fetchItems(key, cls=FilterChoice)
def search(self, title=None, sort=None, maxresults=999999, libtype=None, **kwargs): def search(self, title=None, sort=None, maxresults=None,
""" Search the library. If there are many results, they will be fetched from the server libtype=None, container_start=0, container_size=X_PLEX_CONTAINER_SIZE, **kwargs):
in batches of X_PLEX_CONTAINER_SIZE amounts. If you're only looking for the first <num> """ Search the library. The http requests will be batched in container_size. If you're only looking for the first <num>
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.
@ -518,6 +553,8 @@ class LibrarySection(PlexObject):
maxresults (int): Only return the specified number of results (optional). maxresults (int): Only return the specified number of results (optional).
libtype (str): Filter results to a spcifiec libtype (movie, show, episode, artist, libtype (str): Filter results to a spcifiec libtype (movie, show, episode, artist,
album, track; optional). album, track; optional).
container_start (int): default 0
container_size (int): default X_PLEX_CONTAINER_SIZE in your config file.
**kwargs (dict): 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. Negative filtering also possible, just add an matches allowed. Multiple matches OR together. Negative filtering also possible, just add an
exclamation mark to the end of filter name, e.g. `resolution!=1x1`. exclamation mark to the end of filter name, e.g. `resolution!=1x1`.
@ -549,15 +586,37 @@ class LibrarySection(PlexObject):
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
results, subresults = [], '_init' results = []
args['X-Plex-Container-Start'] = 0 subresults = []
args['X-Plex-Container-Size'] = min(X_PLEX_CONTAINER_SIZE, maxresults) offset = container_start
while subresults and maxresults > len(results):
if maxresults is not None:
container_size = min(container_size, maxresults)
while True:
key = '/library/sections/%s/all%s' % (self.key, utils.joinArgs(args)) key = '/library/sections/%s/all%s' % (self.key, utils.joinArgs(args))
subresults = self.fetchItems(key) subresults = self.fetchItems(key, container_start=container_start,
results += subresults[:maxresults - len(results)] container_size=container_size)
args['X-Plex-Container-Start'] += args['X-Plex-Container-Size'] if not len(subresults):
if offset > self.totalSize:
log.info("container_start is higher then the number of items in the library")
break
results.extend(subresults)
# self.totalSize is not used as a condition in the while loop as
# this require a additional http request.
# self.totalSize is updated from .fetchItems
wanted_number_of_items = self.totalSize - offset
if maxresults is not None:
wanted_number_of_items = min(maxresults, wanted_number_of_items)
container_size = min(container_size, maxresults - len(results))
if wanted_number_of_items <= len(results):
break
container_start += container_size
return results return results
def _cleanSearchFilter(self, category, value, libtype=None): def _cleanSearchFilter(self, category, value, libtype=None):
@ -584,7 +643,7 @@ class LibrarySection(PlexObject):
matches = [k for t, k in lookup.items() if item in t] matches = [k for t, k in lookup.items() if item in t]
if matches: map(result.add, matches); continue if matches: map(result.add, matches); continue
# nothing matched; use raw item value # nothing matched; use raw item value
log.warning('Filter value not listed, using raw item value: %s' % item) log.debug('Filter value not listed, using raw item value: %s' % item)
result.add(item) result.add(item)
return ','.join(result) return ','.join(result)

View file

@ -1,8 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import xml import xml
from urllib.parse import quote_plus
from plexapi import compat, log, settings, utils from plexapi import log, settings, utils
from plexapi.base import PlexObject from plexapi.base import PlexObject
from plexapi.exceptions import BadRequest from plexapi.exceptions import BadRequest
from plexapi.utils import cast from plexapi.utils import cast
@ -619,7 +620,7 @@ class Poster(PlexObject):
def select(self): def select(self):
key = self._initpath[:-1] key = self._initpath[:-1]
data = '%s?url=%s' % (key, compat.quote_plus(self.ratingKey)) data = '%s?url=%s' % (key, quote_plus(self.ratingKey))
try: try:
self._server.query(data, method=self._server._session.put) self._server.query(data, method=self._server._session.put)
except xml.etree.ElementTree.ParseError: except xml.etree.ElementTree.ParseError:

View file

@ -2,16 +2,17 @@
import copy import copy
import threading import threading
import time import time
from xml.etree import ElementTree
import requests import requests
from plexapi import (BASE_HEADERS, CONFIG, TIMEOUT, X_PLEX_ENABLE_FAST_CONNECT, from plexapi import (BASE_HEADERS, CONFIG, TIMEOUT, X_PLEX_ENABLE_FAST_CONNECT,
X_PLEX_IDENTIFIER, log, logfilter, utils) X_PLEX_IDENTIFIER, log, logfilter, utils)
from plexapi.base import PlexObject from plexapi.base import PlexObject
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
from plexapi.client import PlexClient from plexapi.client import PlexClient
from plexapi.compat import ElementTree from plexapi.exceptions import BadRequest, NotFound, Unauthorized
from plexapi.library import LibrarySection from plexapi.library import LibrarySection
from plexapi.server import PlexServer from plexapi.server import PlexServer
from plexapi.sonos import PlexSonosClient
from plexapi.sync import SyncItem, SyncList from plexapi.sync import SyncItem, SyncList
from plexapi.utils import joinArgs from plexapi.utils import joinArgs
from requests.status_codes import _codes as codes from requests.status_codes import _codes as codes
@ -88,6 +89,8 @@ class MyPlexAccount(PlexObject):
def __init__(self, username=None, password=None, token=None, session=None, timeout=None): def __init__(self, username=None, password=None, token=None, session=None, timeout=None):
self._token = token self._token = token
self._session = session or requests.Session() self._session = session or requests.Session()
self._sonos_cache = []
self._sonos_cache_timestamp = 0
data, initpath = self._signin(username, password, timeout) data, initpath = self._signin(username, password, timeout)
super(MyPlexAccount, self).__init__(self, data, initpath) super(MyPlexAccount, self).__init__(self, data, initpath)
@ -209,6 +212,24 @@ class MyPlexAccount(PlexObject):
data = self.query(MyPlexResource.key) data = self.query(MyPlexResource.key)
return [MyPlexResource(self, elem) for elem in data] return [MyPlexResource(self, elem) for elem in data]
def sonos_speakers(self):
if 'companions_sonos' not in self.subscriptionFeatures:
return []
t = time.time()
if t - self._sonos_cache_timestamp > 5:
self._sonos_cache_timestamp = t
data = self.query('https://sonos.plex.tv/resources')
self._sonos_cache = [PlexSonosClient(self, elem) for elem in data]
return self._sonos_cache
def sonos_speaker(self, name):
return next((x for x in self.sonos_speakers() if x.title.split("+")[0].strip() == name), None)
def sonos_speaker_by_id(self, identifier):
return next((x for x in self.sonos_speakers() if x.machineIdentifier.startswith(identifier)), None)
def inviteFriend(self, user, server, sections=None, allowSync=False, allowCameraUpload=False, def inviteFriend(self, user, server, sections=None, allowSync=False, allowCameraUpload=False,
allowChannels=False, filterMovies=None, filterTelevision=None, filterMusic=None): allowChannels=False, filterMovies=None, filterTelevision=None, filterMusic=None):
""" Share library content with the specified user. """ Share library content with the specified user.

View file

@ -1,8 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from urllib.parse import quote_plus
from plexapi import media, utils from plexapi import media, utils
from plexapi.base import PlexPartialObject from plexapi.base import PlexPartialObject
from plexapi.exceptions import NotFound, BadRequest from plexapi.exceptions import BadRequest, NotFound
from plexapi.compat import quote_plus
@utils.registerPlexObject @utils.registerPlexObject

View file

@ -1,11 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from urllib.parse import quote_plus
from plexapi import utils from plexapi import utils
from plexapi.base import PlexPartialObject, Playable from plexapi.base import Playable, PlexPartialObject
from plexapi.exceptions import BadRequest, Unsupported from plexapi.exceptions import BadRequest, Unsupported
from plexapi.library import LibrarySection from plexapi.library import LibrarySection
from plexapi.playqueue import PlayQueue from plexapi.playqueue import PlayQueue
from plexapi.utils import cast, toDatetime from plexapi.utils import cast, toDatetime
from plexapi.compat import quote_plus
@utils.registerPlexObject @utils.registerPlexObject

View file

@ -1,23 +1,29 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from urllib.parse import urlencode
from xml.etree import ElementTree
import requests import requests
from requests.status_codes import _codes as codes # Need these imports to populate utils.PLEXOBJECTS
from plexapi import BASE_HEADERS, CONFIG, TIMEOUT, X_PLEX_CONTAINER_SIZE from plexapi import (BASE_HEADERS, CONFIG, TIMEOUT, X_PLEX_CONTAINER_SIZE, log,
from plexapi import log, logfilter, utils logfilter)
from plexapi import media as _media # noqa: F401
from plexapi import photo as _photo # noqa: F401
from plexapi import playlist as _playlist # noqa: F401
from plexapi import utils
from plexapi import video as _video # noqa: F401
from plexapi.alert import AlertListener from plexapi.alert import AlertListener
from plexapi.base import PlexObject from plexapi.base import PlexObject
from plexapi.client import PlexClient from plexapi.client import PlexClient
from plexapi.compat import ElementTree, urlencode
from plexapi.exceptions import BadRequest, NotFound, Unauthorized from plexapi.exceptions import BadRequest, NotFound, Unauthorized
from plexapi.library import Library, Hub from plexapi.library import Hub, Library
from plexapi.settings import Settings from plexapi.media import Conversion, Optimized
from plexapi.playlist import Playlist from plexapi.playlist import Playlist
from plexapi.playqueue import PlayQueue from plexapi.playqueue import PlayQueue
from plexapi.settings import Settings
from plexapi.utils import cast from plexapi.utils import cast
from plexapi.media import Optimized, Conversion from requests.status_codes import _codes as codes
# Need these imports to populate utils.PLEXOBJECTS from plexapi import audio as _audio # noqa: F401; noqa: F401
from plexapi import (audio as _audio, video as _video, # noqa: F401
photo as _photo, media as _media, playlist as _playlist) # noqa: F401
class PlexServer(PlexObject): class PlexServer(PlexObject):

View file

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from collections import defaultdict from collections import defaultdict
from urllib.parse import quote
from plexapi import log, utils from plexapi import log, utils
from plexapi.base import PlexObject from plexapi.base import PlexObject
from plexapi.compat import quote, string_type
from plexapi.exceptions import BadRequest, NotFound from plexapi.exceptions import BadRequest, NotFound
@ -106,7 +106,7 @@ class Setting(PlexObject):
'bool': {'type': bool, 'cast': _bool_cast, 'tostr': _bool_str}, 'bool': {'type': bool, 'cast': _bool_cast, 'tostr': _bool_str},
'double': {'type': float, 'cast': float, 'tostr': _str}, 'double': {'type': float, 'cast': float, 'tostr': _str},
'int': {'type': int, 'cast': int, 'tostr': _str}, 'int': {'type': int, 'cast': int, 'tostr': _str},
'text': {'type': string_type, 'cast': _str, 'tostr': _str}, 'text': {'type': str, 'cast': _str, 'tostr': _str},
} }
def _loadData(self, data): def _loadData(self, data):

116
plexapi/sonos.py Normal file
View file

@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
import requests
from plexapi import CONFIG, X_PLEX_IDENTIFIER
from plexapi.client import PlexClient
from plexapi.exceptions import BadRequest
from plexapi.playqueue import PlayQueue
class PlexSonosClient(PlexClient):
""" Class for interacting with a Sonos speaker via the Plex API. This class
makes requests to an external Plex API which then forwards the
Sonos-specific commands back to your Plex server & Sonos speakers. Use
of this feature requires an active Plex Pass subscription and Sonos
speakers linked to your Plex account. It also requires remote access to
be working properly.
More details on the Sonos integration are avaialble here:
https://support.plex.tv/articles/218237558-requirements-for-using-plex-for-sonos/
The Sonos API emulates the Plex player control API closely:
https://github.com/plexinc/plex-media-player/wiki/Remote-control-API
Parameters:
account (:class:`~plexapi.myplex.PlexAccount`): PlexAccount instance this
Sonos speaker is associated with.
data (ElementTree): Response from Plex Sonos API used to build this client.
Attributes:
deviceClass (str): "speaker"
lanIP (str): Local IP address of speaker.
machineIdentifier (str): Unique ID for this device.
platform (str): "Sonos"
platformVersion (str): Build version of Sonos speaker firmware.
product (str): "Sonos"
protocol (str): "plex"
protocolCapabilities (list<str>): List of client capabilities (timeline, playback,
playqueues, provider-playback)
server (:class:`~plexapi.server.PlexServer`): Server this client is connected to.
session (:class:`~requests.Session`): Session object used for connection.
title (str): Name of this Sonos speaker.
token (str): X-Plex-Token used for authenication
_baseurl (str): Address of public Plex Sonos API endpoint.
_commandId (int): Counter for commands sent to Plex API.
_token (str): Token associated with linked Plex account.
_session (obj): Requests session object used to access this client.
"""
def __init__(self, account, data):
self._data = data
self.deviceClass = data.attrib.get("deviceClass")
self.machineIdentifier = data.attrib.get("machineIdentifier")
self.product = data.attrib.get("product")
self.platform = data.attrib.get("platform")
self.platformVersion = data.attrib.get("platformVersion")
self.protocol = data.attrib.get("protocol")
self.protocolCapabilities = data.attrib.get("protocolCapabilities")
self.lanIP = data.attrib.get("lanIP")
self.title = data.attrib.get("title")
self._baseurl = "https://sonos.plex.tv"
self._commandId = 0
self._token = account._token
self._session = account._session or requests.Session()
# Dummy values for PlexClient inheritance
self._last_call = 0
self._proxyThroughServer = False
self._showSecrets = CONFIG.get("log.show_secrets", "").lower() == "true"
def playMedia(self, media, offset=0, **params):
if hasattr(media, "playlistType"):
mediatype = media.playlistType
else:
if isinstance(media, PlayQueue):
mediatype = media.items[0].listType
else:
mediatype = media.listType
if mediatype == "audio":
mediatype = "music"
else:
raise BadRequest("Sonos currently only supports music for playback")
server_protocol, server_address, server_port = media._server._baseurl.split(":")
server_address = server_address.strip("/")
server_port = server_port.strip("/")
playqueue = (
media
if isinstance(media, PlayQueue)
else media._server.createPlayQueue(media)
)
self.sendCommand(
"playback/playMedia",
**dict(
{
"type": "music",
"providerIdentifier": "com.plexapp.plugins.library",
"containerKey": "/playQueues/{}?own=1".format(
playqueue.playQueueID
),
"key": media.key,
"offset": offset,
"machineIdentifier": media._server.machineIdentifier,
"protocol": server_protocol,
"address": server_address,
"port": server_port,
"token": media._server.createToken(),
"commandID": self._nextCommandId(),
"X-Plex-Client-Identifier": X_PLEX_IDENTIFIER,
"X-Plex-Token": media._server._token,
"X-Plex-Target-Client-Identifier": self.machineIdentifier,
},
**params
)
)

View file

@ -7,11 +7,15 @@ import zipfile
from datetime import datetime from datetime import datetime
from getpass import getpass from getpass import getpass
from threading import Event, Thread from threading import Event, Thread
from urllib.parse import quote
import requests import requests
from plexapi import compat
from plexapi.exceptions import NotFound from plexapi.exceptions import NotFound
from tqdm import tqdm
try:
from tqdm import tqdm
except ImportError:
tqdm = None
log = logging.getLogger('plexapi') log = logging.getLogger('plexapi')
@ -37,7 +41,7 @@ class SecretsFilter(logging.Filter):
def filter(self, record): def filter(self, record):
cleanargs = list(record.args) cleanargs = list(record.args)
for i in range(len(cleanargs)): for i in range(len(cleanargs)):
if isinstance(cleanargs[i], compat.string_type): if isinstance(cleanargs[i], str):
for secret in self.secrets: for secret in self.secrets:
cleanargs[i] = cleanargs[i].replace(secret, '<hidden>') cleanargs[i] = cleanargs[i].replace(secret, '<hidden>')
record.args = tuple(cleanargs) record.args = tuple(cleanargs)
@ -95,8 +99,8 @@ def joinArgs(args):
return '' return ''
arglist = [] arglist = []
for key in sorted(args, key=lambda x: x.lower()): for key in sorted(args, key=lambda x: x.lower()):
value = compat.ustr(args[key]) value = str(args[key])
arglist.append('%s=%s' % (key, compat.quote(value))) arglist.append('%s=%s' % (key, quote(value, safe='')))
return '?%s' % '&'.join(arglist) return '?%s' % '&'.join(arglist)
@ -144,8 +148,8 @@ def searchType(libtype):
Raises: Raises:
:class:`plexapi.exceptions.NotFound`: Unknown libtype :class:`plexapi.exceptions.NotFound`: Unknown libtype
""" """
libtype = compat.ustr(libtype) libtype = str(libtype)
if libtype in [compat.ustr(v) for v in SEARCHTYPES.values()]: if libtype in [str(v) for v in SEARCHTYPES.values()]:
return libtype return libtype
if SEARCHTYPES.get(libtype) is not None: if SEARCHTYPES.get(libtype) is not None:
return SEARCHTYPES[libtype] return SEARCHTYPES[libtype]
@ -271,7 +275,7 @@ def download(url, token, filename=None, savepath=None, session=None, chunksize=4
response = session.get(url, headers=headers, stream=True) response = session.get(url, headers=headers, stream=True)
# make sure the savepath directory exists # make sure the savepath directory exists
savepath = savepath or os.getcwd() savepath = savepath or os.getcwd()
compat.makedirs(savepath, exist_ok=True) os.makedirs(savepath, exist_ok=True)
# try getting filename from header if not specified in arguments (used for logs, db) # try getting filename from header if not specified in arguments (used for logs, db)
if not filename and response.headers.get('Content-Disposition'): if not filename and response.headers.get('Content-Disposition'):
@ -294,17 +298,17 @@ def download(url, token, filename=None, savepath=None, session=None, chunksize=4
# save the file to disk # save the file to disk
log.info('Downloading: %s', fullpath) log.info('Downloading: %s', fullpath)
if showstatus: # pragma: no cover if showstatus and tqdm: # pragma: no cover
total = int(response.headers.get('content-length', 0)) total = int(response.headers.get('content-length', 0))
bar = tqdm(unit='B', unit_scale=True, total=total, desc=filename) bar = tqdm(unit='B', unit_scale=True, total=total, desc=filename)
with open(fullpath, 'wb') as handle: with open(fullpath, 'wb') as handle:
for chunk in response.iter_content(chunk_size=chunksize): for chunk in response.iter_content(chunk_size=chunksize):
handle.write(chunk) handle.write(chunk)
if showstatus: if showstatus and tqdm:
bar.update(len(chunk)) bar.update(len(chunk))
if showstatus: # pragma: no cover if showstatus and tqdm: # pragma: no cover
bar.close() bar.close()
# check we want to unzip the contents # check we want to unzip the contents
if fullpath.endswith('zip') and unpack: if fullpath.endswith('zip') and unpack:

View file

@ -3,6 +3,3 @@
# pip install -r requirements.txt # pip install -r requirements.txt
#--------------------------------------------------------- #---------------------------------------------------------
requests requests
tqdm
websocket-client
mock; python_version < '3.3'

View file

@ -11,10 +11,13 @@ pytest-cov
pytest-mock<=1.11.1 pytest-mock<=1.11.1
recommonmark recommonmark
requests requests
requests-mock
sphinx sphinx
sphinxcontrib-napoleon sphinxcontrib-napoleon
tqdm tqdm
websocket-client websocket-client
mock; python_version < '3.3'
# Installing sphinx-rtd-theme directly from github above is used until a point release # Installing sphinx-rtd-theme directly from github above is used until a point release
# above 0.4.3 is released. https://github.com/readthedocs/sphinx_rtd_theme/issues/739 # above 0.4.3 is released. https://github.com/readthedocs/sphinx_rtd_theme/issues/739

View file

@ -7,12 +7,12 @@ from os import environ
import plexapi import plexapi
import pytest import pytest
import requests import requests
from plexapi import compat
from plexapi.client import PlexClient from plexapi.client import PlexClient
from plexapi.compat import MagicMock, patch
from plexapi.myplex import MyPlexAccount from plexapi.myplex import MyPlexAccount
from plexapi.server import PlexServer from plexapi.server import PlexServer
from .payloads import ACCOUNT_XML
try: try:
from unittest.mock import patch, MagicMock, mock_open from unittest.mock import patch, MagicMock, mock_open
except ImportError: except ImportError:
@ -138,6 +138,12 @@ def account_synctarget(account_plexpass):
return account_plexpass return account_plexpass
@pytest.fixture()
def mocked_account(requests_mock):
requests_mock.get("https://plex.tv/users/account", text=ACCOUNT_XML)
return MyPlexAccount(token="faketoken")
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def plex(request): def plex(request):
assert SERVER_BASEURL, "Required SERVER_BASEURL not specified." assert SERVER_BASEURL, "Required SERVER_BASEURL not specified."
@ -224,17 +230,17 @@ def collection(plex):
@pytest.fixture() @pytest.fixture()
def artist(music): def artist(music):
return music.get("Infinite State") return music.get("Broke For Free")
@pytest.fixture() @pytest.fixture()
def album(artist): def album(artist):
return artist.album("Unmastered Impulses") return artist.album("Layers")
@pytest.fixture() @pytest.fixture()
def track(album): def track(album):
return album.track("Holy Moment") return album.track("As Colourful as Ever")
@pytest.fixture() @pytest.fixture()
@ -347,7 +353,7 @@ def is_section(key):
def is_string(value, gte=1): def is_string(value, gte=1):
return isinstance(value, compat.string_type) and len(value) >= gte return isinstance(value, str) and len(value) >= gte
def is_thumb(key): def is_thumb(key):

BIN
tests/data/audio_stub.mp3 Normal file

Binary file not shown.

BIN
tests/data/cute_cat.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
tests/data/video_stub.mp4 Normal file

Binary file not shown.

24
tests/payloads.py Normal file
View file

@ -0,0 +1,24 @@
ACCOUNT_XML = """<?xml version="1.0" encoding="UTF-8"?>
<user email="testuser@email.com" id="12345" uuid="1234567890" mailing_list_status="active" thumb="https://plex.tv/users/1234567890abcdef/avatar?c=12345" username="testuser" title="testuser" cloudSyncDevice="" locale="" authenticationToken="faketoken" authToken="faketoken" scrobbleTypes="" restricted="0" home="1" guest="0" queueEmail="queue+1234567890@save.plex.tv" queueUid="" hasPassword="true" homeSize="2" maxHomeSize="15" secure="1" certificateVersion="2">
<subscription active="1" status="Active" plan="lifetime">
<feature id="companions_sonos"/>
</subscription>
<roles>
<role id="plexpass"/>
</roles>
<entitlements all="1"/>
<profile_settings default_audio_language="en" default_subtitle_language="en" auto_select_subtitle="1" auto_select_audio="1" default_subtitle_accessibility="0" default_subtitle_forced="0"/>
<services/>
<username>testuser</username>
<email>testuser@email.com</email>
<joined-at type="datetime">2000-01-01 12:348:56 UTC</joined-at>
<authentication-token>faketoken</authentication-token>
</user>
"""
SONOS_RESOURCES = """<MediaContainer size="3">
<Player title="Speaker 1" machineIdentifier="RINCON_12345678901234561:1234567891" deviceClass="speaker" product="Sonos" platform="Sonos" platformVersion="56.0-76060" protocol="plex" protocolVersion="1" protocolCapabilities="timeline,playback,playqueues,provider-playback" lanIP="192.168.1.11"/>
<Player title="Speaker 2 + 1" machineIdentifier="RINCON_12345678901234562:1234567892" deviceClass="speaker" product="Sonos" platform="Sonos" platformVersion="56.0-76060" protocol="plex" protocolVersion="1" protocolCapabilities="timeline,playback,playqueues,provider-playback" lanIP="192.168.1.12"/>
<Player title="Speaker 3" machineIdentifier="RINCON_12345678901234563:1234567893" deviceClass="speaker" product="Sonos" platform="Sonos" platformVersion="56.0-76060" protocol="plex" protocolVersion="1" protocolCapabilities="timeline,playback,playqueues,provider-playback" lanIP="192.168.1.13"/>
</MediaContainer>
"""

View file

@ -8,29 +8,29 @@ def test_audio_Artist_attr(artist):
artist.reload() artist.reload()
assert utils.is_datetime(artist.addedAt) assert utils.is_datetime(artist.addedAt)
assert artist.countries == [] assert artist.countries == []
assert [i.tag for i in artist.genres] in [[], ['Electronic']] assert "Electronic" in [i.tag for i in artist.genres]
assert utils.is_string(artist.guid, gte=5) assert utils.is_string(artist.guid, gte=5)
assert artist.index == '1' assert artist.index == "1"
assert utils.is_metadata(artist._initpath) assert utils.is_metadata(artist._initpath)
assert utils.is_metadata(artist.key) assert utils.is_metadata(artist.key)
assert utils.is_int(artist.librarySectionID) assert utils.is_int(artist.librarySectionID)
assert artist.listType == 'audio' assert artist.listType == "audio"
assert len(artist.locations) == 1 assert len(artist.locations) == 1
assert len(artist.locations[0]) >= 10 assert len(artist.locations[0]) >= 10
assert artist.ratingKey >= 1 assert artist.ratingKey >= 1
assert artist._server._baseurl == utils.SERVER_BASEURL assert artist._server._baseurl == utils.SERVER_BASEURL
assert isinstance(artist.similar, list) assert isinstance(artist.similar, list)
assert artist.summary == '' assert "Alias" in artist.summary
assert artist.title == 'Infinite State' assert artist.title == "Broke For Free"
assert artist.titleSort == 'Infinite State' assert artist.titleSort == "Broke For Free"
assert artist.type == 'artist' assert artist.type == "artist"
assert utils.is_datetime(artist.updatedAt) assert utils.is_datetime(artist.updatedAt)
assert utils.is_int(artist.viewCount, gte=0) assert utils.is_int(artist.viewCount, gte=0)
def test_audio_Artist_get(artist, music): def test_audio_Artist_get(artist, music):
artist == music.searchArtists(**{'title': 'Infinite State'})[0] artist == music.searchArtists(**{"title": "Broke For Free"})[0]
artist.title == 'Infinite State' artist.title == "Broke For Free"
def test_audio_Artist_history(artist): def test_audio_Artist_history(artist):
@ -39,50 +39,52 @@ def test_audio_Artist_history(artist):
def test_audio_Artist_track(artist): def test_audio_Artist_track(artist):
track = artist.track('Holy Moment') track = artist.track("As Colourful as Ever")
assert track.title == 'Holy Moment' assert track.title == "As Colourful as Ever"
def test_audio_Artist_tracks(artist): def test_audio_Artist_tracks(artist):
tracks = artist.tracks() tracks = artist.tracks()
assert len(tracks) == 14 assert len(tracks) == 1
def test_audio_Artist_album(artist): def test_audio_Artist_album(artist):
album = artist.album('Unmastered Impulses') album = artist.album("Layers")
assert album.title == 'Unmastered Impulses' assert album.title == "Layers"
def test_audio_Artist_albums(artist): def test_audio_Artist_albums(artist):
albums = artist.albums() albums = artist.albums()
assert len(albums) == 1 and albums[0].title == 'Unmastered Impulses' assert len(albums) == 1 and albums[0].title == "Layers"
def test_audio_Album_attrs(album): def test_audio_Album_attrs(album):
assert utils.is_datetime(album.addedAt) assert utils.is_datetime(album.addedAt)
assert [i.tag for i in album.genres] == ['Electronic'] assert isinstance(album.genres, list)
assert album.index == '1' assert album.index == "1"
assert utils.is_metadata(album._initpath) assert utils.is_metadata(album._initpath)
assert utils.is_metadata(album.key) assert utils.is_metadata(album.key)
assert utils.is_int(album.librarySectionID) assert utils.is_int(album.librarySectionID)
assert album.listType == 'audio' assert album.listType == "audio"
assert album.originallyAvailableAt == datetime(2016, 1, 1) if album.originallyAvailableAt:
assert utils.is_datetime(album.originallyAvailableAt)
assert utils.is_metadata(album.parentKey) assert utils.is_metadata(album.parentKey)
assert utils.is_int(album.parentRatingKey) assert utils.is_int(album.parentRatingKey)
if album.parentThumb: if album.parentThumb:
assert utils.is_metadata(album.parentThumb, contains='/thumb/') assert utils.is_metadata(album.parentThumb, contains="/thumb/")
assert album.parentTitle == 'Infinite State' assert album.parentTitle == "Broke For Free"
assert album.ratingKey >= 1 assert album.ratingKey >= 1
assert album._server._baseurl == utils.SERVER_BASEURL assert album._server._baseurl == utils.SERVER_BASEURL
assert album.studio is None assert album.studio is None
assert album.summary == '' assert album.summary == ""
assert utils.is_metadata(album.thumb, contains='/thumb/') if album.thumb:
assert album.title == 'Unmastered Impulses' assert utils.is_metadata(album.thumb, contains="/thumb/")
assert album.titleSort == 'Unmastered Impulses' assert album.title == "Layers"
assert album.type == 'album' assert album.titleSort == "Layers"
assert album.type == "album"
assert utils.is_datetime(album.updatedAt) assert utils.is_datetime(album.updatedAt)
assert utils.is_int(album.viewCount, gte=0) assert utils.is_int(album.viewCount, gte=0)
assert album.year == 2016 assert album.year in (2012,)
assert album.artUrl is None assert album.artUrl is None
@ -99,29 +101,29 @@ def test_audio_Track_history(track):
def test_audio_Album_tracks(album): def test_audio_Album_tracks(album):
tracks = album.tracks() tracks = album.tracks()
track = tracks[0] track = tracks[0]
assert len(tracks) == 14 assert len(tracks) == 1
assert utils.is_metadata(track.grandparentKey) assert utils.is_metadata(track.grandparentKey)
assert utils.is_int(track.grandparentRatingKey) assert utils.is_int(track.grandparentRatingKey)
assert track.grandparentTitle == 'Infinite State' assert track.grandparentTitle == "Broke For Free"
assert track.index == '1' assert track.index == "1"
assert utils.is_metadata(track._initpath) assert utils.is_metadata(track._initpath)
assert utils.is_metadata(track.key) assert utils.is_metadata(track.key)
assert track.listType == 'audio' assert track.listType == "audio"
assert track.originalTitle == 'Kenneth Reitz' assert track.originalTitle in (None, "Broke For Free")
assert utils.is_int(track.parentIndex) # assert utils.is_int(track.parentIndex)
assert utils.is_metadata(track.parentKey) assert utils.is_metadata(track.parentKey)
assert utils.is_int(track.parentRatingKey) assert utils.is_int(track.parentRatingKey)
assert utils.is_metadata(track.parentThumb, contains='/thumb/') assert utils.is_metadata(track.parentThumb, contains="/thumb/")
assert track.parentTitle == 'Unmastered Impulses' assert track.parentTitle == "Layers"
# assert track.ratingCount == 9 # Flaky # assert track.ratingCount == 9 # Flaky
assert utils.is_int(track.ratingKey) assert utils.is_int(track.ratingKey)
assert track._server._baseurl == utils.SERVER_BASEURL assert track._server._baseurl == utils.SERVER_BASEURL
assert track.summary == "" assert track.summary == ""
assert utils.is_metadata(track.thumb, contains='/thumb/') assert utils.is_metadata(track.thumb, contains="/thumb/")
assert track.title == 'Holy Moment' assert track.title == "As Colourful as Ever"
assert track.titleSort == 'Holy Moment' assert track.titleSort == "As Colourful as Ever"
assert not track.transcodeSessions assert not track.transcodeSessions
assert track.type == 'track' assert track.type == "track"
assert utils.is_datetime(track.updatedAt) assert utils.is_datetime(track.updatedAt)
assert utils.is_int(track.viewCount, gte=0) assert utils.is_int(track.viewCount, gte=0)
assert track.viewOffset == 0 assert track.viewOffset == 0
@ -129,46 +131,47 @@ def test_audio_Album_tracks(album):
def test_audio_Album_track(album, track=None): def test_audio_Album_track(album, track=None):
# this is not reloaded. its not that much info missing. # this is not reloaded. its not that much info missing.
track = track or album.track('Holy Moment') track = track or album.track("As Colourful As Ever")
assert utils.is_datetime(track.addedAt) assert utils.is_datetime(track.addedAt)
assert track.duration in [298605, 298606] assert utils.is_int(track.duration)
assert utils.is_metadata(track.grandparentKey) assert utils.is_metadata(track.grandparentKey)
assert utils.is_int(track.grandparentRatingKey) assert utils.is_int(track.grandparentRatingKey)
assert track.grandparentTitle == 'Infinite State' assert track.grandparentTitle == "Broke For Free"
assert int(track.index) == 1 assert int(track.index) == 1
assert utils.is_metadata(track._initpath) assert utils.is_metadata(track._initpath)
assert utils.is_metadata(track.key) assert utils.is_metadata(track.key)
assert track.listType == 'audio' assert track.listType == "audio"
# Assign 0 track.media # Assign 0 track.media
media = track.media[0] media = track.media[0]
assert track.originalTitle == 'Kenneth Reitz' assert track.originalTitle in (None, "As Colourful As Ever")
# Fix me
assert utils.is_int(track.parentIndex) assert utils.is_int(track.parentIndex)
assert utils.is_metadata(track.parentKey) assert utils.is_metadata(track.parentKey)
assert utils.is_int(track.parentRatingKey) assert utils.is_int(track.parentRatingKey)
assert utils.is_metadata(track.parentThumb, contains='/thumb/') assert utils.is_metadata(track.parentThumb, contains="/thumb/")
assert track.parentTitle == 'Unmastered Impulses' assert track.parentTitle == "Layers"
# assert track.ratingCount == 9 # assert track.ratingCount == 9
assert utils.is_int(track.ratingKey) assert utils.is_int(track.ratingKey)
assert track._server._baseurl == utils.SERVER_BASEURL assert track._server._baseurl == utils.SERVER_BASEURL
assert track.summary == '' assert track.summary == ""
assert utils.is_metadata(track.thumb, contains='/thumb/') assert utils.is_metadata(track.thumb, contains="/thumb/")
assert track.title == 'Holy Moment' assert track.title == "As Colourful as Ever"
assert track.titleSort == 'Holy Moment' assert track.titleSort == "As Colourful as Ever"
assert not track.transcodeSessions assert not track.transcodeSessions
assert track.type == 'track' assert track.type == "track"
assert utils.is_datetime(track.updatedAt) assert utils.is_datetime(track.updatedAt)
assert utils.is_int(track.viewCount, gte=0) assert utils.is_int(track.viewCount, gte=0)
assert track.viewOffset == 0 assert track.viewOffset == 0
assert media.aspectRatio is None assert media.aspectRatio is None
assert media.audioChannels == 2 assert media.audioChannels == 2
assert media.audioCodec == 'mp3' assert media.audioCodec == "mp3"
assert media.bitrate in [320, 385] assert media.bitrate == 128
assert media.container == 'mp3' assert media.container == "mp3"
assert media.duration in [298605, 298606] assert utils.is_int(media.duration)
assert media.height is None assert media.height in (None, 1080)
assert utils.is_int(media.id, gte=1) assert utils.is_int(media.id, gte=1)
assert utils.is_metadata(media._initpath) assert utils.is_metadata(media._initpath)
assert media.optimizedForStreaming is None assert media.optimizedForStreaming in (None, True)
# Assign 0 media.parts # Assign 0 media.parts
part = media.parts[0] part = media.parts[0]
assert media._server._baseurl == utils.SERVER_BASEURL assert media._server._baseurl == utils.SERVER_BASEURL
@ -176,69 +179,69 @@ def test_audio_Album_track(album, track=None):
assert media.videoFrameRate is None assert media.videoFrameRate is None
assert media.videoResolution is None assert media.videoResolution is None
assert media.width is None assert media.width is None
assert part.container == 'mp3' assert part.container == "mp3"
assert part.duration in [298605, 298606] assert utils.is_int(part.duration)
assert part.file.endswith('.mp3') assert part.file.endswith(".mp3")
assert utils.is_int(part.id) assert utils.is_int(part.id)
assert utils.is_metadata(part._initpath) assert utils.is_metadata(part._initpath)
assert utils.is_part(part.key) assert utils.is_part(part.key)
assert part._server._baseurl == utils.SERVER_BASEURL assert part._server._baseurl == utils.SERVER_BASEURL
assert part.size == 14360402 assert part.size == 3761053
assert track.artUrl is None assert track.artUrl is None
def test_audio_Album_get(album): def test_audio_Album_get(album):
# alias for album.track() # alias for album.track()
track = album.get('Holy Moment') track = album.get("As Colourful As Ever")
test_audio_Album_track(album, track=track) test_audio_Album_track(album, track=track)
def test_audio_Album_artist(album): def test_audio_Album_artist(album):
artist = album.artist() artist = album.artist()
artist.title == 'Infinite State' artist.title == "Broke For Free"
def test_audio_Track_attrs(album): def test_audio_Track_attrs(album):
track = album.get('Holy Moment').reload() track = album.get("As Colourful As Ever").reload()
assert utils.is_datetime(track.addedAt) assert utils.is_datetime(track.addedAt)
assert track.art is None assert track.art is None
assert track.chapterSource is None assert track.chapterSource is None
assert track.duration in [298605, 298606] assert utils.is_int(track.duration)
assert track.grandparentArt is None assert track.grandparentArt is None
assert utils.is_metadata(track.grandparentKey) assert utils.is_metadata(track.grandparentKey)
assert utils.is_int(track.grandparentRatingKey) assert utils.is_int(track.grandparentRatingKey)
if track.grandparentThumb: if track.grandparentThumb:
assert utils.is_metadata(track.grandparentThumb, contains='/thumb/') assert utils.is_metadata(track.grandparentThumb, contains="/thumb/")
assert track.grandparentTitle == 'Infinite State' assert track.grandparentTitle == "Broke For Free"
assert track.guid.startswith('local://') assert track.guid.startswith("local://")
assert int(track.index) == 1 assert int(track.index) == 1
assert utils.is_metadata(track._initpath) assert utils.is_metadata(track._initpath)
assert utils.is_metadata(track.key) assert utils.is_metadata(track.key)
if track.lastViewedAt: if track.lastViewedAt:
assert utils.is_datetime(track.lastViewedAt) assert utils.is_datetime(track.lastViewedAt)
assert utils.is_int(track.librarySectionID) assert utils.is_int(track.librarySectionID)
assert track.listType == 'audio' assert track.listType == "audio"
# Assign 0 track.media # Assign 0 track.media
media = track.media[0] media = track.media[0]
assert track.moods == [] assert track.moods == []
assert track.originalTitle == 'Kenneth Reitz' assert track.originalTitle in (None, "Broke For Free")
assert int(track.parentIndex) == 1 assert int(track.parentIndex) == 1
assert utils.is_metadata(track.parentKey) assert utils.is_metadata(track.parentKey)
assert utils.is_int(track.parentRatingKey) assert utils.is_int(track.parentRatingKey)
assert utils.is_metadata(track.parentThumb, contains='/thumb/') assert utils.is_metadata(track.parentThumb, contains="/thumb/")
assert track.parentTitle == 'Unmastered Impulses' assert track.parentTitle == "Layers"
assert track.playlistItemID is None assert track.playlistItemID is None
assert track.primaryExtraKey is None assert track.primaryExtraKey is None
#assert utils.is_int(track.ratingCount) # assert utils.is_int(track.ratingCount)
assert utils.is_int(track.ratingKey) assert utils.is_int(track.ratingKey)
assert track._server._baseurl == utils.SERVER_BASEURL assert track._server._baseurl == utils.SERVER_BASEURL
assert track.sessionKey is None assert track.sessionKey is None
assert track.summary == '' assert track.summary == ""
assert utils.is_metadata(track.thumb, contains='/thumb/') assert utils.is_metadata(track.thumb, contains="/thumb/")
assert track.title == 'Holy Moment' assert track.title == "As Colourful as Ever"
assert track.titleSort == 'Holy Moment' assert track.titleSort == "As Colourful as Ever"
assert not track.transcodeSessions assert not track.transcodeSessions
assert track.type == 'track' assert track.type == "track"
assert utils.is_datetime(track.updatedAt) assert utils.is_datetime(track.updatedAt)
assert utils.is_int(track.viewCount, gte=0) assert utils.is_int(track.viewCount, gte=0)
assert track.viewOffset == 0 assert track.viewOffset == 0
@ -246,10 +249,10 @@ def test_audio_Track_attrs(album):
assert track.year is None assert track.year is None
assert media.aspectRatio is None assert media.aspectRatio is None
assert media.audioChannels == 2 assert media.audioChannels == 2
assert media.audioCodec == 'mp3' assert media.audioCodec == "mp3"
assert media.bitrate in [320, 385] assert media.bitrate == 128
assert media.container == 'mp3' assert media.container == "mp3"
assert media.duration in [298605, 298606] assert utils.is_int(media.duration)
assert media.height is None assert media.height is None
assert utils.is_int(media.id, gte=1) assert utils.is_int(media.id, gte=1)
assert utils.is_metadata(media._initpath) assert utils.is_metadata(media._initpath)
@ -261,23 +264,23 @@ def test_audio_Track_attrs(album):
assert media.videoFrameRate is None assert media.videoFrameRate is None
assert media.videoResolution is None assert media.videoResolution is None
assert media.width is None assert media.width is None
assert part.container == 'mp3' assert part.container == "mp3"
assert part.duration in [298605, 298606] assert utils.is_int(part.duration)
assert part.file.endswith('.mp3') assert part.file.endswith(".mp3")
assert utils.is_int(part.id) assert utils.is_int(part.id)
assert utils.is_metadata(part._initpath) assert utils.is_metadata(part._initpath)
assert utils.is_part(part.key) assert utils.is_part(part.key)
# assert part.media == <Media:Holy.Moment> # assert part.media == <Media:Holy.Moment>
assert part._server._baseurl == utils.SERVER_BASEURL assert part._server._baseurl == utils.SERVER_BASEURL
assert part.size == 14360402 assert part.size == 3761053
# Assign 0 part.streams # Assign 0 part.streams
stream = part.streams[0] stream = part.streams[0]
assert stream.audioChannelLayout == 'stereo' assert stream.audioChannelLayout == "stereo"
assert stream.bitDepth is None assert stream.bitDepth is None
assert stream.bitrate == 320 assert stream.bitrate == 128
assert stream.bitrateMode is None assert stream.bitrateMode is None
assert stream.channels == 2 assert stream.channels == 2
assert stream.codec == 'mp3' assert stream.codec == "mp3"
assert stream.codecID is None assert stream.codecID is None
assert stream.dialogNorm is None assert stream.dialogNorm is None
assert stream.duration is None assert stream.duration is None
@ -287,7 +290,7 @@ def test_audio_Track_attrs(album):
assert stream.language is None assert stream.language is None
assert stream.languageCode is None assert stream.languageCode is None
# assert stream.part == <MediaPart:22> # assert stream.part == <MediaPart:22>
assert stream.samplingRate == 44100 assert stream.samplingRate == 48000
assert stream.selected is True assert stream.selected is True
assert stream._server._baseurl == utils.SERVER_BASEURL assert stream._server._baseurl == utils.SERVER_BASEURL
assert stream.streamType == 2 assert stream.streamType == 2
@ -319,13 +322,13 @@ def test_audio_Track_download(monkeydownload, tmpdir, track):
def test_audio_album_download(monkeydownload, album, tmpdir): def test_audio_album_download(monkeydownload, album, tmpdir):
f = album.download(savepath=str(tmpdir)) f = album.download(savepath=str(tmpdir))
assert len(f) == 14 assert len(f) == 1
def test_audio_Artist_download(monkeydownload, artist, tmpdir): def test_audio_Artist_download(monkeydownload, artist, tmpdir):
f = artist.download(savepath=str(tmpdir)) f = artist.download(savepath=str(tmpdir))
assert len(f) == 14 assert len(f) == 1
def test_audio_Album_label(album, patched_http_call): def test_audio_Album_label(album, patched_http_call):
album.addLabel('YO') album.addLabel("YO")

View file

@ -8,7 +8,10 @@ def _check_capabilities(client, capabilities):
supported = client.protocolCapabilities supported = client.protocolCapabilities
for capability in capabilities: for capability in capabilities:
if capability not in supported: if capability not in supported:
pytest.skip("Client %s doesnt support %s capability support %s" % (client.title, capability, supported)) pytest.skip(
"Client %s doesnt support %s capability support %s"
% (client.title, capability, supported)
)
def _check_proxy(plex, client, proxy): def _check_proxy(plex, client, proxy):
@ -123,7 +126,7 @@ def test_client_timeline(plex, client, movies, proxy):
try: try:
# Note: We noticed the isPlaying flag could take up to a full # Note: We noticed the isPlaying flag could take up to a full
# 30 seconds to be updated, hence the long sleeping. # 30 seconds to be updated, hence the long sleeping.
mtype= "video" mtype = "video"
client.stop(mtype) client.stop(mtype)
assert client.isPlayingMedia() is False assert client.isPlayingMedia() is False
print("client.playMedia(movie)") print("client.playMedia(movie)")

15
tests/test_gdm.py Normal file
View file

@ -0,0 +1,15 @@
import pytest
from plexapi.gdm import GDM
@pytest.mark.xfail(reason="Might fail on docker", strict=False)
def test_gdm(plex):
gdm = GDM()
gdm_enabled = plex.settings.get("GdmEnabled")
gdm.scan(timeout=2)
if gdm_enabled:
assert len(gdm.entries)
else:
assert not len(gdm.entries)

View file

@ -193,11 +193,11 @@ def test_library_MusicSection_albums(music):
def test_library_MusicSection_searchTracks(music): def test_library_MusicSection_searchTracks(music):
assert len(music.searchTracks(title="Holy Moment")) assert len(music.searchTracks(title="As Colourful As Ever"))
def test_library_MusicSection_searchAlbums(music): def test_library_MusicSection_searchAlbums(music):
assert len(music.searchAlbums(title="Unmastered Impulses")) assert len(music.searchAlbums(title="Layers"))
def test_library_PhotoSection_searchAlbums(photos, photoalbum): def test_library_PhotoSection_searchAlbums(photos, photoalbum):
@ -260,3 +260,8 @@ def test_crazy_search(plex, movie):
), "Unable to search movie by year." ), "Unable to search movie by year."
assert movie not in movies.search(year=2007), "Unable to filter 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].tag) assert movie in movies.search(actor=movie.actors[0].tag)
assert len(movies.search(container_start=2, maxresults=1)) == 1
assert len(movies.search(container_size=None)) == 4
assert len(movies.search(container_size=1)) == 4
assert len(movies.search(container_start=9999, container_size=1)) == 0
assert len(movies.search(container_start=2, container_size=1)) == 2

View file

@ -1,25 +1,28 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
import pytest
import shlex import shlex
import subprocess import subprocess
from os.path import abspath, dirname, join from os.path import abspath, dirname, join
SKIP_EXAMPLES = ['Example 4'] import pytest
SKIP_EXAMPLES = ["Example 4"]
@pytest.mark.skipif(os.name == 'nt', reason='No make.bat specified for Windows') @pytest.mark.skipif(os.name == "nt", reason="No make.bat specified for Windows")
def test_build_documentation(): def test_build_documentation():
docroot = join(dirname(dirname(abspath(__file__))), 'docs') docroot = join(dirname(dirname(abspath(__file__))), "docs")
cmd = shlex.split('sphinx-build -aE . _build') cmd = shlex.split("sphinx-build -aE . _build")
proc = subprocess.Popen(cmd, cwd=docroot, stdout=subprocess.PIPE, stderr=subprocess.PIPE) proc = subprocess.Popen(
cmd, cwd=docroot, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
status = proc.wait() status = proc.wait()
assert status == 0 assert status == 0
issues = [] issues = []
for output in proc.communicate(): for output in proc.communicate():
for line in str(output).split('\\n'): for line in str(output).split("\\n"):
line = line.lower().strip() line = line.lower().strip()
if 'warning' in line or 'error' in line or 'traceback' in line: if "warning" in line or "error" in line or "traceback" in line:
issues.append(line) issues.append(line)
for line in issues: for line in issues:
print(line) print(line)
@ -29,30 +32,30 @@ def test_build_documentation():
def test_readme_examples(plex): def test_readme_examples(plex):
failed = 0 failed = 0
examples = _fetch_examples() examples = _fetch_examples()
assert len(examples), 'No examples found in README' assert len(examples), "No examples found in README"
for title, example in examples: for title, example in examples:
if _check_run_example(title): if _check_run_example(title):
try: try:
print('\n%s\n%s' % (title, '-' * len(title))) print("\n%s\n%s" % (title, "-" * len(title)))
exec('\n'.join(example)) exec("\n".join(example))
except Exception as err: except Exception as err:
failed += 1 failed += 1
print('Error running test: %s\nError: %s' % (title, err)) print("Error running test: %s\nError: %s" % (title, err))
assert not failed, '%s examples raised an exception.' % failed assert not failed, "%s examples raised an exception." % failed
def _fetch_examples(): def _fetch_examples():
parsing = False parsing = False
examples = [] examples = []
filepath = join(dirname(dirname(abspath(__file__))), 'README.rst') filepath = join(dirname(dirname(abspath(__file__))), "README.rst")
with open(filepath, 'r') as handle: with open(filepath, "r") as handle:
for line in handle.read().split('\n'): for line in handle.read().split("\n"):
line = line[4:] line = line[4:]
if line.startswith('# Example '): if line.startswith("# Example "):
parsing = True parsing = True
title = line.lstrip('# ') title = line.lstrip("# ")
examples.append([title, []]) examples.append([title, []])
elif parsing and line == '': elif parsing and line == "":
parsing = False parsing = False
elif parsing: elif parsing:
examples[-1][1].append(line) examples[-1][1].append(line)

View file

@ -1,40 +1,41 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import pytest import pytest
from plexapi.exceptions import BadRequest, NotFound from plexapi.exceptions import BadRequest, NotFound
from . import conftest as utils from . import conftest as utils
def test_myplex_accounts(account, plex): def test_myplex_accounts(account, plex):
assert account, 'Must specify username, password & resource to run this test.' assert account, "Must specify username, password & resource to run this test."
print('MyPlexAccount:') print("MyPlexAccount:")
print('username: %s' % account.username) print("username: %s" % account.username)
print('email: %s' % account.email) print("email: %s" % account.email)
print('home: %s' % account.home) print("home: %s" % account.home)
print('queueEmail: %s' % account.queueEmail) print("queueEmail: %s" % account.queueEmail)
assert account.username, 'Account has no username' assert account.username, "Account has no username"
assert account.authenticationToken, 'Account has no authenticationToken' assert account.authenticationToken, "Account has no authenticationToken"
assert account.email, 'Account has no email' assert account.email, "Account has no email"
assert account.home is not None, 'Account has no home' assert account.home is not None, "Account has no home"
assert account.queueEmail, 'Account has no queueEmail' assert account.queueEmail, "Account has no queueEmail"
account = plex.account() account = plex.account()
print('Local PlexServer.account():') print("Local PlexServer.account():")
print('username: %s' % account.username) print("username: %s" % account.username)
#print('authToken: %s' % account.authToken) # print('authToken: %s' % account.authToken)
print('signInState: %s' % account.signInState) print("signInState: %s" % account.signInState)
assert account.username, 'Account has no username' assert account.username, "Account has no username"
assert account.authToken, 'Account has no authToken' assert account.authToken, "Account has no authToken"
assert account.signInState, 'Account has no signInState' assert account.signInState, "Account has no signInState"
def test_myplex_resources(account): def test_myplex_resources(account):
assert account, 'Must specify username, password & resource to run this test.' assert account, "Must specify username, password & resource to run this test."
resources = account.resources() resources = account.resources()
for resource in resources: for resource in resources:
name = resource.name or 'Unknown' name = resource.name or "Unknown"
connections = [c.uri for c in resource.connections] connections = [c.uri for c in resource.connections]
connections = ', '.join(connections) if connections else 'None' connections = ", ".join(connections) if connections else "None"
print('%s (%s): %s' % (name, resource.product, connections)) print("%s (%s): %s" % (name, resource.product, connections))
assert resources, 'No resources found for account: %s' % account.name assert resources, "No resources found for account: %s" % account.name
def test_myplex_connect_to_resource(plex, account): def test_myplex_connect_to_resource(plex, account):
@ -48,14 +49,15 @@ def test_myplex_connect_to_resource(plex, account):
def test_myplex_devices(account): def test_myplex_devices(account):
devices = account.devices() devices = account.devices()
for device in devices: for device in devices:
name = device.name or 'Unknown' name = device.name or "Unknown"
connections = ', '.join(device.connections) if device.connections else 'None' connections = ", ".join(device.connections) if device.connections else "None"
print('%s (%s): %s' % (name, device.product, connections)) print("%s (%s): %s" % (name, device.product, connections))
assert devices, 'No devices found for account: %s' % account.name assert devices, "No devices found for account: %s" % account.name
def test_myplex_device(account, plex): def test_myplex_device(account, plex):
from plexapi import X_PLEX_DEVICE_NAME from plexapi import X_PLEX_DEVICE_NAME
assert account.device(plex.friendlyName) assert account.device(plex.friendlyName)
assert account.device(X_PLEX_DEVICE_NAME) assert account.device(X_PLEX_DEVICE_NAME)
@ -63,22 +65,24 @@ def test_myplex_device(account, plex):
def _test_myplex_connect_to_device(account): def _test_myplex_connect_to_device(account):
devices = account.devices() devices = account.devices()
for device in devices: for device in devices:
if device.name == 'some client name' and len(device.connections): if device.name == "some client name" and len(device.connections):
break break
client = device.connect() client = device.connect()
assert client, 'Unable to connect to device' assert client, "Unable to connect to device"
def test_myplex_users(account): def test_myplex_users(account):
users = account.users() users = account.users()
if not len(users): if not len(users):
return pytest.skip('You have to add a shared account into your MyPlex') return pytest.skip("You have to add a shared account into your MyPlex")
print('Found %s users.' % len(users)) print("Found %s users." % len(users))
user = account.user(users[0].title) user = account.user(users[0].title)
print('Found user: %s' % user) print("Found user: %s" % user)
assert user, 'Could not find user %s' % users[0].title assert user, "Could not find user %s" % users[0].title
assert len(users[0].servers[0].sections()) > 0, "Couldn't info about the shared libraries" assert (
len(users[0].servers[0].sections()) > 0
), "Couldn't info about the shared libraries"
def test_myplex_resource(account, plex): def test_myplex_resource(account, plex):
@ -95,25 +99,25 @@ def test_myplex_webhooks(account):
def test_myplex_addwebhooks(account): def test_myplex_addwebhooks(account):
if account.subscriptionActive: if account.subscriptionActive:
assert 'http://example.com' in account.addWebhook('http://example.com') assert "http://example.com" in account.addWebhook("http://example.com")
else: else:
with pytest.raises(BadRequest): with pytest.raises(BadRequest):
account.addWebhook('http://example.com') account.addWebhook("http://example.com")
def test_myplex_deletewebhooks(account): def test_myplex_deletewebhooks(account):
if account.subscriptionActive: if account.subscriptionActive:
assert 'http://example.com' not in account.deleteWebhook('http://example.com') assert "http://example.com" not in account.deleteWebhook("http://example.com")
else: else:
with pytest.raises(BadRequest): with pytest.raises(BadRequest):
account.deleteWebhook('http://example.com') account.deleteWebhook("http://example.com")
def test_myplex_optout(account_once): def test_myplex_optout(account_once):
def enabled(): def enabled():
ele = account_once.query('https://plex.tv/api/v2/user/privacy') ele = account_once.query("https://plex.tv/api/v2/user/privacy")
lib = ele.attrib.get('optOutLibraryStats') lib = ele.attrib.get("optOutLibraryStats")
play = ele.attrib.get('optOutPlayback') play = ele.attrib.get("optOutPlayback")
return bool(int(lib)), bool(int(play)) return bool(int(lib)), bool(int(play))
account_once.optOut(library=True, playback=True) account_once.optOut(library=True, playback=True)
@ -123,17 +127,25 @@ def test_myplex_optout(account_once):
def test_myplex_inviteFriend_remove(account, plex, mocker): def test_myplex_inviteFriend_remove(account, plex, mocker):
inv_user = 'hellowlol' inv_user = "hellowlol"
vid_filter = {'contentRating': ['G'], 'label': ['foo']} vid_filter = {"contentRating": ["G"], "label": ["foo"]}
secs = plex.library.sections() secs = plex.library.sections()
ids = account._getSectionIds(plex.machineIdentifier, secs) ids = account._getSectionIds(plex.machineIdentifier, secs)
with mocker.patch.object(account, '_getSectionIds', return_value=ids): with mocker.patch.object(account, "_getSectionIds", return_value=ids):
with utils.callable_http_patch(): with utils.callable_http_patch():
account.inviteFriend(inv_user, plex, secs, allowSync=True, allowCameraUpload=True, account.inviteFriend(
allowChannels=False, filterMovies=vid_filter, filterTelevision=vid_filter, inv_user,
filterMusic={'label': ['foo']}) plex,
secs,
allowSync=True,
allowCameraUpload=True,
allowChannels=False,
filterMovies=vid_filter,
filterTelevision=vid_filter,
filterMusic={"label": ["foo"]},
)
assert inv_user not in [u.title for u in account.users()] assert inv_user not in [u.title for u in account.users()]
@ -143,51 +155,68 @@ def test_myplex_inviteFriend_remove(account, plex, mocker):
def test_myplex_updateFriend(account, plex, mocker, shared_username): def test_myplex_updateFriend(account, plex, mocker, shared_username):
vid_filter = {'contentRating': ['G'], 'label': ['foo']} vid_filter = {"contentRating": ["G"], "label": ["foo"]}
secs = plex.library.sections() secs = plex.library.sections()
user = account.user(shared_username) user = account.user(shared_username)
ids = account._getSectionIds(plex.machineIdentifier, secs) ids = account._getSectionIds(plex.machineIdentifier, secs)
with mocker.patch.object(account, '_getSectionIds', return_value=ids): with mocker.patch.object(account, "_getSectionIds", return_value=ids):
with mocker.patch.object(account, 'user', return_value=user): with mocker.patch.object(account, "user", return_value=user):
with utils.callable_http_patch(): with utils.callable_http_patch():
account.updateFriend(shared_username, plex, secs, allowSync=True, removeSections=True, account.updateFriend(
allowCameraUpload=True, allowChannels=False, filterMovies=vid_filter, shared_username,
filterTelevision=vid_filter, filterMusic={'label': ['foo']}) plex,
secs,
allowSync=True,
removeSections=True,
allowCameraUpload=True,
allowChannels=False,
filterMovies=vid_filter,
filterTelevision=vid_filter,
filterMusic={"label": ["foo"]},
)
def test_myplex_createExistingUser(account, plex, shared_username): def test_myplex_createExistingUser(account, plex, shared_username):
user = account.user(shared_username) user = account.user(shared_username)
url = 'https://plex.tv/api/invites/requested/{}?friend=0&server=0&home=1'.format(user.id) url = "https://plex.tv/api/invites/requested/{}?friend=0&server=0&home=1".format(
user.id
)
account.createExistingUser(user, plex) account.createExistingUser(user, plex)
assert shared_username in [u.username for u in account.users() if u.home is True] assert shared_username in [u.username for u in account.users() if u.home is True]
# Remove Home invite # Remove Home invite
account.query(url, account._session.delete) account.query(url, account._session.delete)
# Confirm user was removed from home and has returned to friend # Confirm user was removed from home and has returned to friend
assert shared_username not in [u.username for u in plex.myPlexAccount().users() if u.home is True] assert shared_username not in [
assert shared_username in [u.username for u in plex.myPlexAccount().users() if u.home is False] u.username for u in plex.myPlexAccount().users() if u.home is True
]
assert shared_username in [
u.username for u in plex.myPlexAccount().users() if u.home is False
]
@pytest.mark.skip(reason="broken test?") @pytest.mark.skip(reason="broken test?")
def test_myplex_createHomeUser_remove(account, plex): def test_myplex_createHomeUser_remove(account, plex):
homeuser = 'New Home User' homeuser = "New Home User"
account.createHomeUser(homeuser, plex) account.createHomeUser(homeuser, plex)
assert homeuser in [u.title for u in plex.myPlexAccount().users() if u.home is True] assert homeuser in [u.title for u in plex.myPlexAccount().users() if u.home is True]
account.removeHomeUser(homeuser) account.removeHomeUser(homeuser)
assert homeuser not in [u.title for u in plex.myPlexAccount().users() if u.home is True] assert homeuser not in [
u.title for u in plex.myPlexAccount().users() if u.home is True
]
def test_myplex_plexpass_attributes(account_plexpass): def test_myplex_plexpass_attributes(account_plexpass):
assert account_plexpass.subscriptionActive assert account_plexpass.subscriptionActive
assert account_plexpass.subscriptionStatus == 'Active' assert account_plexpass.subscriptionStatus == "Active"
assert account_plexpass.subscriptionPlan assert account_plexpass.subscriptionPlan
assert 'sync' in account_plexpass.subscriptionFeatures assert "sync" in account_plexpass.subscriptionFeatures
assert 'premium_music_metadata' in account_plexpass.subscriptionFeatures assert "premium_music_metadata" in account_plexpass.subscriptionFeatures
assert 'plexpass' in account_plexpass.roles assert "plexpass" in account_plexpass.roles
assert set(account_plexpass.entitlements) == utils.ENTITLEMENTS assert set(account_plexpass.entitlements) == utils.ENTITLEMENTS
def test_myplex_claimToken(account): def test_myplex_claimToken(account):
assert account.claimToken().startswith('claim-') assert account.claimToken().startswith("claim-")

View file

@ -2,37 +2,33 @@
def test_navigate_around_show(account, plex): def test_navigate_around_show(account, plex):
show = plex.library.section('TV Shows').get('The 100') show = plex.library.section("TV Shows").get("The 100")
seasons = show.seasons() seasons = show.seasons()
season = show.season('Season 1') season = show.season("Season 1")
episodes = show.episodes() episodes = show.episodes()
episode = show.episode('Pilot') episode = show.episode("Pilot")
assert 'Season 1' in [s.title for s in seasons], 'Unable to list season:' 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 "Pilot" in [e.title for e in episodes], "Unable to list episode:"
assert show.season(1) == season assert show.season(1) == season
assert show.episode('Pilot') == episode, 'Unable to get show episode:' assert show.episode("Pilot") == episode, "Unable to get show episode:"
assert season.episode('Pilot') == episode, 'Unable to get season episode:' assert season.episode("Pilot") == episode, "Unable to get season episode:"
assert season.show() == show, 'season.show() doesnt match expected show.' assert season.show() == show, "season.show() doesnt match expected show."
assert episode.show() == show, 'episode.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.' assert episode.season() == season, "episode.season() doesnt match expected season."
def test_navigate_around_artist(account, plex): def test_navigate_around_artist(account, plex):
artist = plex.library.section('Music').get('Infinite State') artist = plex.library.section("Music").get("Broke For Free")
albums = artist.albums() albums = artist.albums()
album = artist.album('Unmastered Impulses') album = artist.album("Layers")
tracks = artist.tracks() tracks = artist.tracks()
track = artist.track('Mantra') track = artist.track("As Colourful as Ever")
print('Navigating around artist: %s' % artist) print("Navigating around artist: %s" % artist)
print('Albums: %s...' % albums[:3]) print("Album: %s" % album)
print('Album: %s' % album) print("Tracks: %s..." % tracks)
print('Tracks: %s...' % tracks[:3]) print("Track: %s" % track)
print('Track: %s' % track) assert artist.track("As Colourful as Ever") == track, "Unable to get artist track."
assert 'Unmastered Impulses' in [a.title for a in albums], 'Unable to list album.' assert album.track("As Colourful as Ever") == track, "Unable to get album track."
assert 'Mantra' in [e.title for e in tracks], 'Unable to list track.' assert album.artist() == artist, "album.artist() doesnt match expected artist."
assert artist.album('Unmastered Impulses') == album, 'Unable to get artist album.' assert track.artist() == artist, "track.artist() doesnt match expected artist."
assert artist.track('Mantra') == track, 'Unable to get artist track.' assert track.album() == album, "track.album() doesnt match expected album."
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.'

View file

@ -1,9 +1,7 @@
def test_photo_Photoalbum(photoalbum): def test_photo_Photoalbum(photoalbum):
assert len(photoalbum.albums()) == 3 assert len(photoalbum.albums()) == 3
assert len(photoalbum.photos()) == 3 assert len(photoalbum.photos()) == 3
cats_in_bed = photoalbum.album('Cats in bed') cats_in_bed = photoalbum.album("Cats in bed")
assert len(cats_in_bed.photos()) == 7 assert len(cats_in_bed.photos()) == 7
a_pic = cats_in_bed.photo('photo7') a_pic = cats_in_bed.photo("photo7")
assert a_pic assert a_pic

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import time import time
import pytest import pytest

View file

@ -4,7 +4,6 @@ import time
import pytest import pytest
from PIL import Image, ImageStat from PIL import Image, ImageStat
from plexapi.compat import patch
from plexapi.exceptions import BadRequest, NotFound from plexapi.exceptions import BadRequest, NotFound
from plexapi.server import PlexServer from plexapi.server import PlexServer
from plexapi.utils import download from plexapi.utils import download
@ -19,11 +18,11 @@ def test_server_attr(plex, account):
assert len(plex.machineIdentifier) == 40 assert len(plex.machineIdentifier) == 40
assert plex.myPlex is True assert plex.myPlex is True
# if you run the tests very shortly after server creation the state in rare cases may be `unknown` # if you run the tests very shortly after server creation the state in rare cases may be `unknown`
assert plex.myPlexMappingState in ('mapped', 'unknown') assert plex.myPlexMappingState in ("mapped", "unknown")
assert plex.myPlexSigninState == 'ok' assert plex.myPlexSigninState == "ok"
assert utils.is_int(plex.myPlexSubscription, gte=0) assert utils.is_int(plex.myPlexSubscription, gte=0)
assert re.match(utils.REGEX_EMAIL, plex.myPlexUsername) assert re.match(utils.REGEX_EMAIL, plex.myPlexUsername)
assert plex.platform in ('Linux', 'Windows') assert plex.platform in ("Linux", "Windows")
assert len(plex.platformVersion) >= 5 assert len(plex.platformVersion) >= 5
assert plex._token == account.authenticationToken assert plex._token == account.authenticationToken
assert utils.is_int(plex.transcoderActiveVideoSessions, gte=0) assert utils.is_int(plex.transcoderActiveVideoSessions, gte=0)
@ -54,28 +53,37 @@ def test_server_library(plex):
def test_server_url(plex): def test_server_url(plex):
assert 'ohno' in plex.url('ohno') assert "ohno" in plex.url("ohno")
def test_server_transcodeImage(tmpdir, plex, show): def test_server_transcodeImage(tmpdir, plex, show):
width, height = 500, 500 width, height = 500, 500
imgurl = plex.transcodeImage(show.banner, height, width) imgurl = plex.transcodeImage(show.banner, height, width)
gray = imgurl = plex.transcodeImage(show.banner, height, width, saturation=0) gray = imgurl = plex.transcodeImage(show.banner, height, width, saturation=0)
resized_img = download(imgurl, plex._token, savepath=str(tmpdir), filename='resize_image') resized_img = download(
original_img = download(show._server.url(show.banner), plex._token, savepath=str(tmpdir), filename='original_img') imgurl, plex._token, savepath=str(tmpdir), filename="resize_image"
grayscale_img = download(gray, plex._token, savepath=str(tmpdir), filename='grayscale_img') )
original_img = download(
show._server.url(show.banner),
plex._token,
savepath=str(tmpdir),
filename="original_img",
)
grayscale_img = download(
gray, plex._token, savepath=str(tmpdir), filename="grayscale_img"
)
with Image.open(resized_img) as image: with Image.open(resized_img) as image:
assert width, height == image.size assert width, height == image.size
with Image.open(original_img) as image: with Image.open(original_img) as image:
assert width, height != image.size assert width, height != image.size
assert _detect_color_image(grayscale_img, thumb_size=150) == 'grayscale' assert _detect_color_image(grayscale_img, thumb_size=150) == "grayscale"
def _detect_color_image(file, thumb_size=150, MSE_cutoff=22, adjust_color_bias=True): def _detect_color_image(file, thumb_size=150, MSE_cutoff=22, adjust_color_bias=True):
# http://stackoverflow.com/questions/20068945/detect-if-image-is-color-grayscale-or-black-and-white-with-python-pil # http://stackoverflow.com/questions/20068945/detect-if-image-is-color-grayscale-or-black-and-white-with-python-pil
pilimg = Image.open(file) pilimg = Image.open(file)
bands = pilimg.getbands() bands = pilimg.getbands()
if bands == ('R', 'G', 'B') or bands == ('R', 'G', 'B', 'A'): if bands == ("R", "G", "B") or bands == ("R", "G", "B", "A"):
thumb = pilimg.resize((thumb_size, thumb_size)) thumb = pilimg.resize((thumb_size, thumb_size))
sse, bias = 0, [0, 0, 0] sse, bias = 0, [0, 0, 0]
if adjust_color_bias: if adjust_color_bias:
@ -83,11 +91,13 @@ def _detect_color_image(file, thumb_size=150, MSE_cutoff=22, adjust_color_bias=T
bias = [b - sum(bias) / 3 for b in bias] bias = [b - sum(bias) / 3 for b in bias]
for pixel in thumb.getdata(): for pixel in thumb.getdata():
mu = sum(pixel) / 3 mu = sum(pixel) / 3
sse += sum((pixel[i] - mu - bias[i]) * (pixel[i] - mu - bias[i]) for i in [0, 1, 2]) 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) mse = float(sse) / (thumb_size * thumb_size)
return 'grayscale' if mse <= MSE_cutoff else 'color' return "grayscale" if mse <= MSE_cutoff else "color"
elif len(bands) == 1: elif len(bands) == 1:
return 'blackandwhite' return "blackandwhite"
def test_server_fetchitem_notfound(plex): def test_server_fetchitem_notfound(plex):
@ -99,16 +109,16 @@ def test_server_search(plex, movie):
title = movie.title title = movie.title
# this search seem to fail on my computer but not at travis, wtf. # this search seem to fail on my computer but not at travis, wtf.
assert plex.search(title) assert plex.search(title)
assert plex.search(title, mediatype='movie') assert plex.search(title, mediatype="movie")
def test_server_playlist(plex, show): def test_server_playlist(plex, show):
episodes = show.episodes() episodes = show.episodes()
playlist = plex.createPlaylist('test_playlist', episodes[:3]) playlist = plex.createPlaylist("test_playlist", episodes[:3])
try: try:
assert playlist.title == 'test_playlist' assert playlist.title == "test_playlist"
with pytest.raises(NotFound): with pytest.raises(NotFound):
plex.playlist('<playlist-not-found>') plex.playlist("<playlist-not-found>")
finally: finally:
playlist.delete() playlist.delete()
@ -117,7 +127,7 @@ def test_server_playlists(plex, show):
playlists = plex.playlists() playlists = plex.playlists()
count = len(playlists) count = len(playlists)
episodes = show.episodes() episodes = show.episodes()
playlist = plex.createPlaylist('test_playlist', episodes[:3]) playlist = plex.createPlaylist("test_playlist", episodes[:3])
try: try:
playlists = plex.playlists() playlists = plex.playlists()
assert len(playlists) == count + 1 assert len(playlists) == count + 1
@ -133,9 +143,9 @@ def test_server_history(plex, movie):
def test_server_Server_query(plex): def test_server_Server_query(plex):
assert plex.query('/') assert plex.query("/")
with pytest.raises(NotFound): with pytest.raises(NotFound):
assert plex.query('/asdf/1234/asdf', headers={'random_headers': '1234'}) assert plex.query("/asdf/1234/asdf", headers={"random_headers": "1234"})
def test_server_Server_session(account): def test_server_Server_session(account):
@ -144,28 +154,31 @@ def test_server_Server_session(account):
def __init__(self): def __init__(self):
super(self.__class__, self).__init__() super(self.__class__, self).__init__()
self.plexapi_session_test = True self.plexapi_session_test = True
# Test Code # Test Code
plex = PlexServer(utils.SERVER_BASEURL, account.authenticationToken, session=MySession()) plex = PlexServer(
assert hasattr(plex._session, 'plexapi_session_test') utils.SERVER_BASEURL, account.authenticationToken, session=MySession()
)
assert hasattr(plex._session, "plexapi_session_test")
@pytest.mark.authenticated @pytest.mark.authenticated
def test_server_token_in_headers(plex): def test_server_token_in_headers(plex):
headers = plex._headers() headers = plex._headers()
assert 'X-Plex-Token' in headers assert "X-Plex-Token" in headers
assert len(headers['X-Plex-Token']) >= 1 assert len(headers["X-Plex-Token"]) >= 1
def test_server_createPlayQueue(plex, movie): def test_server_createPlayQueue(plex, movie):
playqueue = plex.createPlayQueue(movie, shuffle=1, repeat=1) playqueue = plex.createPlayQueue(movie, shuffle=1, repeat=1)
assert 'shuffle=1' in playqueue._initpath assert "shuffle=1" in playqueue._initpath
assert 'repeat=1' in playqueue._initpath assert "repeat=1" in playqueue._initpath
assert playqueue.playQueueShuffled is True assert playqueue.playQueueShuffled is True
def test_server_client_not_found(plex): def test_server_client_not_found(plex):
with pytest.raises(NotFound): with pytest.raises(NotFound):
plex.client('<This-client-should-not-be-found>') plex.client("<This-client-should-not-be-found>")
def test_server_sessions(plex): def test_server_sessions(plex):
@ -174,38 +187,41 @@ def test_server_sessions(plex):
def test_server_isLatest(plex, mocker): def test_server_isLatest(plex, mocker):
from os import environ from os import environ
is_latest = plex.isLatest() is_latest = plex.isLatest()
if environ.get('PLEX_CONTAINER_TAG') and environ['PLEX_CONTAINER_TAG'] != 'latest': if environ.get("PLEX_CONTAINER_TAG") and environ["PLEX_CONTAINER_TAG"] != "latest":
assert not is_latest assert not is_latest
else: else:
return pytest.skip('Do not forget to run with PLEX_CONTAINER_TAG != latest to ensure that update is available') return pytest.skip(
"Do not forget to run with PLEX_CONTAINER_TAG != latest to ensure that update is available"
)
def test_server_installUpdate(plex, mocker): def test_server_installUpdate(plex, mocker):
m = mocker.MagicMock(release='aa') m = mocker.MagicMock(release="aa")
with patch('plexapi.server.PlexServer.check_for_update', return_value=m): with utils.patch('plexapi.server.PlexServer.check_for_update', return_value=m):
with utils.callable_http_patch(): with utils.callable_http_patch():
plex.installUpdate() plex.installUpdate()
def test_server_check_for_update(plex, mocker): def test_server_check_for_update(plex, mocker):
class R(): class R:
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.download_key = 'plex.tv/release/1337' self.download_key = "plex.tv/release/1337"
self.version = '1337' self.version = "1337"
self.added = 'gpu transcode' self.added = "gpu transcode"
self.fixed = 'fixed rare bug' self.fixed = "fixed rare bug"
self.downloadURL = 'http://path-to-update' self.downloadURL = "http://path-to-update"
self.state = 'downloaded' self.state = "downloaded"
with patch('plexapi.server.PlexServer.check_for_update', return_value=R()): with utils.patch('plexapi.server.PlexServer.check_for_update', return_value=R()):
rel = plex.check_for_update(force=False, download=True) rel = plex.check_for_update(force=False, download=True)
assert rel.download_key == 'plex.tv/release/1337' assert rel.download_key == "plex.tv/release/1337"
assert rel.version == '1337' assert rel.version == "1337"
assert rel.added == 'gpu transcode' assert rel.added == "gpu transcode"
assert rel.fixed == 'fixed rare bug' assert rel.fixed == "fixed rare bug"
assert rel.downloadURL == 'http://path-to-update' assert rel.downloadURL == "http://path-to-update"
assert rel.state == 'downloaded' assert rel.state == "downloaded"
@pytest.mark.client @pytest.mark.client
@ -222,31 +238,42 @@ def test_server_clients(plex):
@pytest.mark.authenticated @pytest.mark.authenticated
@pytest.mark.xfail(strict=False)
def test_server_account(plex): def test_server_account(plex):
account = plex.account() account = plex.account()
assert account.authToken assert account.authToken
# TODO: Figure out why this is missing from time to time. # TODO: Figure out why this is missing from time to time.
# assert account.mappingError == 'publisherror' # assert account.mappingError == 'publisherror'
assert account.mappingErrorMessage is None assert account.mappingErrorMessage is None
assert account.mappingState == 'mapped' assert account.mappingState == "mapped"
if account.mappingError != 'unreachable': if account.mappingError != "unreachable":
assert re.match(utils.REGEX_IPADDR, account.privateAddress) if account.privateAddress is not None:
# This seems to fail way to often..
if len(account.privateAddress):
assert re.match(utils.REGEX_IPADDR, account.privateAddress)
else:
assert account.privateAddress == ""
assert int(account.privatePort) >= 1000 assert int(account.privatePort) >= 1000
assert re.match(utils.REGEX_IPADDR, account.publicAddress) assert re.match(utils.REGEX_IPADDR, account.publicAddress)
assert int(account.publicPort) >= 1000 assert int(account.publicPort) >= 1000
else: else:
assert account.privateAddress == '' assert account.privateAddress == ""
assert int(account.privatePort) == 0 assert int(account.privatePort) == 0
assert account.publicAddress == '' assert account.publicAddress == ""
assert int(account.publicPort) == 0 assert int(account.publicPort) == 0
assert account.signInState == 'ok' assert account.signInState == "ok"
assert isinstance(account.subscriptionActive, bool) assert isinstance(account.subscriptionActive, bool)
if account.subscriptionActive: if account.subscriptionActive:
assert len(account.subscriptionFeatures) assert len(account.subscriptionFeatures)
# Below check keeps failing.. it should go away. # Below check keeps failing.. it should go away.
# else: assert sorted(account.subscriptionFeatures) == ['adaptive_bitrate', # else: assert sorted(account.subscriptionFeatures) == ['adaptive_bitrate',
# 'download_certificates', 'federated-auth', 'news'] # 'download_certificates', 'federated-auth', 'news']
assert account.subscriptionState == 'Active' if account.subscriptionActive else 'Unknown' assert (
account.subscriptionState == "Active"
if account.subscriptionActive
else "Unknown"
)
assert re.match(utils.REGEX_EMAIL, account.username) assert re.match(utils.REGEX_EMAIL, account.username)

View file

@ -1,31 +1,30 @@
def test_settings_group(plex): def test_settings_group(plex):
assert plex.settings.group('general') assert plex.settings.group("general")
def test_settings_get(plex): def test_settings_get(plex):
# This is the value since it we havnt set any friendlyname # This is the value since it we havnt set any friendlyname
# plex just default to computer name but it NOT in the settings. # plex just default to computer name but it NOT in the settings.
# check this one. why is this bytes instead of string. # check this one. why is this bytes instead of string.
value = plex.settings.get('FriendlyName').value value = plex.settings.get("FriendlyName").value
# Should not be bytes, fix this when py2 is dropped # Should not be bytes, fix this when py2 is dropped
assert isinstance(value, bytes) assert isinstance(value, bytes)
def test_settings_set(plex): def test_settings_set(plex):
cd = plex.settings.get('autoEmptyTrash') cd = plex.settings.get("autoEmptyTrash")
old_value = cd.value old_value = cd.value
new_value = not old_value new_value = not old_value
cd.set(new_value) cd.set(new_value)
plex.settings.save() plex.settings.save()
plex._settings = None plex._settings = None
assert plex.settings.get('autoEmptyTrash').value == new_value assert plex.settings.get("autoEmptyTrash").value == new_value
def test_settings_set_str(plex): def test_settings_set_str(plex):
cd = plex.settings.get('OnDeckWindow') cd = plex.settings.get("OnDeckWindow")
new_value = 99 new_value = 99
cd.set(new_value) cd.set(new_value)
plex.settings.save() plex.settings.save()
plex._settings = None plex._settings = None
assert plex.settings.get('OnDeckWindow').value == 99 assert plex.settings.get("OnDeckWindow").value == 99

28
tests/test_sonos.py Normal file
View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
from .payloads import SONOS_RESOURCES
def test_sonos_resources(mocked_account, requests_mock):
requests_mock.get("https://sonos.plex.tv/resources", text=SONOS_RESOURCES)
speakers = mocked_account.sonos_speakers()
assert len(speakers) == 3
# Finds individual speaker by name
speaker1 = mocked_account.sonos_speaker("Speaker 1")
assert speaker1.machineIdentifier == "RINCON_12345678901234561:1234567891"
# Finds speaker as part of group
speaker1 = mocked_account.sonos_speaker("Speaker 2")
assert speaker1.machineIdentifier == "RINCON_12345678901234562:1234567892"
# Finds speaker by Plex identifier
speaker3 = mocked_account.sonos_speaker_by_id("RINCON_12345678901234563:1234567893")
assert speaker3.title == "Speaker 3"
# Finds speaker by Sonos identifier
speaker3 = mocked_account.sonos_speaker_by_id("RINCON_12345678901234563")
assert speaker3.title == "Speaker 3"
assert mocked_account.sonos_speaker("Speaker X") is None
assert mocked_account.sonos_speaker_by_id("ID_DOES_NOT_EXIST") is None

View file

@ -1,7 +1,8 @@
from plexapi.exceptions import BadRequest from plexapi.exceptions import BadRequest
from . import conftest as utils from plexapi.sync import (AUDIO_BITRATE_192_KBPS, PHOTO_QUALITY_MEDIUM,
VIDEO_QUALITY_3_MBPS_720p)
from plexapi.sync import VIDEO_QUALITY_3_MBPS_720p, AUDIO_BITRATE_192_KBPS, PHOTO_QUALITY_MEDIUM from . import conftest as utils
def get_sync_item_from_server(device, sync_item): def get_sync_item_from_server(device, sync_item):
@ -16,14 +17,14 @@ def is_sync_item_missing(device, sync_item):
def test_current_device_got_sync_target(clear_sync_device): def test_current_device_got_sync_target(clear_sync_device):
assert 'sync-target' in clear_sync_device.provides assert "sync-target" in clear_sync_device.provides
def get_media(item, server): def get_media(item, server):
try: try:
return item.getMedia() return item.getMedia()
except BadRequest as e: except BadRequest as e:
if 'not_found' in str(e): if "not_found" in str(e):
server.refreshSync() server.refreshSync()
return None return None
else: else:
@ -33,9 +34,16 @@ def get_media(item, server):
def test_add_movie_to_sync(clear_sync_device, movie): def test_add_movie_to_sync(clear_sync_device, movie):
new_item = movie.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) new_item = movie.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device)
movie._server.refreshSync() movie._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=movie._server) delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=movie._server
)
assert len(media_list) == 1 assert len(media_list) == 1
assert media_list[0].ratingKey == movie.ratingKey assert media_list[0].ratingKey == movie.ratingKey
@ -43,33 +51,58 @@ def test_add_movie_to_sync(clear_sync_device, movie):
def test_delete_sync_item(clear_sync_device, movie): def test_delete_sync_item(clear_sync_device, movie):
new_item = movie.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) new_item = movie.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device)
movie._server.refreshSync() movie._server.refreshSync()
new_item_in_myplex = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, new_item_in_myplex = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
sync_items = clear_sync_device.syncItems() sync_items = clear_sync_device.syncItems()
for item in sync_items.items: for item in sync_items.items:
item.delete() item.delete()
utils.wait_until(is_sync_item_missing, delay=0.5, timeout=3, device=clear_sync_device, sync_item=new_item_in_myplex) utils.wait_until(
is_sync_item_missing,
delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item_in_myplex,
)
def test_add_show_to_sync(clear_sync_device, show): def test_add_show_to_sync(clear_sync_device, show):
new_item = show.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) new_item = show.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device)
show._server.refreshSync() show._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
episodes = show.episodes() episodes = show.episodes()
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=show._server) media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=show._server
)
assert len(episodes) == len(media_list) assert len(episodes) == len(media_list)
assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list]
def test_add_season_to_sync(clear_sync_device, show): def test_add_season_to_sync(clear_sync_device, show):
season = show.season('Season 1') season = show.season("Season 1")
new_item = season.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) new_item = season.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device)
season._server.refreshSync() season._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
episodes = season.episodes() episodes = season.episodes()
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=season._server) media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=season._server
)
assert len(episodes) == len(media_list) assert len(episodes) == len(media_list)
assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list]
@ -77,80 +110,131 @@ def test_add_season_to_sync(clear_sync_device, show):
def test_add_episode_to_sync(clear_sync_device, episode): def test_add_episode_to_sync(clear_sync_device, episode):
new_item = episode.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) new_item = episode.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device)
episode._server.refreshSync() episode._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=episode._server) delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=episode._server
)
assert 1 == len(media_list) assert 1 == len(media_list)
assert episode.ratingKey == media_list[0].ratingKey assert episode.ratingKey == media_list[0].ratingKey
def test_limited_watched(clear_sync_device, show): def test_limited_watched(clear_sync_device, show):
show.markUnwatched() show.markUnwatched()
new_item = show.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device, limit=5, unwatched=False) new_item = show.sync(
VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device, limit=5, unwatched=False
)
show._server.refreshSync() show._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
episodes = show.episodes()[:5] episodes = show.episodes()[:5]
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=show._server) media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=show._server
)
assert 5 == len(media_list) assert 5 == len(media_list)
assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list]
episodes[0].markWatched() episodes[0].markWatched()
show._server.refreshSync() show._server.refreshSync()
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=show._server) media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=show._server
)
assert 5 == len(media_list) assert 5 == len(media_list)
assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list]
def test_limited_unwatched(clear_sync_device, show): def test_limited_unwatched(clear_sync_device, show):
show.markUnwatched() show.markUnwatched()
new_item = show.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device, limit=5, unwatched=True) new_item = show.sync(
VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device, limit=5, unwatched=True
)
show._server.refreshSync() show._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
episodes = show.episodes(viewCount=0)[:5] episodes = show.episodes(viewCount=0)[:5]
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=show._server) media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=show._server
)
assert len(episodes) == len(media_list) assert len(episodes) == len(media_list)
assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list]
episodes[0].markWatched() episodes[0].markWatched()
show._server.refreshSync() show._server.refreshSync()
episodes = show.episodes(viewCount=0)[:5] episodes = show.episodes(viewCount=0)[:5]
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=show._server) media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=show._server
)
assert len(episodes) == len(media_list) assert len(episodes) == len(media_list)
assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list]
def test_unlimited_and_watched(clear_sync_device, show): def test_unlimited_and_watched(clear_sync_device, show):
show.markUnwatched() show.markUnwatched()
new_item = show.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device, unwatched=False) new_item = show.sync(
VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device, unwatched=False
)
show._server.refreshSync() show._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
episodes = show.episodes() episodes = show.episodes()
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=show._server) media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=show._server
)
assert len(episodes) == len(media_list) assert len(episodes) == len(media_list)
assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list]
episodes[0].markWatched() episodes[0].markWatched()
show._server.refreshSync() show._server.refreshSync()
episodes = show.episodes() episodes = show.episodes()
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=show._server) media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=show._server
)
assert len(episodes) == len(media_list) assert len(episodes) == len(media_list)
assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list]
def test_unlimited_and_unwatched(clear_sync_device, show): def test_unlimited_and_unwatched(clear_sync_device, show):
show.markUnwatched() show.markUnwatched()
new_item = show.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device, unwatched=True) new_item = show.sync(
VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device, unwatched=True
)
show._server.refreshSync() show._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
episodes = show.episodes(viewCount=0) episodes = show.episodes(viewCount=0)
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=show._server) media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=show._server
)
assert len(episodes) == len(media_list) assert len(episodes) == len(media_list)
assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list]
episodes[0].markWatched() episodes[0].markWatched()
show._server.refreshSync() show._server.refreshSync()
episodes = show.episodes(viewCount=0) episodes = show.episodes(viewCount=0)
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=show._server) media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=show._server
)
assert len(episodes) == len(media_list) assert len(episodes) == len(media_list)
assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list] assert [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list]
@ -158,10 +242,17 @@ def test_unlimited_and_unwatched(clear_sync_device, show):
def test_add_music_artist_to_sync(clear_sync_device, artist): def test_add_music_artist_to_sync(clear_sync_device, artist):
new_item = artist.sync(AUDIO_BITRATE_192_KBPS, client=clear_sync_device) new_item = artist.sync(AUDIO_BITRATE_192_KBPS, client=clear_sync_device)
artist._server.refreshSync() artist._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
tracks = artist.tracks() tracks = artist.tracks()
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=artist._server) media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=artist._server
)
assert len(tracks) == len(media_list) assert len(tracks) == len(media_list)
assert [t.ratingKey for t in tracks] == [m.ratingKey for m in media_list] assert [t.ratingKey for t in tracks] == [m.ratingKey for m in media_list]
@ -169,10 +260,17 @@ def test_add_music_artist_to_sync(clear_sync_device, artist):
def test_add_music_album_to_sync(clear_sync_device, album): def test_add_music_album_to_sync(clear_sync_device, album):
new_item = album.sync(AUDIO_BITRATE_192_KBPS, client=clear_sync_device) new_item = album.sync(AUDIO_BITRATE_192_KBPS, client=clear_sync_device)
album._server.refreshSync() album._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
tracks = album.tracks() tracks = album.tracks()
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=album._server) media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=album._server
)
assert len(tracks) == len(media_list) assert len(tracks) == len(media_list)
assert [t.ratingKey for t in tracks] == [m.ratingKey for m in media_list] assert [t.ratingKey for t in tracks] == [m.ratingKey for m in media_list]
@ -180,20 +278,34 @@ def test_add_music_album_to_sync(clear_sync_device, album):
def test_add_music_track_to_sync(clear_sync_device, track): def test_add_music_track_to_sync(clear_sync_device, track):
new_item = track.sync(AUDIO_BITRATE_192_KBPS, client=clear_sync_device) new_item = track.sync(AUDIO_BITRATE_192_KBPS, client=clear_sync_device)
track._server.refreshSync() track._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=track._server) delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=track._server
)
assert 1 == len(media_list) assert 1 == len(media_list)
assert track.ratingKey == media_list[0].ratingKey assert track.ratingKey == media_list[0].ratingKey
def test_add_photo_to_sync(clear_sync_device, photoalbum): def test_add_photo_to_sync(clear_sync_device, photoalbum):
photo = photoalbum.photo('photo1') photo = photoalbum.photo("photo1")
new_item = photo.sync(PHOTO_QUALITY_MEDIUM, client=clear_sync_device) new_item = photo.sync(PHOTO_QUALITY_MEDIUM, client=clear_sync_device)
photo._server.refreshSync() photo._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=photo._server) delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=photo._server
)
assert 1 == len(media_list) assert 1 == len(media_list)
assert photo.ratingKey == media_list[0].ratingKey assert photo.ratingKey == media_list[0].ratingKey
@ -201,10 +313,17 @@ def test_add_photo_to_sync(clear_sync_device, photoalbum):
def test_sync_entire_library_movies(clear_sync_device, movies): def test_sync_entire_library_movies(clear_sync_device, movies):
new_item = movies.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) new_item = movies.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device)
movies._server.refreshSync() movies._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
section_content = movies.all() section_content = movies.all()
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=movies._server) media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=movies._server
)
assert len(section_content) == len(media_list) assert len(section_content) == len(media_list)
assert [e.ratingKey for e in section_content] == [m.ratingKey for m in media_list] assert [e.ratingKey for e in section_content] == [m.ratingKey for m in media_list]
@ -212,10 +331,17 @@ def test_sync_entire_library_movies(clear_sync_device, movies):
def test_sync_entire_library_tvshows(clear_sync_device, tvshows): def test_sync_entire_library_tvshows(clear_sync_device, tvshows):
new_item = tvshows.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) new_item = tvshows.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device)
tvshows._server.refreshSync() tvshows._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
section_content = tvshows.searchEpisodes() section_content = tvshows.searchEpisodes()
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=tvshows._server) media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=tvshows._server
)
assert len(section_content) == len(media_list) assert len(section_content) == len(media_list)
assert [e.ratingKey for e in section_content] == [m.ratingKey for m in media_list] assert [e.ratingKey for e in section_content] == [m.ratingKey for m in media_list]
@ -223,10 +349,17 @@ def test_sync_entire_library_tvshows(clear_sync_device, tvshows):
def test_sync_entire_library_music(clear_sync_device, music): def test_sync_entire_library_music(clear_sync_device, music):
new_item = music.sync(AUDIO_BITRATE_192_KBPS, client=clear_sync_device) new_item = music.sync(AUDIO_BITRATE_192_KBPS, client=clear_sync_device)
music._server.refreshSync() music._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
section_content = music.searchTracks() section_content = music.searchTracks()
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=music._server) media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=music._server
)
assert len(section_content) == len(media_list) assert len(section_content) == len(media_list)
assert [e.ratingKey for e in section_content] == [m.ratingKey for m in media_list] assert [e.ratingKey for e in section_content] == [m.ratingKey for m in media_list]
@ -234,23 +367,39 @@ def test_sync_entire_library_music(clear_sync_device, music):
def test_sync_entire_library_photos(clear_sync_device, photos): def test_sync_entire_library_photos(clear_sync_device, photos):
new_item = photos.sync(PHOTO_QUALITY_MEDIUM, client=clear_sync_device) new_item = photos.sync(PHOTO_QUALITY_MEDIUM, client=clear_sync_device)
photos._server.refreshSync() photos._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
# It's not that easy, to just get all the photos within the library, so let`s query for photos with device!=0x0 # It's not that easy, to just get all the photos within the library, so let`s query for photos with device!=0x0
section_content = photos.search(libtype='photo', **{'device!': '0x0'}) section_content = photos.search(libtype="photo", **{"device!": "0x0"})
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=photos._server) media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=photos._server
)
assert len(section_content) == len(media_list) assert len(section_content) == len(media_list)
assert [e.ratingKey for e in section_content] == [m.ratingKey for m in media_list] assert [e.ratingKey for e in section_content] == [m.ratingKey for m in media_list]
def test_playlist_movie_sync(plex, clear_sync_device, movies): def test_playlist_movie_sync(plex, clear_sync_device, movies):
items = movies.all() items = movies.all()
playlist = plex.createPlaylist('Sync: Movies', items) playlist = plex.createPlaylist("Sync: Movies", items)
new_item = playlist.sync(videoQuality=VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) new_item = playlist.sync(
videoQuality=VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device
)
playlist._server.refreshSync() playlist._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=playlist._server) delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=playlist._server
)
assert len(items) == len(media_list) assert len(items) == len(media_list)
assert [e.ratingKey for e in items] == [m.ratingKey for m in media_list] assert [e.ratingKey for e in items] == [m.ratingKey for m in media_list]
playlist.delete() playlist.delete()
@ -258,12 +407,21 @@ def test_playlist_movie_sync(plex, clear_sync_device, movies):
def test_playlist_tvshow_sync(plex, clear_sync_device, show): def test_playlist_tvshow_sync(plex, clear_sync_device, show):
items = show.episodes() items = show.episodes()
playlist = plex.createPlaylist('Sync: TV Show', items) playlist = plex.createPlaylist("Sync: TV Show", items)
new_item = playlist.sync(videoQuality=VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) new_item = playlist.sync(
videoQuality=VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device
)
playlist._server.refreshSync() playlist._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=playlist._server) delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=playlist._server
)
assert len(items) == len(media_list) assert len(items) == len(media_list)
assert [e.ratingKey for e in items] == [m.ratingKey for m in media_list] assert [e.ratingKey for e in items] == [m.ratingKey for m in media_list]
playlist.delete() playlist.delete()
@ -271,12 +429,21 @@ def test_playlist_tvshow_sync(plex, clear_sync_device, show):
def test_playlist_mixed_sync(plex, clear_sync_device, movie, episode): def test_playlist_mixed_sync(plex, clear_sync_device, movie, episode):
items = [movie, episode] items = [movie, episode]
playlist = plex.createPlaylist('Sync: Mixed', items) playlist = plex.createPlaylist("Sync: Mixed", items)
new_item = playlist.sync(videoQuality=VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device) new_item = playlist.sync(
videoQuality=VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device
)
playlist._server.refreshSync() playlist._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=playlist._server) delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=playlist._server
)
assert len(items) == len(media_list) assert len(items) == len(media_list)
assert [e.ratingKey for e in items] == [m.ratingKey for m in media_list] assert [e.ratingKey for e in items] == [m.ratingKey for m in media_list]
playlist.delete() playlist.delete()
@ -284,12 +451,21 @@ def test_playlist_mixed_sync(plex, clear_sync_device, movie, episode):
def test_playlist_music_sync(plex, clear_sync_device, artist): def test_playlist_music_sync(plex, clear_sync_device, artist):
items = artist.tracks() items = artist.tracks()
playlist = plex.createPlaylist('Sync: Music', items) playlist = plex.createPlaylist("Sync: Music", items)
new_item = playlist.sync(audioBitrate=AUDIO_BITRATE_192_KBPS, client=clear_sync_device) new_item = playlist.sync(
audioBitrate=AUDIO_BITRATE_192_KBPS, client=clear_sync_device
)
playlist._server.refreshSync() playlist._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=playlist._server) delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=playlist._server
)
assert len(items) == len(media_list) assert len(items) == len(media_list)
assert [e.ratingKey for e in items] == [m.ratingKey for m in media_list] assert [e.ratingKey for e in items] == [m.ratingKey for m in media_list]
playlist.delete() playlist.delete()
@ -297,12 +473,21 @@ def test_playlist_music_sync(plex, clear_sync_device, artist):
def test_playlist_photos_sync(plex, clear_sync_device, photoalbum): def test_playlist_photos_sync(plex, clear_sync_device, photoalbum):
items = photoalbum.photos() items = photoalbum.photos()
playlist = plex.createPlaylist('Sync: Photos', items) playlist = plex.createPlaylist("Sync: Photos", items)
new_item = playlist.sync(photoResolution=PHOTO_QUALITY_MEDIUM, client=clear_sync_device) new_item = playlist.sync(
photoResolution=PHOTO_QUALITY_MEDIUM, client=clear_sync_device
)
playlist._server.refreshSync() playlist._server.refreshSync()
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device, item = utils.wait_until(
sync_item=new_item) get_sync_item_from_server,
media_list = utils.wait_until(get_media, delay=0.25, timeout=3, item=item, server=playlist._server) delay=0.5,
timeout=3,
device=clear_sync_device,
sync_item=new_item,
)
media_list = utils.wait_until(
get_media, delay=0.25, timeout=3, item=item, server=playlist._server
)
assert len(items) == len(media_list) assert len(items) == len(media_list)
assert [e.ratingKey for e in items] == [m.ratingKey for m in media_list] assert [e.ratingKey for e in items] == [m.ratingKey for m in media_list]
playlist.delete() playlist.delete()

View file

@ -7,14 +7,17 @@ from plexapi.exceptions import NotFound
def test_utils_toDatetime(): def test_utils_toDatetime():
assert str(utils.toDatetime('2006-03-03', format='%Y-%m-%d')) == '2006-03-03 00:00:00' assert (
#assert str(utils.toDatetime('0'))[:-9] in ['1970-01-01', '1969-12-31'] str(utils.toDatetime("2006-03-03", format="%Y-%m-%d")) == "2006-03-03 00:00:00"
)
# assert str(utils.toDatetime('0'))[:-9] in ['1970-01-01', '1969-12-31']
def test_utils_threaded(): def test_utils_threaded():
def _squared(num, results, i, job_is_done_event=None): def _squared(num, results, i, job_is_done_event=None):
time.sleep(0.5) time.sleep(0.5)
results[i] = num * num results[i] = num * num
starttime = time.time() starttime = time.time()
results = utils.threaded(_squared, [[1], [2], [3], [4], [5]]) results = utils.threaded(_squared, [[1], [2], [3], [4], [5]])
assert results == [1, 4, 9, 16, 25] assert results == [1, 4, 9, 16, 25]
@ -28,28 +31,28 @@ def test_utils_downloadSessionImages():
def test_utils_searchType(): def test_utils_searchType():
st = utils.searchType('movie') st = utils.searchType("movie")
assert st == 1 assert st == 1
movie = utils.searchType(1) movie = utils.searchType(1)
assert movie == '1' assert movie == "1"
with pytest.raises(NotFound): with pytest.raises(NotFound):
utils.searchType('kekekekeke') utils.searchType("kekekekeke")
def test_utils_joinArgs(): def test_utils_joinArgs():
test_dict = {'genre': 'action', 'type': 1337} test_dict = {"genre": "action", "type": 1337}
assert utils.joinArgs(test_dict) == '?genre=action&type=1337' assert utils.joinArgs(test_dict) == "?genre=action&type=1337"
def test_utils_cast(): def test_utils_cast():
int_int = utils.cast(int, 1) int_int = utils.cast(int, 1)
int_str = utils.cast(int, '1') int_str = utils.cast(int, "1")
bool_str = utils.cast(bool, '1') bool_str = utils.cast(bool, "1")
bool_int = utils.cast(bool, 1) bool_int = utils.cast(bool, 1)
float_int = utils.cast(float, 1) float_int = utils.cast(float, 1)
float_float = utils.cast(float, 1.0) float_float = utils.cast(float, 1.0)
float_str = utils.cast(float, '1.2') float_str = utils.cast(float, "1.2")
float_nan = utils.cast(float, 'wut?') float_nan = utils.cast(float, "wut?")
assert int_int == 1 and isinstance(int_int, int) assert int_int == 1 and isinstance(int_int, int)
assert int_str == 1 and isinstance(int_str, int) assert int_str == 1 and isinstance(int_str, int)
assert bool_str is True assert bool_str is True
@ -59,7 +62,7 @@ def test_utils_cast():
assert float_str == 1.2 and isinstance(float_str, float) assert float_str == 1.2 and isinstance(float_str, float)
assert float_nan != float_nan # nan is never equal assert float_nan != float_nan # nan is never equal
with pytest.raises(ValueError): with pytest.raises(ValueError):
bool_str = utils.cast(bool, 'kek') bool_str = utils.cast(bool, "kek")
def test_utils_download(plex, episode): def test_utils_download(plex, episode):
@ -67,5 +70,9 @@ def test_utils_download(plex, episode):
locations = episode.locations[0] locations = episode.locations[0]
session = episode._server._session session = episode._server._session
assert utils.download(url, plex._token, filename=locations, mocked=True) assert utils.download(url, plex._token, filename=locations, mocked=True)
assert utils.download(url, plex._token, filename=locations, session=session, mocked=True) assert utils.download(
assert utils.download(episode.thumbUrl, plex._token, filename=episode.title, mocked=True) url, plex._token, filename=locations, session=session, mocked=True
)
assert utils.download(
episode.thumbUrl, plex._token, filename=episode.title, mocked=True
)

View file

@ -2,6 +2,7 @@
import os import os
from datetime import datetime from datetime import datetime
from time import sleep from time import sleep
from urllib.parse import quote_plus
import pytest import pytest
from plexapi.exceptions import BadRequest, NotFound from plexapi.exceptions import BadRequest, NotFound
@ -20,15 +21,26 @@ def test_video_Movie_attributeerror(movie):
def test_video_ne(movies): def test_video_ne(movies):
assert len(movies.fetchItems('/library/sections/%s/all' % movies.key, title__ne='Sintel')) == 3 assert (
len(
movies.fetchItems(
"/library/sections/%s/all" % movies.key, title__ne="Sintel"
)
)
== 3
)
def test_video_Movie_delete(movie, patched_http_call): def test_video_Movie_delete(movie, patched_http_call):
movie.delete() movie.delete()
def test_video_Movie_merge(movie, patched_http_call):
movie.merge(1337)
def test_video_Movie_addCollection(movie): def test_video_Movie_addCollection(movie):
labelname = 'Random_label' labelname = "Random_label"
org_collection = [tag.tag for tag in movie.collections if tag] org_collection = [tag.tag for tag in movie.collections if tag]
assert labelname not in org_collection assert labelname not in org_collection
movie.addCollection(labelname) movie.addCollection(labelname)
@ -41,12 +53,18 @@ def test_video_Movie_addCollection(movie):
def test_video_Movie_getStreamURL(movie, account): def test_video_Movie_getStreamURL(movie, account):
key = movie.ratingKey key = movie.ratingKey
assert movie.getStreamURL() == '{0}/video/:/transcode/universal/start.m3u8?X-Plex-Platform=Chrome&copyts=1&mediaIndex=0&offset=0&path=%2Flibrary%2Fmetadata%2F{1}&X-Plex-Token={2}'.format(utils.SERVER_BASEURL, key, account.authenticationToken) # noqa assert movie.getStreamURL() == "{0}/video/:/transcode/universal/start.m3u8?X-Plex-Platform=Chrome&copyts=1&mediaIndex=0&offset=0&path=%2Flibrary%2Fmetadata%2F{1}&X-Plex-Token={2}".format(
assert movie.getStreamURL(videoResolution='800x600') == '{0}/video/:/transcode/universal/start.m3u8?X-Plex-Platform=Chrome&copyts=1&mediaIndex=0&offset=0&path=%2Flibrary%2Fmetadata%2F{1}&videoResolution=800x600&X-Plex-Token={2}'.format(utils.SERVER_BASEURL, key, account.authenticationToken) # noqa utils.SERVER_BASEURL, key, account.authenticationToken
) # noqa
assert movie.getStreamURL(
videoResolution="800x600"
) == "{0}/video/:/transcode/universal/start.m3u8?X-Plex-Platform=Chrome&copyts=1&mediaIndex=0&offset=0&path=%2Flibrary%2Fmetadata%2F{1}&videoResolution=800x600&X-Plex-Token={2}".format(
utils.SERVER_BASEURL, key, account.authenticationToken
) # noqa
def test_video_Movie_isFullObject_and_reload(plex): def test_video_Movie_isFullObject_and_reload(plex):
movie = plex.library.section('Movies').get('Sita Sings the Blues') movie = plex.library.section("Movies").get("Sita Sings the Blues")
assert movie.isFullObject() is False assert movie.isFullObject() is False
movie.reload() movie.reload()
assert movie.isFullObject() is True assert movie.isFullObject() is True
@ -54,7 +72,7 @@ def test_video_Movie_isFullObject_and_reload(plex):
assert movie_via_search.isFullObject() is False assert movie_via_search.isFullObject() is False
movie_via_search.reload() movie_via_search.reload()
assert movie_via_search.isFullObject() is True assert movie_via_search.isFullObject() is True
movie_via_section_search = plex.library.section('Movies').search(movie.title)[0] movie_via_section_search = plex.library.section("Movies").search(movie.title)[0]
assert movie_via_section_search.isFullObject() is False assert movie_via_section_search.isFullObject() is False
movie_via_section_search.reload() movie_via_section_search.reload()
assert movie_via_section_search.isFullObject() is True assert movie_via_section_search.isFullObject() is True
@ -82,7 +100,7 @@ def test_video_Movie_iterParts(movie):
def test_video_Movie_download(monkeydownload, tmpdir, movie): def test_video_Movie_download(monkeydownload, tmpdir, movie):
filepaths1 = movie.download(savepath=str(tmpdir)) filepaths1 = movie.download(savepath=str(tmpdir))
assert len(filepaths1) >= 1 assert len(filepaths1) >= 1
filepaths2 = movie.download(savepath=str(tmpdir), videoResolution='500x300') filepaths2 = movie.download(savepath=str(tmpdir), videoResolution="500x300")
assert len(filepaths2) >= 1 assert len(filepaths2) >= 1
@ -101,7 +119,7 @@ def test_video_Movie_upload_select_remove_subtitle(movie, subtitle):
movie.uploadSubtitles(filepath) movie.uploadSubtitles(filepath)
movie.reload() movie.reload()
subtitles = [sub.title for sub in movie.subtitleStreams()] subtitles = [sub.title for sub in movie.subtitleStreams()]
subname = subtitle.name.rsplit('.', 1)[0] subname = subtitle.name.rsplit(".", 1)[0]
assert subname in subtitles assert subname in subtitles
subtitleSelection = movie.subtitleStreams()[0] subtitleSelection = movie.subtitleStreams()[0]
@ -124,31 +142,37 @@ def test_video_Movie_upload_select_remove_subtitle(movie, subtitle):
def test_video_Movie_attrs(movies): def test_video_Movie_attrs(movies):
movie = movies.get('Sita Sings the Blues') movie = movies.get("Sita Sings the Blues")
assert len(movie.locations[0]) >= 10 assert len(movie.locations[0]) >= 10
assert utils.is_datetime(movie.addedAt) assert utils.is_datetime(movie.addedAt)
assert utils.is_metadata(movie.art) assert utils.is_metadata(movie.art)
assert movie.artUrl assert movie.artUrl
assert movie.audienceRating == 8.5 assert movie.audienceRating == 8.5
# Disabled this since it failed on the last run, wasnt in the original xml result. # Disabled this since it failed on the last run, wasnt in the original xml result.
#assert movie.audienceRatingImage == 'rottentomatoes://image.rating.upright' # assert movie.audienceRatingImage == 'rottentomatoes://image.rating.upright'
movie.reload() # RELOAD movie.reload() # RELOAD
assert movie.chapterSource is None assert movie.chapterSource is None
assert movie.collections == [] assert movie.collections == []
assert movie.contentRating in utils.CONTENTRATINGS assert movie.contentRating in utils.CONTENTRATINGS
assert all([i.tag in ['US', 'USA'] for i in movie.countries]) assert all([i.tag in ["US", "USA"] for i in movie.countries])
assert [i.tag for i in movie.directors] == ['Nina Paley'] assert [i.tag for i in movie.directors] == ["Nina Paley"]
assert movie.duration >= 160000 assert movie.duration >= 160000
assert movie.fields == [] assert movie.fields == []
assert movie.posters() assert movie.posters()
assert sorted([i.tag for i in movie.genres]) == ['Animation', 'Comedy', 'Fantasy', 'Musical', 'Romance'] assert sorted([i.tag for i in movie.genres]) == [
assert movie.guid == 'com.plexapp.agents.imdb://tt1172203?lang=en' "Animation",
"Comedy",
"Fantasy",
"Musical",
"Romance",
]
assert movie.guid == "com.plexapp.agents.imdb://tt1172203?lang=en"
assert utils.is_metadata(movie._initpath) assert utils.is_metadata(movie._initpath)
assert utils.is_metadata(movie.key) assert utils.is_metadata(movie.key)
if movie.lastViewedAt: if movie.lastViewedAt:
assert utils.is_datetime(movie.lastViewedAt) assert utils.is_datetime(movie.lastViewedAt)
assert int(movie.librarySectionID) >= 1 assert int(movie.librarySectionID) >= 1
assert movie.listType == 'video' assert movie.listType == "video"
assert movie.originalTitle is None assert movie.originalTitle is None
assert utils.is_datetime(movie.originallyAvailableAt) assert utils.is_datetime(movie.originallyAvailableAt)
assert movie.playlistItemID is None assert movie.playlistItemID is None
@ -156,25 +180,30 @@ def test_video_Movie_attrs(movies):
assert utils.is_metadata(movie.primaryExtraKey) assert utils.is_metadata(movie.primaryExtraKey)
assert [i.tag for i in movie.producers] == [] assert [i.tag for i in movie.producers] == []
assert float(movie.rating) >= 6.4 assert float(movie.rating) >= 6.4
#assert movie.ratingImage == 'rottentomatoes://image.rating.ripe' # assert movie.ratingImage == 'rottentomatoes://image.rating.ripe'
assert movie.ratingKey >= 1 assert movie.ratingKey >= 1
assert set(sorted([i.tag for i in movie.roles])) >= {'Aladdin Ullah', 'Annette Hanshaw', 'Aseem Chhabra', 'Debargo Sanyal'} # noqa assert set(sorted([i.tag for i in movie.roles])) >= {
"Aladdin Ullah",
"Annette Hanshaw",
"Aseem Chhabra",
"Debargo Sanyal",
} # noqa
assert movie._server._baseurl == utils.SERVER_BASEURL assert movie._server._baseurl == utils.SERVER_BASEURL
assert movie.sessionKey is None assert movie.sessionKey is None
assert movie.studio == 'Nina Paley' assert movie.studio == "Nina Paley"
assert utils.is_string(movie.summary, gte=100) assert utils.is_string(movie.summary, gte=100)
assert movie.tagline == 'The Greatest Break-Up Story Ever Told' assert movie.tagline == "The Greatest Break-Up Story Ever Told"
assert utils.is_thumb(movie.thumb) assert utils.is_thumb(movie.thumb)
assert movie.title == 'Sita Sings the Blues' assert movie.title == "Sita Sings the Blues"
assert movie.titleSort == 'Sita Sings the Blues' assert movie.titleSort == "Sita Sings the Blues"
assert not movie.transcodeSessions assert not movie.transcodeSessions
assert movie.type == 'movie' assert movie.type == "movie"
assert movie.updatedAt > datetime(2017, 1, 1) assert movie.updatedAt > datetime(2017, 1, 1)
assert movie.userRating is None assert movie.userRating is None
assert movie.viewCount == 0 assert movie.viewCount == 0
assert utils.is_int(movie.viewOffset, gte=0) assert utils.is_int(movie.viewOffset, gte=0)
assert movie.viewedAt is None assert movie.viewedAt is None
assert sorted([i.tag for i in movie.writers][:4]) == ['Nina Paley'] # noqa assert sorted([i.tag for i in movie.writers][:4]) == ["Nina Paley"] # noqa
assert movie.year == 2008 assert movie.year == 2008
# Audio # Audio
audio = movie.media[0].parts[0].audioStreams()[0] audio = movie.media[0].parts[0].audioStreams()[0]
@ -218,10 +247,13 @@ def test_video_Movie_attrs(movies):
assert utils.is_int(media.width, gte=200) assert utils.is_int(media.width, gte=200)
# Video # Video
video = movie.media[0].parts[0].videoStreams()[0] video = movie.media[0].parts[0].videoStreams()[0]
assert video.bitDepth in (8, None) # Different versions of Plex Server return different values assert video.bitDepth in (
8,
None,
) # Different versions of Plex Server return different values
assert utils.is_int(video.bitrate) assert utils.is_int(video.bitrate)
assert video.cabac is None assert video.cabac is None
assert video.chromaSubsampling in ('4:2:0', None) assert video.chromaSubsampling in ("4:2:0", None)
assert video.codec in utils.CODECS assert video.codec in utils.CODECS
assert video.codecID is None assert video.codecID is None
assert video.colorSpace is None assert video.colorSpace is None
@ -238,7 +270,7 @@ def test_video_Movie_attrs(movies):
assert utils.is_int(video.level) assert utils.is_int(video.level)
assert video.profile in utils.PROFILES assert video.profile in utils.PROFILES
assert utils.is_int(video.refFrames) assert utils.is_int(video.refFrames)
assert video.scanType in ('progressive', None) assert video.scanType in ("progressive", None)
assert video.selected is False assert video.selected is False
assert video._server._baseurl == utils.SERVER_BASEURL assert video._server._baseurl == utils.SERVER_BASEURL
assert utils.is_int(video.streamType) assert utils.is_int(video.streamType)
@ -262,7 +294,7 @@ def test_video_Movie_attrs(movies):
assert stream1.bitDepth in (8, None) assert stream1.bitDepth in (8, None)
assert utils.is_int(stream1.bitrate) assert utils.is_int(stream1.bitrate)
assert stream1.cabac is None assert stream1.cabac is None
assert stream1.chromaSubsampling in ('4:2:0', None) assert stream1.chromaSubsampling in ("4:2:0", None)
assert stream1.codec in utils.CODECS assert stream1.codec in utils.CODECS
assert stream1.codecID is None assert stream1.codecID is None
assert stream1.colorSpace is None assert stream1.colorSpace is None
@ -279,7 +311,7 @@ def test_video_Movie_attrs(movies):
assert utils.is_int(stream1.level) assert utils.is_int(stream1.level)
assert stream1.profile in utils.PROFILES assert stream1.profile in utils.PROFILES
assert utils.is_int(stream1.refFrames) assert utils.is_int(stream1.refFrames)
assert stream1.scanType in ('progressive', None) assert stream1.scanType in ("progressive", None)
assert stream1.selected is False assert stream1.selected is False
assert stream1._server._baseurl == utils.SERVER_BASEURL assert stream1._server._baseurl == utils.SERVER_BASEURL
assert utils.is_int(stream1.streamType) assert utils.is_int(stream1.streamType)
@ -318,8 +350,107 @@ def test_video_Movie_history(movie):
movie.markUnwatched() movie.markUnwatched()
def test_video_Movie_match(movies):
sectionAgent = movies.agent
sectionAgents = [agent.identifier for agent in movies.agents() if agent.shortIdentifier != 'none']
sectionAgents.remove(sectionAgent)
altAgent = sectionAgents[0]
movie = movies.all()[0]
title = movie.title
year = str(movie.year)
titleUrlEncode = quote_plus(title)
def parse_params(key):
params = key.split('?', 1)[1]
params = params.split("&")
return {x.split("=")[0]: x.split("=")[1] for x in params}
results = movie.matches(title="", year="")
if results:
initpath = results[0]._initpath
assert initpath.startswith(movie.key)
params = initpath.split(movie.key)[1]
parsedParams = parse_params(params)
assert parsedParams.get('manual') == '1'
assert parsedParams.get('title') == ""
assert parsedParams.get('year') == ""
assert parsedParams.get('agent') == sectionAgent
else:
assert len(results) == 0
results = movie.matches(title=title, year="", agent=sectionAgent)
if results:
initpath = results[0]._initpath
assert initpath.startswith(movie.key)
params = initpath.split(movie.key)[1]
parsedParams = parse_params(params)
assert parsedParams.get('manual') == '1'
assert parsedParams.get('title') == titleUrlEncode
assert parsedParams.get('year') == ""
assert parsedParams.get('agent') == sectionAgent
else:
assert len(results) == 0
results = movie.matches(title=title, agent=sectionAgent)
if results:
initpath = results[0]._initpath
assert initpath.startswith(movie.key)
params = initpath.split(movie.key)[1]
parsedParams = parse_params(params)
assert parsedParams.get('manual') == '1'
assert parsedParams.get('title') == titleUrlEncode
assert parsedParams.get('year') == year
assert parsedParams.get('agent') == sectionAgent
else:
assert len(results) == 0
results = movie.matches(title="", year="")
if results:
initpath = results[0]._initpath
assert initpath.startswith(movie.key)
params = initpath.split(movie.key)[1]
parsedParams = parse_params(params)
assert parsedParams.get('manual') == '1'
assert parsedParams.get('agent') == sectionAgent
else:
assert len(results) == 0
results = movie.matches(title="", year="", agent=altAgent)
if results:
initpath = results[0]._initpath
assert initpath.startswith(movie.key)
params = initpath.split(movie.key)[1]
parsedParams = parse_params(params)
assert parsedParams.get('manual') == '1'
assert parsedParams.get('agent') == altAgent
else:
assert len(results) == 0
results = movie.matches(agent=altAgent)
if results:
initpath = results[0]._initpath
assert initpath.startswith(movie.key)
params = initpath.split(movie.key)[1]
parsedParams = parse_params(params)
assert parsedParams.get('manual') == '1'
assert parsedParams.get('agent') == altAgent
else:
assert len(results) == 0
results = movie.matches()
if results:
initpath = results[0]._initpath
assert initpath.startswith(movie.key)
params = initpath.split(movie.key)[1]
parsedParams = parse_params(params)
assert parsedParams.get('manual') == '1'
else:
assert len(results) == 0
def test_video_Show(show): def test_video_Show(show):
assert show.title == 'Game of Thrones' assert show.title == "Game of Thrones"
def test_video_Episode_split(episode, patched_http_call): def test_video_Episode_split(episode, patched_http_call):
@ -335,26 +466,30 @@ def test_video_Episode_updateProgress(episode, patched_http_call):
def test_video_Episode_updateTimeline(episode, patched_http_call): def test_video_Episode_updateTimeline(episode, patched_http_call):
episode.updateTimeline(10 * 60 * 1000, state='playing', duration=episode.duration) # 10 minutes. episode.updateTimeline(
10 * 60 * 1000, state="playing", duration=episode.duration
) # 10 minutes.
def test_video_Episode_stop(episode, mocker, patched_http_call): def test_video_Episode_stop(episode, mocker, patched_http_call):
mocker.patch.object(episode, 'session', return_value=list(mocker.MagicMock(id='hello'))) mocker.patch.object(
episode, "session", return_value=list(mocker.MagicMock(id="hello"))
)
episode.stop(reason="It's past bedtime!") episode.stop(reason="It's past bedtime!")
def test_video_Show_attrs(show): def test_video_Show_attrs(show):
assert utils.is_datetime(show.addedAt) assert utils.is_datetime(show.addedAt)
assert utils.is_metadata(show.art, contains='/art/') assert utils.is_metadata(show.art, contains="/art/")
assert utils.is_metadata(show.banner, contains='/banner/') assert utils.is_metadata(show.banner, contains="/banner/")
assert utils.is_int(show.childCount) assert utils.is_int(show.childCount)
assert show.contentRating in utils.CONTENTRATINGS assert show.contentRating in utils.CONTENTRATINGS
assert utils.is_int(show.duration, gte=1600000) assert utils.is_int(show.duration, gte=1600000)
assert utils.is_section(show._initpath) assert utils.is_section(show._initpath)
# Check reloading the show loads the full list of genres # Check reloading the show loads the full list of genres
assert not {'Adventure', 'Drama'} - {i.tag for i in show.genres} assert not {"Adventure", "Drama"} - {i.tag for i in show.genres}
show.reload() show.reload()
assert sorted([i.tag for i in show.genres]) == ['Adventure', 'Drama', 'Fantasy'] assert sorted([i.tag for i in show.genres]) == ["Adventure", "Drama", "Fantasy"]
# So the initkey should have changed because of the reload # So the initkey should have changed because of the reload
assert utils.is_metadata(show._initpath) assert utils.is_metadata(show._initpath)
assert utils.is_int(show.index) assert utils.is_int(show.index)
@ -362,21 +497,31 @@ def test_video_Show_attrs(show):
if show.lastViewedAt: if show.lastViewedAt:
assert utils.is_datetime(show.lastViewedAt) assert utils.is_datetime(show.lastViewedAt)
assert utils.is_int(show.leafCount) assert utils.is_int(show.leafCount)
assert show.listType == 'video' assert show.listType == "video"
assert len(show.locations[0]) >= 10 assert len(show.locations[0]) >= 10
assert utils.is_datetime(show.originallyAvailableAt) assert utils.is_datetime(show.originallyAvailableAt)
assert show.rating >= 8.0 assert show.rating >= 8.0
assert utils.is_int(show.ratingKey) assert utils.is_int(show.ratingKey)
assert sorted([i.tag for i in show.roles])[:4] == ['Aidan Gillen', 'Aimee Richardson', 'Alexander Siddig', 'Alfie Allen'] # noqa assert sorted([i.tag for i in show.roles])[:4] == [
assert sorted([i.tag for i in show.actors])[:4] == ['Aidan Gillen', 'Aimee Richardson', 'Alexander Siddig', 'Alfie Allen'] # noqa "Aidan Gillen",
"Aimee Richardson",
"Alexander Siddig",
"Alfie Allen",
] # noqa
assert sorted([i.tag for i in show.actors])[:4] == [
"Aidan Gillen",
"Aimee Richardson",
"Alexander Siddig",
"Alfie Allen",
] # noqa
assert show._server._baseurl == utils.SERVER_BASEURL assert show._server._baseurl == utils.SERVER_BASEURL
assert show.studio == 'HBO' assert show.studio == "HBO"
assert utils.is_string(show.summary, gte=100) assert utils.is_string(show.summary, gte=100)
assert utils.is_metadata(show.theme, contains='/theme/') assert utils.is_metadata(show.theme, contains="/theme/")
assert utils.is_metadata(show.thumb, contains='/thumb/') assert utils.is_metadata(show.thumb, contains="/thumb/")
assert show.title == 'Game of Thrones' assert show.title == "Game of Thrones"
assert show.titleSort == 'Game of Thrones' assert show.titleSort == "Game of Thrones"
assert show.type == 'show' assert show.type == "show"
assert utils.is_datetime(show.updatedAt) assert utils.is_datetime(show.updatedAt)
assert utils.is_int(show.viewCount, gte=0) assert utils.is_int(show.viewCount, gte=0)
assert utils.is_int(show.viewedLeafCount, gte=0) assert utils.is_int(show.viewedLeafCount, gte=0)
@ -392,14 +537,14 @@ def test_video_Show_history(show):
def test_video_Show_watched(tvshows): def test_video_Show_watched(tvshows):
show = tvshows.get('The 100') show = tvshows.get("The 100")
show.episodes()[0].markWatched() show.episodes()[0].markWatched()
watched = show.watched() watched = show.watched()
assert len(watched) == 1 and watched[0].title == 'Pilot' assert len(watched) == 1 and watched[0].title == "Pilot"
def test_video_Show_unwatched(tvshows): def test_video_Show_unwatched(tvshows):
show = tvshows.get('The 100') show = tvshows.get("The 100")
episodes = show.episodes() episodes = show.episodes()
episodes[0].markWatched() episodes[0].markWatched()
unwatched = show.unwatched() unwatched = show.unwatched()
@ -409,21 +554,21 @@ def test_video_Show_unwatched(tvshows):
def test_video_Show_location(plex): def test_video_Show_location(plex):
# This should be a part of test test_video_Show_attrs but is excluded # This should be a part of test test_video_Show_attrs but is excluded
# because of https://github.com/mjs7231/python-plexapi/issues/97 # because of https://github.com/mjs7231/python-plexapi/issues/97
show = plex.library.section('TV Shows').get('The 100') show = plex.library.section("TV Shows").get("The 100")
assert len(show.locations) >= 1 assert len(show.locations) >= 1
def test_video_Show_reload(plex): def test_video_Show_reload(plex):
show = plex.library.section('TV Shows').get('Game of Thrones') show = plex.library.section("TV Shows").get("Game of Thrones")
assert utils.is_metadata(show._initpath, prefix='/library/sections/') assert utils.is_metadata(show._initpath, prefix="/library/sections/")
assert len(show.roles) == 3 assert len(show.roles) == 3
show.reload() show.reload()
assert utils.is_metadata(show._initpath, prefix='/library/metadata/') assert utils.is_metadata(show._initpath, prefix="/library/metadata/")
assert len(show.roles) > 3 assert len(show.roles) > 3
def test_video_Show_episodes(tvshows): def test_video_Show_episodes(tvshows):
show = tvshows.get('The 100') show = tvshows.get("The 100")
episodes = show.episodes() episodes = show.episodes()
episodes[0].markWatched() episodes[0].markWatched()
unwatched = show.episodes(viewCount=0) unwatched = show.episodes(viewCount=0)
@ -437,7 +582,7 @@ def test_video_Show_download(monkeydownload, tmpdir, show):
def test_video_Season_download(monkeydownload, tmpdir, show): def test_video_Season_download(monkeydownload, tmpdir, show):
season = show.season('Season 1') season = show.season("Season 1")
filepaths = season.download(savepath=str(tmpdir)) filepaths = season.download(savepath=str(tmpdir))
assert len(filepaths) >= 4 assert len(filepaths) >= 4
@ -445,14 +590,16 @@ def test_video_Season_download(monkeydownload, tmpdir, show):
def test_video_Episode_download(monkeydownload, tmpdir, episode): def test_video_Episode_download(monkeydownload, tmpdir, episode):
f = episode.download(savepath=str(tmpdir)) f = episode.download(savepath=str(tmpdir))
assert len(f) == 1 assert len(f) == 1
with_sceen_size = episode.download(savepath=str(tmpdir), **{'videoResolution': '500x300'}) with_sceen_size = episode.download(
savepath=str(tmpdir), **{"videoResolution": "500x300"}
)
assert len(with_sceen_size) == 1 assert len(with_sceen_size) == 1
def test_video_Show_thumbUrl(show): def test_video_Show_thumbUrl(show):
assert utils.SERVER_BASEURL in show.thumbUrl assert utils.SERVER_BASEURL in show.thumbUrl
assert '/library/metadata/' in show.thumbUrl assert "/library/metadata/" in show.thumbUrl
assert '/thumb/' in show.thumbUrl assert "/thumb/" in show.thumbUrl
# Analyze seems to fail intermittently # Analyze seems to fail intermittently
@ -476,7 +623,7 @@ def test_video_Show_refresh(show):
def test_video_Show_get(show): def test_video_Show_get(show):
assert show.get('Winter Is Coming').title == 'Winter Is Coming' assert show.get("Winter Is Coming").title == "Winter Is Coming"
def test_video_Show_isWatched(show): def test_video_Show_isWatched(show):
@ -485,11 +632,11 @@ def test_video_Show_isWatched(show):
def test_video_Show_section(show): def test_video_Show_section(show):
section = show.section() section = show.section()
assert section.title == 'TV Shows' assert section.title == "TV Shows"
def test_video_Episode(show): def test_video_Episode(show):
episode = show.episode('Winter Is Coming') episode = show.episode("Winter Is Coming")
assert episode == show.episode(season=1, episode=1) assert episode == show.episode(season=1, episode=1)
with pytest.raises(BadRequest): with pytest.raises(BadRequest):
show.episode() show.episode()
@ -507,7 +654,7 @@ def test_video_Episode_history(episode):
# Analyze seems to fail intermittently # Analyze seems to fail intermittently
@pytest.mark.xfail @pytest.mark.xfail
def test_video_Episode_analyze(tvshows): def test_video_Episode_analyze(tvshows):
episode = tvshows.get('Game of Thrones').episode(season=1, episode=1) episode = tvshows.get("Game of Thrones").episode(season=1, episode=1)
episode.analyze() episode.analyze()
@ -515,31 +662,33 @@ def test_video_Episode_attrs(episode):
assert utils.is_datetime(episode.addedAt) assert utils.is_datetime(episode.addedAt)
assert episode.contentRating in utils.CONTENTRATINGS assert episode.contentRating in utils.CONTENTRATINGS
if len(episode.directors): if len(episode.directors):
assert [i.tag for i in episode.directors] == ['Tim Van Patten'] assert [i.tag for i in episode.directors] == ["Tim Van Patten"]
assert utils.is_int(episode.duration, gte=120000) assert utils.is_int(episode.duration, gte=120000)
assert episode.grandparentTitle == 'Game of Thrones' assert episode.grandparentTitle == "Game of Thrones"
assert episode.index == 1 assert episode.index == 1
assert utils.is_metadata(episode._initpath) assert utils.is_metadata(episode._initpath)
assert utils.is_metadata(episode.key) assert utils.is_metadata(episode.key)
assert episode.listType == 'video' assert episode.listType == "video"
assert utils.is_datetime(episode.originallyAvailableAt) assert utils.is_datetime(episode.originallyAvailableAt)
assert utils.is_int(episode.parentIndex) assert utils.is_int(episode.parentIndex)
assert utils.is_metadata(episode.parentKey) assert utils.is_metadata(episode.parentKey)
assert utils.is_int(episode.parentRatingKey) assert utils.is_int(episode.parentRatingKey)
assert utils.is_metadata(episode.parentThumb, contains='/thumb/') assert utils.is_metadata(episode.parentThumb, contains="/thumb/")
assert episode.rating >= 7.7 assert episode.rating >= 7.7
assert utils.is_int(episode.ratingKey) assert utils.is_int(episode.ratingKey)
assert episode._server._baseurl == utils.SERVER_BASEURL assert episode._server._baseurl == utils.SERVER_BASEURL
assert utils.is_string(episode.summary, gte=100) assert utils.is_string(episode.summary, gte=100)
assert utils.is_metadata(episode.thumb, contains='/thumb/') assert utils.is_metadata(episode.thumb, contains="/thumb/")
assert episode.title == 'Winter Is Coming' assert episode.title == "Winter Is Coming"
assert episode.titleSort == 'Winter Is Coming' assert episode.titleSort == "Winter Is Coming"
assert not episode.transcodeSessions assert not episode.transcodeSessions
assert episode.type == 'episode' assert episode.type == "episode"
assert utils.is_datetime(episode.updatedAt) assert utils.is_datetime(episode.updatedAt)
assert utils.is_int(episode.viewCount, gte=0) assert utils.is_int(episode.viewCount, gte=0)
assert episode.viewOffset == 0 assert episode.viewOffset == 0
assert sorted([i.tag for i in episode.writers]) == sorted(['David Benioff', 'D. B. Weiss']) assert sorted([i.tag for i in episode.writers]) == sorted(
["David Benioff", "D. B. Weiss"]
)
assert episode.year == 2011 assert episode.year == 2011
assert episode.isWatched in [True, False] assert episode.isWatched in [True, False]
# Media # Media
@ -577,12 +726,12 @@ def test_video_Episode_attrs(episode):
def test_video_Season(show): def test_video_Season(show):
seasons = show.seasons() seasons = show.seasons()
assert len(seasons) == 2 assert len(seasons) == 2
assert ['Season 1', 'Season 2'] == [s.title for s in seasons[:2]] assert ["Season 1", "Season 2"] == [s.title for s in seasons[:2]]
assert show.season('Season 1') == seasons[0] assert show.season("Season 1") == seasons[0]
def test_video_Season_history(show): def test_video_Season_history(show):
season = show.season('Season 1') season = show.season("Season 1")
season.markWatched() season.markWatched()
history = season.history() history = season.history()
assert len(history) assert len(history)
@ -590,7 +739,7 @@ def test_video_Season_history(show):
def test_video_Season_attrs(show): def test_video_Season_attrs(show):
season = show.season('Season 1') season = show.season("Season 1")
assert utils.is_datetime(season.addedAt) assert utils.is_datetime(season.addedAt)
assert season.index == 1 assert season.index == 1
assert utils.is_metadata(season._initpath) assert utils.is_metadata(season._initpath)
@ -598,17 +747,17 @@ def test_video_Season_attrs(show):
if season.lastViewedAt: if season.lastViewedAt:
assert utils.is_datetime(season.lastViewedAt) assert utils.is_datetime(season.lastViewedAt)
assert utils.is_int(season.leafCount, gte=3) assert utils.is_int(season.leafCount, gte=3)
assert season.listType == 'video' assert season.listType == "video"
assert utils.is_metadata(season.parentKey) assert utils.is_metadata(season.parentKey)
assert utils.is_int(season.parentRatingKey) assert utils.is_int(season.parentRatingKey)
assert season.parentTitle == 'Game of Thrones' assert season.parentTitle == "Game of Thrones"
assert utils.is_int(season.ratingKey) assert utils.is_int(season.ratingKey)
assert season._server._baseurl == utils.SERVER_BASEURL assert season._server._baseurl == utils.SERVER_BASEURL
assert season.summary == '' assert season.summary == ""
assert utils.is_metadata(season.thumb, contains='/thumb/') assert utils.is_metadata(season.thumb, contains="/thumb/")
assert season.title == 'Season 1' assert season.title == "Season 1"
assert season.titleSort == 'Season 1' assert season.titleSort == "Season 1"
assert season.type == 'season' assert season.type == "season"
assert utils.is_datetime(season.updatedAt) assert utils.is_datetime(season.updatedAt)
assert utils.is_int(season.viewCount, gte=0) assert utils.is_int(season.viewCount, gte=0)
assert utils.is_int(season.viewedLeafCount, gte=0) assert utils.is_int(season.viewedLeafCount, gte=0)
@ -617,34 +766,34 @@ def test_video_Season_attrs(show):
def test_video_Season_show(show): def test_video_Season_show(show):
season = show.seasons()[0] season = show.seasons()[0]
season_by_name = show.season('Season 1') season_by_name = show.season("Season 1")
assert show.ratingKey == season.parentRatingKey and season_by_name.parentRatingKey assert show.ratingKey == season.parentRatingKey and season_by_name.parentRatingKey
assert season.ratingKey == season_by_name.ratingKey assert season.ratingKey == season_by_name.ratingKey
def test_video_Season_watched(tvshows): def test_video_Season_watched(tvshows):
show = tvshows.get('Game of Thrones') show = tvshows.get("Game of Thrones")
season = show.season(1) season = show.season(1)
sne = show.season('Season 1') sne = show.season("Season 1")
assert season == sne assert season == sne
season.markWatched() season.markWatched()
assert season.isWatched assert season.isWatched
def test_video_Season_unwatched(tvshows): def test_video_Season_unwatched(tvshows):
season = tvshows.get('Game of Thrones').season(1) season = tvshows.get("Game of Thrones").season(1)
season.markUnwatched() season.markUnwatched()
assert not season.isWatched assert not season.isWatched
def test_video_Season_get(show): def test_video_Season_get(show):
episode = show.season(1).get('Winter Is Coming') episode = show.season(1).get("Winter Is Coming")
assert episode.title == 'Winter Is Coming' assert episode.title == "Winter Is Coming"
def test_video_Season_episode(show): def test_video_Season_episode(show):
episode = show.season(1).get('Winter Is Coming') episode = show.season(1).get("Winter Is Coming")
assert episode.title == 'Winter Is Coming' assert episode.title == "Winter Is Coming"
def test_video_Season_episode_by_index(show): def test_video_Season_episode_by_index(show):
@ -659,34 +808,62 @@ def test_video_Season_episodes(show):
def test_that_reload_return_the_same_object(plex): def test_that_reload_return_the_same_object(plex):
# we want to check this that all the urls are correct # we want to check this that all the urls are correct
movie_library_search = plex.library.section('Movies').search('Elephants Dream')[0] movie_library_search = plex.library.section("Movies").search("Elephants Dream")[0]
movie_search = plex.search('Elephants Dream')[0] movie_search = plex.search("Elephants Dream")[0]
movie_section_get = plex.library.section('Movies').get('Elephants Dream') movie_section_get = plex.library.section("Movies").get("Elephants Dream")
movie_library_search_key = movie_library_search.key movie_library_search_key = movie_library_search.key
movie_search_key = movie_search.key movie_search_key = movie_search.key
movie_section_get_key = movie_section_get.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 # noqa assert (
tvshow_library_search = plex.library.section('TV Shows').search('The 100')[0] movie_library_search_key
tvshow_search = plex.search('The 100')[0] == movie_library_search.reload().key
tvshow_section_get = plex.library.section('TV Shows').get('The 100') == movie_search_key
== movie_search.reload().key
== movie_section_get_key
== movie_section_get.reload().key
) # noqa
tvshow_library_search = plex.library.section("TV Shows").search("The 100")[0]
tvshow_search = plex.search("The 100")[0]
tvshow_section_get = plex.library.section("TV Shows").get("The 100")
tvshow_library_search_key = tvshow_library_search.key tvshow_library_search_key = tvshow_library_search.key
tvshow_search_key = tvshow_search.key tvshow_search_key = tvshow_search.key
tvshow_section_get_key = tvshow_section_get.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 # noqa 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
) # noqa
season_library_search = tvshow_library_search.season(1) season_library_search = tvshow_library_search.season(1)
season_search = tvshow_search.season(1) season_search = tvshow_search.season(1)
season_section_get = tvshow_section_get.season(1) season_section_get = tvshow_section_get.season(1)
season_library_search_key = season_library_search.key season_library_search_key = season_library_search.key
season_search_key = season_search.key season_search_key = season_search.key
season_section_get_key = season_section_get.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 # noqa 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
) # noqa
episode_library_search = tvshow_library_search.episode(season=1, episode=1) episode_library_search = tvshow_library_search.episode(season=1, episode=1)
episode_search = tvshow_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_section_get = tvshow_section_get.episode(season=1, episode=1)
episode_library_search_key = episode_library_search.key episode_library_search_key = episode_library_search.key
episode_search_key = episode_search.key episode_search_key = episode_search.key
episode_section_get_key = episode_section_get.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 # noqa 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
) # noqa
def test_video_exists_accessible(movie, episode): def test_video_exists_accessible(movie, episode):
@ -703,7 +880,9 @@ def test_video_exists_accessible(movie, episode):
assert episode.media[0].parts[0].accessible is True assert episode.media[0].parts[0].accessible is True
@pytest.mark.skip(reason='broken? assert len(plex.conversions()) == 1 may fail on some builds') @pytest.mark.skip(
reason="broken? assert len(plex.conversions()) == 1 may fail on some builds"
)
def test_video_optimize(movie, plex): def test_video_optimize(movie, plex):
plex.optimizedItems(removeAll=True) plex.optimizedItems(removeAll=True)
movie.optimize(targetTagID=1) movie.optimize(targetTagID=1)

View file

@ -1,55 +1,232 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
The script is used to bootstrap a docker container with Plex and with The script is used to bootstrap a the test enviroment for plexapi
all the libraries required for testing. with all the libraries required for testing.
By default this uses a docker.
It can be used manually using:
python plex-bootraptest.py --no-docker --server-name name_of_server --account Hellowlol --password yourpassword
""" """
import argparse import argparse
import os import os
import plexapi import shutil
import socket import socket
import time import time
import zipfile
from glob import glob from glob import glob
from shutil import copyfile, rmtree from os import makedirs
from shutil import copyfile, which
from subprocess import call from subprocess import call
from tqdm import tqdm
from uuid import uuid4 from uuid import uuid4
from plexapi.compat import which, makedirs
import plexapi
from plexapi.exceptions import BadRequest, NotFound from plexapi.exceptions import BadRequest, NotFound
from plexapi.myplex import MyPlexAccount from plexapi.myplex import MyPlexAccount
from plexapi.server import PlexServer from plexapi.server import PlexServer
from plexapi.utils import download, SEARCHTYPES from plexapi.utils import SEARCHTYPES
from tqdm import tqdm
DOCKER_CMD = [ DOCKER_CMD = [
'docker', 'run', '-d', "docker",
'--name', 'plex-test-%(container_name_extra)s%(image_tag)s', "run",
'--restart', 'on-failure', "-d",
'-p', '32400:32400/tcp', "--name",
'-p', '3005:3005/tcp', "plex-test-%(container_name_extra)s%(image_tag)s",
'-p', '8324:8324/tcp', "--restart",
'-p', '32469:32469/tcp', "on-failure",
'-p', '1900:1900/udp', "-p",
'-p', '32410:32410/udp', "32400:32400/tcp",
'-p', '32412:32412/udp', "-p",
'-p', '32413:32413/udp', "3005:3005/tcp",
'-p', '32414:32414/udp', "-p",
'-e', 'TZ="Europe/London"', "8324:8324/tcp",
'-e', 'PLEX_CLAIM=%(claim_token)s', "-p",
'-e', 'ADVERTISE_IP=http://%(advertise_ip)s:32400/', "32469:32469/tcp",
'-h', '%(hostname)s', "-p",
'-e', 'TZ="%(timezone)s"', "1900:1900/udp",
'-v', '%(destination)s/db:/config', "-p",
'-v', '%(destination)s/transcode:/transcode', "32410:32410/udp",
'-v', '%(destination)s/media:/data', "-p",
'plexinc/pms-docker:%(image_tag)s' "32412:32412/udp",
"-p",
"32413:32413/udp",
"-p",
"32414:32414/udp",
"-e",
'TZ="Europe/London"',
"-e",
"PLEX_CLAIM=%(claim_token)s",
"-e",
"ADVERTISE_IP=http://%(advertise_ip)s:32400/",
"-h",
"%(hostname)s",
"-e",
'TZ="%(timezone)s"',
"-v",
"%(destination)s/db:/config",
"-v",
"%(destination)s/transcode:/transcode",
"-v",
"%(destination)s/media:/data",
"plexinc/pms-docker:%(image_tag)s",
] ]
BASE_DIR_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STUB_MOVIE_PATH = os.path.join(BASE_DIR_PATH, "tests", "data", "video_stub.mp4")
STUB_MP3_PATH = os.path.join(BASE_DIR_PATH, "tests", "data", "audio_stub.mp3")
STUB_IMAGE_PATH = os.path.join(BASE_DIR_PATH, "tests", "data", "cute_cat.jpg")
def check_ext(path, ext):
"""I hate glob so much."""
result = []
for root, dirs, fil in os.walk(path):
for f in fil:
fp = os.path.join(root, f)
if fp.endswith(ext):
result.append(fp)
return result
class ExistingSection(Exception):
"""This server has sections, exiting"""
def __init__(self, *args):
raise SystemExit("This server has sections exiting")
def clean_pms(server, path):
for section in server.library.sections():
print("Deleting %s" % section.title)
section.delete()
server.library.cleanBundles()
server.library.optimize()
print("optimized db and removed any bundles")
shutil.rmtree(path, ignore_errors=False, onerror=None)
print("Deleted %s" % path)
def setup_music(music_path):
print("Setup files for music section..")
makedirs(music_path, exist_ok=True)
all_music = {
"Broke for free": {
"Layers": [
"1 - As Colorful As Ever.mp3",
#"02 - Knock Knock.mp3",
#"03 - Only Knows.mp3",
#"04 - If.mp3",
#"05 - Note Drop.mp3",
#"06 - Murmur.mp3",
#"07 - Spellbound.mp3",
#"08 - The Collector.mp3",
#"09 - Quit Bitching.mp3",
#"10 - A Year.mp3",
]
},
}
for artist, album in all_music.items():
for k, v in album.items():
artist_album = os.path.join(music_path, artist, k)
makedirs(artist_album, exist_ok=True)
for song in v:
copyfile(STUB_MP3_PATH, os.path.join(artist_album, song))
return len(check_ext(music_path, (".mp3")))
def setup_movies(movies_path):
print("Setup files for the Movies section..")
makedirs(movies_path, exist_ok=True)
if len(glob(movies_path + "/*.mkv", recursive=True)) == 4:
return 4
required_movies = {
"Elephants Dream": 2006,
"Sita Sings the Blues": 2008,
"Big Buck Bunny": 2008,
"Sintel": 2010,
}
expected_media_count = 0
for name, year in required_movies.items():
expected_media_count += 1
if not os.path.isfile(get_movie_path(movies_path, name, year)):
copyfile(STUB_MOVIE_PATH, get_movie_path(movies_path, name, year))
return expected_media_count
def setup_images(photos_path):
print("Setup files for the Photos section..")
makedirs(photos_path, exist_ok=True)
# expected_photo_count = 0
folders = {
("Cats",): 3,
("Cats", "Cats in bed"): 7,
("Cats", "Cats not in bed"): 1,
("Cats", "Not cats in bed"): 1,
}
has_photos = 0
for folder_path, required_cnt in folders.items():
folder_path = os.path.join(photos_path, *folder_path)
makedirs(folder_path, exist_ok=True)
photos_in_folder = len(glob(os.path.join(folder_path, "/*.jpg")))
while photos_in_folder < required_cnt:
# Dunno why this is need got permission error on photo0.jpg
photos_in_folder += 1
full_path = os.path.join(folder_path, "photo%d.jpg" % photos_in_folder)
copyfile(STUB_IMAGE_PATH, full_path)
has_photos += photos_in_folder
return len(check_ext(photos_path, (".jpg")))
def setup_show(tvshows_path):
print("Setup files for the TV-Shows section..")
makedirs(tvshows_path, exist_ok=True)
makedirs(os.path.join(tvshows_path, "Game of Thrones"), exist_ok=True)
makedirs(os.path.join(tvshows_path, "The 100"), exist_ok=True)
required_tv_shows = {
"Game of Thrones": [list(range(1, 11)), list(range(1, 11))],
"The 100": [list(range(1, 14)), list(range(1, 17))],
}
expected_media_count = 0
for show_name, seasons in required_tv_shows.items():
for season_id, episodes in enumerate(seasons, start=1):
for episode_id in episodes:
expected_media_count += 1
episode_path = get_tvshow_path(
tvshows_path, show_name, season_id, episode_id
)
if not os.path.isfile(episode_path):
copyfile(STUB_MOVIE_PATH, episode_path)
return expected_media_count
def get_default_ip(): def get_default_ip():
""" Return the first IP address of the current machine if available. """ """ Return the first IP address of the current machine if available. """
available_ips = list(set([i[4][0] for i in socket.getaddrinfo(socket.gethostname(), None) available_ips = list(
if i[4][0] not in ('127.0.0.1', '::1') and not i[4][0].startswith('fe80:')])) set(
[
i[4][0]
for i in socket.getaddrinfo(socket.gethostname(), None)
if i[4][0] not in ("127.0.0.1", "::1")
and not i[4][0].startswith("fe80:")
]
)
)
return available_ips[0] if len(available_ips) else None return available_ips[0] if len(available_ips) else None
@ -62,14 +239,14 @@ def get_plex_account(opts):
return None return None
def get_movie_path(name, year): def get_movie_path(movies_path, name, year):
""" Return a movie path given its title and year. """ """ Return a movie path given its title and year. """
return os.path.join(movies_path, '%s (%d).mp4' % (name, year)) return os.path.join(movies_path, "%s (%d).mp4" % (name, year))
def get_tvshow_path(name, season, episode): def get_tvshow_path(tvshows_path, name, season, episode):
""" Return a TV show path given its title, season, and episode. """ """ Return a TV show path given its title, season, and episode. """
return os.path.join(tvshows_path, name, 'S%02dE%02d.mp4' % (season, episode)) return os.path.join(tvshows_path, name, "S%02dE%02d.mp4" % (season, episode))
def add_library_section(server, section): def add_library_section(server, section):
@ -83,52 +260,63 @@ def add_library_section(server, section):
server.library.add(**section) server.library.add(**section)
return True return True
except BadRequest as err: except BadRequest as err:
if 'server is still starting up. Please retry later' in str(err): if "server is still starting up. Please retry later" in str(err):
time.sleep(1) time.sleep(1)
continue continue
raise raise
runtime = time.time() - start runtime = time.time() - start
raise SystemExit('Timeout adding section to Plex instance.') raise SystemExit("Timeout adding section to Plex instance.")
def create_section(server, section, opts): def create_section(server, section, opts):
processed_media = 0 processed_media = 0
expected_media_count = section.pop('expected_media_count', 0) expected_media_count = section.pop("expected_media_count", 0)
expected_media_type = (section['type'], ) expected_media_type = (section["type"],)
if section['type'] == 'artist': if section["type"] == "artist":
expected_media_type = ('artist', 'album', 'track') expected_media_type = ("artist", "album", "track")
expected_media_type = tuple(SEARCHTYPES[t] for t in expected_media_type) expected_media_type = tuple(SEARCHTYPES[t] for t in expected_media_type)
def alert_callback(data): def alert_callback(data):
""" Listen to the Plex notifier to determine when metadata scanning is complete. """ """ Listen to the Plex notifier to determine when metadata scanning is complete. """
global processed_media global processed_media
if data['type'] == 'timeline': if data["type"] == "timeline":
for entry in data['TimelineEntry']: for entry in data["TimelineEntry"]:
if entry.get('identifier', 'com.plexapp.plugins.library') == 'com.plexapp.plugins.library': if (
entry.get("identifier", "com.plexapp.plugins.library")
== "com.plexapp.plugins.library"
):
# Missed mediaState means that media was processed (analyzed & thumbnailed) # Missed mediaState means that media was processed (analyzed & thumbnailed)
if 'mediaState' not in entry and entry['type'] in expected_media_type: if (
"mediaState" not in entry
and entry["type"] in expected_media_type
):
# state=5 means record processed, applicable only when metadata source was set # state=5 means record processed, applicable only when metadata source was set
if entry['state'] == 5: if entry["state"] == 5:
cnt = 1 cnt = 1
if entry['type'] == SEARCHTYPES['show']: if entry["type"] == SEARCHTYPES["show"]:
show = server.library.sectionByID(str(entry['sectionID'])).get(entry['title']) show = server.library.sectionByID(
str(entry["sectionID"])
).get(entry["title"])
cnt = show.leafCount cnt = show.leafCount
bar.update(cnt) bar.update(cnt)
processed_media += cnt processed_media += cnt
# state=1 means record processed, when no metadata source was set # state=1 means record processed, when no metadata source was set
elif entry['state'] == 1 and entry['type'] == SEARCHTYPES['photo']: elif (
entry["state"] == 1
and entry["type"] == SEARCHTYPES["photo"]
):
bar.update() bar.update()
processed_media += 1 processed_media += 1
runtime = 0 runtime = 0
start = time.time() start = time.time()
bar = tqdm(desc='Scanning section ' + section['name'], total=expected_media_count) bar = tqdm(desc="Scanning section " + section["name"], total=expected_media_count)
notifier = server.startAlertListener(alert_callback) notifier = server.startAlertListener(alert_callback)
time.sleep(3) time.sleep(3)
add_library_section(server, section) add_library_section(server, section)
while bar.n < bar.total: while bar.n < bar.total:
if runtime >= 120: if runtime >= 120:
print('Metadata scan taking too long, but will continue anyway..') print("Metadata scan taking too long, but will continue anyway..")
break break
time.sleep(3) time.sleep(3)
runtime = int(time.time() - start) runtime = int(time.time() - start)
@ -136,60 +324,134 @@ def create_section(server, section, opts):
notifier.stop() notifier.stop()
if __name__ == '__main__': if __name__ == "__main__":
default_ip = get_default_ip() default_ip = get_default_ip()
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
# Authentication arguments # Authentication arguments
mg = parser.add_mutually_exclusive_group() mg = parser.add_mutually_exclusive_group()
g = mg.add_argument_group() g = mg.add_argument_group()
g.add_argument('--username', help='Your Plex username') g.add_argument("--username", help="Your Plex username")
g.add_argument('--password', help='Your Plex password') g.add_argument("--password", help="Your Plex password")
mg.add_argument('--token', help='Plex.tv authentication token', default=plexapi.CONFIG.get('auth.server_token')) mg.add_argument(
mg.add_argument('--unclaimed', help='Do not claim the server', default=False, action='store_true') "--token",
help="Plex.tv authentication token",
default=plexapi.CONFIG.get("auth.server_token"),
)
mg.add_argument(
"--unclaimed",
help="Do not claim the server",
default=False,
action="store_true",
)
# Test environment arguments # Test environment arguments
parser.add_argument('--timezone', help='Timezone to set inside plex', default='UTC') # noqa parser.add_argument(
parser.add_argument('--destination', help='Local path where to store all the media', default=os.path.join(os.getcwd(), 'plex')) # noqa "--no-docker", help="Use docker", default=False, action="store_true"
parser.add_argument('--advertise-ip', help='IP address which should be advertised by new Plex instance', required=default_ip is None, default=default_ip) # noqa )
parser.add_argument('--docker-tag', help='Docker image tag to install', default='latest') # noqa parser.add_argument(
parser.add_argument('--bootstrap-timeout', help='Timeout for each step of bootstrap, in seconds (default: %(default)s)', default=180, type=int) # noqa "--timezone", help="Timezone to set inside plex", default="UTC"
parser.add_argument('--server-name', help='Name for the new server', default='plex-test-docker-%s' % str(uuid4())) # noqa ) # noqa
parser.add_argument('--accept-eula', help='Accept Plex`s EULA', default=False, action='store_true') # noqa parser.add_argument(
parser.add_argument('--without-movies', help='Do not create Movies section', default=True, dest='with_movies', action='store_false') # noqa "--destination",
parser.add_argument('--without-shows', help='Do not create TV Shows section', default=True, dest='with_shows', action='store_false') # noqa help="Local path where to store all the media",
parser.add_argument('--without-music', help='Do not create Music section', default=True, dest='with_music', action='store_false') # noqa default=os.path.join(os.getcwd(), "plex"),
parser.add_argument('--without-photos', help='Do not create Photos section', default=True, dest='with_photos', action='store_false') # noqa ) # noqa
parser.add_argument('--show-token', help='Display access token after bootstrap', default=False, action='store_true') # noqa parser.add_argument(
"--advertise-ip",
help="IP address which should be advertised by new Plex instance",
required=default_ip is None,
default=default_ip,
) # noqa
parser.add_argument(
"--docker-tag", help="Docker image tag to install", default="latest"
) # noqa
parser.add_argument(
"--bootstrap-timeout",
help="Timeout for each step of bootstrap, in seconds (default: %(default)s)",
default=180,
type=int,
) # noqa
parser.add_argument(
"--server-name",
help="Name for the new server",
default="plex-test-docker-%s" % str(uuid4()),
) # noqa
parser.add_argument(
"--accept-eula", help="Accept Plex`s EULA", default=False, action="store_true"
) # noqa
parser.add_argument(
"--without-movies",
help="Do not create Movies section",
default=True,
dest="with_movies",
action="store_false",
) # noqa
parser.add_argument(
"--without-shows",
help="Do not create TV Shows section",
default=True,
dest="with_shows",
action="store_false",
) # noqa
parser.add_argument(
"--without-music",
help="Do not create Music section",
default=True,
dest="with_music",
action="store_false",
) # noqa
parser.add_argument(
"--without-photos",
help="Do not create Photos section",
default=True,
dest="with_photos",
action="store_false",
) # noqa
parser.add_argument(
"--show-token",
help="Display access token after bootstrap",
default=False,
action="store_true",
) # noqa
opts = parser.parse_args() opts = parser.parse_args()
# Download the Plex Docker image
print('Creating Plex instance named %s with advertised ip %s' % (opts.server_name, opts.advertise_ip))
if which('docker') is None:
print('Docker is required to be available')
exit(1)
if call(['docker', 'pull', 'plexinc/pms-docker:%s' % opts.docker_tag]) != 0:
print('Got an error when executing docker pull!')
exit(1)
# Start the Plex Docker container
account = get_plex_account(opts) account = get_plex_account(opts)
path = os.path.realpath(os.path.expanduser(opts.destination)) path = os.path.realpath(os.path.expanduser(opts.destination))
makedirs(os.path.join(path, 'media'), exist_ok=True) media_path = os.path.join(path, "media")
arg_bindings = { makedirs(media_path, exist_ok=True)
'destination': path,
'hostname': opts.server_name, # Download the Plex Docker image
'claim_token': account.claimToken() if account else '', if opts.no_docker is False:
'timezone': opts.timezone, print(
'advertise_ip': opts.advertise_ip, "Creating Plex instance named %s with advertised ip %s"
'image_tag': opts.docker_tag, % (opts.server_name, opts.advertise_ip)
'container_name_extra': '' if account else 'unclaimed-' )
} if which("docker") is None:
docker_cmd = [c % arg_bindings for c in DOCKER_CMD] print("Docker is required to be available")
exit_code = call(docker_cmd) exit(1)
if exit_code != 0: if call(["docker", "pull", "plexinc/pms-docker:%s" % opts.docker_tag]) != 0:
raise SystemExit('Error %s while starting the Plex docker container' % exit_code) print("Got an error when executing docker pull!")
exit(1)
# Start the Plex Docker container
arg_bindings = {
"destination": path,
"hostname": opts.server_name,
"claim_token": account.claimToken() if account else "",
"timezone": opts.timezone,
"advertise_ip": opts.advertise_ip,
"image_tag": opts.docker_tag,
"container_name_extra": "" if account else "unclaimed-",
}
docker_cmd = [c % arg_bindings for c in DOCKER_CMD]
exit_code = call(docker_cmd)
if exit_code != 0:
raise SystemExit(
"Error %s while starting the Plex docker container" % exit_code
)
# Wait for the Plex container to start # Wait for the Plex container to start
print('Waiting for the Plex container to start..') print("Waiting for the Plex to start..")
start = time.time() start = time.time()
runtime = 0 runtime = 0
server = None server = None
@ -198,145 +460,127 @@ if __name__ == '__main__':
if account: if account:
server = account.device(opts.server_name).connect() server = account.device(opts.server_name).connect()
else: else:
server = PlexServer('http://%s:32400' % opts.advertise_ip) server = PlexServer("http://%s:32400" % opts.advertise_ip)
if opts.accept_eula: if opts.accept_eula:
server.settings.get('acceptedEULA').set(True) server.settings.get("acceptedEULA").set(True)
server.settings.save() server.settings.save()
except KeyboardInterrupt:
break
except Exception as err: except Exception as err:
print(err) print(err)
time.sleep(1) time.sleep(1)
runtime = time.time() - start runtime = time.time() - start
if not server: if not server:
raise SystemExit('Server didnt appear in your account after %ss' % opts.bootstrap_timeout) raise SystemExit(
print('Plex container started after %ss, downloading content' % int(runtime)) "Server didnt appear in your account after %ss" % opts.bootstrap_timeout
)
# Download video_stub.mp4 print("Plex container started after %ss, setting up content" % int(runtime))
print('Downloading video_stub.mp4..')
if opts.with_movies or opts.with_shows:
media_stub_path = os.path.join(path, 'media', 'video_stub.mp4')
if not os.path.isfile(media_stub_path):
download('http://www.mytvtestpatterns.com/mytvtestpatterns/Default/GetFile?p=PhilipsCircleMP4', '',
filename='video_stub.mp4', savepath=os.path.join(path, 'media'), showstatus=True)
sections = [] sections = []
# Lets add a check here do somebody dont mess up
# there normal server if they run manual tests.
# Like i did....
if len(server.library.sections()) and opts.no_docker is True:
ans = input(
"The server has %s sections, do you wish to remove it?\n> "
% len(server.library.sections())
)
if ans in ("y", "Y", "Yes"):
ans = input(
"Are you really sure you want to delete %s libraries? There is no way back\n> "
% len(server.library.sections())
)
if ans in ("y", "Y", "Yes"):
clean_pms(server, path)
else:
raise ExistingSection()
else:
raise ExistingSection()
# Prepare Movies section # Prepare Movies section
if opts.with_movies: if opts.with_movies:
print('Preparing movie section..') movies_path = os.path.join(media_path, "Movies")
movies_path = os.path.join(path, 'media', 'Movies') num_movies = setup_movies(movies_path)
makedirs(movies_path, exist_ok=True) sections.append(
required_movies = { dict(
'Elephants Dream': 2006, name="Movies",
'Sita Sings the Blues': 2008, type="movie",
'Big Buck Bunny': 2008, location="/data/Movies" if opts.no_docker is False else movies_path,
'Sintel': 2010, agent="com.plexapp.agents.imdb",
} scanner="Plex Movie Scanner",
expected_media_count = 0 expected_media_count=num_movies,
for name, year in required_movies.items(): )
expected_media_count += 1 )
if not os.path.isfile(get_movie_path(name, year)):
copyfile(media_stub_path, get_movie_path(name, year))
sections.append(dict(name='Movies', type='movie', location='/data/Movies', agent='com.plexapp.agents.imdb',
scanner='Plex Movie Scanner', expected_media_count=expected_media_count))
# Prepare TV Show section # Prepare TV Show section
if opts.with_shows: if opts.with_shows:
print('Preparing TV-Shows section..') tvshows_path = os.path.join(media_path, "TV-Shows")
tvshows_path = os.path.join(path, 'media', 'TV-Shows') num_ep = setup_show(tvshows_path)
makedirs(os.path.join(tvshows_path, 'Game of Thrones'), exist_ok=True)
makedirs(os.path.join(tvshows_path, 'The 100'), exist_ok=True) sections.append(
required_tv_shows = { dict(
'Game of Thrones': [list(range(1, 11)), list(range(1, 11))], name="TV Shows",
'The 100': [list(range(1, 14)), list(range(1, 17))] type="show",
} location="/data/TV-Shows" if opts.no_docker is False else tvshows_path,
expected_media_count = 0 agent="com.plexapp.agents.thetvdb",
for show_name, seasons in required_tv_shows.items(): scanner="Plex Series Scanner",
for season_id, episodes in enumerate(seasons, start=1): expected_media_count=num_ep,
for episode_id in episodes: )
expected_media_count += 1 )
episode_path = get_tvshow_path(show_name, season_id, episode_id)
if not os.path.isfile(episode_path):
copyfile(get_movie_path('Sintel', 2010), episode_path)
sections.append(dict(name='TV Shows', type='show', location='/data/TV-Shows',
agent='com.plexapp.agents.thetvdb', scanner='Plex Series Scanner',
expected_media_count=expected_media_count))
# Prepare Music section # Prepare Music section
if opts.with_music: if opts.with_music:
print('Preparing Music section..') music_path = os.path.join(media_path, "Music")
music_path = os.path.join(path, 'media', 'Music') song_c = setup_music(music_path)
makedirs(music_path, exist_ok=True)
expected_media_count = 0 sections.append(
artist_dst = os.path.join(music_path, 'Infinite State') dict(
dest_path = os.path.join(artist_dst, 'Unmastered Impulses') name="Music",
if not os.path.isdir(dest_path): type="artist",
zip_path = os.path.join(artist_dst, 'Unmastered Impulses.zip') location="/data/Music" if opts.no_docker is False else music_path,
if os.path.isfile(zip_path): agent="com.plexapp.agents.lastfm",
with zipfile.ZipFile(zip_path, 'r') as handle: scanner="Plex Music Scanner",
handle.extractall(artist_dst) expected_media_count=song_c,
else: )
download('https://github.com/kennethreitz/unmastered-impulses/archive/master.zip', '', )
filename='Unmastered Impulses.zip', savepath=artist_dst, unpack=True, showstatus=True)
os.rename(os.path.join(artist_dst, 'unmastered-impulses-master', 'mp3'), dest_path)
rmtree(os.path.join(artist_dst, 'unmastered-impulses-master'))
expected_media_count += len(glob(os.path.join(dest_path, '*.mp3'))) + 2 # wait for artist & album
artist_dst = os.path.join(music_path, 'Broke For Free')
dest_path = os.path.join(artist_dst, 'Layers')
if not os.path.isdir(dest_path):
zip_path = os.path.join(artist_dst, 'Layers.zip')
if not os.path.isfile(zip_path):
download('https://archive.org/compress/Layers-11520/formats=VBR%20MP3&file=/Layers-11520.zip', '',
filename='Layers.zip', savepath=artist_dst, showstatus=True)
makedirs(dest_path, exist_ok=True)
with zipfile.ZipFile(zip_path, 'r') as handle:
handle.extractall(dest_path)
expected_media_count += len(glob(os.path.join(dest_path, '*.mp3'))) + 2 # wait for artist & album
sections.append(dict(name='Music', type='artist', location='/data/Music',
agent='com.plexapp.agents.lastfm', scanner='Plex Music Scanner',
expected_media_count=expected_media_count))
# Prepare Photos section # Prepare Photos section
if opts.with_photos: if opts.with_photos:
print('Preparing Photos section..') photos_path = os.path.join(media_path, "Photos")
photos_path = os.path.join(path, 'media', 'Photos') has_photos = setup_images(photos_path)
makedirs(photos_path, exist_ok=True)
expected_photo_count = 0 sections.append(
folders = { dict(
('Cats', ): 3, name="Photos",
('Cats', 'Cats in bed'): 7, type="photo",
('Cats', 'Cats not in bed'): 1, location="/data/Photos" if opts.no_docker is False else photos_path,
('Cats', 'Not cats in bed'): 1, agent="com.plexapp.agents.none",
} scanner="Plex Photo Scanner",
has_photos = 0 expected_media_count=has_photos,
for folder_path, required_cnt in folders.items(): )
folder_path = os.path.join(photos_path, *folder_path) )
photos_in_folder = len(glob(os.path.join(folder_path, '*.jpg')))
while photos_in_folder < required_cnt:
photos_in_folder += 1
download('https://picsum.photos/800/600/?random', '',
filename='photo%d.jpg' % photos_in_folder, savepath=folder_path)
has_photos += photos_in_folder
sections.append(dict(name='Photos', type='photo', location='/data/Photos',
agent='com.plexapp.agents.none', scanner='Plex Photo Scanner',
expected_media_count=has_photos))
# Create the Plex library in our instance # Create the Plex library in our instance
if sections: if sections:
print('Creating the Plex libraries in our instance') print("Creating the Plex libraries on %s" % server.friendlyName)
for section in sections: for section in sections:
create_section(server, section, opts) create_section(server, section, opts)
# Share this instance with the specified username # Share this instance with the specified username
if account: if account:
shared_username = os.environ.get('SHARED_USERNAME', 'PKKid') shared_username = os.environ.get("SHARED_USERNAME", "PKKid")
try: try:
user = account.user(shared_username) user = account.user(shared_username)
account.updateFriend(user, server) account.updateFriend(user, server)
print('The server was shared with user %s' % shared_username) print("The server was shared with user %s" % shared_username)
except NotFound: except NotFound:
pass pass
# Finished: Display our Plex details # Finished: Display our Plex details
print('Base URL is %s' % server.url('', False)) print("Base URL is %s" % server.url("", False))
if account and opts.show_token: if account and opts.show_token:
print('Auth token is %s' % account.authenticationToken) print("Auth token is %s" % account.authenticationToken)
print('Server %s is ready to use!' % opts.server_name) print("Server %s is ready to use!" % opts.server_name)

View file

@ -10,10 +10,9 @@ Original contribution by lad1337.
import argparse import argparse
import os import os
import re import re
import shutil from urllib.parse import unquote
from plexapi import utils from plexapi import utils
from plexapi.compat import unquote
from plexapi.video import Episode, Movie, Show from plexapi.video import Episode, Movie, Show
VALID_TYPES = (Movie, Episode, Show) VALID_TYPES = (Movie, Episode, Show)
@ -63,7 +62,7 @@ def get_item_from_url(url):
raise SystemExit('Unknown or ambiguous client id: %s' % clientid) raise SystemExit('Unknown or ambiguous client id: %s' % clientid)
server = servers[0].connect() server = servers[0].connect()
return server.fetchItem(key) return server.fetchItem(key)
if __name__ == '__main__': if __name__ == '__main__':
# Command line parser # Command line parser
from plexapi import CONFIG from plexapi import CONFIG
@ -73,7 +72,7 @@ if __name__ == '__main__':
default=CONFIG.get('auth.myplex_username')) default=CONFIG.get('auth.myplex_username'))
parser.add_argument('-p', '--password', help='Your Plex password', parser.add_argument('-p', '--password', help='Your Plex password',
default=CONFIG.get('auth.myplex_password')) default=CONFIG.get('auth.myplex_password'))
parser.add_argument('--url', default=None, help='Download from URL (only paste after !)') parser.add_argument('--url', default=None, help='Download from URL (only paste after !)')
opts = parser.parse_args() opts = parser.parse_args()
# Search item to download # Search item to download
account = utils.getMyPlexAccount(opts) account = utils.getMyPlexAccount(opts)
@ -86,4 +85,3 @@ if __name__ == '__main__':
filepath = utils.download(url, token=account.authenticationToken, filename=filename, savepath=os.getcwd(), filepath = utils.download(url, token=account.authenticationToken, filename=filename, savepath=os.getcwd(),
session=item._server._session, showstatus=True) session=item._server._session, showstatus=True)
#print(' %s' % filepath) #print(' %s' % filepath)