style: lint all python files (#1228)

This commit is contained in:
ReenigneArcher 2023-08-28 23:29:39 -04:00 committed by GitHub
parent dba3369f55
commit a6b6ebfbff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 131 additions and 105 deletions

10
.flake8
View file

@ -5,12 +5,14 @@
# E701: multiple statements on one line (colon) # E701: multiple statements on one line (colon)
# E702: multiple statements on one line (semicolon) # E702: multiple statements on one line (semicolon)
# E731: do not assign a lambda expression, use a def # E731: do not assign a lambda expression, use a def
# W293: blank line contains whitespace
# W503: line break before binary operator # W503: line break before binary operator
# W605: invalid escape sequence # W605: invalid escape sequence
[flake8] [flake8]
ignore=E128,E701,E702,E731,W293,W503,W605 ignore=E128,E701,E702,E731,W503,W605
exclude=compat.py exclude=compat.py,venv
per-file-ignores =
tests/payloads.py:E501
max-complexity = -1 max-complexity = -1
max-line-length = 125 # The GitHub editor is 127 chars wide
max-line-length = 127
show-source = True show-source = True

View file

@ -54,9 +54,13 @@ jobs:
run: | run: |
. venv/bin/activate . venv/bin/activate
# stop the build if there are Python syntax errors or undefined names # stop the build if there are Python syntax errors or undefined names
flake8 plexapi --count --select=E9,F63,F7,F82 --show-source --statistics echo "::group::flake8 pass 1"
flake8 --count --select=E9,F63,F7,F82 --show-source --statistics
echo "::endgroup::"
# The GitHub editor is 127 chars wide # The GitHub editor is 127 chars wide
flake8 plexapi --count --max-complexity=12 --max-line-length=127 --statistics echo "::group::flake8 pass 2"
flake8 --count --max-complexity=12 --max-line-length=127 --statistics
echo "::endgroup::"
pytest: pytest:

View file

@ -12,12 +12,13 @@
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
import copy, sys import copy
from os.path import abspath, dirname, join from os.path import abspath, dirname, join
import sys
path = dirname(dirname(abspath(__file__))) path = dirname(dirname(abspath(__file__)))
sys.path.append(path) sys.path.append(path)
sys.path.append(join(path, 'plexapi')) sys.path.append(join(path, 'plexapi'))
import plexapi import plexapi # noqa: E402
extensions = [ extensions = [
'sphinx.ext.autodoc', 'sphinx.ext.autodoc',
@ -27,13 +28,17 @@ extensions = [
] ]
# -- Monkey-patch docstring to not auto-link :ivars ------------------------ # -- Monkey-patch docstring to not auto-link :ivars ------------------------
from sphinx.domains.python import PythonDomain from sphinx.domains.python import PythonDomain # noqa: E402
print('Monkey-patching PythonDomain.resolve_xref()') print('Monkey-patching PythonDomain.resolve_xref()')
old_resolve_xref = copy.deepcopy(PythonDomain.resolve_xref) old_resolve_xref = copy.deepcopy(PythonDomain.resolve_xref)
def new_resolve_xref(*args): def new_resolve_xref(*args):
if '.' not in args[5]: # target if '.' not in args[5]: # target
return None return None
return old_resolve_xref(*args) return old_resolve_xref(*args)
PythonDomain.resolve_xref = new_resolve_xref PythonDomain.resolve_xref = new_resolve_xref
# -- Napoleon Settings ----------------------------------------------------- # -- Napoleon Settings -----------------------------------------------------
@ -79,7 +84,7 @@ author = 'M.Shepanski'
# The short X.Y version. # The short X.Y version.
version = plexapi.VERSION version = plexapi.VERSION
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
#release = '2.0.2' # release = '2.0.2'
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
@ -137,7 +142,7 @@ html_context = {'css_files': ['_static/custom.css']}
html_theme_options = { html_theme_options = {
'collapse_navigation': False, 'collapse_navigation': False,
'display_version': False, 'display_version': False,
#'navigation_depth': 3, # 'navigation_depth': 3,
} }
@ -238,17 +243,17 @@ htmlhelp_basename = 'PythonPlexAPIdoc'
# -- Options for LaTeX output --------------------------------------------- # -- Options for LaTeX output ---------------------------------------------
latex_elements = { latex_elements = {
# The paper size ('letterpaper' or 'a4paper'). # The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper', # 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt'). # The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt', # 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble. # Additional stuff for the LaTeX preamble.
# 'preamble': '', # 'preamble': '',
# Latex figure (float) alignment # Latex figure (float) alignment
# 'figure_align': 'htbp', # 'figure_align': 'htbp',
} }
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples

View file

@ -919,7 +919,7 @@ class PlexSession(object):
def stop(self, reason=''): def stop(self, reason=''):
""" Stop playback for the session. """ Stop playback for the session.
Parameters: Parameters:
reason (str): Message displayed to the user for stopping playback. reason (str): Message displayed to the user for stopping playback.
""" """

View file

@ -400,7 +400,7 @@ class Collection(
@deprecated('use editTitle, editSortTitle, editContentRating, and editSummary instead') @deprecated('use editTitle, editSortTitle, editContentRating, and editSummary instead')
def edit(self, title=None, titleSort=None, contentRating=None, summary=None, **kwargs): def edit(self, title=None, titleSort=None, contentRating=None, summary=None, **kwargs):
""" Edit the collection. """ Edit the collection.
Parameters: Parameters:
title (str, optional): The title of the collection. title (str, optional): The title of the collection.
titleSort (str, optional): The sort title of the collection. titleSort (str, optional): The sort title of the collection.

View file

@ -542,7 +542,7 @@ class LibrarySection(PlexObject):
def addLocations(self, location): def addLocations(self, location):
""" Add a location to a library. """ Add a location to a library.
Parameters: Parameters:
location (str or list): A single folder path, list of paths. location (str or list): A single folder path, list of paths.
@ -565,7 +565,7 @@ class LibrarySection(PlexObject):
def removeLocations(self, location): def removeLocations(self, location):
""" Remove a location from a library. """ Remove a location from a library.
Parameters: Parameters:
location (str or list): A single folder path, list of paths. location (str or list): A single folder path, list of paths.
@ -744,7 +744,7 @@ class LibrarySection(PlexObject):
def lockAllField(self, field, libtype=None): def lockAllField(self, field, libtype=None):
""" Lock a field for all items in the library. """ Lock a field for all items in the library.
Parameters: Parameters:
field (str): The field to lock (e.g. thumb, rating, collection). field (str): The field to lock (e.g. thumb, rating, collection).
libtype (str, optional): The library type to lock (movie, show, season, episode, libtype (str, optional): The library type to lock (movie, show, season, episode,
@ -754,7 +754,7 @@ class LibrarySection(PlexObject):
def unlockAllField(self, field, libtype=None): def unlockAllField(self, field, libtype=None):
""" Unlock a field for all items in the library. """ Unlock a field for all items in the library.
Parameters: Parameters:
field (str): The field to unlock (e.g. thumb, rating, collection). field (str): The field to unlock (e.g. thumb, rating, collection).
libtype (str, optional): The library type to lock (movie, show, season, episode, libtype (str, optional): The library type to lock (movie, show, season, episode,
@ -847,7 +847,7 @@ class LibrarySection(PlexObject):
""" """
_key = ('/library/sections/{key}/{filter}?includeMeta=1&includeAdvanced=1' _key = ('/library/sections/{key}/{filter}?includeMeta=1&includeAdvanced=1'
'&X-Plex-Container-Start=0&X-Plex-Container-Size=0') '&X-Plex-Container-Start=0&X-Plex-Container-Size=0')
key = _key.format(key=self.key, filter='all') key = _key.format(key=self.key, filter='all')
data = self._server.query(key) data = self._server.query(key)
self._filterTypes = self.findItems(data, FilteringType, rtag='Meta') self._filterTypes = self.findItems(data, FilteringType, rtag='Meta')
@ -894,7 +894,7 @@ class LibrarySection(PlexObject):
def getFieldType(self, fieldType): def getFieldType(self, fieldType):
""" Returns a :class:`~plexapi.library.FilteringFieldType` for a specified fieldType. """ Returns a :class:`~plexapi.library.FilteringFieldType` for a specified fieldType.
Parameters: Parameters:
fieldType (str): The data type for the field (tag, integer, string, boolean, date, fieldType (str): The data type for the field (tag, integer, string, boolean, date,
subtitleLanguage, audioLanguage, resolution). subtitleLanguage, audioLanguage, resolution).
@ -927,7 +927,7 @@ class LibrarySection(PlexObject):
""" """
return self.getFilterType(libtype).filters return self.getFilterType(libtype).filters
def listSorts(self, libtype=None): def listSorts(self, libtype=None):
""" Returns a list of available :class:`~plexapi.library.FilteringSort` for a specified libtype. """ Returns a list of available :class:`~plexapi.library.FilteringSort` for a specified libtype.
This is the list of options in the sorting dropdown menu This is the list of options in the sorting dropdown menu
@ -970,7 +970,7 @@ class LibrarySection(PlexObject):
""" Returns a list of available :class:`~plexapi.library.FilteringOperator` for a specified fieldType. """ Returns a list of available :class:`~plexapi.library.FilteringOperator` for a specified fieldType.
This is the list of options in the custom filter operator dropdown menu This is the list of options in the custom filter operator dropdown menu
(`screenshot <../_static/images/LibrarySection.search.png>`__). (`screenshot <../_static/images/LibrarySection.search.png>`__).
Parameters: Parameters:
fieldType (str): The data type for the field (tag, integer, string, boolean, date, fieldType (str): The data type for the field (tag, integer, string, boolean, date,
subtitleLanguage, audioLanguage, resolution). subtitleLanguage, audioLanguage, resolution).
@ -992,7 +992,7 @@ class LibrarySection(PlexObject):
:class:`~plexapi.library.FilteringFilter` or filter field. :class:`~plexapi.library.FilteringFilter` or filter field.
This is the list of available values for a custom filter This is the list of available values for a custom filter
(`screenshot <../_static/images/LibrarySection.search.png>`__). (`screenshot <../_static/images/LibrarySection.search.png>`__).
Parameters: Parameters:
field (str): :class:`~plexapi.library.FilteringFilter` object, field (str): :class:`~plexapi.library.FilteringFilter` object,
or the name of the field (genre, year, contentRating, etc.). or the name of the field (genre, year, contentRating, etc.).
@ -1024,7 +1024,7 @@ class LibrarySection(PlexObject):
availableFilters = [f.filter for f in self.listFilters(libtype)] availableFilters = [f.filter for f in self.listFilters(libtype)]
raise NotFound(f'Unknown filter field "{field}" for libtype "{libtype}". ' raise NotFound(f'Unknown filter field "{field}" for libtype "{libtype}". '
f'Available filters: {availableFilters}') from None f'Available filters: {availableFilters}') from None
data = self._server.query(field.key) data = self._server.query(field.key)
return self.findItems(data, FilterChoice) return self.findItems(data, FilterChoice)
@ -1111,7 +1111,7 @@ class LibrarySection(PlexObject):
except (ValueError, AttributeError): except (ValueError, AttributeError):
raise BadRequest(f'Invalid value "{value}" for filter field "{filterField.key}", ' raise BadRequest(f'Invalid value "{value}" for filter field "{filterField.key}", '
f'value should be type {fieldType.type}') from None f'value should be type {fieldType.type}') from None
return results return results
def _validateFieldValueDate(self, value): def _validateFieldValueDate(self, value):
@ -1345,7 +1345,7 @@ class LibrarySection(PlexObject):
Tag type filter values can be a :class:`~plexapi.library.FilterChoice` object, Tag type filter values can be a :class:`~plexapi.library.FilterChoice` object,
:class:`~plexapi.media.MediaTag` object, the exact name :attr:`MediaTag.tag` (*str*), :class:`~plexapi.media.MediaTag` object, the exact name :attr:`MediaTag.tag` (*str*),
or the exact id :attr:`MediaTag.id` (*int*). or the exact id :attr:`MediaTag.id` (*int*).
Date type filter values can be a ``datetime`` object, a relative date using a one of the Date type filter values can be a ``datetime`` object, a relative date using a one of the
available date suffixes (e.g. ``30d``) (*str*), or a date in ``YYYY-MM-DD`` (*str*) format. available date suffixes (e.g. ``30d``) (*str*), or a date in ``YYYY-MM-DD`` (*str*) format.
@ -1358,7 +1358,7 @@ class LibrarySection(PlexObject):
* ``w``: ``weeks`` * ``w``: ``weeks``
* ``mon``: ``months`` * ``mon``: ``months``
* ``y``: ``years`` * ``y``: ``years``
Multiple values can be ``OR`` together by providing a list of values. Multiple values can be ``OR`` together by providing a list of values.
Examples: Examples:
@ -1686,10 +1686,10 @@ class LibrarySection(PlexObject):
""" Validates the specified items are from this library and of the same type. """ """ Validates the specified items are from this library and of the same type. """
if items is None or items == []: if items is None or items == []:
raise BadRequest('No items specified.') raise BadRequest('No items specified.')
if not isinstance(items, list): if not isinstance(items, list):
items = [items] items = [items]
itemType = items[0].type itemType = items[0].type
for item in items: for item in items:
if item.librarySectionID != self.key: if item.librarySectionID != self.key:
@ -3102,6 +3102,7 @@ class FirstCharacter(PlexObject):
size (str): Total amount of library items starting with this character. size (str): Total amount of library items starting with this character.
title (str): Character (#, !, A, B, C, ...). title (str): Character (#, !, A, B, C, ...).
""" """
def _loadData(self, data): def _loadData(self, data):
""" Load attribute values from Plex XML response. """ """ Load attribute values from Plex XML response. """
self._data = data self._data = data

View file

@ -1008,6 +1008,7 @@ class BaseResource(PlexObject):
selected (bool): True if the resource is currently selected. selected (bool): True if the resource is currently selected.
thumb (str): The URL to retrieve the resource thumbnail. thumb (str): The URL to retrieve the resource thumbnail.
""" """
def _loadData(self, data): def _loadData(self, data):
self._data = data self._data = data
self.key = data.attrib.get('key') self.key = data.attrib.get('key')

View file

@ -39,7 +39,7 @@ class AdvancedSettingsMixin:
pref = preferences[settingID] pref = preferences[settingID]
except KeyError: except KeyError:
raise NotFound(f'{value} not found in {list(preferences.keys())}') raise NotFound(f'{value} not found in {list(preferences.keys())}')
enumValues = pref.enumValues enumValues = pref.enumValues
if enumValues.get(value, enumValues.get(str(value))): if enumValues.get(value, enumValues.get(str(value))):
data[settingID] = value data[settingID] = value
@ -69,7 +69,7 @@ class SmartFilterMixin:
filters = {} filters = {}
filterOp = 'and' filterOp = 'and'
filterGroups = [[]] filterGroups = [[]]
for key, value in parse_qsl(content.query): for key, value in parse_qsl(content.query):
# Move = sign to key when operator is == # Move = sign to key when operator is ==
if value.startswith('='): if value.startswith('='):
@ -96,11 +96,11 @@ class SmartFilterMixin:
filterGroups.pop() filterGroups.pop()
else: else:
filterGroups[-1].append({key: value}) filterGroups[-1].append({key: value})
if filterGroups: if filterGroups:
filters['filters'] = self._formatFilterGroups(filterGroups.pop()) filters['filters'] = self._formatFilterGroups(filterGroups.pop())
return filters return filters
def _formatFilterGroups(self, groups): def _formatFilterGroups(self, groups):
""" Formats the filter groups into the advanced search rules. """ """ Formats the filter groups into the advanced search rules. """
if len(groups) == 1 and isinstance(groups[0], list): if len(groups) == 1 and isinstance(groups[0], list):
@ -131,7 +131,7 @@ class SplitMergeMixin:
def merge(self, ratingKeys): def merge(self, ratingKeys):
""" Merge other Plex objects into the current object. """ Merge other Plex objects into the current object.
Parameters: Parameters:
ratingKeys (list): A list of rating keys to merge. ratingKeys (list): A list of rating keys to merge.
""" """
@ -320,7 +320,7 @@ class RatingMixin:
class ArtUrlMixin: class ArtUrlMixin:
""" Mixin for Plex objects that can have a background artwork url. """ """ Mixin for Plex objects that can have a background artwork url. """
@property @property
def artUrl(self): def artUrl(self):
""" Return the art url for the Plex object. """ """ Return the art url for the Plex object. """
@ -349,7 +349,7 @@ class ArtMixin(ArtUrlMixin, ArtLockMixin):
def uploadArt(self, url=None, filepath=None): def uploadArt(self, url=None, filepath=None):
""" Upload a background artwork from a url or filepath. """ Upload a background artwork from a url or filepath.
Parameters: Parameters:
url (str): The full URL to the image to upload. url (str): The full URL to the image to upload.
filepath (str): The full file path the the image to upload or file-like object. filepath (str): The full file path the the image to upload or file-like object.
@ -365,7 +365,7 @@ class ArtMixin(ArtUrlMixin, ArtLockMixin):
def setArt(self, art): def setArt(self, art):
""" Set the background artwork for a Plex object. """ Set the background artwork for a Plex object.
Parameters: Parameters:
art (:class:`~plexapi.media.Art`): The art object to select. art (:class:`~plexapi.media.Art`): The art object to select.
""" """
@ -425,7 +425,7 @@ class PosterMixin(PosterUrlMixin, PosterLockMixin):
def setPoster(self, poster): def setPoster(self, poster):
""" Set the poster for a Plex object. """ Set the poster for a Plex object.
Parameters: Parameters:
poster (:class:`~plexapi.media.Poster`): The poster object to select. poster (:class:`~plexapi.media.Poster`): The poster object to select.
""" """
@ -491,11 +491,11 @@ class ThemeMixin(ThemeUrlMixin, ThemeLockMixin):
class EditFieldMixin: class EditFieldMixin:
""" Mixin for editing Plex object fields. """ """ Mixin for editing Plex object fields. """
def editField(self, field, value, locked=True, **kwargs): def editField(self, field, value, locked=True, **kwargs):
""" Edit the field of a Plex object. All field editing methods can be chained together. """ Edit the field of a Plex object. All field editing methods can be chained together.
Also see :func:`~plexapi.base.PlexPartialObject.batchEdits` for batch editing fields. Also see :func:`~plexapi.base.PlexPartialObject.batchEdits` for batch editing fields.
Parameters: Parameters:
field (str): The name of the field to edit. field (str): The name of the field to edit.
value (str): The value to edit the field to. value (str): The value to edit the field to.

View file

@ -674,7 +674,7 @@ class MyPlexAccount(PlexObject):
if (invite.username and invite.email and invite.id and username.lower() in if (invite.username and invite.email and invite.id and username.lower() in
(invite.username.lower(), invite.email.lower(), str(invite.id))): (invite.username.lower(), invite.email.lower(), str(invite.id))):
return invite return invite
raise NotFound(f'Unable to find invite {username}') raise NotFound(f'Unable to find invite {username}')
def pendingInvites(self, includeSent=True, includeReceived=True): def pendingInvites(self, includeSent=True, includeReceived=True):
@ -952,7 +952,7 @@ class MyPlexAccount(PlexObject):
""" """
if not isinstance(items, list): if not isinstance(items, list):
items = [items] items = [items]
for item in items: for item in items:
if self.onWatchlist(item): if self.onWatchlist(item):
raise BadRequest(f'"{item.title}" is already on the watchlist') raise BadRequest(f'"{item.title}" is already on the watchlist')
@ -973,7 +973,7 @@ class MyPlexAccount(PlexObject):
""" """
if not isinstance(items, list): if not isinstance(items, list):
items = [items] items = [items]
for item in items: for item in items:
if not self.onWatchlist(item): if not self.onWatchlist(item):
raise BadRequest(f'"{item.title}" is not on the watchlist') raise BadRequest(f'"{item.title}" is not on the watchlist')
@ -1944,7 +1944,7 @@ class AccountOptOut(PlexObject):
def optOutManaged(self): def optOutManaged(self):
""" Sets the Online Media Source to "Disabled for Managed Users". """ Sets the Online Media Source to "Disabled for Managed Users".
Raises: Raises:
:exc:`~plexapi.exceptions.BadRequest`: When trying to opt out music. :exc:`~plexapi.exceptions.BadRequest`: When trying to opt out music.
""" """

View file

@ -256,7 +256,7 @@ class Photo(
List<str> of file paths where the photo is found on disk. List<str> of file paths where the photo is found on disk.
""" """
return [part.file for item in self.media for part in item.parts if part] return [part.file for item in self.media for part in item.parts if part]
def sync(self, resolution, client=None, clientId=None, limit=None, title=None): def sync(self, resolution, client=None, clientId=None, limit=None, title=None):
""" Add current photo as sync item for specified device. """ Add current photo as sync item for specified device.
See :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions. See :func:`~plexapi.myplex.MyPlexAccount.sync` for possible exceptions.

View file

@ -155,7 +155,7 @@ class Playlist(
sectionKey = int(match.group(1)) sectionKey = int(match.group(1))
self._section = self._server.library.sectionByID(sectionKey) self._section = self._server.library.sectionByID(sectionKey)
return self._section return self._section
# Try to get the library section from the first item in the playlist # Try to get the library section from the first item in the playlist
if self.items(): if self.items():
self._section = self.items()[0].section() self._section = self.items()[0].section()
@ -314,7 +314,7 @@ class Playlist(
def edit(self, title=None, summary=None): def edit(self, title=None, summary=None):
""" Edit the playlist. """ Edit the playlist.
Parameters: Parameters:
title (str, optional): The title of the playlist. title (str, optional): The title of the playlist.
summary (str, optional): The summary of the playlist. summary (str, optional): The summary of the playlist.
@ -432,7 +432,7 @@ class Playlist(
def copyToUser(self, user): def copyToUser(self, user):
""" Copy playlist to another user account. """ Copy playlist to another user account.
Parameters: Parameters:
user (:class:`~plexapi.myplex.MyPlexUser` or str): `MyPlexUser` object, username, user (:class:`~plexapi.myplex.MyPlexUser` or str): `MyPlexUser` object, username,
email, or user id of the user to copy the playlist to. email, or user id of the user to copy the playlist to.

View file

@ -202,7 +202,7 @@ class PlexServer(PlexObject):
def claim(self, account): def claim(self, account):
""" Claim the Plex server using a :class:`~plexapi.myplex.MyPlexAccount`. """ Claim the Plex server using a :class:`~plexapi.myplex.MyPlexAccount`.
This will only work with an unclaimed server on localhost or the same subnet. This will only work with an unclaimed server on localhost or the same subnet.
Parameters: Parameters:
account (:class:`~plexapi.myplex.MyPlexAccount`): The account used to account (:class:`~plexapi.myplex.MyPlexAccount`): The account used to
claim the server. claim the server.
@ -245,7 +245,7 @@ class PlexServer(PlexObject):
def switchUser(self, user, session=None, timeout=None): def switchUser(self, user, session=None, timeout=None):
""" Returns a new :class:`~plexapi.server.PlexServer` object logged in as the given username. """ Returns a new :class:`~plexapi.server.PlexServer` object logged in as the given username.
Note: Only the admin account can switch to other users. Note: Only the admin account can switch to other users.
Parameters: Parameters:
user (:class:`~plexapi.myplex.MyPlexUser` or str): `MyPlexUser` object, username, user (:class:`~plexapi.myplex.MyPlexUser` or str): `MyPlexUser` object, username,
email, or user id of the user to log in to the server. email, or user id of the user to log in to the server.
@ -590,7 +590,7 @@ class PlexServer(PlexObject):
def runButlerTask(self, task): def runButlerTask(self, task):
""" Manually run a butler task immediately instead of waiting for the scheduled task to run. """ Manually run a butler task immediately instead of waiting for the scheduled task to run.
Note: The butler task is run asynchronously. Check Plex Web to monitor activity. Note: The butler task is run asynchronously. Check Plex Web to monitor activity.
Parameters: Parameters:
task (str): The name of the task to run. (e.g. 'BackupDatabase') task (str): The name of the task to run. (e.g. 'BackupDatabase')
@ -666,7 +666,7 @@ class PlexServer(PlexObject):
args['librarySectionID'] = librarySectionID args['librarySectionID'] = librarySectionID
if mindate: if mindate:
args['viewedAt>'] = int(mindate.timestamp()) args['viewedAt>'] = int(mindate.timestamp())
key = f'/status/sessions/history/all{utils.joinArgs(args)}' key = f'/status/sessions/history/all{utils.joinArgs(args)}'
return self.fetchItems(key, maxresults=maxresults) return self.fetchItems(key, maxresults=maxresults)
@ -1258,7 +1258,7 @@ class StatisticsResources(PlexObject):
@utils.registerPlexObject @utils.registerPlexObject
class ButlerTask(PlexObject): class ButlerTask(PlexObject):
""" Represents a single scheduled butler task. """ Represents a single scheduled butler task.
Attributes: Attributes:
TAG (str): 'ButlerTask' TAG (str): 'ButlerTask'
description (str): The description of the task. description (str): The description of the task.
@ -1291,7 +1291,7 @@ class Identity(PlexObject):
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__}:{self.machineIdentifier}>" return f"<{self.__class__.__name__}:{self.machineIdentifier}>"
def _loadData(self, data): def _loadData(self, data):
self._data = data self._data = data
self.claimed = utils.cast(bool, data.attrib.get('claimed')) self.claimed = utils.cast(bool, data.attrib.get('claimed'))

View file

@ -23,7 +23,6 @@ you can set items to be synced to your app) you need to init some variables.
You have to fake platform/device/model because transcoding profiles are hardcoded in Plex, and you obviously have You have to fake platform/device/model because transcoding profiles are hardcoded in Plex, and you obviously have
to explicitly specify that your app supports `sync-target`. to explicitly specify that your app supports `sync-target`.
""" """
import requests import requests
import plexapi import plexapi

View file

@ -88,7 +88,7 @@ def test_history_PlexHistory(plex, movie):
movie.markPlayed() movie.markPlayed()
history = plex.history() history = plex.history()
assert len(history) assert len(history)
hist = history[0] hist = history[0]
assert hist.source() == movie assert hist.source() == movie
assert hist.accountID assert hist.accountID
@ -106,14 +106,20 @@ def test_history_User(account, shared_username):
user = account.user(shared_username) user = account.user(shared_username)
history = user.history() history = user.history()
assert isinstance(history, list)
def test_history_UserServer(account, shared_username, plex): def test_history_UserServer(account, shared_username, plex):
userSharedServer = account.user(shared_username).server(plex.friendlyName) userSharedServer = account.user(shared_username).server(plex.friendlyName)
history = userSharedServer.history() history = userSharedServer.history()
assert isinstance(history, list)
def test_history_UserSection(account, shared_username, plex): def test_history_UserSection(account, shared_username, plex):
userSharedServerSection = ( userSharedServerSection = (
account.user(shared_username).server(plex.friendlyName).section("Movies") account.user(shared_username).server(plex.friendlyName).section("Movies")
) )
history = userSharedServerSection.history() history = userSharedServerSection.history()
assert isinstance(history, list)

View file

@ -611,7 +611,7 @@ def test_library_MusicSection_search(music, artist):
album.removeMood("test_search", locked=False) album.removeMood("test_search", locked=False)
album.removeCollection("test_search", locked=False) album.removeCollection("test_search", locked=False)
album.removeLabel("test_search", locked=False) album.removeLabel("test_search", locked=False)
track = album.track(track=1) track = album.track(track=1)
track.addMood("test_search") track.addMood("test_search")
_test_library_search(music, track) _test_library_search(music, track)
@ -777,7 +777,7 @@ def test_library_search_exceptions(movies):
movies.search(sort="titleSort:bad") movies.search(sort="titleSort:bad")
def _test_library_search(library, obj): def _test_library_search(library, obj): # noqa: C901
# Create & operator # Create & operator
AndOperator = namedtuple("AndOperator", ["key", "title"]) AndOperator = namedtuple("AndOperator", ["key", "title"])
andOp = AndOperator("&=", "and") andOp = AndOperator("&=", "and")

View file

@ -317,7 +317,7 @@ def _test_mixins_edit_theme(obj):
obj.unlockTheme() obj.unlockTheme()
obj.reload() obj.reload()
assert "theme" not in _fields() assert "theme" not in _fields()
# Lock the theme # Lock the theme
obj.lockTheme() obj.lockTheme()
obj.reload() obj.reload()

View file

@ -28,6 +28,7 @@ def test_navigate_around_artist(account, plex):
print(f"Album: {album}") print(f"Album: {album}")
print(f"Tracks: {tracks}...") print(f"Tracks: {tracks}...")
print(f"Track: {track}") print(f"Track: {track}")
assert isinstance(albums, list), "Unable to list artist albums."
assert artist.track("As Colourful as Ever") == track, "Unable to get artist 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.track("As Colourful as Ever") == track, "Unable to get album track."
assert album.artist() == artist, "album.artist() doesn't match expected artist." assert album.artist() == artist, "album.artist() doesn't match expected artist."

View file

@ -436,7 +436,7 @@ def test_server_system_devices(plex):
assert len(device.name) or device.name == "" assert len(device.name) or device.name == ""
assert len(device.platform) or device.platform == "" assert len(device.platform) or device.platform == ""
assert plex.systemDevice(device.id) == device assert plex.systemDevice(device.id) == device
@pytest.mark.authenticated @pytest.mark.authenticated
def test_server_dashboard_bandwidth(account_plexpass, plex): def test_server_dashboard_bandwidth(account_plexpass, plex):

View file

@ -383,7 +383,7 @@ def test_video_Movie_download(monkeydownload, tmpdir, movie):
def test_video_Movie_videoStreams(movie): def test_video_Movie_videoStreams(movie):
assert movie.videoStreams() assert movie.videoStreams()
def test_video_Movie_audioStreams(movie): def test_video_Movie_audioStreams(movie):
assert movie.audioStreams() assert movie.audioStreams()
@ -418,7 +418,7 @@ def test_video_Movie_upload_select_remove_subtitle(movie, subtitle):
try: try:
os.remove(filepath) os.remove(filepath)
except: except OSError:
pass pass
@ -632,7 +632,7 @@ def test_video_Movie_batchEdits(movie):
assert movie.tagline == tagline assert movie.tagline == tagline
assert movie.studio == studio assert movie.studio == studio
assert not movie.fields assert not movie.fields
with pytest.raises(BadRequest): with pytest.raises(BadRequest):
movie.saveEdits() movie.saveEdits()

View file

@ -17,7 +17,7 @@ import os
from datetime import datetime from datetime import datetime
from plexapi.server import PlexServer from plexapi.server import PlexServer
TAGS = {'keep5':5, 'keep10':10, 'keep15':15, 'keepSeason':'season'} TAGS = {'keep5': 5, 'keep10': 10, 'keep15': 15, 'keepSeason': 'season'}
datestr = lambda: datetime.now().strftime('%Y-%m-%d %H:%M:%S') datestr = lambda: datetime.now().strftime('%Y-%m-%d %H:%M:%S')
@ -40,7 +40,7 @@ def keep_episodes(show, keep):
""" Delete all but last count episodes in show. """ """ Delete all but last count episodes in show. """
deleted = 0 deleted = 0
print('%s Cleaning %s to %s episodes.' % (datestr(), show.title, keep)) print('%s Cleaning %s to %s episodes.' % (datestr(), show.title, keep))
sort = lambda x:x.originallyAvailableAt or x.addedAt sort = lambda x: x.originallyAvailableAt or x.addedAt
items = sorted(show.episodes(), key=sort, reverse=True) items = sorted(show.episodes(), key=sort, reverse=True)
for episode in items[keep:]: for episode in items[keep:]:
delete_episode(episode) delete_episode(episode)

View file

@ -3,8 +3,9 @@
""" """
Backup and restore the watched status of Plex libraries to a json file. Backup and restore the watched status of Plex libraries to a json file.
""" """
import argparse, json import argparse
from collections import defaultdict from collections import defaultdict
import json
from plexapi import utils from plexapi import utils
SECTIONS = ('movie', 'show') SECTIONS = ('movie', 'show')
@ -32,7 +33,7 @@ def _item_key(item):
def _iter_sections(plex, opts): def _iter_sections(plex, opts):
libraries = opts.libraries.split(',') if opts.libraries else [] libraries = opts.libraries.split(',') if opts.libraries else []
libraries = [l.strip().lower() for l in libraries] libraries = [lib.strip().lower() for lib in libraries]
for section in plex.library.sections(): for section in plex.library.sections():
title = section.title.lower() title = section.title.lower()
if section.type in SECTIONS and (not libraries or title in libraries): if section.type in SECTIONS and (not libraries or title in libraries):
@ -76,11 +77,11 @@ def restore_watched(plex, opts):
skey = section.title.lower() skey = section.title.lower()
for item in _iter_items(section): for item in _iter_items(section):
ikey = _item_key(item) ikey = _item_key(item)
sval = source.get(skey,{}).get(ikey) sval = source.get(skey, {}).get(ikey)
if sval is None: if sval is None:
raise SystemExit('%s not found' % ikey) raise SystemExit('%s not found' % ikey)
if (sval is not None and item.isWatched != sval) and (not opts.watchedonly or sval): if (sval is not None and item.isWatched != sval) and (not opts.watchedonly or sval):
differences[skey][ikey] = {'isWatched':sval, 'item':item} differences[skey][ikey] = {'isWatched': sval, 'item': item}
print('Applying %s differences to destination' % len(differences)) print('Applying %s differences to destination' % len(differences))
import pprint; pprint.pprint(differences) import pprint; pprint.pprint(differences)
@ -93,7 +94,8 @@ if __name__ == '__main__':
parser.add_argument('-u', '--username', default=CONFIG.get('auth.myplex_username'), help='Plex username') parser.add_argument('-u', '--username', default=CONFIG.get('auth.myplex_username'), help='Plex username')
parser.add_argument('-p', '--password', default=CONFIG.get('auth.myplex_password'), help='Plex password') parser.add_argument('-p', '--password', default=CONFIG.get('auth.myplex_password'), help='Plex password')
parser.add_argument('-s', '--servername', help='Plex server name') parser.add_argument('-s', '--servername', help='Plex server name')
parser.add_argument('-w', '--watchedonly', default=False, action='store_true', help='Only backup or restore watched items.') parser.add_argument('-w', '--watchedonly', default=False, action='store_true',
help='Only backup or restore watched items.')
parser.add_argument('-l', '--libraries', help='Only backup or restore the specified libraries (comma separated).') parser.add_argument('-l', '--libraries', help='Only backup or restore the specified libraries (comma separated).')
opts = parser.parse_args() opts = parser.parse_args()
account = utils.getMyPlexAccount(opts) account = utils.getMyPlexAccount(opts)

View file

@ -121,15 +121,15 @@ def setup_music(music_path, docker=False):
"Broke for free": { "Broke for free": {
"Layers": [ "Layers": [
"1 - As Colorful As Ever.mp3", "1 - As Colorful As Ever.mp3",
#"02 - Knock Knock.mp3", # "02 - Knock Knock.mp3",
#"03 - Only Knows.mp3", # "03 - Only Knows.mp3",
#"04 - If.mp3", # "04 - If.mp3",
#"05 - Note Drop.mp3", # "05 - Note Drop.mp3",
#"06 - Murmur.mp3", # "06 - Murmur.mp3",
#"07 - Spellbound.mp3", # "07 - Spellbound.mp3",
#"08 - The Collector.mp3", # "08 - The Collector.mp3",
#"09 - Quit Bitching.mp3", # "09 - Quit Bitching.mp3",
#"10 - A Year.mp3", # "10 - A Year.mp3",
] ]
}, },
@ -279,7 +279,7 @@ def add_library_section(server, section):
raise SystemExit("Timeout adding section to Plex instance.") raise SystemExit("Timeout adding section to Plex instance.")
def create_section(server, section, opts): def create_section(server, section, opts): # noqa: C901
processed_media = 0 processed_media = 0
expected_media_count = section.pop("expected_media_count", 0) expected_media_count = section.pop("expected_media_count", 0)
expected_media_type = (section["type"],) expected_media_type = (section["type"],)
@ -337,7 +337,7 @@ def create_section(server, section, opts):
notifier.stop() notifier.stop()
if __name__ == "__main__": if __name__ == "__main__": # noqa: C901
default_ip = get_default_ip() default_ip = get_default_ip()
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
# Authentication arguments # Authentication arguments

View file

@ -23,7 +23,7 @@ def search_for_item(url=None):
servers = [s for s in account.resources() if 'server' in s.provides] servers = [s for s in account.resources() if 'server' in s.provides]
server = utils.choose('Choose a Server', servers, 'name').connect() server = utils.choose('Choose a Server', servers, 'name').connect()
query = input('What are you looking for?: ') query = input('What are you looking for?: ')
item = [] item = []
items = [i for i in server.search(query) if i.__class__ in VALID_TYPES] items = [i for i in server.search(query) if i.__class__ in VALID_TYPES]
items = utils.choose('Choose result', items, lambda x: '(%s) %s' % (x.type.title(), x.title[0:60])) items = utils.choose('Choose result', items, lambda x: '(%s) %s' % (x.type.title(), x.title[0:60]))
@ -63,10 +63,10 @@ def get_item_from_url(url):
server = servers[0].connect() server = servers[0].connect()
return server.fetchItem(key) return server.fetchItem(key)
if __name__ == '__main__': if __name__ == '__main__':
# Command line parser # Command line parser
from plexapi import CONFIG from plexapi import CONFIG
from tqdm import tqdm
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('-u', '--username', help='Your Plex username', parser.add_argument('-u', '--username', help='Your Plex username',
default=CONFIG.get('auth.myplex_username')) default=CONFIG.get('auth.myplex_username'))
@ -84,4 +84,3 @@ if __name__ == '__main__':
url = item._server.url('%s?download=1' % part.key) url = item._server.url('%s?download=1' % part.key)
filepath = utils.download(url, token=item._server._token, filename=filename, savepath=os.getcwd(), filepath = utils.download(url, token=item._server._token, filename=filename, savepath=os.getcwd(),
session=item._server._session, showstatus=True) session=item._server._session, showstatus=True)
#print(' %s' % filepath)

View file

@ -6,7 +6,14 @@ items to build a collection of attributes on each media type. The resulting list
can be compared with the current object implementation in python-plexapi to track can be compared with the current object implementation in python-plexapi to track
new attributes and deprecate old ones. new attributes and deprecate old ones.
""" """
import argparse, copy, pickle, plexapi, os, re, sys, time import argparse
import copy
import pickle
import plexapi
import os
import re
import sys
import time
from os.path import abspath, dirname, join from os.path import abspath, dirname, join
from collections import defaultdict from collections import defaultdict
from datetime import datetime from datetime import datetime
@ -296,19 +303,19 @@ class PlexAttributes():
def _safe_connect(self, elem): def _safe_connect(self, elem):
try: try:
return elem.connect() return elem.connect()
except: except Exception:
return None return None
def _safe_reload(self, elem): def _safe_reload(self, elem):
try: try:
elem.reload() elem.reload()
except: except Exception:
pass pass
def _(text, color): def _(text, color):
FMTSTR = '\033[%dm%s\033[0m' FMTSTR = '\033[%dm%s\033[0m'
COLORS = {'blue':34, 'cyan':36, 'green':32, 'grey':30, 'purple':35, 'red':31, 'white':37, 'yellow':33} COLORS = {'blue': 34, 'cyan': 36, 'green': 32, 'grey': 30, 'purple': 35, 'red': 31, 'white': 37, 'yellow': 33}
return FMTSTR % (COLORS[color], text) return FMTSTR % (COLORS[color], text)

View file

@ -10,7 +10,7 @@ from os.path import abspath, dirname, join
from plexapi import utils from plexapi import utils
from plexapi.server import PlexServer from plexapi.server import PlexServer
GROUPNAMES = {'butler':'Scheduled Task', 'dlna':'DLNA'} GROUPNAMES = {'butler': 'Scheduled Task', 'dlna': 'DLNA'}
OUTPUT = join(dirname(dirname(abspath(__file__))), 'docs/settingslist.rst') OUTPUT = join(dirname(dirname(abspath(__file__))), 'docs/settingslist.rst')
@ -20,7 +20,7 @@ def _setting_group(setting):
return setting.group return setting.group
def _write_settings(handle, groups, group): def _write_settings(handle, groups, group): # noqa: C901
title = GROUPNAMES.get(group, group.title()) title = GROUPNAMES.get(group, group.title())
print('\n%s Settings\n%s' % (title, '-' * (len(title) + 9))) print('\n%s Settings\n%s' % (title, '-' * (len(title) + 9)))
handle.write('%s Settings\n%s\n' % (title, '-' * (len(title) + 9))) handle.write('%s Settings\n%s\n' % (title, '-' * (len(title) + 9)))

View file

@ -47,7 +47,7 @@ def _list_devices(account, servers):
def _test_servers(servers): def _test_servers(servers):
items, seen = [], set() items, seen = [], set()
print('Finding Plex clients..') print('Finding Plex clients..')
listargs = [[PlexServer, s, t, None, 5] for s,t in servers.items()] listargs = [[PlexServer, s, t, None, 5] for s, t in servers.items()]
results = utils.threaded(_connect, listargs) results = utils.threaded(_connect, listargs)
for url, token, plex, runtime in results: for url, token, plex, runtime in results:
clients = plex.clients() if plex else [] clients = plex.clients() if plex else []

View file

@ -41,17 +41,17 @@ def _iter_items(search):
if __name__ == '__main__': if __name__ == '__main__':
datestr = lambda: datetime.now().strftime('%Y-%m-%d %H:%M:%S') # noqa datestr = lambda: datetime.now().strftime('%Y-%m-%d %H:%M:%S') # noqa
print('{datestr} Starting plex-markwatched script..'.format(datestr=datestr())) print(f'{datestr()} Starting plex-markwatched script..')
plex = PlexServer() plex = PlexServer()
for section in plex.library.sections(): for section in plex.library.sections():
print('{datestr} Checking {section.title} for unwatched items..'.format(datestr=datestr())) print(f'{datestr()} Checking {section.title} for unwatched items..')
for item in _iter_items(section.search(collection='markwatched')): for item in _iter_items(section.search(collection='markwatched')):
if not item.isWatched: if not item.isWatched:
print('{datestr} Marking {_get_title(item)} watched.'.format(datestr=datestr())) print(f'{datestr()} Marking {_get_title(item)} watched.')
item.markWatched() item.markWatched()
# Check all OnDeck items # Check all OnDeck items
print('{datestr} Checking OnDeck for unwatched items..'.format(datestr=datestr())) print(f'{datestr()} Checking OnDeck for unwatched items.')
for item in plex.library.onDeck(): for item in plex.library.onDeck():
if not item.isWatched and _has_markwatched_tag(item): if not item.isWatched and _has_markwatched_tag(item):
print('{datestr} Marking {_get_title(item)} watched.'.format(datestr=datestr())) print(f'{datestr()} Marking {_get_title(item)} watched.')
item.markWatched() item.markWatched()

View file

@ -90,10 +90,9 @@ def main():
if arguments.tag: if arguments.tag:
subprocess.run(["git", "tag", str(bumped), "-m", f"Release {bumped}"]) subprocess.run(["git", "tag", str(bumped), "-m", f"Release {bumped}"])
def test_bump_version(): def test_bump_version():
"""Make sure it all works.""" """Make sure it all works."""
import pytest
assert bump_version(Version("4.7.0"), "patch") == Version("4.7.1") assert bump_version(Version("4.7.0"), "patch") == Version("4.7.1")
assert bump_version(Version("4.7.0"), "minor") == Version("4.8.0") assert bump_version(Version("4.7.0"), "minor") == Version("4.8.0")
assert bump_version(Version("4.7.3"), "minor") == Version("4.8.0") assert bump_version(Version("4.7.3"), "minor") == Version("4.8.0")