Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: pkkid/python-plexapi
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 4.15.7
Choose a base ref
...
head repository: pkkid/python-plexapi
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 4.15.8
Choose a head ref
  • 17 commits
  • 13 files changed
  • 5 contributors

Commits on Feb 4, 2024

  1. Adds 2FA code input to tools/plex-gettoken.py (#1319)

    * Use getpass for password input
    
    * Add 2FA code input
    JonnyWong16 authored Feb 4, 2024
    Copy the full SHA
    768aa58 View commit details
  2. fix(scripts): offset butler tasks to reduce ci random failures (#1320)

    * fix(scripts): offset butler tasks to reduce ci random failures
    
    * Apply suggestions from code review
    
    Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
    
    ---------
    
    Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
    ReenigneArcher and JonnyWong16 authored Feb 4, 2024
    Copy the full SHA
    097b989 View commit details
  3. Copy the full SHA
    8bb9f5f View commit details
  4. Copy the full SHA
    b3ef1c2 View commit details
  5. Simplify building MediaPartStream objects (#1328)

    * Simplify MediaPart._buildStreams
    
    * Remove isChildOf check for photo and track attributes
    
    * Add parent object when automatically building PlexObject
    
    * Remove check of track only attribute in tests
    JonnyWong16 authored Feb 4, 2024
    Copy the full SHA
    e3d90a5 View commit details
  6. Copy the full SHA
    654ed50 View commit details
  7. Copy the full SHA
    dfc5471 View commit details
  8. Copy the full SHA
    d9539a3 View commit details
  9. Refactor cast function in utils.py (#1340)

    - less indentation, more readable code
    - early return
    Dr-Blank authored Feb 4, 2024
    Copy the full SHA
    ba384e0 View commit details
  10. bugfix: pass existing filters for albums method of Artist (#1347)

    * bugfix: pass existing filters for `albums` method of `Artist`
    
    * add test for bugfix
    Dr-Blank authored Feb 4, 2024
    Copy the full SHA
    fe648f6 View commit details
  11. Fix: Update guid filter examples for fetchItems (#1350)

    * Update imdb guid filter with full agent name
    
    it does not work otherwise like "^" anchor is used in the filter
    
    * Add legacy imdb tt* for imdb to guid filter
    
    * Add new agent Guid tag filtering example
    
    * Add themoviedb prefix as well
    glensc authored Feb 4, 2024
    Copy the full SHA
    019d3b8 View commit details
  12. Bump pillow from 10.1.0 to 10.2.0 (#1331)

    Bumps [pillow](https://github.com/python-pillow/Pillow) from 10.1.0 to 10.2.0.
    - [Release notes](https://github.com/python-pillow/Pillow/releases)
    - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
    - [Commits](python-pillow/Pillow@10.1.0...10.2.0)
    
    ---
    updated-dependencies:
    - dependency-name: pillow
      dependency-type: direct:development
      update-type: version-update:semver-minor
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Feb 4, 2024
    Copy the full SHA
    08033e1 View commit details
  13. Bump flake8 from 6.1.0 to 7.0.0 (#1334)

    Bumps [flake8](https://github.com/pycqa/flake8) from 6.1.0 to 7.0.0.
    - [Commits](PyCQA/flake8@6.1.0...7.0.0)
    
    ---
    updated-dependencies:
    - dependency-name: flake8
      dependency-type: direct:development
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Feb 4, 2024
    Copy the full SHA
    eb9ead4 View commit details
  14. Bump pytest from 7.4.3 to 8.0.0 (#1349)

    Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.3 to 8.0.0.
    - [Release notes](https://github.com/pytest-dev/pytest/releases)
    - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
    - [Commits](pytest-dev/pytest@7.4.3...8.0.0)
    
    ---
    updated-dependencies:
    - dependency-name: pytest
      dependency-type: direct:development
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Feb 4, 2024
    Copy the full SHA
    be9375a View commit details
  15. Bump actions/cache from 3 to 4 (#1346)

    Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4.
    - [Release notes](https://github.com/actions/cache/releases)
    - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
    - [Commits](actions/cache@v3...v4)
    
    ---
    updated-dependencies:
    - dependency-name: actions/cache
      dependency-type: direct:production
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Feb 4, 2024
    Copy the full SHA
    07de1e2 View commit details
  16. Bump nick-fields/retry from 2.9.0 to 3.0.0 (#1352)

    Bumps [nick-fields/retry](https://github.com/nick-fields/retry) from 2.9.0 to 3.0.0.
    - [Release notes](https://github.com/nick-fields/retry/releases)
    - [Changelog](https://github.com/nick-fields/retry/blob/master/.releaserc.js)
    - [Commits](nick-fields/retry@v2.9.0...v3.0.0)
    
    ---
    updated-dependencies:
    - dependency-name: nick-fields/retry
      dependency-type: direct:production
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Feb 4, 2024
    Copy the full SHA
    4d01736 View commit details
  17. Release 4.15.8

    JonnyWong16 committed Feb 4, 2024
    Copy the full SHA
    fb1ce36 View commit details
Showing with 111 additions and 71 deletions.
  1. +5 −5 .github/workflows/ci.yaml
  2. +2 −2 plexapi/audio.py
  3. +3 −2 plexapi/base.py
  4. +1 −1 plexapi/const.py
  5. +30 −8 plexapi/library.py
  6. +21 −24 plexapi/media.py
  7. +5 −1 plexapi/myplex.py
  8. +17 −18 plexapi/utils.py
  9. +3 −3 requirements_dev.txt
  10. +1 −1 tests/test_audio.py
  11. +0 −4 tests/test_video.py
  12. +19 −0 tools/plex-bootstraptest.py
  13. +4 −2 tools/plex-gettoken.py
10 changes: 5 additions & 5 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -30,7 +30,7 @@ jobs:

- name: Restore Python ${{ steps.python.outputs.python-version }} virtual environment
id: cache-venv
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: venv
key: >-
@@ -94,7 +94,7 @@ jobs:

- name: Restore Python ${{ steps.python.outputs.python-version }} virtual environment
id: cache-venv
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: venv
key: >-
@@ -135,7 +135,7 @@ jobs:
- name: Cache PMS Docker image
id: docker-cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ~/.cache/docker/plexinc
key: ${{ runner.os }}-docker-pms-${{ steps.docker-digest.outputs.digest }}
@@ -158,7 +158,7 @@ jobs:
echo "PLEXAPI_AUTH_SERVER_TOKEN=${{ secrets.PLEXAPI_AUTH_SERVER_TOKEN }}" >> $GITHUB_ENV
- name: Bootstrap ${{ matrix.plex }} Plex server
uses: nick-fields/retry@v2.9.0
uses: nick-fields/retry@v3.0.0
with:
max_attempts: 3
timeout_minutes: 2
@@ -230,7 +230,7 @@ jobs:

- name: Restore Python ${{ steps.python.outputs.python-version }} virtual environment
id: cache-venv
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: venv
key: >-
4 changes: 2 additions & 2 deletions plexapi/audio.py
Original file line number Diff line number Diff line change
@@ -227,7 +227,7 @@ def albums(self, **kwargs):
""" Returns a list of :class:`~plexapi.audio.Album` objects by the artist. """
return self.section().search(
libtype='album',
filters={'artist.id': self.ratingKey},
filters={**kwargs.pop('filters', {}), 'artist.id': self.ratingKey},
**kwargs
)

@@ -289,7 +289,7 @@ def metadataDirectory(self):
@utils.registerPlexObject
class Album(
Audio,
UnmatchMatchMixin, RatingMixin,
SplitMergeMixin, UnmatchMatchMixin, RatingMixin,
ArtMixin, PosterMixin, ThemeUrlMixin,
AlbumEditMixins
):
5 changes: 3 additions & 2 deletions plexapi/base.py
Original file line number Diff line number Diff line change
@@ -98,7 +98,7 @@ def _buildItem(self, elem, cls=None, initpath=None):
ecls = utils.PLEXOBJECTS.get(ehash, utils.PLEXOBJECTS.get(elem.tag))
# log.debug('Building %s as %s', elem.tag, ecls.__name__)
if ecls is not None:
return ecls(self._server, elem, initpath)
return ecls(self._server, elem, initpath, parent=self)
raise UnknownType(f"Unknown library type <{elem.tag} type='{etype}'../>")

def _buildItemOrNone(self, elem, cls=None, initpath=None):
@@ -227,7 +227,8 @@ def fetchItems(self, ekey, cls=None, container_start=None, container_size=None,
fetchItem(ekey, viewCount__gte=0)
fetchItem(ekey, Media__container__in=["mp4", "mkv"])
fetchItem(ekey, guid__iregex=r"(imdb://|themoviedb://)")
fetchItem(ekey, guid__regex=r"com\.plexapp\.agents\.(imdb|themoviedb)://|tt\d+")
fetchItem(ekey, guid__id__regex=r"(imdb|tmdb|tvdb)://")
fetchItem(ekey, Media__Part__file__startswith="D:\\Movies")
"""
2 changes: 1 addition & 1 deletion plexapi/const.py
Original file line number Diff line number Diff line change
@@ -4,6 +4,6 @@
# Library version
MAJOR_VERSION = 4
MINOR_VERSION = 15
PATCH_VERSION = 7
PATCH_VERSION = 8
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
38 changes: 30 additions & 8 deletions plexapi/library.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
import re
import warnings
from collections import defaultdict
from datetime import datetime
from functools import cached_property
from urllib.parse import parse_qs, quote_plus, urlencode, urlparse
@@ -41,14 +43,22 @@ def _loadData(self, data):
def _loadSections(self):
""" Loads and caches all the library sections. """
key = '/library/sections'
self._sectionsByID = {}
self._sectionsByTitle = {}
sectionsByID = {}
sectionsByTitle = defaultdict(list)
libcls = {
'movie': MovieSection,
'show': ShowSection,
'artist': MusicSection,
'photo': PhotoSection,
}

for elem in self._server.query(key):
for cls in (MovieSection, ShowSection, MusicSection, PhotoSection):
if elem.attrib.get('type') == cls.TYPE:
section = cls(self._server, elem, key)
self._sectionsByID[section.key] = section
self._sectionsByTitle[section.title.lower().strip()] = section
section = libcls.get(elem.attrib.get('type'), LibrarySection)(self._server, elem, initpath=key)
sectionsByID[section.key] = section
sectionsByTitle[section.title.lower().strip()].append(section)

self._sectionsByID = sectionsByID
self._sectionsByTitle = dict(sectionsByTitle)

def sections(self):
""" Returns a list of all media sections in this library. Library sections may be any of
@@ -60,18 +70,30 @@ def sections(self):

def section(self, title):
""" Returns the :class:`~plexapi.library.LibrarySection` that matches the specified title.
Note: Multiple library sections with the same title is ambiguous.
Use :func:`~plexapi.library.Library.sectionByID` instead for an exact match.
Parameters:
title (str): Title of the section to return.
Raises:
:exc:`~plexapi.exceptions.NotFound`: The library section title is not found on the server.
"""
normalized_title = title.lower().strip()
if not self._sectionsByTitle or normalized_title not in self._sectionsByTitle:
self._loadSections()
try:
return self._sectionsByTitle[normalized_title]
sections = self._sectionsByTitle[normalized_title]
except KeyError:
raise NotFound(f'Invalid library section: {title}') from None

if len(sections) > 1:
warnings.warn(
'Multiple library sections with the same title found, use "sectionByID" instead. '
'Returning the last section.'
)
return sections[-1]

def sectionByID(self, sectionID):
""" Returns the :class:`~plexapi.library.LibrarySection` that matches the specified sectionID.
45 changes: 21 additions & 24 deletions plexapi/media.py
Original file line number Diff line number Diff line change
@@ -37,7 +37,7 @@ class Media(PlexObject):
videoResolution (str): The video resolution of the media (ex: sd).
width (int): The width of the video in pixels (ex: 608).
<Photo_only_attributes>: The following attributes are only available for photos.
Photo_only_attributes: The following attributes are only available for photos.
* aperture (str): The aperture used to take the photo.
* exposure (str): The exposure used to take the photo.
@@ -74,13 +74,13 @@ def _loadData(self, data):
self.width = utils.cast(int, data.attrib.get('width'))
self.uuid = data.attrib.get('uuid')

if self._isChildOf(etag='Photo'):
self.aperture = data.attrib.get('aperture')
self.exposure = data.attrib.get('exposure')
self.iso = utils.cast(int, data.attrib.get('iso'))
self.lens = data.attrib.get('lens')
self.make = data.attrib.get('make')
self.model = data.attrib.get('model')
# Photo only attributes
self.aperture = data.attrib.get('aperture')
self.exposure = data.attrib.get('exposure')
self.iso = utils.cast(int, data.attrib.get('iso'))
self.lens = data.attrib.get('lens')
self.make = data.attrib.get('make')
self.model = data.attrib.get('model')

parent = self._parent()
self._parentKey = parent.key
@@ -158,11 +158,8 @@ def _loadData(self, data):
self.videoProfile = data.attrib.get('videoProfile')

def _buildStreams(self, data):
streams = []
for cls in (VideoStream, AudioStream, SubtitleStream, LyricStream):
items = self.findItems(data, cls, streamType=cls.STREAMTYPE)
streams.extend(items)
return streams
""" Returns a list of :class:`~plexapi.media.MediaPartStream` objects in this MediaPart. """
return self.findItems(data)

@property
def hasPreviewThumbnails(self):
@@ -384,7 +381,7 @@ class AudioStream(MediaPartStream):
samplingRate (int): The sampling rate of the audio stream (ex: xxx)
streamIdentifier (int): The stream identifier of the audio stream.
<Track_only_attributes>: The following attributes are only available for tracks.
Track_only_attributes: The following attributes are only available for tracks.
* albumGain (float): The gain for the album.
* albumPeak (float): The peak for the album.
@@ -411,16 +408,16 @@ def _loadData(self, data):
self.samplingRate = utils.cast(int, data.attrib.get('samplingRate'))
self.streamIdentifier = utils.cast(int, data.attrib.get('streamIdentifier'))

if self._isChildOf(etag='Track'):
self.albumGain = utils.cast(float, data.attrib.get('albumGain'))
self.albumPeak = utils.cast(float, data.attrib.get('albumPeak'))
self.albumRange = utils.cast(float, data.attrib.get('albumRange'))
self.endRamp = data.attrib.get('endRamp')
self.gain = utils.cast(float, data.attrib.get('gain'))
self.loudness = utils.cast(float, data.attrib.get('loudness'))
self.lra = utils.cast(float, data.attrib.get('lra'))
self.peak = utils.cast(float, data.attrib.get('peak'))
self.startRamp = data.attrib.get('startRamp')
# Track only attributes
self.albumGain = utils.cast(float, data.attrib.get('albumGain'))
self.albumPeak = utils.cast(float, data.attrib.get('albumPeak'))
self.albumRange = utils.cast(float, data.attrib.get('albumRange'))
self.endRamp = data.attrib.get('endRamp')
self.gain = utils.cast(float, data.attrib.get('gain'))
self.loudness = utils.cast(float, data.attrib.get('loudness'))
self.lra = utils.cast(float, data.attrib.get('lra'))
self.peak = utils.cast(float, data.attrib.get('peak'))
self.startRamp = data.attrib.get('startRamp')

def setSelected(self):
""" Sets this audio stream as the selected audio stream.
6 changes: 5 additions & 1 deletion plexapi/myplex.py
Original file line number Diff line number Diff line change
@@ -1704,7 +1704,9 @@ def __init__(self, session=None, requestTimeout=None, headers=None, oauth=False)

@property
def pin(self):
""" Return the 4 character PIN used for linking a device at https://plex.tv/link. """
""" Return the 4 character PIN used for linking a device at
https://plex.tv/link.
"""
if self._oauth:
raise BadRequest('Cannot use PIN for Plex OAuth login')
return self._code
@@ -1736,6 +1738,7 @@ def oauthUrl(self, forwardUrl=None):

def run(self, callback=None, timeout=None):
""" Starts the thread which monitors the PIN login state.
Parameters:
callback (Callable[str]): Callback called with the received authentication token (optional).
timeout (int): Timeout in seconds waiting for the PIN login to succeed (optional).
@@ -1758,6 +1761,7 @@ def run(self, callback=None, timeout=None):

def waitForLogin(self):
""" Waits for the PIN login to succeed or expire.
Parameters:
callback (Callable[str]): Callback called with the received authentication token (optional).
timeout (int): Timeout in seconds waiting for the PIN login to succeed (optional).
35 changes: 17 additions & 18 deletions plexapi/utils.py
Original file line number Diff line number Diff line change
@@ -144,22 +144,21 @@ def cast(func, value):
func (func): Callback function to used cast to type (int, bool, float).
value (any): value to be cast and returned.
"""
if value is not None:
if func == bool:
if value in (1, True, "1", "true"):
return True
elif value in (0, False, "0", "false"):
return False
else:
raise ValueError(value)

elif func in (int, float):
try:
return func(value)
except ValueError:
return float('nan')
return func(value)
return value
if value is None:
return value
if func == bool:
if value in (1, True, "1", "true"):
return True
if value in (0, False, "0", "false"):
return False
raise ValueError(value)

if func in (int, float):
try:
return func(value)
except ValueError:
return float('nan')
return func(value)


def joinArgs(args):
@@ -329,7 +328,7 @@ def toDatetime(value, format=None):
return None
try:
return datetime.fromtimestamp(value)
except (OSError, OverflowError):
except (OSError, OverflowError, ValueError):
try:
return datetime.fromtimestamp(0) + timedelta(seconds=value)
except OverflowError:
@@ -407,7 +406,7 @@ def downloadSessionImages(server, filename=None, height=150, width=150,
return info


def download(url, token, filename=None, savepath=None, session=None, chunksize=4024, # noqa: C901
def download(url, token, filename=None, savepath=None, session=None, chunksize=4096, # noqa: C901
unpack=False, mocked=False, showstatus=False):
""" Helper to download a thumb, videofile or other media item. Returns the local
path to the downloaded file.
6 changes: 3 additions & 3 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -3,9 +3,9 @@
# pip install -r requirements_dev.txt
#---------------------------------------------------------
coveralls==3.3.1
flake8==6.1.0
pillow==10.1.0
pytest==7.4.3
flake8==7.0.0
pillow==10.2.0
pytest==8.0.0
pytest-cache==1.0
pytest-cov==4.1.0
pytest-mock==3.12.0
2 changes: 1 addition & 1 deletion tests/test_audio.py
Original file line number Diff line number Diff line change
@@ -78,7 +78,7 @@ def test_audio_Artist_album(artist):


def test_audio_Artist_albums(artist):
albums = artist.albums()
albums = artist.albums(filters={})
assert len(albums) == 1 and albums[0].title == "Layers"


4 changes: 0 additions & 4 deletions tests/test_video.py
Original file line number Diff line number Diff line change
@@ -135,8 +135,6 @@ def test_video_Movie_attrs(movies):
assert audio._server._baseurl == utils.SERVER_BASEURL
assert audio.title is None
assert audio.type == 2
with pytest.raises(AttributeError):
assert audio.albumGain is None # Check track only attributes are not available
# Media
media = movie.media[0]
assert media.aspectRatio >= 1.3
@@ -160,8 +158,6 @@ def test_video_Movie_attrs(movies):
assert media.videoProfile == "main"
assert media.videoResolution in utils.RESOLUTIONS
assert utils.is_int(media.width, gte=200)
with pytest.raises(AttributeError):
assert media.aperture is None # Check photo only attributes are not available
# Video
video = movie.media[0].parts[0].videoStreams()[0]
assert video.anamorphic is None
19 changes: 19 additions & 0 deletions tools/plex-bootstraptest.py
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@
"""
import argparse
from datetime import datetime
import os
import shutil
import socket
@@ -506,6 +507,24 @@ def alert_callback(data):
server.settings.get("GenerateBIFBehavior").set("never")
server.settings.get("GenerateChapterThumbBehavior").set("never")
server.settings.get("LoudnessAnalysisBehavior").set("never")

# disable butler tasks
current_hour = datetime.now().hour
start_hour = (current_hour + 12) % 24
end_hour = (current_hour + 15) % 24
server.settings.get("ButlerStartHour").set(start_hour)
server.settings.get("ButlerEndHour").set(end_hour)

# find all butler settings
for setting in server.settings.all():
if setting.id.lower().startswith("butler") and isinstance(setting.value, bool):
try:
setting.set(False)
print("Disabled setting '{}'".format(setting))
except NotFound:
print("Setting '{}' not found".format(setting))

# save settings
server.settings.save()

sections = []
Loading