mirror of
https://github.com/pkkid/python-plexapi
synced 2024-11-24 12:43:06 +00:00
Merge branch 'master' into intro_marker
This commit is contained in:
commit
1d8d76ef56
48 changed files with 1884 additions and 937 deletions
|
@ -1,3 +1,8 @@
|
|||
[run]
|
||||
omit = */site-packages/plexapi/*
|
||||
|
||||
|
||||
|
||||
[report]
|
||||
exclude_lines =
|
||||
pragma: no cover
|
||||
|
|
21
.readthedocs.yml
Normal file
21
.readthedocs.yml
Normal 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
|
||||
|
|
@ -42,6 +42,10 @@ after_success:
|
|||
after_script:
|
||||
- '[ -z "${PLEXAPI_AUTH_MYPLEX_USERNAME}" ] || PYTHONPATH="$PWD:$PYTHONPATH" python -u tools/plex-teardowntest.py'
|
||||
|
||||
|
||||
notifications:
|
||||
webhooks: https://coveralls.io/webhook
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- python: 3.6
|
||||
|
@ -72,8 +76,8 @@ jobs:
|
|||
- PLEX_CONTAINER_TAG=latest
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: mjs7231
|
||||
user: hellowlol
|
||||
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:
|
||||
tags: true
|
||||
|
|
80
README.rst
80
README.rst
|
@ -6,6 +6,12 @@ Python-PlexAPI
|
|||
:target: https://travis-ci.org/pkkid/python-plexapi
|
||||
.. image:: https://coveralls.io/repos/github/pkkid/python-plexapi/badge.svg?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
|
||||
|
@ -15,7 +21,7 @@ Plex Web Client. A few of the many features we currently support are:
|
|||
|
||||
* Navigate local or remote shared libraries.
|
||||
* 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.
|
||||
|
||||
|
||||
|
@ -129,35 +135,65 @@ Usage Examples
|
|||
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
|
||||
--------------------------
|
||||
|
||||
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:
|
||||
* 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
|
||||
.. code-block:: bash
|
||||
|
||||
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`.
|
||||
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:
|
||||
|
||||
* `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
7
docs/modules/gdm.rst
Normal 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
7
docs/modules/sonos.rst
Normal file
|
@ -0,0 +1,7 @@
|
|||
.. include:: ../global.rst
|
||||
|
||||
Sonos :modname:`plexapi.sonos`
|
||||
--------------------------------
|
||||
.. automodule:: plexapi.sonos
|
||||
:members:
|
||||
:show-inheritance:
|
|
@ -17,6 +17,7 @@
|
|||
modules/client
|
||||
modules/config
|
||||
modules/exceptions
|
||||
modules/gdm
|
||||
modules/library
|
||||
modules/media
|
||||
modules/myplex
|
||||
|
@ -25,6 +26,8 @@
|
|||
modules/playqueue
|
||||
modules/server
|
||||
modules/settings
|
||||
modules/sonos
|
||||
modules/sync
|
||||
modules/utils
|
||||
modules/video
|
||||
|
||||
|
|
|
@ -3,9 +3,10 @@ import logging
|
|||
import os
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from platform import uname
|
||||
from uuid import getnode
|
||||
|
||||
from plexapi.config import PlexConfig, reset_base_headers
|
||||
from plexapi.utils import SecretsFilter
|
||||
from uuid import getnode
|
||||
|
||||
# Load User Defined Config
|
||||
DEFAULT_CONFIG_PATH = os.path.expanduser('~/.config/plexapi/config.ini')
|
||||
|
@ -14,7 +15,7 @@ CONFIG = PlexConfig(CONFIG_PATH)
|
|||
|
||||
# PlexAPI Settings
|
||||
PROJECT = 'PlexAPI'
|
||||
VERSION = '3.4.0'
|
||||
VERSION = '3.6.0'
|
||||
TIMEOUT = CONFIG.get('plexapi.timeout', 30, 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)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import threading
|
||||
import websocket
|
||||
|
||||
from plexapi import log
|
||||
|
||||
|
||||
|
@ -40,6 +40,11 @@ class AlertListener(threading.Thread):
|
|||
self._ws = None
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
import websocket
|
||||
except ImportError:
|
||||
log.warning("Can't use the AlertListener without websocket")
|
||||
return
|
||||
# create the websocket connection
|
||||
url = self._server.url(self.key, includeToken=True).replace('http', 'ws')
|
||||
log.info('Starting AlertListener: %s', url)
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from plexapi import media, utils
|
||||
from plexapi.base import Playable, PlexPartialObject
|
||||
from plexapi.compat import quote_plus
|
||||
|
||||
|
||||
class Audio(PlexPartialObject):
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
from urllib.parse import quote_plus, urlencode
|
||||
|
||||
from plexapi import log, utils
|
||||
from plexapi.compat import quote_plus, urlencode
|
||||
from plexapi.exceptions import BadRequest, NotFound, UnknownType, Unsupported
|
||||
from plexapi.utils import tag_helper
|
||||
|
||||
|
@ -142,19 +142,21 @@ class PlexObject(object):
|
|||
clsname = cls.__name__ if cls else 'None'
|
||||
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
|
||||
and attrs. See :func:`~plexapi.base.PlexObject.fetchItem` for more details
|
||||
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 = {}
|
||||
for key, value in dict(kwargs).items():
|
||||
if key == "container_start":
|
||||
url_kw["X-Plex-Container-Start"] = kwargs.pop(key)
|
||||
if key == "container_size":
|
||||
url_kw["X-Plex-Container-Size"] = kwargs.pop(key)
|
||||
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')
|
||||
|
@ -519,29 +521,33 @@ class PlexPartialObject(PlexObject):
|
|||
key = '/library/metadata/%s/matches' % self.ratingKey
|
||||
params = {'manual': 1}
|
||||
|
||||
if any([agent, title, year, language]):
|
||||
if title is None:
|
||||
params['title'] = self.title
|
||||
else:
|
||||
params['title'] = title
|
||||
if agent and not any([title, year, language]):
|
||||
params['language'] = self.section().language
|
||||
params['agent'] = utils.getAgentIdentifier(self.section(), agent)
|
||||
else:
|
||||
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:
|
||||
params['year'] = self.year
|
||||
else:
|
||||
params['year'] = year
|
||||
if year is None:
|
||||
params['year'] = self.year
|
||||
else:
|
||||
params['year'] = year
|
||||
|
||||
params['language'] = language or self.section().language
|
||||
params['language'] = language or self.section().language
|
||||
|
||||
if agent is None:
|
||||
params['agent'] = self.section().agent
|
||||
else:
|
||||
params['agent'] = utils.getAgentIdentifier(self.section(), agent)
|
||||
if agent is None:
|
||||
params['agent'] = self.section().agent
|
||||
else:
|
||||
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)
|
||||
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.
|
||||
|
||||
Parameters:
|
||||
|
@ -549,10 +555,15 @@ class PlexPartialObject(PlexObject):
|
|||
False allows user to provide the match
|
||||
searchResult (:class:`~plexapi.media.SearchResult`): Search result from
|
||||
~plexapi.base.matches()
|
||||
agent (str): Agent name to be used (imdb, thetvdb, themoviedb, etc.)
|
||||
"""
|
||||
key = '/library/metadata/%s/match' % self.ratingKey
|
||||
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:
|
||||
raise NotFound('fixMatch() requires either auto=True or '
|
||||
'searchResult=:class:`~plexapi.media.SearchResult`.')
|
||||
|
@ -651,6 +662,14 @@ class Playable(object):
|
|||
key = '%s/split' % self.key
|
||||
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):
|
||||
"""Unmatch a media file."""
|
||||
key = '%s/unmatch' % self.key
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import time
|
||||
from xml.etree import ElementTree
|
||||
|
||||
import requests
|
||||
from plexapi import BASE_HEADERS, CONFIG, TIMEOUT, log, logfilter, utils
|
||||
from plexapi.base import PlexObject
|
||||
from plexapi.compat import ElementTree
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unauthorized, Unsupported
|
||||
from plexapi.playqueue import PlayQueue
|
||||
from requests.status_codes import _codes as codes
|
||||
|
@ -157,7 +157,7 @@ class PlexClient(PlexObject):
|
|||
log.debug('%s %s', method.__name__.upper(), url)
|
||||
headers = self._headers(**headers or {})
|
||||
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]
|
||||
errtext = response.text.replace('\n', ' ')
|
||||
message = '(%s) %s; %s %s' % (response.status_code, codename, response.url, errtext)
|
||||
|
|
|
@ -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
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from plexapi.compat import ConfigParser
|
||||
from configparser import ConfigParser
|
||||
|
||||
|
||||
class PlexConfig(ConfigParser):
|
||||
|
@ -13,6 +13,7 @@ class PlexConfig(ConfigParser):
|
|||
Parameters:
|
||||
path (str): Path of the configuration file to load.
|
||||
"""
|
||||
|
||||
def __init__(self, path):
|
||||
ConfigParser.__init__(self)
|
||||
self.read(path)
|
||||
|
|
|
@ -4,9 +4,8 @@ Support for discovery using GDM (Good Day Mate), multicast protocol by Plex.
|
|||
# Licensed Apache 2.0
|
||||
# From https://github.com/home-assistant/netdisco/netdisco/gdm.py
|
||||
|
||||
Inspired by
|
||||
hippojay's plexGDM:
|
||||
https://github.com/hippojay/script.plexbmc.helper/resources/lib/plexgdm.py
|
||||
Inspired by:
|
||||
hippojay's plexGDM: https://github.com/hippojay/script.plexbmc.helper/resources/lib/plexgdm.py
|
||||
iBaa's PlexConnect: https://github.com/iBaa/PlexConnect/PlexAPI.py
|
||||
"""
|
||||
import socket
|
||||
|
@ -50,31 +49,33 @@ class GDM:
|
|||
|
||||
Examples of the dict list assigned to self.entries by this function:
|
||||
|
||||
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)}]
|
||||
Server:
|
||||
|
||||
Clients:
|
||||
[{'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)}]
|
||||
[{'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': {'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')
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from urllib.parse import quote, quote_plus, unquote, urlencode
|
||||
|
||||
from plexapi import X_PLEX_CONTAINER_SIZE, log, utils
|
||||
from plexapi.base import PlexObject
|
||||
from plexapi.compat import quote_plus, unquote, urlencode
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
from plexapi.media import MediaTag
|
||||
from plexapi.settings import Setting
|
||||
|
@ -360,6 +361,40 @@ class LibrarySection(PlexObject):
|
|||
# Private attrs as we dont want a reload.
|
||||
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
|
||||
def totalSize(self):
|
||||
if self._total_size is None:
|
||||
|
@ -404,7 +439,7 @@ class LibrarySection(PlexObject):
|
|||
Parameters:
|
||||
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)
|
||||
|
||||
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))
|
||||
return self.fetchItems(key, cls=FilterChoice)
|
||||
|
||||
def search(self, title=None, sort=None, maxresults=999999, libtype=None, **kwargs):
|
||||
""" Search the library. If there are many results, they will be fetched from the server
|
||||
in batches of X_PLEX_CONTAINER_SIZE amounts. If you're only looking for the first <num>
|
||||
def search(self, title=None, sort=None, maxresults=None,
|
||||
libtype=None, container_start=0, container_size=X_PLEX_CONTAINER_SIZE, **kwargs):
|
||||
""" 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
|
||||
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).
|
||||
libtype (str): Filter results to a spcifiec libtype (movie, show, episode, artist,
|
||||
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
|
||||
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`.
|
||||
|
@ -549,15 +586,37 @@ class LibrarySection(PlexObject):
|
|||
args['sort'] = self._cleanSearchSort(sort)
|
||||
if libtype is not None:
|
||||
args['type'] = utils.searchType(libtype)
|
||||
# iterate over the results
|
||||
results, subresults = [], '_init'
|
||||
args['X-Plex-Container-Start'] = 0
|
||||
args['X-Plex-Container-Size'] = min(X_PLEX_CONTAINER_SIZE, maxresults)
|
||||
while subresults and maxresults > len(results):
|
||||
|
||||
results = []
|
||||
subresults = []
|
||||
offset = container_start
|
||||
|
||||
if maxresults is not None:
|
||||
container_size = min(container_size, maxresults)
|
||||
while True:
|
||||
key = '/library/sections/%s/all%s' % (self.key, utils.joinArgs(args))
|
||||
subresults = self.fetchItems(key)
|
||||
results += subresults[:maxresults - len(results)]
|
||||
args['X-Plex-Container-Start'] += args['X-Plex-Container-Size']
|
||||
subresults = self.fetchItems(key, container_start=container_start,
|
||||
container_size=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
|
||||
|
||||
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]
|
||||
if matches: map(result.add, matches); continue
|
||||
# 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)
|
||||
return ','.join(result)
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
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.exceptions import BadRequest
|
||||
from plexapi.utils import cast
|
||||
|
@ -619,7 +620,7 @@ class Poster(PlexObject):
|
|||
|
||||
def select(self):
|
||||
key = self._initpath[:-1]
|
||||
data = '%s?url=%s' % (key, compat.quote_plus(self.ratingKey))
|
||||
data = '%s?url=%s' % (key, quote_plus(self.ratingKey))
|
||||
try:
|
||||
self._server.query(data, method=self._server._session.put)
|
||||
except xml.etree.ElementTree.ParseError:
|
||||
|
|
|
@ -2,16 +2,17 @@
|
|||
import copy
|
||||
import threading
|
||||
import time
|
||||
from xml.etree import ElementTree
|
||||
|
||||
import requests
|
||||
from plexapi import (BASE_HEADERS, CONFIG, TIMEOUT, X_PLEX_ENABLE_FAST_CONNECT,
|
||||
X_PLEX_IDENTIFIER, log, logfilter, utils)
|
||||
from plexapi.base import PlexObject
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
|
||||
from plexapi.client import PlexClient
|
||||
from plexapi.compat import ElementTree
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
|
||||
from plexapi.library import LibrarySection
|
||||
from plexapi.server import PlexServer
|
||||
from plexapi.sonos import PlexSonosClient
|
||||
from plexapi.sync import SyncItem, SyncList
|
||||
from plexapi.utils import joinArgs
|
||||
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):
|
||||
self._token = token
|
||||
self._session = session or requests.Session()
|
||||
self._sonos_cache = []
|
||||
self._sonos_cache_timestamp = 0
|
||||
data, initpath = self._signin(username, password, timeout)
|
||||
super(MyPlexAccount, self).__init__(self, data, initpath)
|
||||
|
||||
|
@ -209,6 +212,24 @@ class MyPlexAccount(PlexObject):
|
|||
data = self.query(MyPlexResource.key)
|
||||
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,
|
||||
allowChannels=False, filterMovies=None, filterTelevision=None, filterMusic=None):
|
||||
""" Share library content with the specified user.
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from plexapi import media, utils
|
||||
from plexapi.base import PlexPartialObject
|
||||
from plexapi.exceptions import NotFound, BadRequest
|
||||
from plexapi.compat import quote_plus
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from plexapi import utils
|
||||
from plexapi.base import PlexPartialObject, Playable
|
||||
from plexapi.base import Playable, PlexPartialObject
|
||||
from plexapi.exceptions import BadRequest, Unsupported
|
||||
from plexapi.library import LibrarySection
|
||||
from plexapi.playqueue import PlayQueue
|
||||
from plexapi.utils import cast, toDatetime
|
||||
from plexapi.compat import quote_plus
|
||||
|
||||
|
||||
@utils.registerPlexObject
|
||||
|
|
|
@ -1,23 +1,29 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from urllib.parse import urlencode
|
||||
from xml.etree import ElementTree
|
||||
|
||||
import requests
|
||||
from requests.status_codes import _codes as codes
|
||||
from plexapi import BASE_HEADERS, CONFIG, TIMEOUT, X_PLEX_CONTAINER_SIZE
|
||||
from plexapi import log, logfilter, utils
|
||||
# Need these imports to populate utils.PLEXOBJECTS
|
||||
from plexapi import (BASE_HEADERS, CONFIG, TIMEOUT, X_PLEX_CONTAINER_SIZE, log,
|
||||
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.base import PlexObject
|
||||
from plexapi.client import PlexClient
|
||||
from plexapi.compat import ElementTree, urlencode
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
|
||||
from plexapi.library import Library, Hub
|
||||
from plexapi.settings import Settings
|
||||
from plexapi.library import Hub, Library
|
||||
from plexapi.media import Conversion, Optimized
|
||||
from plexapi.playlist import Playlist
|
||||
from plexapi.playqueue import PlayQueue
|
||||
from plexapi.settings import Settings
|
||||
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, video as _video, # noqa: F401
|
||||
photo as _photo, media as _media, playlist as _playlist) # noqa: F401
|
||||
from plexapi import audio as _audio # noqa: F401; noqa: F401
|
||||
|
||||
|
||||
class PlexServer(PlexObject):
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from collections import defaultdict
|
||||
from urllib.parse import quote
|
||||
|
||||
from plexapi import log, utils
|
||||
from plexapi.base import PlexObject
|
||||
from plexapi.compat import quote, string_type
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
|
||||
|
||||
|
@ -106,7 +106,7 @@ class Setting(PlexObject):
|
|||
'bool': {'type': bool, 'cast': _bool_cast, 'tostr': _bool_str},
|
||||
'double': {'type': float, 'cast': float, '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):
|
||||
|
|
116
plexapi/sonos.py
Normal file
116
plexapi/sonos.py
Normal 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
|
||||
)
|
||||
)
|
|
@ -7,11 +7,15 @@ import zipfile
|
|||
from datetime import datetime
|
||||
from getpass import getpass
|
||||
from threading import Event, Thread
|
||||
from urllib.parse import quote
|
||||
|
||||
import requests
|
||||
from plexapi import compat
|
||||
from plexapi.exceptions import NotFound
|
||||
from tqdm import tqdm
|
||||
|
||||
try:
|
||||
from tqdm import tqdm
|
||||
except ImportError:
|
||||
tqdm = None
|
||||
|
||||
log = logging.getLogger('plexapi')
|
||||
|
||||
|
@ -37,7 +41,7 @@ class SecretsFilter(logging.Filter):
|
|||
def filter(self, record):
|
||||
cleanargs = list(record.args)
|
||||
for i in range(len(cleanargs)):
|
||||
if isinstance(cleanargs[i], compat.string_type):
|
||||
if isinstance(cleanargs[i], str):
|
||||
for secret in self.secrets:
|
||||
cleanargs[i] = cleanargs[i].replace(secret, '<hidden>')
|
||||
record.args = tuple(cleanargs)
|
||||
|
@ -95,8 +99,8 @@ def joinArgs(args):
|
|||
return ''
|
||||
arglist = []
|
||||
for key in sorted(args, key=lambda x: x.lower()):
|
||||
value = compat.ustr(args[key])
|
||||
arglist.append('%s=%s' % (key, compat.quote(value)))
|
||||
value = str(args[key])
|
||||
arglist.append('%s=%s' % (key, quote(value, safe='')))
|
||||
return '?%s' % '&'.join(arglist)
|
||||
|
||||
|
||||
|
@ -144,8 +148,8 @@ def searchType(libtype):
|
|||
Raises:
|
||||
:class:`plexapi.exceptions.NotFound`: Unknown libtype
|
||||
"""
|
||||
libtype = compat.ustr(libtype)
|
||||
if libtype in [compat.ustr(v) for v in SEARCHTYPES.values()]:
|
||||
libtype = str(libtype)
|
||||
if libtype in [str(v) for v in SEARCHTYPES.values()]:
|
||||
return libtype
|
||||
if SEARCHTYPES.get(libtype) is not None:
|
||||
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)
|
||||
# make sure the savepath directory exists
|
||||
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)
|
||||
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
|
||||
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))
|
||||
bar = tqdm(unit='B', unit_scale=True, total=total, desc=filename)
|
||||
|
||||
with open(fullpath, 'wb') as handle:
|
||||
for chunk in response.iter_content(chunk_size=chunksize):
|
||||
handle.write(chunk)
|
||||
if showstatus:
|
||||
if showstatus and tqdm:
|
||||
bar.update(len(chunk))
|
||||
|
||||
if showstatus: # pragma: no cover
|
||||
if showstatus and tqdm: # pragma: no cover
|
||||
bar.close()
|
||||
# check we want to unzip the contents
|
||||
if fullpath.endswith('zip') and unpack:
|
||||
|
|
|
@ -3,6 +3,3 @@
|
|||
# pip install -r requirements.txt
|
||||
#---------------------------------------------------------
|
||||
requests
|
||||
tqdm
|
||||
websocket-client
|
||||
mock; python_version < '3.3'
|
||||
|
|
|
@ -11,10 +11,13 @@ pytest-cov
|
|||
pytest-mock<=1.11.1
|
||||
recommonmark
|
||||
requests
|
||||
requests-mock
|
||||
sphinx
|
||||
sphinxcontrib-napoleon
|
||||
tqdm
|
||||
websocket-client
|
||||
mock; python_version < '3.3'
|
||||
|
||||
|
||||
# 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
|
||||
|
|
|
@ -7,12 +7,12 @@ from os import environ
|
|||
import plexapi
|
||||
import pytest
|
||||
import requests
|
||||
from plexapi import compat
|
||||
from plexapi.client import PlexClient
|
||||
from plexapi.compat import MagicMock, patch
|
||||
from plexapi.myplex import MyPlexAccount
|
||||
from plexapi.server import PlexServer
|
||||
|
||||
from .payloads import ACCOUNT_XML
|
||||
|
||||
try:
|
||||
from unittest.mock import patch, MagicMock, mock_open
|
||||
except ImportError:
|
||||
|
@ -138,6 +138,12 @@ def account_synctarget(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")
|
||||
def plex(request):
|
||||
assert SERVER_BASEURL, "Required SERVER_BASEURL not specified."
|
||||
|
@ -224,17 +230,17 @@ def collection(plex):
|
|||
|
||||
@pytest.fixture()
|
||||
def artist(music):
|
||||
return music.get("Infinite State")
|
||||
return music.get("Broke For Free")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def album(artist):
|
||||
return artist.album("Unmastered Impulses")
|
||||
return artist.album("Layers")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def track(album):
|
||||
return album.track("Holy Moment")
|
||||
return album.track("As Colourful as Ever")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
|
@ -347,7 +353,7 @@ def is_section(key):
|
|||
|
||||
|
||||
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):
|
||||
|
|
BIN
tests/data/audio_stub.mp3
Normal file
BIN
tests/data/audio_stub.mp3
Normal file
Binary file not shown.
BIN
tests/data/cute_cat.jpg
Normal file
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
BIN
tests/data/video_stub.mp4
Normal file
Binary file not shown.
24
tests/payloads.py
Normal file
24
tests/payloads.py
Normal 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>
|
||||
"""
|
|
@ -8,29 +8,29 @@ def test_audio_Artist_attr(artist):
|
|||
artist.reload()
|
||||
assert utils.is_datetime(artist.addedAt)
|
||||
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 artist.index == '1'
|
||||
assert artist.index == "1"
|
||||
assert utils.is_metadata(artist._initpath)
|
||||
assert utils.is_metadata(artist.key)
|
||||
assert utils.is_int(artist.librarySectionID)
|
||||
assert artist.listType == 'audio'
|
||||
assert artist.listType == "audio"
|
||||
assert len(artist.locations) == 1
|
||||
assert len(artist.locations[0]) >= 10
|
||||
assert artist.ratingKey >= 1
|
||||
assert artist._server._baseurl == utils.SERVER_BASEURL
|
||||
assert isinstance(artist.similar, list)
|
||||
assert artist.summary == ''
|
||||
assert artist.title == 'Infinite State'
|
||||
assert artist.titleSort == 'Infinite State'
|
||||
assert artist.type == 'artist'
|
||||
assert "Alias" in artist.summary
|
||||
assert artist.title == "Broke For Free"
|
||||
assert artist.titleSort == "Broke For Free"
|
||||
assert artist.type == "artist"
|
||||
assert utils.is_datetime(artist.updatedAt)
|
||||
assert utils.is_int(artist.viewCount, gte=0)
|
||||
|
||||
|
||||
def test_audio_Artist_get(artist, music):
|
||||
artist == music.searchArtists(**{'title': 'Infinite State'})[0]
|
||||
artist.title == 'Infinite State'
|
||||
artist == music.searchArtists(**{"title": "Broke For Free"})[0]
|
||||
artist.title == "Broke For Free"
|
||||
|
||||
|
||||
def test_audio_Artist_history(artist):
|
||||
|
@ -39,50 +39,52 @@ def test_audio_Artist_history(artist):
|
|||
|
||||
|
||||
def test_audio_Artist_track(artist):
|
||||
track = artist.track('Holy Moment')
|
||||
assert track.title == 'Holy Moment'
|
||||
track = artist.track("As Colourful as Ever")
|
||||
assert track.title == "As Colourful as Ever"
|
||||
|
||||
|
||||
def test_audio_Artist_tracks(artist):
|
||||
tracks = artist.tracks()
|
||||
assert len(tracks) == 14
|
||||
assert len(tracks) == 1
|
||||
|
||||
|
||||
def test_audio_Artist_album(artist):
|
||||
album = artist.album('Unmastered Impulses')
|
||||
assert album.title == 'Unmastered Impulses'
|
||||
album = artist.album("Layers")
|
||||
assert album.title == "Layers"
|
||||
|
||||
|
||||
def test_audio_Artist_albums(artist):
|
||||
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):
|
||||
assert utils.is_datetime(album.addedAt)
|
||||
assert [i.tag for i in album.genres] == ['Electronic']
|
||||
assert album.index == '1'
|
||||
assert isinstance(album.genres, list)
|
||||
assert album.index == "1"
|
||||
assert utils.is_metadata(album._initpath)
|
||||
assert utils.is_metadata(album.key)
|
||||
assert utils.is_int(album.librarySectionID)
|
||||
assert album.listType == 'audio'
|
||||
assert album.originallyAvailableAt == datetime(2016, 1, 1)
|
||||
assert album.listType == "audio"
|
||||
if album.originallyAvailableAt:
|
||||
assert utils.is_datetime(album.originallyAvailableAt)
|
||||
assert utils.is_metadata(album.parentKey)
|
||||
assert utils.is_int(album.parentRatingKey)
|
||||
if album.parentThumb:
|
||||
assert utils.is_metadata(album.parentThumb, contains='/thumb/')
|
||||
assert album.parentTitle == 'Infinite State'
|
||||
assert utils.is_metadata(album.parentThumb, contains="/thumb/")
|
||||
assert album.parentTitle == "Broke For Free"
|
||||
assert album.ratingKey >= 1
|
||||
assert album._server._baseurl == utils.SERVER_BASEURL
|
||||
assert album.studio is None
|
||||
assert album.summary == ''
|
||||
assert utils.is_metadata(album.thumb, contains='/thumb/')
|
||||
assert album.title == 'Unmastered Impulses'
|
||||
assert album.titleSort == 'Unmastered Impulses'
|
||||
assert album.type == 'album'
|
||||
assert album.summary == ""
|
||||
if album.thumb:
|
||||
assert utils.is_metadata(album.thumb, contains="/thumb/")
|
||||
assert album.title == "Layers"
|
||||
assert album.titleSort == "Layers"
|
||||
assert album.type == "album"
|
||||
assert utils.is_datetime(album.updatedAt)
|
||||
assert utils.is_int(album.viewCount, gte=0)
|
||||
assert album.year == 2016
|
||||
assert album.year in (2012,)
|
||||
assert album.artUrl is None
|
||||
|
||||
|
||||
|
@ -99,29 +101,29 @@ def test_audio_Track_history(track):
|
|||
def test_audio_Album_tracks(album):
|
||||
tracks = album.tracks()
|
||||
track = tracks[0]
|
||||
assert len(tracks) == 14
|
||||
assert len(tracks) == 1
|
||||
assert utils.is_metadata(track.grandparentKey)
|
||||
assert utils.is_int(track.grandparentRatingKey)
|
||||
assert track.grandparentTitle == 'Infinite State'
|
||||
assert track.index == '1'
|
||||
assert track.grandparentTitle == "Broke For Free"
|
||||
assert track.index == "1"
|
||||
assert utils.is_metadata(track._initpath)
|
||||
assert utils.is_metadata(track.key)
|
||||
assert track.listType == 'audio'
|
||||
assert track.originalTitle == 'Kenneth Reitz'
|
||||
assert utils.is_int(track.parentIndex)
|
||||
assert track.listType == "audio"
|
||||
assert track.originalTitle in (None, "Broke For Free")
|
||||
# assert utils.is_int(track.parentIndex)
|
||||
assert utils.is_metadata(track.parentKey)
|
||||
assert utils.is_int(track.parentRatingKey)
|
||||
assert utils.is_metadata(track.parentThumb, contains='/thumb/')
|
||||
assert track.parentTitle == 'Unmastered Impulses'
|
||||
assert utils.is_metadata(track.parentThumb, contains="/thumb/")
|
||||
assert track.parentTitle == "Layers"
|
||||
# assert track.ratingCount == 9 # Flaky
|
||||
assert utils.is_int(track.ratingKey)
|
||||
assert track._server._baseurl == utils.SERVER_BASEURL
|
||||
assert track.summary == ""
|
||||
assert utils.is_metadata(track.thumb, contains='/thumb/')
|
||||
assert track.title == 'Holy Moment'
|
||||
assert track.titleSort == 'Holy Moment'
|
||||
assert utils.is_metadata(track.thumb, contains="/thumb/")
|
||||
assert track.title == "As Colourful as Ever"
|
||||
assert track.titleSort == "As Colourful as Ever"
|
||||
assert not track.transcodeSessions
|
||||
assert track.type == 'track'
|
||||
assert track.type == "track"
|
||||
assert utils.is_datetime(track.updatedAt)
|
||||
assert utils.is_int(track.viewCount, gte=0)
|
||||
assert track.viewOffset == 0
|
||||
|
@ -129,46 +131,47 @@ def test_audio_Album_tracks(album):
|
|||
|
||||
def test_audio_Album_track(album, track=None):
|
||||
# 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 track.duration in [298605, 298606]
|
||||
assert utils.is_int(track.duration)
|
||||
assert utils.is_metadata(track.grandparentKey)
|
||||
assert utils.is_int(track.grandparentRatingKey)
|
||||
assert track.grandparentTitle == 'Infinite State'
|
||||
assert track.grandparentTitle == "Broke For Free"
|
||||
assert int(track.index) == 1
|
||||
assert utils.is_metadata(track._initpath)
|
||||
assert utils.is_metadata(track.key)
|
||||
assert track.listType == 'audio'
|
||||
assert track.listType == "audio"
|
||||
# Assign 0 track.media
|
||||
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_metadata(track.parentKey)
|
||||
assert utils.is_int(track.parentRatingKey)
|
||||
assert utils.is_metadata(track.parentThumb, contains='/thumb/')
|
||||
assert track.parentTitle == 'Unmastered Impulses'
|
||||
assert utils.is_metadata(track.parentThumb, contains="/thumb/")
|
||||
assert track.parentTitle == "Layers"
|
||||
# assert track.ratingCount == 9
|
||||
assert utils.is_int(track.ratingKey)
|
||||
assert track._server._baseurl == utils.SERVER_BASEURL
|
||||
assert track.summary == ''
|
||||
assert utils.is_metadata(track.thumb, contains='/thumb/')
|
||||
assert track.title == 'Holy Moment'
|
||||
assert track.titleSort == 'Holy Moment'
|
||||
assert track.summary == ""
|
||||
assert utils.is_metadata(track.thumb, contains="/thumb/")
|
||||
assert track.title == "As Colourful as Ever"
|
||||
assert track.titleSort == "As Colourful as Ever"
|
||||
assert not track.transcodeSessions
|
||||
assert track.type == 'track'
|
||||
assert track.type == "track"
|
||||
assert utils.is_datetime(track.updatedAt)
|
||||
assert utils.is_int(track.viewCount, gte=0)
|
||||
assert track.viewOffset == 0
|
||||
assert media.aspectRatio is None
|
||||
assert media.audioChannels == 2
|
||||
assert media.audioCodec == 'mp3'
|
||||
assert media.bitrate in [320, 385]
|
||||
assert media.container == 'mp3'
|
||||
assert media.duration in [298605, 298606]
|
||||
assert media.height is None
|
||||
assert media.audioCodec == "mp3"
|
||||
assert media.bitrate == 128
|
||||
assert media.container == "mp3"
|
||||
assert utils.is_int(media.duration)
|
||||
assert media.height in (None, 1080)
|
||||
assert utils.is_int(media.id, gte=1)
|
||||
assert utils.is_metadata(media._initpath)
|
||||
assert media.optimizedForStreaming is None
|
||||
assert media.optimizedForStreaming in (None, True)
|
||||
# Assign 0 media.parts
|
||||
part = media.parts[0]
|
||||
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.videoResolution is None
|
||||
assert media.width is None
|
||||
assert part.container == 'mp3'
|
||||
assert part.duration in [298605, 298606]
|
||||
assert part.file.endswith('.mp3')
|
||||
assert part.container == "mp3"
|
||||
assert utils.is_int(part.duration)
|
||||
assert part.file.endswith(".mp3")
|
||||
assert utils.is_int(part.id)
|
||||
assert utils.is_metadata(part._initpath)
|
||||
assert utils.is_part(part.key)
|
||||
assert part._server._baseurl == utils.SERVER_BASEURL
|
||||
assert part.size == 14360402
|
||||
assert part.size == 3761053
|
||||
assert track.artUrl is None
|
||||
|
||||
|
||||
def test_audio_Album_get(album):
|
||||
# alias for album.track()
|
||||
track = album.get('Holy Moment')
|
||||
track = album.get("As Colourful As Ever")
|
||||
test_audio_Album_track(album, track=track)
|
||||
|
||||
|
||||
def test_audio_Album_artist(album):
|
||||
artist = album.artist()
|
||||
artist.title == 'Infinite State'
|
||||
artist.title == "Broke For Free"
|
||||
|
||||
|
||||
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 track.art 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 utils.is_metadata(track.grandparentKey)
|
||||
assert utils.is_int(track.grandparentRatingKey)
|
||||
if track.grandparentThumb:
|
||||
assert utils.is_metadata(track.grandparentThumb, contains='/thumb/')
|
||||
assert track.grandparentTitle == 'Infinite State'
|
||||
assert track.guid.startswith('local://')
|
||||
assert utils.is_metadata(track.grandparentThumb, contains="/thumb/")
|
||||
assert track.grandparentTitle == "Broke For Free"
|
||||
assert track.guid.startswith("local://")
|
||||
assert int(track.index) == 1
|
||||
assert utils.is_metadata(track._initpath)
|
||||
assert utils.is_metadata(track.key)
|
||||
if track.lastViewedAt:
|
||||
assert utils.is_datetime(track.lastViewedAt)
|
||||
assert utils.is_int(track.librarySectionID)
|
||||
assert track.listType == 'audio'
|
||||
assert track.listType == "audio"
|
||||
# Assign 0 track.media
|
||||
media = track.media[0]
|
||||
assert track.moods == []
|
||||
assert track.originalTitle == 'Kenneth Reitz'
|
||||
assert track.originalTitle in (None, "Broke For Free")
|
||||
assert int(track.parentIndex) == 1
|
||||
assert utils.is_metadata(track.parentKey)
|
||||
assert utils.is_int(track.parentRatingKey)
|
||||
assert utils.is_metadata(track.parentThumb, contains='/thumb/')
|
||||
assert track.parentTitle == 'Unmastered Impulses'
|
||||
assert utils.is_metadata(track.parentThumb, contains="/thumb/")
|
||||
assert track.parentTitle == "Layers"
|
||||
assert track.playlistItemID 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 track._server._baseurl == utils.SERVER_BASEURL
|
||||
assert track.sessionKey is None
|
||||
assert track.summary == ''
|
||||
assert utils.is_metadata(track.thumb, contains='/thumb/')
|
||||
assert track.title == 'Holy Moment'
|
||||
assert track.titleSort == 'Holy Moment'
|
||||
assert track.summary == ""
|
||||
assert utils.is_metadata(track.thumb, contains="/thumb/")
|
||||
assert track.title == "As Colourful as Ever"
|
||||
assert track.titleSort == "As Colourful as Ever"
|
||||
assert not track.transcodeSessions
|
||||
assert track.type == 'track'
|
||||
assert track.type == "track"
|
||||
assert utils.is_datetime(track.updatedAt)
|
||||
assert utils.is_int(track.viewCount, gte=0)
|
||||
assert track.viewOffset == 0
|
||||
|
@ -246,10 +249,10 @@ def test_audio_Track_attrs(album):
|
|||
assert track.year is None
|
||||
assert media.aspectRatio is None
|
||||
assert media.audioChannels == 2
|
||||
assert media.audioCodec == 'mp3'
|
||||
assert media.bitrate in [320, 385]
|
||||
assert media.container == 'mp3'
|
||||
assert media.duration in [298605, 298606]
|
||||
assert media.audioCodec == "mp3"
|
||||
assert media.bitrate == 128
|
||||
assert media.container == "mp3"
|
||||
assert utils.is_int(media.duration)
|
||||
assert media.height is None
|
||||
assert utils.is_int(media.id, gte=1)
|
||||
assert utils.is_metadata(media._initpath)
|
||||
|
@ -261,23 +264,23 @@ def test_audio_Track_attrs(album):
|
|||
assert media.videoFrameRate is None
|
||||
assert media.videoResolution is None
|
||||
assert media.width is None
|
||||
assert part.container == 'mp3'
|
||||
assert part.duration in [298605, 298606]
|
||||
assert part.file.endswith('.mp3')
|
||||
assert part.container == "mp3"
|
||||
assert utils.is_int(part.duration)
|
||||
assert part.file.endswith(".mp3")
|
||||
assert utils.is_int(part.id)
|
||||
assert utils.is_metadata(part._initpath)
|
||||
assert utils.is_part(part.key)
|
||||
# assert part.media == <Media:Holy.Moment>
|
||||
assert part._server._baseurl == utils.SERVER_BASEURL
|
||||
assert part.size == 14360402
|
||||
assert part.size == 3761053
|
||||
# Assign 0 part.streams
|
||||
stream = part.streams[0]
|
||||
assert stream.audioChannelLayout == 'stereo'
|
||||
assert stream.audioChannelLayout == "stereo"
|
||||
assert stream.bitDepth is None
|
||||
assert stream.bitrate == 320
|
||||
assert stream.bitrate == 128
|
||||
assert stream.bitrateMode is None
|
||||
assert stream.channels == 2
|
||||
assert stream.codec == 'mp3'
|
||||
assert stream.codec == "mp3"
|
||||
assert stream.codecID is None
|
||||
assert stream.dialogNorm is None
|
||||
assert stream.duration is None
|
||||
|
@ -287,7 +290,7 @@ def test_audio_Track_attrs(album):
|
|||
assert stream.language is None
|
||||
assert stream.languageCode is None
|
||||
# assert stream.part == <MediaPart:22>
|
||||
assert stream.samplingRate == 44100
|
||||
assert stream.samplingRate == 48000
|
||||
assert stream.selected is True
|
||||
assert stream._server._baseurl == utils.SERVER_BASEURL
|
||||
assert stream.streamType == 2
|
||||
|
@ -319,13 +322,13 @@ def test_audio_Track_download(monkeydownload, tmpdir, track):
|
|||
|
||||
def test_audio_album_download(monkeydownload, album, tmpdir):
|
||||
f = album.download(savepath=str(tmpdir))
|
||||
assert len(f) == 14
|
||||
assert len(f) == 1
|
||||
|
||||
|
||||
def test_audio_Artist_download(monkeydownload, artist, tmpdir):
|
||||
f = artist.download(savepath=str(tmpdir))
|
||||
assert len(f) == 14
|
||||
assert len(f) == 1
|
||||
|
||||
|
||||
def test_audio_Album_label(album, patched_http_call):
|
||||
album.addLabel('YO')
|
||||
album.addLabel("YO")
|
||||
|
|
|
@ -8,7 +8,10 @@ def _check_capabilities(client, capabilities):
|
|||
supported = client.protocolCapabilities
|
||||
for capability in capabilities:
|
||||
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):
|
||||
|
@ -123,7 +126,7 @@ def test_client_timeline(plex, client, movies, proxy):
|
|||
try:
|
||||
# Note: We noticed the isPlaying flag could take up to a full
|
||||
# 30 seconds to be updated, hence the long sleeping.
|
||||
mtype= "video"
|
||||
mtype = "video"
|
||||
client.stop(mtype)
|
||||
assert client.isPlayingMedia() is False
|
||||
print("client.playMedia(movie)")
|
||||
|
|
15
tests/test_gdm.py
Normal file
15
tests/test_gdm.py
Normal 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)
|
|
@ -193,11 +193,11 @@ def test_library_MusicSection_albums(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):
|
||||
assert len(music.searchAlbums(title="Unmastered Impulses"))
|
||||
assert len(music.searchAlbums(title="Layers"))
|
||||
|
||||
|
||||
def test_library_PhotoSection_searchAlbums(photos, photoalbum):
|
||||
|
@ -260,3 +260,8 @@ def test_crazy_search(plex, movie):
|
|||
), "Unable to search movie by year."
|
||||
assert movie not in movies.search(year=2007), "Unable to filter movie by year."
|
||||
assert movie in movies.search(actor=movie.actors[0].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
|
||||
|
|
|
@ -1,25 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import pytest
|
||||
import shlex
|
||||
import subprocess
|
||||
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():
|
||||
docroot = join(dirname(dirname(abspath(__file__))), 'docs')
|
||||
cmd = shlex.split('sphinx-build -aE . _build')
|
||||
proc = subprocess.Popen(cmd, cwd=docroot, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
docroot = join(dirname(dirname(abspath(__file__))), "docs")
|
||||
cmd = shlex.split("sphinx-build -aE . _build")
|
||||
proc = subprocess.Popen(
|
||||
cmd, cwd=docroot, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
status = proc.wait()
|
||||
assert status == 0
|
||||
issues = []
|
||||
for output in proc.communicate():
|
||||
for line in str(output).split('\\n'):
|
||||
for line in str(output).split("\\n"):
|
||||
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)
|
||||
for line in issues:
|
||||
print(line)
|
||||
|
@ -29,30 +32,30 @@ def test_build_documentation():
|
|||
def test_readme_examples(plex):
|
||||
failed = 0
|
||||
examples = _fetch_examples()
|
||||
assert len(examples), 'No examples found in README'
|
||||
assert len(examples), "No examples found in README"
|
||||
for title, example in examples:
|
||||
if _check_run_example(title):
|
||||
try:
|
||||
print('\n%s\n%s' % (title, '-' * len(title)))
|
||||
exec('\n'.join(example))
|
||||
print("\n%s\n%s" % (title, "-" * len(title)))
|
||||
exec("\n".join(example))
|
||||
except Exception as err:
|
||||
failed += 1
|
||||
print('Error running test: %s\nError: %s' % (title, err))
|
||||
assert not failed, '%s examples raised an exception.' % failed
|
||||
print("Error running test: %s\nError: %s" % (title, err))
|
||||
assert not failed, "%s examples raised an exception." % failed
|
||||
|
||||
|
||||
def _fetch_examples():
|
||||
parsing = False
|
||||
examples = []
|
||||
filepath = join(dirname(dirname(abspath(__file__))), 'README.rst')
|
||||
with open(filepath, 'r') as handle:
|
||||
for line in handle.read().split('\n'):
|
||||
filepath = join(dirname(dirname(abspath(__file__))), "README.rst")
|
||||
with open(filepath, "r") as handle:
|
||||
for line in handle.read().split("\n"):
|
||||
line = line[4:]
|
||||
if line.startswith('# Example '):
|
||||
if line.startswith("# Example "):
|
||||
parsing = True
|
||||
title = line.lstrip('# ')
|
||||
title = line.lstrip("# ")
|
||||
examples.append([title, []])
|
||||
elif parsing and line == '':
|
||||
elif parsing and line == "":
|
||||
parsing = False
|
||||
elif parsing:
|
||||
examples[-1][1].append(line)
|
||||
|
|
|
@ -1,40 +1,41 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pytest
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
|
||||
from . import conftest as utils
|
||||
|
||||
|
||||
def test_myplex_accounts(account, plex):
|
||||
assert account, 'Must specify username, password & resource to run this test.'
|
||||
print('MyPlexAccount:')
|
||||
print('username: %s' % account.username)
|
||||
print('email: %s' % account.email)
|
||||
print('home: %s' % account.home)
|
||||
print('queueEmail: %s' % account.queueEmail)
|
||||
assert account.username, 'Account has no username'
|
||||
assert account.authenticationToken, 'Account has no authenticationToken'
|
||||
assert account.email, 'Account has no email'
|
||||
assert account.home is not None, 'Account has no home'
|
||||
assert account.queueEmail, 'Account has no queueEmail'
|
||||
assert account, "Must specify username, password & resource to run this test."
|
||||
print("MyPlexAccount:")
|
||||
print("username: %s" % account.username)
|
||||
print("email: %s" % account.email)
|
||||
print("home: %s" % account.home)
|
||||
print("queueEmail: %s" % account.queueEmail)
|
||||
assert account.username, "Account has no username"
|
||||
assert account.authenticationToken, "Account has no authenticationToken"
|
||||
assert account.email, "Account has no email"
|
||||
assert account.home is not None, "Account has no home"
|
||||
assert account.queueEmail, "Account has no queueEmail"
|
||||
account = plex.account()
|
||||
print('Local PlexServer.account():')
|
||||
print('username: %s' % account.username)
|
||||
#print('authToken: %s' % account.authToken)
|
||||
print('signInState: %s' % account.signInState)
|
||||
assert account.username, 'Account has no username'
|
||||
assert account.authToken, 'Account has no authToken'
|
||||
assert account.signInState, 'Account has no signInState'
|
||||
print("Local PlexServer.account():")
|
||||
print("username: %s" % account.username)
|
||||
# print('authToken: %s' % account.authToken)
|
||||
print("signInState: %s" % account.signInState)
|
||||
assert account.username, "Account has no username"
|
||||
assert account.authToken, "Account has no authToken"
|
||||
assert account.signInState, "Account has no signInState"
|
||||
|
||||
|
||||
def test_myplex_resources(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()
|
||||
for resource in resources:
|
||||
name = resource.name or 'Unknown'
|
||||
name = resource.name or "Unknown"
|
||||
connections = [c.uri for c in resource.connections]
|
||||
connections = ', '.join(connections) if connections else 'None'
|
||||
print('%s (%s): %s' % (name, resource.product, connections))
|
||||
assert resources, 'No resources found for account: %s' % account.name
|
||||
connections = ", ".join(connections) if connections else "None"
|
||||
print("%s (%s): %s" % (name, resource.product, connections))
|
||||
assert resources, "No resources found for account: %s" % account.name
|
||||
|
||||
|
||||
def test_myplex_connect_to_resource(plex, account):
|
||||
|
@ -48,14 +49,15 @@ def test_myplex_connect_to_resource(plex, account):
|
|||
def test_myplex_devices(account):
|
||||
devices = account.devices()
|
||||
for device in devices:
|
||||
name = device.name or 'Unknown'
|
||||
connections = ', '.join(device.connections) if device.connections else 'None'
|
||||
print('%s (%s): %s' % (name, device.product, connections))
|
||||
assert devices, 'No devices found for account: %s' % account.name
|
||||
name = device.name or "Unknown"
|
||||
connections = ", ".join(device.connections) if device.connections else "None"
|
||||
print("%s (%s): %s" % (name, device.product, connections))
|
||||
assert devices, "No devices found for account: %s" % account.name
|
||||
|
||||
|
||||
def test_myplex_device(account, plex):
|
||||
from plexapi import X_PLEX_DEVICE_NAME
|
||||
|
||||
assert account.device(plex.friendlyName)
|
||||
assert account.device(X_PLEX_DEVICE_NAME)
|
||||
|
||||
|
@ -63,22 +65,24 @@ def test_myplex_device(account, plex):
|
|||
def _test_myplex_connect_to_device(account):
|
||||
devices = account.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
|
||||
client = device.connect()
|
||||
assert client, 'Unable to connect to device'
|
||||
assert client, "Unable to connect to device"
|
||||
|
||||
|
||||
def test_myplex_users(account):
|
||||
users = account.users()
|
||||
if not len(users):
|
||||
return pytest.skip('You have to add a shared account into your MyPlex')
|
||||
print('Found %s users.' % len(users))
|
||||
return pytest.skip("You have to add a shared account into your MyPlex")
|
||||
print("Found %s users." % len(users))
|
||||
user = account.user(users[0].title)
|
||||
print('Found user: %s' % user)
|
||||
assert user, 'Could not find user %s' % users[0].title
|
||||
print("Found user: %s" % user)
|
||||
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):
|
||||
|
@ -95,25 +99,25 @@ def test_myplex_webhooks(account):
|
|||
|
||||
def test_myplex_addwebhooks(account):
|
||||
if account.subscriptionActive:
|
||||
assert 'http://example.com' in account.addWebhook('http://example.com')
|
||||
assert "http://example.com" in account.addWebhook("http://example.com")
|
||||
else:
|
||||
with pytest.raises(BadRequest):
|
||||
account.addWebhook('http://example.com')
|
||||
account.addWebhook("http://example.com")
|
||||
|
||||
|
||||
def test_myplex_deletewebhooks(account):
|
||||
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:
|
||||
with pytest.raises(BadRequest):
|
||||
account.deleteWebhook('http://example.com')
|
||||
account.deleteWebhook("http://example.com")
|
||||
|
||||
|
||||
def test_myplex_optout(account_once):
|
||||
def enabled():
|
||||
ele = account_once.query('https://plex.tv/api/v2/user/privacy')
|
||||
lib = ele.attrib.get('optOutLibraryStats')
|
||||
play = ele.attrib.get('optOutPlayback')
|
||||
ele = account_once.query("https://plex.tv/api/v2/user/privacy")
|
||||
lib = ele.attrib.get("optOutLibraryStats")
|
||||
play = ele.attrib.get("optOutPlayback")
|
||||
return bool(int(lib)), bool(int(play))
|
||||
|
||||
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):
|
||||
inv_user = 'hellowlol'
|
||||
vid_filter = {'contentRating': ['G'], 'label': ['foo']}
|
||||
inv_user = "hellowlol"
|
||||
vid_filter = {"contentRating": ["G"], "label": ["foo"]}
|
||||
secs = plex.library.sections()
|
||||
|
||||
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():
|
||||
|
||||
account.inviteFriend(inv_user, plex, secs, allowSync=True, allowCameraUpload=True,
|
||||
allowChannels=False, filterMovies=vid_filter, filterTelevision=vid_filter,
|
||||
filterMusic={'label': ['foo']})
|
||||
account.inviteFriend(
|
||||
inv_user,
|
||||
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()]
|
||||
|
||||
|
@ -143,51 +155,68 @@ def test_myplex_inviteFriend_remove(account, plex, mocker):
|
|||
|
||||
|
||||
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()
|
||||
user = account.user(shared_username)
|
||||
|
||||
ids = account._getSectionIds(plex.machineIdentifier, secs)
|
||||
with mocker.patch.object(account, '_getSectionIds', return_value=ids):
|
||||
with mocker.patch.object(account, 'user', return_value=user):
|
||||
with mocker.patch.object(account, "_getSectionIds", return_value=ids):
|
||||
with mocker.patch.object(account, "user", return_value=user):
|
||||
with utils.callable_http_patch():
|
||||
|
||||
account.updateFriend(shared_username, plex, secs, allowSync=True, removeSections=True,
|
||||
allowCameraUpload=True, allowChannels=False, filterMovies=vid_filter,
|
||||
filterTelevision=vid_filter, filterMusic={'label': ['foo']})
|
||||
account.updateFriend(
|
||||
shared_username,
|
||||
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):
|
||||
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)
|
||||
assert shared_username in [u.username for u in account.users() if u.home is True]
|
||||
# Remove Home invite
|
||||
account.query(url, account._session.delete)
|
||||
# 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 in [u.username for u in plex.myPlexAccount().users() if u.home is False]
|
||||
assert shared_username not in [
|
||||
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?")
|
||||
def test_myplex_createHomeUser_remove(account, plex):
|
||||
homeuser = 'New Home User'
|
||||
homeuser = "New Home User"
|
||||
account.createHomeUser(homeuser, plex)
|
||||
assert homeuser in [u.title for u in plex.myPlexAccount().users() if u.home is True]
|
||||
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):
|
||||
assert account_plexpass.subscriptionActive
|
||||
assert account_plexpass.subscriptionStatus == 'Active'
|
||||
assert account_plexpass.subscriptionStatus == "Active"
|
||||
assert account_plexpass.subscriptionPlan
|
||||
assert 'sync' in account_plexpass.subscriptionFeatures
|
||||
assert 'premium_music_metadata' in account_plexpass.subscriptionFeatures
|
||||
assert 'plexpass' in account_plexpass.roles
|
||||
assert "sync" in account_plexpass.subscriptionFeatures
|
||||
assert "premium_music_metadata" in account_plexpass.subscriptionFeatures
|
||||
assert "plexpass" in account_plexpass.roles
|
||||
assert set(account_plexpass.entitlements) == utils.ENTITLEMENTS
|
||||
|
||||
|
||||
def test_myplex_claimToken(account):
|
||||
assert account.claimToken().startswith('claim-')
|
||||
assert account.claimToken().startswith("claim-")
|
||||
|
|
|
@ -2,37 +2,33 @@
|
|||
|
||||
|
||||
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()
|
||||
season = show.season('Season 1')
|
||||
season = show.season("Season 1")
|
||||
episodes = show.episodes()
|
||||
episode = show.episode('Pilot')
|
||||
assert 'Season 1' in [s.title for s in seasons], 'Unable to list season:'
|
||||
assert 'Pilot' in [e.title for e in episodes], 'Unable to list episode:'
|
||||
episode = show.episode("Pilot")
|
||||
assert "Season 1" in [s.title for s in seasons], "Unable to list season:"
|
||||
assert "Pilot" in [e.title for e in episodes], "Unable to list episode:"
|
||||
assert show.season(1) == season
|
||||
assert show.episode('Pilot') == episode, 'Unable to get show episode:'
|
||||
assert season.episode('Pilot') == episode, 'Unable to get season episode:'
|
||||
assert season.show() == show, 'season.show() doesnt match expected show.'
|
||||
assert episode.show() == show, 'episode.show() doesnt match expected show.'
|
||||
assert episode.season() == season, 'episode.season() doesnt match expected season.'
|
||||
assert show.episode("Pilot") == episode, "Unable to get show episode:"
|
||||
assert season.episode("Pilot") == episode, "Unable to get season episode:"
|
||||
assert season.show() == show, "season.show() doesnt match expected show."
|
||||
assert episode.show() == show, "episode.show() doesnt match expected show."
|
||||
assert episode.season() == season, "episode.season() doesnt match expected season."
|
||||
|
||||
|
||||
def test_navigate_around_artist(account, plex):
|
||||
artist = plex.library.section('Music').get('Infinite State')
|
||||
artist = plex.library.section("Music").get("Broke For Free")
|
||||
albums = artist.albums()
|
||||
album = artist.album('Unmastered Impulses')
|
||||
album = artist.album("Layers")
|
||||
tracks = artist.tracks()
|
||||
track = artist.track('Mantra')
|
||||
print('Navigating around artist: %s' % artist)
|
||||
print('Albums: %s...' % albums[:3])
|
||||
print('Album: %s' % album)
|
||||
print('Tracks: %s...' % tracks[:3])
|
||||
print('Track: %s' % track)
|
||||
assert 'Unmastered Impulses' in [a.title for a in albums], 'Unable to list album.'
|
||||
assert 'Mantra' in [e.title for e in tracks], 'Unable to list track.'
|
||||
assert artist.album('Unmastered Impulses') == album, 'Unable to get artist album.'
|
||||
assert artist.track('Mantra') == track, 'Unable to get artist track.'
|
||||
assert album.track('Mantra') == track, 'Unable to get album track.'
|
||||
assert album.artist() == artist, 'album.artist() doesnt match expected artist.'
|
||||
assert track.artist() == artist, 'track.artist() doesnt match expected artist.'
|
||||
assert track.album() == album, 'track.album() doesnt match expected album.'
|
||||
track = artist.track("As Colourful as Ever")
|
||||
print("Navigating around artist: %s" % artist)
|
||||
print("Album: %s" % album)
|
||||
print("Tracks: %s..." % tracks)
|
||||
print("Track: %s" % track)
|
||||
assert artist.track("As Colourful as Ever") == track, "Unable to get artist track."
|
||||
assert album.track("As Colourful as Ever") == 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."
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
|
||||
|
||||
def test_photo_Photoalbum(photoalbum):
|
||||
assert len(photoalbum.albums()) == 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
|
||||
a_pic = cats_in_bed.photo('photo7')
|
||||
a_pic = cats_in_bed.photo("photo7")
|
||||
assert a_pic
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import time
|
|||
|
||||
import pytest
|
||||
from PIL import Image, ImageStat
|
||||
from plexapi.compat import patch
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
from plexapi.server import PlexServer
|
||||
from plexapi.utils import download
|
||||
|
@ -19,11 +18,11 @@ def test_server_attr(plex, account):
|
|||
assert len(plex.machineIdentifier) == 40
|
||||
assert plex.myPlex is True
|
||||
# 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.myPlexSigninState == 'ok'
|
||||
assert plex.myPlexMappingState in ("mapped", "unknown")
|
||||
assert plex.myPlexSigninState == "ok"
|
||||
assert utils.is_int(plex.myPlexSubscription, gte=0)
|
||||
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 plex._token == account.authenticationToken
|
||||
assert utils.is_int(plex.transcoderActiveVideoSessions, gte=0)
|
||||
|
@ -54,28 +53,37 @@ def test_server_library(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):
|
||||
width, height = 500, 500
|
||||
imgurl = plex.transcodeImage(show.banner, height, width)
|
||||
gray = imgurl = plex.transcodeImage(show.banner, height, width, saturation=0)
|
||||
resized_img = download(imgurl, plex._token, savepath=str(tmpdir), filename='resize_image')
|
||||
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')
|
||||
resized_img = download(
|
||||
imgurl, plex._token, savepath=str(tmpdir), filename="resize_image"
|
||||
)
|
||||
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:
|
||||
assert width, height == image.size
|
||||
with Image.open(original_img) as image:
|
||||
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):
|
||||
# http://stackoverflow.com/questions/20068945/detect-if-image-is-color-grayscale-or-black-and-white-with-python-pil
|
||||
pilimg = Image.open(file)
|
||||
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))
|
||||
sse, bias = 0, [0, 0, 0]
|
||||
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]
|
||||
for pixel in thumb.getdata():
|
||||
mu = sum(pixel) / 3
|
||||
sse += sum((pixel[i] - mu - bias[i]) * (pixel[i] - mu - bias[i]) for i in [0, 1, 2])
|
||||
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)
|
||||
return 'grayscale' if mse <= MSE_cutoff else 'color'
|
||||
return "grayscale" if mse <= MSE_cutoff else "color"
|
||||
elif len(bands) == 1:
|
||||
return 'blackandwhite'
|
||||
return "blackandwhite"
|
||||
|
||||
|
||||
def test_server_fetchitem_notfound(plex):
|
||||
|
@ -99,16 +109,16 @@ def test_server_search(plex, movie):
|
|||
title = movie.title
|
||||
# this search seem to fail on my computer but not at travis, wtf.
|
||||
assert plex.search(title)
|
||||
assert plex.search(title, mediatype='movie')
|
||||
assert plex.search(title, mediatype="movie")
|
||||
|
||||
|
||||
def test_server_playlist(plex, show):
|
||||
episodes = show.episodes()
|
||||
playlist = plex.createPlaylist('test_playlist', episodes[:3])
|
||||
playlist = plex.createPlaylist("test_playlist", episodes[:3])
|
||||
try:
|
||||
assert playlist.title == 'test_playlist'
|
||||
assert playlist.title == "test_playlist"
|
||||
with pytest.raises(NotFound):
|
||||
plex.playlist('<playlist-not-found>')
|
||||
plex.playlist("<playlist-not-found>")
|
||||
finally:
|
||||
playlist.delete()
|
||||
|
||||
|
@ -117,7 +127,7 @@ def test_server_playlists(plex, show):
|
|||
playlists = plex.playlists()
|
||||
count = len(playlists)
|
||||
episodes = show.episodes()
|
||||
playlist = plex.createPlaylist('test_playlist', episodes[:3])
|
||||
playlist = plex.createPlaylist("test_playlist", episodes[:3])
|
||||
try:
|
||||
playlists = plex.playlists()
|
||||
assert len(playlists) == count + 1
|
||||
|
@ -133,9 +143,9 @@ def test_server_history(plex, movie):
|
|||
|
||||
|
||||
def test_server_Server_query(plex):
|
||||
assert plex.query('/')
|
||||
assert plex.query("/")
|
||||
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):
|
||||
|
@ -144,28 +154,31 @@ def test_server_Server_session(account):
|
|||
def __init__(self):
|
||||
super(self.__class__, self).__init__()
|
||||
self.plexapi_session_test = True
|
||||
|
||||
# Test Code
|
||||
plex = PlexServer(utils.SERVER_BASEURL, account.authenticationToken, session=MySession())
|
||||
assert hasattr(plex._session, 'plexapi_session_test')
|
||||
plex = PlexServer(
|
||||
utils.SERVER_BASEURL, account.authenticationToken, session=MySession()
|
||||
)
|
||||
assert hasattr(plex._session, "plexapi_session_test")
|
||||
|
||||
|
||||
@pytest.mark.authenticated
|
||||
def test_server_token_in_headers(plex):
|
||||
headers = plex._headers()
|
||||
assert 'X-Plex-Token' in headers
|
||||
assert len(headers['X-Plex-Token']) >= 1
|
||||
assert "X-Plex-Token" in headers
|
||||
assert len(headers["X-Plex-Token"]) >= 1
|
||||
|
||||
|
||||
def test_server_createPlayQueue(plex, movie):
|
||||
playqueue = plex.createPlayQueue(movie, shuffle=1, repeat=1)
|
||||
assert 'shuffle=1' in playqueue._initpath
|
||||
assert 'repeat=1' in playqueue._initpath
|
||||
assert "shuffle=1" in playqueue._initpath
|
||||
assert "repeat=1" in playqueue._initpath
|
||||
assert playqueue.playQueueShuffled is True
|
||||
|
||||
|
||||
def test_server_client_not_found(plex):
|
||||
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):
|
||||
|
@ -174,38 +187,41 @@ def test_server_sessions(plex):
|
|||
|
||||
def test_server_isLatest(plex, mocker):
|
||||
from os import environ
|
||||
|
||||
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
|
||||
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):
|
||||
m = mocker.MagicMock(release='aa')
|
||||
with patch('plexapi.server.PlexServer.check_for_update', return_value=m):
|
||||
m = mocker.MagicMock(release="aa")
|
||||
with utils.patch('plexapi.server.PlexServer.check_for_update', return_value=m):
|
||||
with utils.callable_http_patch():
|
||||
plex.installUpdate()
|
||||
|
||||
|
||||
def test_server_check_for_update(plex, mocker):
|
||||
class R():
|
||||
class R:
|
||||
def __init__(self, **kwargs):
|
||||
self.download_key = 'plex.tv/release/1337'
|
||||
self.version = '1337'
|
||||
self.added = 'gpu transcode'
|
||||
self.fixed = 'fixed rare bug'
|
||||
self.downloadURL = 'http://path-to-update'
|
||||
self.state = 'downloaded'
|
||||
self.download_key = "plex.tv/release/1337"
|
||||
self.version = "1337"
|
||||
self.added = "gpu transcode"
|
||||
self.fixed = "fixed rare bug"
|
||||
self.downloadURL = "http://path-to-update"
|
||||
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)
|
||||
assert rel.download_key == 'plex.tv/release/1337'
|
||||
assert rel.version == '1337'
|
||||
assert rel.added == 'gpu transcode'
|
||||
assert rel.fixed == 'fixed rare bug'
|
||||
assert rel.downloadURL == 'http://path-to-update'
|
||||
assert rel.state == 'downloaded'
|
||||
assert rel.download_key == "plex.tv/release/1337"
|
||||
assert rel.version == "1337"
|
||||
assert rel.added == "gpu transcode"
|
||||
assert rel.fixed == "fixed rare bug"
|
||||
assert rel.downloadURL == "http://path-to-update"
|
||||
assert rel.state == "downloaded"
|
||||
|
||||
|
||||
@pytest.mark.client
|
||||
|
@ -222,31 +238,42 @@ def test_server_clients(plex):
|
|||
|
||||
|
||||
@pytest.mark.authenticated
|
||||
@pytest.mark.xfail(strict=False)
|
||||
def test_server_account(plex):
|
||||
account = plex.account()
|
||||
assert account.authToken
|
||||
# TODO: Figure out why this is missing from time to time.
|
||||
# assert account.mappingError == 'publisherror'
|
||||
assert account.mappingErrorMessage is None
|
||||
assert account.mappingState == 'mapped'
|
||||
if account.mappingError != 'unreachable':
|
||||
assert re.match(utils.REGEX_IPADDR, account.privateAddress)
|
||||
assert account.mappingState == "mapped"
|
||||
if account.mappingError != "unreachable":
|
||||
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 re.match(utils.REGEX_IPADDR, account.publicAddress)
|
||||
assert int(account.publicPort) >= 1000
|
||||
else:
|
||||
assert account.privateAddress == ''
|
||||
assert account.privateAddress == ""
|
||||
assert int(account.privatePort) == 0
|
||||
assert account.publicAddress == ''
|
||||
assert account.publicAddress == ""
|
||||
assert int(account.publicPort) == 0
|
||||
assert account.signInState == 'ok'
|
||||
assert account.signInState == "ok"
|
||||
assert isinstance(account.subscriptionActive, bool)
|
||||
if account.subscriptionActive:
|
||||
assert len(account.subscriptionFeatures)
|
||||
# Below check keeps failing.. it should go away.
|
||||
# else: assert sorted(account.subscriptionFeatures) == ['adaptive_bitrate',
|
||||
# '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)
|
||||
|
||||
|
||||
|
|
|
@ -1,31 +1,30 @@
|
|||
|
||||
def test_settings_group(plex):
|
||||
assert plex.settings.group('general')
|
||||
assert plex.settings.group("general")
|
||||
|
||||
|
||||
def test_settings_get(plex):
|
||||
# This is the value since it we havnt set any friendlyname
|
||||
# plex just default to computer name but it NOT in the settings.
|
||||
# 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
|
||||
assert isinstance(value, bytes)
|
||||
|
||||
|
||||
def test_settings_set(plex):
|
||||
cd = plex.settings.get('autoEmptyTrash')
|
||||
cd = plex.settings.get("autoEmptyTrash")
|
||||
old_value = cd.value
|
||||
new_value = not old_value
|
||||
cd.set(new_value)
|
||||
plex.settings.save()
|
||||
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):
|
||||
cd = plex.settings.get('OnDeckWindow')
|
||||
cd = plex.settings.get("OnDeckWindow")
|
||||
new_value = 99
|
||||
cd.set(new_value)
|
||||
plex.settings.save()
|
||||
plex._settings = None
|
||||
assert plex.settings.get('OnDeckWindow').value == 99
|
||||
assert plex.settings.get("OnDeckWindow").value == 99
|
||||
|
|
28
tests/test_sonos.py
Normal file
28
tests/test_sonos.py
Normal 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
|
|
@ -1,7 +1,8 @@
|
|||
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):
|
||||
|
@ -16,14 +17,14 @@ def is_sync_item_missing(device, sync_item):
|
|||
|
||||
|
||||
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):
|
||||
try:
|
||||
return item.getMedia()
|
||||
except BadRequest as e:
|
||||
if 'not_found' in str(e):
|
||||
if "not_found" in str(e):
|
||||
server.refreshSync()
|
||||
return None
|
||||
else:
|
||||
|
@ -33,9 +34,16 @@ def get_media(item, server):
|
|||
def test_add_movie_to_sync(clear_sync_device, movie):
|
||||
new_item = movie.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device)
|
||||
movie._server.refreshSync()
|
||||
item = utils.wait_until(get_sync_item_from_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)
|
||||
item = utils.wait_until(
|
||||
get_sync_item_from_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 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):
|
||||
new_item = movie.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device)
|
||||
movie._server.refreshSync()
|
||||
new_item_in_myplex = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device,
|
||||
sync_item=new_item)
|
||||
new_item_in_myplex = utils.wait_until(
|
||||
get_sync_item_from_server,
|
||||
delay=0.5,
|
||||
timeout=3,
|
||||
device=clear_sync_device,
|
||||
sync_item=new_item,
|
||||
)
|
||||
sync_items = clear_sync_device.syncItems()
|
||||
for item in sync_items.items:
|
||||
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):
|
||||
new_item = show.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device)
|
||||
show._server.refreshSync()
|
||||
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device,
|
||||
sync_item=new_item)
|
||||
item = utils.wait_until(
|
||||
get_sync_item_from_server,
|
||||
delay=0.5,
|
||||
timeout=3,
|
||||
device=clear_sync_device,
|
||||
sync_item=new_item,
|
||||
)
|
||||
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 [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list]
|
||||
|
||||
|
||||
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)
|
||||
season._server.refreshSync()
|
||||
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device,
|
||||
sync_item=new_item)
|
||||
item = utils.wait_until(
|
||||
get_sync_item_from_server,
|
||||
delay=0.5,
|
||||
timeout=3,
|
||||
device=clear_sync_device,
|
||||
sync_item=new_item,
|
||||
)
|
||||
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 [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):
|
||||
new_item = episode.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device)
|
||||
episode._server.refreshSync()
|
||||
item = utils.wait_until(get_sync_item_from_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)
|
||||
item = utils.wait_until(
|
||||
get_sync_item_from_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 episode.ratingKey == media_list[0].ratingKey
|
||||
|
||||
|
||||
def test_limited_watched(clear_sync_device, show):
|
||||
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()
|
||||
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device,
|
||||
sync_item=new_item)
|
||||
item = utils.wait_until(
|
||||
get_sync_item_from_server,
|
||||
delay=0.5,
|
||||
timeout=3,
|
||||
device=clear_sync_device,
|
||||
sync_item=new_item,
|
||||
)
|
||||
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 [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list]
|
||||
episodes[0].markWatched()
|
||||
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 [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list]
|
||||
|
||||
|
||||
def test_limited_unwatched(clear_sync_device, show):
|
||||
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()
|
||||
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device,
|
||||
sync_item=new_item)
|
||||
item = utils.wait_until(
|
||||
get_sync_item_from_server,
|
||||
delay=0.5,
|
||||
timeout=3,
|
||||
device=clear_sync_device,
|
||||
sync_item=new_item,
|
||||
)
|
||||
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 [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list]
|
||||
episodes[0].markWatched()
|
||||
show._server.refreshSync()
|
||||
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 [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list]
|
||||
|
||||
|
||||
def test_unlimited_and_watched(clear_sync_device, show):
|
||||
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()
|
||||
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device,
|
||||
sync_item=new_item)
|
||||
item = utils.wait_until(
|
||||
get_sync_item_from_server,
|
||||
delay=0.5,
|
||||
timeout=3,
|
||||
device=clear_sync_device,
|
||||
sync_item=new_item,
|
||||
)
|
||||
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 [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list]
|
||||
episodes[0].markWatched()
|
||||
show._server.refreshSync()
|
||||
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 [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list]
|
||||
|
||||
|
||||
def test_unlimited_and_unwatched(clear_sync_device, show):
|
||||
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()
|
||||
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device,
|
||||
sync_item=new_item)
|
||||
item = utils.wait_until(
|
||||
get_sync_item_from_server,
|
||||
delay=0.5,
|
||||
timeout=3,
|
||||
device=clear_sync_device,
|
||||
sync_item=new_item,
|
||||
)
|
||||
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 [e.ratingKey for e in episodes] == [m.ratingKey for m in media_list]
|
||||
episodes[0].markWatched()
|
||||
show._server.refreshSync()
|
||||
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 [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):
|
||||
new_item = artist.sync(AUDIO_BITRATE_192_KBPS, client=clear_sync_device)
|
||||
artist._server.refreshSync()
|
||||
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device,
|
||||
sync_item=new_item)
|
||||
item = utils.wait_until(
|
||||
get_sync_item_from_server,
|
||||
delay=0.5,
|
||||
timeout=3,
|
||||
device=clear_sync_device,
|
||||
sync_item=new_item,
|
||||
)
|
||||
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 [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):
|
||||
new_item = album.sync(AUDIO_BITRATE_192_KBPS, client=clear_sync_device)
|
||||
album._server.refreshSync()
|
||||
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device,
|
||||
sync_item=new_item)
|
||||
item = utils.wait_until(
|
||||
get_sync_item_from_server,
|
||||
delay=0.5,
|
||||
timeout=3,
|
||||
device=clear_sync_device,
|
||||
sync_item=new_item,
|
||||
)
|
||||
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 [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):
|
||||
new_item = track.sync(AUDIO_BITRATE_192_KBPS, client=clear_sync_device)
|
||||
track._server.refreshSync()
|
||||
item = utils.wait_until(get_sync_item_from_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)
|
||||
item = utils.wait_until(
|
||||
get_sync_item_from_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 track.ratingKey == media_list[0].ratingKey
|
||||
|
||||
|
||||
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)
|
||||
photo._server.refreshSync()
|
||||
item = utils.wait_until(get_sync_item_from_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)
|
||||
item = utils.wait_until(
|
||||
get_sync_item_from_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 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):
|
||||
new_item = movies.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device)
|
||||
movies._server.refreshSync()
|
||||
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device,
|
||||
sync_item=new_item)
|
||||
item = utils.wait_until(
|
||||
get_sync_item_from_server,
|
||||
delay=0.5,
|
||||
timeout=3,
|
||||
device=clear_sync_device,
|
||||
sync_item=new_item,
|
||||
)
|
||||
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 [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):
|
||||
new_item = tvshows.sync(VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device)
|
||||
tvshows._server.refreshSync()
|
||||
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device,
|
||||
sync_item=new_item)
|
||||
item = utils.wait_until(
|
||||
get_sync_item_from_server,
|
||||
delay=0.5,
|
||||
timeout=3,
|
||||
device=clear_sync_device,
|
||||
sync_item=new_item,
|
||||
)
|
||||
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 [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):
|
||||
new_item = music.sync(AUDIO_BITRATE_192_KBPS, client=clear_sync_device)
|
||||
music._server.refreshSync()
|
||||
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device,
|
||||
sync_item=new_item)
|
||||
item = utils.wait_until(
|
||||
get_sync_item_from_server,
|
||||
delay=0.5,
|
||||
timeout=3,
|
||||
device=clear_sync_device,
|
||||
sync_item=new_item,
|
||||
)
|
||||
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 [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):
|
||||
new_item = photos.sync(PHOTO_QUALITY_MEDIUM, client=clear_sync_device)
|
||||
photos._server.refreshSync()
|
||||
item = utils.wait_until(get_sync_item_from_server, delay=0.5, timeout=3, device=clear_sync_device,
|
||||
sync_item=new_item)
|
||||
item = utils.wait_until(
|
||||
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
|
||||
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)
|
||||
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
|
||||
)
|
||||
assert len(section_content) == len(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):
|
||||
items = movies.all()
|
||||
playlist = plex.createPlaylist('Sync: Movies', items)
|
||||
new_item = playlist.sync(videoQuality=VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device)
|
||||
playlist = plex.createPlaylist("Sync: Movies", items)
|
||||
new_item = playlist.sync(
|
||||
videoQuality=VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device
|
||||
)
|
||||
playlist._server.refreshSync()
|
||||
item = utils.wait_until(get_sync_item_from_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)
|
||||
item = utils.wait_until(
|
||||
get_sync_item_from_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 [e.ratingKey for e in items] == [m.ratingKey for m in media_list]
|
||||
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):
|
||||
items = show.episodes()
|
||||
playlist = plex.createPlaylist('Sync: TV Show', items)
|
||||
new_item = playlist.sync(videoQuality=VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device)
|
||||
playlist = plex.createPlaylist("Sync: TV Show", items)
|
||||
new_item = playlist.sync(
|
||||
videoQuality=VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device
|
||||
)
|
||||
playlist._server.refreshSync()
|
||||
item = utils.wait_until(get_sync_item_from_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)
|
||||
item = utils.wait_until(
|
||||
get_sync_item_from_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 [e.ratingKey for e in items] == [m.ratingKey for m in media_list]
|
||||
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):
|
||||
items = [movie, episode]
|
||||
playlist = plex.createPlaylist('Sync: Mixed', items)
|
||||
new_item = playlist.sync(videoQuality=VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device)
|
||||
playlist = plex.createPlaylist("Sync: Mixed", items)
|
||||
new_item = playlist.sync(
|
||||
videoQuality=VIDEO_QUALITY_3_MBPS_720p, client=clear_sync_device
|
||||
)
|
||||
playlist._server.refreshSync()
|
||||
item = utils.wait_until(get_sync_item_from_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)
|
||||
item = utils.wait_until(
|
||||
get_sync_item_from_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 [e.ratingKey for e in items] == [m.ratingKey for m in media_list]
|
||||
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):
|
||||
items = artist.tracks()
|
||||
playlist = plex.createPlaylist('Sync: Music', items)
|
||||
new_item = playlist.sync(audioBitrate=AUDIO_BITRATE_192_KBPS, client=clear_sync_device)
|
||||
playlist = plex.createPlaylist("Sync: Music", items)
|
||||
new_item = playlist.sync(
|
||||
audioBitrate=AUDIO_BITRATE_192_KBPS, client=clear_sync_device
|
||||
)
|
||||
playlist._server.refreshSync()
|
||||
item = utils.wait_until(get_sync_item_from_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)
|
||||
item = utils.wait_until(
|
||||
get_sync_item_from_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 [e.ratingKey for e in items] == [m.ratingKey for m in media_list]
|
||||
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):
|
||||
items = photoalbum.photos()
|
||||
playlist = plex.createPlaylist('Sync: Photos', items)
|
||||
new_item = playlist.sync(photoResolution=PHOTO_QUALITY_MEDIUM, client=clear_sync_device)
|
||||
playlist = plex.createPlaylist("Sync: Photos", items)
|
||||
new_item = playlist.sync(
|
||||
photoResolution=PHOTO_QUALITY_MEDIUM, client=clear_sync_device
|
||||
)
|
||||
playlist._server.refreshSync()
|
||||
item = utils.wait_until(get_sync_item_from_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)
|
||||
item = utils.wait_until(
|
||||
get_sync_item_from_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 [e.ratingKey for e in items] == [m.ratingKey for m in media_list]
|
||||
playlist.delete()
|
||||
|
|
|
@ -7,14 +7,17 @@ from plexapi.exceptions import NotFound
|
|||
|
||||
|
||||
def test_utils_toDatetime():
|
||||
assert str(utils.toDatetime('2006-03-03', format='%Y-%m-%d')) == '2006-03-03 00:00:00'
|
||||
#assert str(utils.toDatetime('0'))[:-9] in ['1970-01-01', '1969-12-31']
|
||||
assert (
|
||||
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 _squared(num, results, i, job_is_done_event=None):
|
||||
time.sleep(0.5)
|
||||
results[i] = num * num
|
||||
|
||||
starttime = time.time()
|
||||
results = utils.threaded(_squared, [[1], [2], [3], [4], [5]])
|
||||
assert results == [1, 4, 9, 16, 25]
|
||||
|
@ -28,28 +31,28 @@ def test_utils_downloadSessionImages():
|
|||
|
||||
|
||||
def test_utils_searchType():
|
||||
st = utils.searchType('movie')
|
||||
st = utils.searchType("movie")
|
||||
assert st == 1
|
||||
movie = utils.searchType(1)
|
||||
assert movie == '1'
|
||||
assert movie == "1"
|
||||
with pytest.raises(NotFound):
|
||||
utils.searchType('kekekekeke')
|
||||
utils.searchType("kekekekeke")
|
||||
|
||||
|
||||
def test_utils_joinArgs():
|
||||
test_dict = {'genre': 'action', 'type': 1337}
|
||||
assert utils.joinArgs(test_dict) == '?genre=action&type=1337'
|
||||
test_dict = {"genre": "action", "type": 1337}
|
||||
assert utils.joinArgs(test_dict) == "?genre=action&type=1337"
|
||||
|
||||
|
||||
def test_utils_cast():
|
||||
int_int = utils.cast(int, 1)
|
||||
int_str = utils.cast(int, '1')
|
||||
bool_str = utils.cast(bool, '1')
|
||||
int_str = utils.cast(int, "1")
|
||||
bool_str = utils.cast(bool, "1")
|
||||
bool_int = utils.cast(bool, 1)
|
||||
float_int = utils.cast(float, 1)
|
||||
float_float = utils.cast(float, 1.0)
|
||||
float_str = utils.cast(float, '1.2')
|
||||
float_nan = utils.cast(float, 'wut?')
|
||||
float_str = utils.cast(float, "1.2")
|
||||
float_nan = utils.cast(float, "wut?")
|
||||
assert int_int == 1 and isinstance(int_int, int)
|
||||
assert int_str == 1 and isinstance(int_str, int)
|
||||
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_nan != float_nan # nan is never equal
|
||||
with pytest.raises(ValueError):
|
||||
bool_str = utils.cast(bool, 'kek')
|
||||
bool_str = utils.cast(bool, "kek")
|
||||
|
||||
|
||||
def test_utils_download(plex, episode):
|
||||
|
@ -67,5 +70,9 @@ def test_utils_download(plex, episode):
|
|||
locations = episode.locations[0]
|
||||
session = episode._server._session
|
||||
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(episode.thumbUrl, plex._token, filename=episode.title, mocked=True)
|
||||
assert utils.download(
|
||||
url, plex._token, filename=locations, session=session, mocked=True
|
||||
)
|
||||
assert utils.download(
|
||||
episode.thumbUrl, plex._token, filename=episode.title, mocked=True
|
||||
)
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import os
|
||||
from datetime import datetime
|
||||
from time import sleep
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
import pytest
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
|
@ -20,15 +21,26 @@ def test_video_Movie_attributeerror(movie):
|
|||
|
||||
|
||||
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):
|
||||
movie.delete()
|
||||
|
||||
|
||||
def test_video_Movie_merge(movie, patched_http_call):
|
||||
movie.merge(1337)
|
||||
|
||||
|
||||
def test_video_Movie_addCollection(movie):
|
||||
labelname = 'Random_label'
|
||||
labelname = "Random_label"
|
||||
org_collection = [tag.tag for tag in movie.collections if tag]
|
||||
assert labelname not in org_collection
|
||||
movie.addCollection(labelname)
|
||||
|
@ -41,12 +53,18 @@ def test_video_Movie_addCollection(movie):
|
|||
|
||||
def test_video_Movie_getStreamURL(movie, account):
|
||||
key = movie.ratingKey
|
||||
assert movie.getStreamURL() == '{0}/video/:/transcode/universal/start.m3u8?X-Plex-Platform=Chrome©ts=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(videoResolution='800x600') == '{0}/video/:/transcode/universal/start.m3u8?X-Plex-Platform=Chrome©ts=1&mediaIndex=0&offset=0&path=%2Flibrary%2Fmetadata%2F{1}&videoResolution=800x600&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©ts=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(
|
||||
videoResolution="800x600"
|
||||
) == "{0}/video/:/transcode/universal/start.m3u8?X-Plex-Platform=Chrome©ts=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):
|
||||
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
|
||||
movie.reload()
|
||||
assert movie.isFullObject() is True
|
||||
|
@ -54,7 +72,7 @@ def test_video_Movie_isFullObject_and_reload(plex):
|
|||
assert movie_via_search.isFullObject() is False
|
||||
movie_via_search.reload()
|
||||
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
|
||||
movie_via_section_search.reload()
|
||||
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):
|
||||
filepaths1 = movie.download(savepath=str(tmpdir))
|
||||
assert len(filepaths1) >= 1
|
||||
filepaths2 = movie.download(savepath=str(tmpdir), videoResolution='500x300')
|
||||
filepaths2 = movie.download(savepath=str(tmpdir), videoResolution="500x300")
|
||||
assert len(filepaths2) >= 1
|
||||
|
||||
|
||||
|
@ -101,7 +119,7 @@ def test_video_Movie_upload_select_remove_subtitle(movie, subtitle):
|
|||
movie.uploadSubtitles(filepath)
|
||||
movie.reload()
|
||||
subtitles = [sub.title for sub in movie.subtitleStreams()]
|
||||
subname = subtitle.name.rsplit('.', 1)[0]
|
||||
subname = subtitle.name.rsplit(".", 1)[0]
|
||||
assert subname in subtitles
|
||||
|
||||
subtitleSelection = movie.subtitleStreams()[0]
|
||||
|
@ -124,31 +142,37 @@ def test_video_Movie_upload_select_remove_subtitle(movie, subtitle):
|
|||
|
||||
|
||||
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 utils.is_datetime(movie.addedAt)
|
||||
assert utils.is_metadata(movie.art)
|
||||
assert movie.artUrl
|
||||
assert movie.audienceRating == 8.5
|
||||
# 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
|
||||
assert movie.chapterSource is None
|
||||
assert movie.collections == []
|
||||
assert movie.contentRating in utils.CONTENTRATINGS
|
||||
assert all([i.tag in ['US', 'USA'] for i in movie.countries])
|
||||
assert [i.tag for i in movie.directors] == ['Nina Paley']
|
||||
assert all([i.tag in ["US", "USA"] for i in movie.countries])
|
||||
assert [i.tag for i in movie.directors] == ["Nina Paley"]
|
||||
assert movie.duration >= 160000
|
||||
assert movie.fields == []
|
||||
assert movie.posters()
|
||||
assert sorted([i.tag for i in movie.genres]) == ['Animation', 'Comedy', 'Fantasy', 'Musical', 'Romance']
|
||||
assert movie.guid == 'com.plexapp.agents.imdb://tt1172203?lang=en'
|
||||
assert sorted([i.tag for i in movie.genres]) == [
|
||||
"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.key)
|
||||
if movie.lastViewedAt:
|
||||
assert utils.is_datetime(movie.lastViewedAt)
|
||||
assert int(movie.librarySectionID) >= 1
|
||||
assert movie.listType == 'video'
|
||||
assert movie.listType == "video"
|
||||
assert movie.originalTitle is None
|
||||
assert utils.is_datetime(movie.originallyAvailableAt)
|
||||
assert movie.playlistItemID is None
|
||||
|
@ -156,25 +180,30 @@ def test_video_Movie_attrs(movies):
|
|||
assert utils.is_metadata(movie.primaryExtraKey)
|
||||
assert [i.tag for i in movie.producers] == []
|
||||
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 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.sessionKey is None
|
||||
assert movie.studio == 'Nina Paley'
|
||||
assert movie.studio == "Nina Paley"
|
||||
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 movie.title == 'Sita Sings the Blues'
|
||||
assert movie.titleSort == 'Sita Sings the Blues'
|
||||
assert movie.title == "Sita Sings the Blues"
|
||||
assert movie.titleSort == "Sita Sings the Blues"
|
||||
assert not movie.transcodeSessions
|
||||
assert movie.type == 'movie'
|
||||
assert movie.type == "movie"
|
||||
assert movie.updatedAt > datetime(2017, 1, 1)
|
||||
assert movie.userRating is None
|
||||
assert movie.viewCount == 0
|
||||
assert utils.is_int(movie.viewOffset, gte=0)
|
||||
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
|
||||
# Audio
|
||||
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)
|
||||
# Video
|
||||
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 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.codecID is None
|
||||
assert video.colorSpace is None
|
||||
|
@ -238,7 +270,7 @@ def test_video_Movie_attrs(movies):
|
|||
assert utils.is_int(video.level)
|
||||
assert video.profile in utils.PROFILES
|
||||
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._server._baseurl == utils.SERVER_BASEURL
|
||||
assert utils.is_int(video.streamType)
|
||||
|
@ -262,7 +294,7 @@ def test_video_Movie_attrs(movies):
|
|||
assert stream1.bitDepth in (8, None)
|
||||
assert utils.is_int(stream1.bitrate)
|
||||
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.codecID is None
|
||||
assert stream1.colorSpace is None
|
||||
|
@ -279,7 +311,7 @@ def test_video_Movie_attrs(movies):
|
|||
assert utils.is_int(stream1.level)
|
||||
assert stream1.profile in utils.PROFILES
|
||||
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._server._baseurl == utils.SERVER_BASEURL
|
||||
assert utils.is_int(stream1.streamType)
|
||||
|
@ -318,8 +350,107 @@ def test_video_Movie_history(movie):
|
|||
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):
|
||||
assert show.title == 'Game of Thrones'
|
||||
assert show.title == "Game of Thrones"
|
||||
|
||||
|
||||
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):
|
||||
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):
|
||||
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!")
|
||||
|
||||
|
||||
def test_video_Show_attrs(show):
|
||||
assert utils.is_datetime(show.addedAt)
|
||||
assert utils.is_metadata(show.art, contains='/art/')
|
||||
assert utils.is_metadata(show.banner, contains='/banner/')
|
||||
assert utils.is_metadata(show.art, contains="/art/")
|
||||
assert utils.is_metadata(show.banner, contains="/banner/")
|
||||
assert utils.is_int(show.childCount)
|
||||
assert show.contentRating in utils.CONTENTRATINGS
|
||||
assert utils.is_int(show.duration, gte=1600000)
|
||||
assert utils.is_section(show._initpath)
|
||||
# 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()
|
||||
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
|
||||
assert utils.is_metadata(show._initpath)
|
||||
assert utils.is_int(show.index)
|
||||
|
@ -362,21 +497,31 @@ def test_video_Show_attrs(show):
|
|||
if show.lastViewedAt:
|
||||
assert utils.is_datetime(show.lastViewedAt)
|
||||
assert utils.is_int(show.leafCount)
|
||||
assert show.listType == 'video'
|
||||
assert show.listType == "video"
|
||||
assert len(show.locations[0]) >= 10
|
||||
assert utils.is_datetime(show.originallyAvailableAt)
|
||||
assert show.rating >= 8.0
|
||||
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.actors])[:4] == ['Aidan Gillen', 'Aimee Richardson', 'Alexander Siddig', 'Alfie Allen'] # noqa
|
||||
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.actors])[:4] == [
|
||||
"Aidan Gillen",
|
||||
"Aimee Richardson",
|
||||
"Alexander Siddig",
|
||||
"Alfie Allen",
|
||||
] # noqa
|
||||
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_metadata(show.theme, contains='/theme/')
|
||||
assert utils.is_metadata(show.thumb, contains='/thumb/')
|
||||
assert show.title == 'Game of Thrones'
|
||||
assert show.titleSort == 'Game of Thrones'
|
||||
assert show.type == 'show'
|
||||
assert utils.is_metadata(show.theme, contains="/theme/")
|
||||
assert utils.is_metadata(show.thumb, contains="/thumb/")
|
||||
assert show.title == "Game of Thrones"
|
||||
assert show.titleSort == "Game of Thrones"
|
||||
assert show.type == "show"
|
||||
assert utils.is_datetime(show.updatedAt)
|
||||
assert utils.is_int(show.viewCount, 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):
|
||||
show = tvshows.get('The 100')
|
||||
show = tvshows.get("The 100")
|
||||
show.episodes()[0].markWatched()
|
||||
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):
|
||||
show = tvshows.get('The 100')
|
||||
show = tvshows.get("The 100")
|
||||
episodes = show.episodes()
|
||||
episodes[0].markWatched()
|
||||
unwatched = show.unwatched()
|
||||
|
@ -409,21 +554,21 @@ def test_video_Show_unwatched(tvshows):
|
|||
def test_video_Show_location(plex):
|
||||
# This should be a part of test test_video_Show_attrs but is excluded
|
||||
# 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
|
||||
|
||||
|
||||
def test_video_Show_reload(plex):
|
||||
show = plex.library.section('TV Shows').get('Game of Thrones')
|
||||
assert utils.is_metadata(show._initpath, prefix='/library/sections/')
|
||||
show = plex.library.section("TV Shows").get("Game of Thrones")
|
||||
assert utils.is_metadata(show._initpath, prefix="/library/sections/")
|
||||
assert len(show.roles) == 3
|
||||
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
|
||||
|
||||
|
||||
def test_video_Show_episodes(tvshows):
|
||||
show = tvshows.get('The 100')
|
||||
show = tvshows.get("The 100")
|
||||
episodes = show.episodes()
|
||||
episodes[0].markWatched()
|
||||
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):
|
||||
season = show.season('Season 1')
|
||||
season = show.season("Season 1")
|
||||
filepaths = season.download(savepath=str(tmpdir))
|
||||
assert len(filepaths) >= 4
|
||||
|
||||
|
@ -445,14 +590,16 @@ def test_video_Season_download(monkeydownload, tmpdir, show):
|
|||
def test_video_Episode_download(monkeydownload, tmpdir, episode):
|
||||
f = episode.download(savepath=str(tmpdir))
|
||||
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
|
||||
|
||||
|
||||
def test_video_Show_thumbUrl(show):
|
||||
assert utils.SERVER_BASEURL in show.thumbUrl
|
||||
assert '/library/metadata/' in show.thumbUrl
|
||||
assert '/thumb/' in show.thumbUrl
|
||||
assert "/library/metadata/" in show.thumbUrl
|
||||
assert "/thumb/" in show.thumbUrl
|
||||
|
||||
|
||||
# Analyze seems to fail intermittently
|
||||
|
@ -476,7 +623,7 @@ def test_video_Show_refresh(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):
|
||||
|
@ -485,11 +632,11 @@ def test_video_Show_isWatched(show):
|
|||
|
||||
def test_video_Show_section(show):
|
||||
section = show.section()
|
||||
assert section.title == 'TV Shows'
|
||||
assert section.title == "TV Shows"
|
||||
|
||||
|
||||
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)
|
||||
with pytest.raises(BadRequest):
|
||||
show.episode()
|
||||
|
@ -507,7 +654,7 @@ def test_video_Episode_history(episode):
|
|||
# Analyze seems to fail intermittently
|
||||
@pytest.mark.xfail
|
||||
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()
|
||||
|
||||
|
||||
|
@ -515,31 +662,33 @@ def test_video_Episode_attrs(episode):
|
|||
assert utils.is_datetime(episode.addedAt)
|
||||
assert episode.contentRating in utils.CONTENTRATINGS
|
||||
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 episode.grandparentTitle == 'Game of Thrones'
|
||||
assert episode.grandparentTitle == "Game of Thrones"
|
||||
assert episode.index == 1
|
||||
assert utils.is_metadata(episode._initpath)
|
||||
assert utils.is_metadata(episode.key)
|
||||
assert episode.listType == 'video'
|
||||
assert episode.listType == "video"
|
||||
assert utils.is_datetime(episode.originallyAvailableAt)
|
||||
assert utils.is_int(episode.parentIndex)
|
||||
assert utils.is_metadata(episode.parentKey)
|
||||
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 utils.is_int(episode.ratingKey)
|
||||
assert episode._server._baseurl == utils.SERVER_BASEURL
|
||||
assert utils.is_string(episode.summary, gte=100)
|
||||
assert utils.is_metadata(episode.thumb, contains='/thumb/')
|
||||
assert episode.title == 'Winter Is Coming'
|
||||
assert episode.titleSort == 'Winter Is Coming'
|
||||
assert utils.is_metadata(episode.thumb, contains="/thumb/")
|
||||
assert episode.title == "Winter Is Coming"
|
||||
assert episode.titleSort == "Winter Is Coming"
|
||||
assert not episode.transcodeSessions
|
||||
assert episode.type == 'episode'
|
||||
assert episode.type == "episode"
|
||||
assert utils.is_datetime(episode.updatedAt)
|
||||
assert utils.is_int(episode.viewCount, gte=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.isWatched in [True, False]
|
||||
# Media
|
||||
|
@ -577,12 +726,12 @@ def test_video_Episode_attrs(episode):
|
|||
def test_video_Season(show):
|
||||
seasons = show.seasons()
|
||||
assert len(seasons) == 2
|
||||
assert ['Season 1', 'Season 2'] == [s.title for s in seasons[:2]]
|
||||
assert show.season('Season 1') == seasons[0]
|
||||
assert ["Season 1", "Season 2"] == [s.title for s in seasons[:2]]
|
||||
assert show.season("Season 1") == seasons[0]
|
||||
|
||||
|
||||
def test_video_Season_history(show):
|
||||
season = show.season('Season 1')
|
||||
season = show.season("Season 1")
|
||||
season.markWatched()
|
||||
history = season.history()
|
||||
assert len(history)
|
||||
|
@ -590,7 +739,7 @@ def test_video_Season_history(show):
|
|||
|
||||
|
||||
def test_video_Season_attrs(show):
|
||||
season = show.season('Season 1')
|
||||
season = show.season("Season 1")
|
||||
assert utils.is_datetime(season.addedAt)
|
||||
assert season.index == 1
|
||||
assert utils.is_metadata(season._initpath)
|
||||
|
@ -598,17 +747,17 @@ def test_video_Season_attrs(show):
|
|||
if season.lastViewedAt:
|
||||
assert utils.is_datetime(season.lastViewedAt)
|
||||
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_int(season.parentRatingKey)
|
||||
assert season.parentTitle == 'Game of Thrones'
|
||||
assert season.parentTitle == "Game of Thrones"
|
||||
assert utils.is_int(season.ratingKey)
|
||||
assert season._server._baseurl == utils.SERVER_BASEURL
|
||||
assert season.summary == ''
|
||||
assert utils.is_metadata(season.thumb, contains='/thumb/')
|
||||
assert season.title == 'Season 1'
|
||||
assert season.titleSort == 'Season 1'
|
||||
assert season.type == 'season'
|
||||
assert season.summary == ""
|
||||
assert utils.is_metadata(season.thumb, contains="/thumb/")
|
||||
assert season.title == "Season 1"
|
||||
assert season.titleSort == "Season 1"
|
||||
assert season.type == "season"
|
||||
assert utils.is_datetime(season.updatedAt)
|
||||
assert utils.is_int(season.viewCount, 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):
|
||||
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 season.ratingKey == season_by_name.ratingKey
|
||||
|
||||
|
||||
def test_video_Season_watched(tvshows):
|
||||
show = tvshows.get('Game of Thrones')
|
||||
show = tvshows.get("Game of Thrones")
|
||||
season = show.season(1)
|
||||
sne = show.season('Season 1')
|
||||
sne = show.season("Season 1")
|
||||
assert season == sne
|
||||
season.markWatched()
|
||||
assert season.isWatched
|
||||
|
||||
|
||||
def test_video_Season_unwatched(tvshows):
|
||||
season = tvshows.get('Game of Thrones').season(1)
|
||||
season = tvshows.get("Game of Thrones").season(1)
|
||||
season.markUnwatched()
|
||||
assert not season.isWatched
|
||||
|
||||
|
||||
def test_video_Season_get(show):
|
||||
episode = show.season(1).get('Winter Is Coming')
|
||||
assert episode.title == 'Winter Is Coming'
|
||||
episode = show.season(1).get("Winter Is Coming")
|
||||
assert episode.title == "Winter Is Coming"
|
||||
|
||||
|
||||
def test_video_Season_episode(show):
|
||||
episode = show.season(1).get('Winter Is Coming')
|
||||
assert episode.title == 'Winter Is Coming'
|
||||
episode = show.season(1).get("Winter Is Coming")
|
||||
assert episode.title == "Winter Is Coming"
|
||||
|
||||
|
||||
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):
|
||||
# we want to check this that all the urls are correct
|
||||
movie_library_search = plex.library.section('Movies').search('Elephants Dream')[0]
|
||||
movie_search = plex.search('Elephants Dream')[0]
|
||||
movie_section_get = plex.library.section('Movies').get('Elephants Dream')
|
||||
movie_library_search = plex.library.section("Movies").search("Elephants Dream")[0]
|
||||
movie_search = plex.search("Elephants Dream")[0]
|
||||
movie_section_get = plex.library.section("Movies").get("Elephants Dream")
|
||||
movie_library_search_key = movie_library_search.key
|
||||
movie_search_key = movie_search.key
|
||||
movie_section_get_key = movie_section_get.key
|
||||
assert movie_library_search_key == movie_library_search.reload().key == movie_search_key == movie_search.reload().key == movie_section_get_key == movie_section_get.reload().key # 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')
|
||||
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
|
||||
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_search_key = tvshow_search.key
|
||||
tvshow_section_get_key = tvshow_section_get.key
|
||||
assert tvshow_library_search_key == tvshow_library_search.reload().key == tvshow_search_key == tvshow_search.reload().key == tvshow_section_get_key == tvshow_section_get.reload().key # 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_search = tvshow_search.season(1)
|
||||
season_section_get = tvshow_section_get.season(1)
|
||||
season_library_search_key = season_library_search.key
|
||||
season_search_key = season_search.key
|
||||
season_section_get_key = season_section_get.key
|
||||
assert season_library_search_key == season_library_search.reload().key == season_search_key == season_search.reload().key == season_section_get_key == season_section_get.reload().key # 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_search = tvshow_search.episode(season=1, episode=1)
|
||||
episode_section_get = tvshow_section_get.episode(season=1, episode=1)
|
||||
episode_library_search_key = episode_library_search.key
|
||||
episode_search_key = episode_search.key
|
||||
episode_section_get_key = episode_section_get.key
|
||||
assert episode_library_search_key == episode_library_search.reload().key == episode_search_key == episode_search.reload().key == episode_section_get_key == episode_section_get.reload().key # 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):
|
||||
|
@ -703,7 +880,9 @@ def test_video_exists_accessible(movie, episode):
|
|||
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):
|
||||
plex.optimizedItems(removeAll=True)
|
||||
movie.optimize(targetTagID=1)
|
||||
|
|
|
@ -1,55 +1,232 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
The script is used to bootstrap a docker container with Plex and with
|
||||
all the libraries required for testing.
|
||||
The script is used to bootstrap a the test enviroment for plexapi
|
||||
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 os
|
||||
import plexapi
|
||||
import shutil
|
||||
import socket
|
||||
import time
|
||||
import zipfile
|
||||
from glob import glob
|
||||
from shutil import copyfile, rmtree
|
||||
from os import makedirs
|
||||
from shutil import copyfile, which
|
||||
from subprocess import call
|
||||
from tqdm import tqdm
|
||||
from uuid import uuid4
|
||||
from plexapi.compat import which, makedirs
|
||||
|
||||
import plexapi
|
||||
from plexapi.exceptions import BadRequest, NotFound
|
||||
from plexapi.myplex import MyPlexAccount
|
||||
from plexapi.server import PlexServer
|
||||
from plexapi.utils import download, SEARCHTYPES
|
||||
from plexapi.utils import SEARCHTYPES
|
||||
from tqdm import tqdm
|
||||
|
||||
DOCKER_CMD = [
|
||||
'docker', 'run', '-d',
|
||||
'--name', 'plex-test-%(container_name_extra)s%(image_tag)s',
|
||||
'--restart', 'on-failure',
|
||||
'-p', '32400:32400/tcp',
|
||||
'-p', '3005:3005/tcp',
|
||||
'-p', '8324:8324/tcp',
|
||||
'-p', '32469:32469/tcp',
|
||||
'-p', '1900:1900/udp',
|
||||
'-p', '32410:32410/udp',
|
||||
'-p', '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'
|
||||
"docker",
|
||||
"run",
|
||||
"-d",
|
||||
"--name",
|
||||
"plex-test-%(container_name_extra)s%(image_tag)s",
|
||||
"--restart",
|
||||
"on-failure",
|
||||
"-p",
|
||||
"32400:32400/tcp",
|
||||
"-p",
|
||||
"3005:3005/tcp",
|
||||
"-p",
|
||||
"8324:8324/tcp",
|
||||
"-p",
|
||||
"32469:32469/tcp",
|
||||
"-p",
|
||||
"1900:1900/udp",
|
||||
"-p",
|
||||
"32410:32410/udp",
|
||||
"-p",
|
||||
"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():
|
||||
""" 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)
|
||||
if i[4][0] not in ('127.0.0.1', '::1') and not i[4][0].startswith('fe80:')]))
|
||||
available_ips = list(
|
||||
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
|
||||
|
||||
|
||||
|
@ -62,14 +239,14 @@ def get_plex_account(opts):
|
|||
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 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 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):
|
||||
|
@ -83,52 +260,63 @@ def add_library_section(server, section):
|
|||
server.library.add(**section)
|
||||
return True
|
||||
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)
|
||||
continue
|
||||
raise
|
||||
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):
|
||||
processed_media = 0
|
||||
expected_media_count = section.pop('expected_media_count', 0)
|
||||
expected_media_type = (section['type'], )
|
||||
if section['type'] == 'artist':
|
||||
expected_media_type = ('artist', 'album', 'track')
|
||||
expected_media_count = section.pop("expected_media_count", 0)
|
||||
expected_media_type = (section["type"],)
|
||||
if section["type"] == "artist":
|
||||
expected_media_type = ("artist", "album", "track")
|
||||
expected_media_type = tuple(SEARCHTYPES[t] for t in expected_media_type)
|
||||
|
||||
def alert_callback(data):
|
||||
""" Listen to the Plex notifier to determine when metadata scanning is complete. """
|
||||
global processed_media
|
||||
if data['type'] == 'timeline':
|
||||
for entry in data['TimelineEntry']:
|
||||
if entry.get('identifier', 'com.plexapp.plugins.library') == 'com.plexapp.plugins.library':
|
||||
if data["type"] == "timeline":
|
||||
for entry in data["TimelineEntry"]:
|
||||
if (
|
||||
entry.get("identifier", "com.plexapp.plugins.library")
|
||||
== "com.plexapp.plugins.library"
|
||||
):
|
||||
# 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
|
||||
if entry['state'] == 5:
|
||||
if entry["state"] == 5:
|
||||
cnt = 1
|
||||
if entry['type'] == SEARCHTYPES['show']:
|
||||
show = server.library.sectionByID(str(entry['sectionID'])).get(entry['title'])
|
||||
if entry["type"] == SEARCHTYPES["show"]:
|
||||
show = server.library.sectionByID(
|
||||
str(entry["sectionID"])
|
||||
).get(entry["title"])
|
||||
cnt = show.leafCount
|
||||
bar.update(cnt)
|
||||
processed_media += cnt
|
||||
# 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()
|
||||
processed_media += 1
|
||||
|
||||
runtime = 0
|
||||
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)
|
||||
time.sleep(3)
|
||||
add_library_section(server, section)
|
||||
while bar.n < bar.total:
|
||||
if runtime >= 120:
|
||||
print('Metadata scan taking too long, but will continue anyway..')
|
||||
print("Metadata scan taking too long, but will continue anyway..")
|
||||
break
|
||||
time.sleep(3)
|
||||
runtime = int(time.time() - start)
|
||||
|
@ -136,60 +324,134 @@ def create_section(server, section, opts):
|
|||
notifier.stop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
default_ip = get_default_ip()
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
# Authentication arguments
|
||||
mg = parser.add_mutually_exclusive_group()
|
||||
g = mg.add_argument_group()
|
||||
g.add_argument('--username', help='Your Plex username')
|
||||
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('--unclaimed', help='Do not claim the server', default=False, action='store_true')
|
||||
g.add_argument("--username", help="Your Plex username")
|
||||
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(
|
||||
"--unclaimed",
|
||||
help="Do not claim the server",
|
||||
default=False,
|
||||
action="store_true",
|
||||
)
|
||||
# Test environment arguments
|
||||
parser.add_argument('--timezone', help='Timezone to set inside plex', default='UTC') # noqa
|
||||
parser.add_argument('--destination', help='Local path where to store all the media', default=os.path.join(os.getcwd(), 'plex')) # 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
|
||||
parser.add_argument(
|
||||
"--no-docker", help="Use docker", default=False, action="store_true"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--timezone", help="Timezone to set inside plex", default="UTC"
|
||||
) # noqa
|
||||
parser.add_argument(
|
||||
"--destination",
|
||||
help="Local path where to store all the media",
|
||||
default=os.path.join(os.getcwd(), "plex"),
|
||||
) # 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()
|
||||
|
||||
# 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)
|
||||
path = os.path.realpath(os.path.expanduser(opts.destination))
|
||||
makedirs(os.path.join(path, 'media'), exist_ok=True)
|
||||
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)
|
||||
media_path = os.path.join(path, "media")
|
||||
makedirs(media_path, exist_ok=True)
|
||||
|
||||
# Download the Plex Docker image
|
||||
if opts.no_docker is False:
|
||||
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
|
||||
|
||||
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
|
||||
print('Waiting for the Plex container to start..')
|
||||
print("Waiting for the Plex to start..")
|
||||
start = time.time()
|
||||
runtime = 0
|
||||
server = None
|
||||
|
@ -198,145 +460,127 @@ if __name__ == '__main__':
|
|||
if account:
|
||||
server = account.device(opts.server_name).connect()
|
||||
else:
|
||||
server = PlexServer('http://%s:32400' % opts.advertise_ip)
|
||||
server = PlexServer("http://%s:32400" % opts.advertise_ip)
|
||||
if opts.accept_eula:
|
||||
server.settings.get('acceptedEULA').set(True)
|
||||
server.settings.get("acceptedEULA").set(True)
|
||||
server.settings.save()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
|
||||
except Exception as err:
|
||||
print(err)
|
||||
time.sleep(1)
|
||||
runtime = time.time() - start
|
||||
if not server:
|
||||
raise SystemExit('Server didnt appear in your account after %ss' % opts.bootstrap_timeout)
|
||||
print('Plex container started after %ss, downloading content' % int(runtime))
|
||||
|
||||
# Download video_stub.mp4
|
||||
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)
|
||||
raise SystemExit(
|
||||
"Server didnt appear in your account after %ss" % opts.bootstrap_timeout
|
||||
)
|
||||
print("Plex container started after %ss, setting up content" % int(runtime))
|
||||
|
||||
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
|
||||
if opts.with_movies:
|
||||
print('Preparing movie section..')
|
||||
movies_path = os.path.join(path, 'media', 'Movies')
|
||||
makedirs(movies_path, exist_ok=True)
|
||||
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(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))
|
||||
movies_path = os.path.join(media_path, "Movies")
|
||||
num_movies = setup_movies(movies_path)
|
||||
sections.append(
|
||||
dict(
|
||||
name="Movies",
|
||||
type="movie",
|
||||
location="/data/Movies" if opts.no_docker is False else movies_path,
|
||||
agent="com.plexapp.agents.imdb",
|
||||
scanner="Plex Movie Scanner",
|
||||
expected_media_count=num_movies,
|
||||
)
|
||||
)
|
||||
|
||||
# Prepare TV Show section
|
||||
if opts.with_shows:
|
||||
print('Preparing TV-Shows section..')
|
||||
tvshows_path = os.path.join(path, 'media', 'TV-Shows')
|
||||
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(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))
|
||||
tvshows_path = os.path.join(media_path, "TV-Shows")
|
||||
num_ep = setup_show(tvshows_path)
|
||||
|
||||
sections.append(
|
||||
dict(
|
||||
name="TV Shows",
|
||||
type="show",
|
||||
location="/data/TV-Shows" if opts.no_docker is False else tvshows_path,
|
||||
agent="com.plexapp.agents.thetvdb",
|
||||
scanner="Plex Series Scanner",
|
||||
expected_media_count=num_ep,
|
||||
)
|
||||
)
|
||||
|
||||
# Prepare Music section
|
||||
if opts.with_music:
|
||||
print('Preparing Music section..')
|
||||
music_path = os.path.join(path, 'media', 'Music')
|
||||
makedirs(music_path, exist_ok=True)
|
||||
expected_media_count = 0
|
||||
artist_dst = os.path.join(music_path, 'Infinite State')
|
||||
dest_path = os.path.join(artist_dst, 'Unmastered Impulses')
|
||||
if not os.path.isdir(dest_path):
|
||||
zip_path = os.path.join(artist_dst, 'Unmastered Impulses.zip')
|
||||
if os.path.isfile(zip_path):
|
||||
with zipfile.ZipFile(zip_path, 'r') as handle:
|
||||
handle.extractall(artist_dst)
|
||||
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))
|
||||
music_path = os.path.join(media_path, "Music")
|
||||
song_c = setup_music(music_path)
|
||||
|
||||
sections.append(
|
||||
dict(
|
||||
name="Music",
|
||||
type="artist",
|
||||
location="/data/Music" if opts.no_docker is False else music_path,
|
||||
agent="com.plexapp.agents.lastfm",
|
||||
scanner="Plex Music Scanner",
|
||||
expected_media_count=song_c,
|
||||
)
|
||||
)
|
||||
|
||||
# Prepare Photos section
|
||||
if opts.with_photos:
|
||||
print('Preparing Photos section..')
|
||||
photos_path = os.path.join(path, 'media', 'Photos')
|
||||
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)
|
||||
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))
|
||||
photos_path = os.path.join(media_path, "Photos")
|
||||
has_photos = setup_images(photos_path)
|
||||
|
||||
sections.append(
|
||||
dict(
|
||||
name="Photos",
|
||||
type="photo",
|
||||
location="/data/Photos" if opts.no_docker is False else photos_path,
|
||||
agent="com.plexapp.agents.none",
|
||||
scanner="Plex Photo Scanner",
|
||||
expected_media_count=has_photos,
|
||||
)
|
||||
)
|
||||
|
||||
# Create the Plex library in our instance
|
||||
if sections:
|
||||
print('Creating the Plex libraries in our instance')
|
||||
print("Creating the Plex libraries on %s" % server.friendlyName)
|
||||
for section in sections:
|
||||
create_section(server, section, opts)
|
||||
|
||||
# Share this instance with the specified username
|
||||
if account:
|
||||
shared_username = os.environ.get('SHARED_USERNAME', 'PKKid')
|
||||
shared_username = os.environ.get("SHARED_USERNAME", "PKKid")
|
||||
try:
|
||||
user = account.user(shared_username)
|
||||
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:
|
||||
pass
|
||||
|
||||
# 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:
|
||||
print('Auth token is %s' % account.authenticationToken)
|
||||
print('Server %s is ready to use!' % opts.server_name)
|
||||
print("Auth token is %s" % account.authenticationToken)
|
||||
print("Server %s is ready to use!" % opts.server_name)
|
||||
|
|
|
@ -10,10 +10,9 @@ Original contribution by lad1337.
|
|||
import argparse
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from urllib.parse import unquote
|
||||
|
||||
from plexapi import utils
|
||||
from plexapi.compat import unquote
|
||||
from plexapi.video import Episode, Movie, 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)
|
||||
server = servers[0].connect()
|
||||
return server.fetchItem(key)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Command line parser
|
||||
from plexapi import CONFIG
|
||||
|
@ -73,7 +72,7 @@ if __name__ == '__main__':
|
|||
default=CONFIG.get('auth.myplex_username'))
|
||||
parser.add_argument('-p', '--password', help='Your Plex 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()
|
||||
# Search item to download
|
||||
account = utils.getMyPlexAccount(opts)
|
||||
|
@ -86,4 +85,3 @@ if __name__ == '__main__':
|
|||
filepath = utils.download(url, token=account.authenticationToken, filename=filename, savepath=os.getcwd(),
|
||||
session=item._server._session, showstatus=True)
|
||||
#print(' %s' % filepath)
|
||||
|
||||
|
|
Loading…
Reference in a new issue