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.9
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.10
Choose a head ref
  • 12 commits
  • 14 files changed
  • 4 contributors

Commits on Feb 17, 2024

  1. Copy the full SHA
    6e4ef6b View commit details
  2. Add slug attributes to Movie, Show, Season, and Episode (#1317)

    * Add slug attributes to videos
    
    * Add tests for slug attributes
    JonnyWong16 authored Feb 17, 2024
    Copy the full SHA
    b831aae View commit details
  3. Add genres attribute to Track (#1318)

    * Add genres attribute to Track
    
    * Test track genres
    
    * Add GenreMixin to Tracks
    
    * Add test for editing track genre
    JonnyWong16 authored Feb 17, 2024
    Copy the full SHA
    284a577 View commit details
  4. Feature: Add source property to playlist items to support remote play…

    …list entries (#1335)
    
    * Add source property to Video
    
    A Playlist entry if added a remote server item has field "source" initialized with value like `server://<server_id>/com.plexapp.plugins.library`
    
    * Add source to Episode
    
    * Fix flake8 error
    
    E261 at least two spaces before inline comment
    
    * Add source to Track
    
    * Add source to Photo
    
    * Rename the field to sourceURI
    
    * Update plexapi/audio.py
    
    Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
    
    * Update plexapi/photo.py
    
    Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
    
    * Update plexapi/video.py
    
    Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
    
    * Update plexapi/video.py
    
    Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
    
    * Fix flake line length issue
    
    ---------
    
    Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
    glensc and JonnyWong16 authored Feb 17, 2024
    Copy the full SHA
    9d9dca8 View commit details
  5. Refactor attribute filtering in PlexObject class (#1341)

    * Refactor attribute filtering in PlexObject class
    
    - minor performance imporvement
    
    * Update plexapi/base.py
    
    Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
    
    ---------
    
    Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
    Dr-Blank and JonnyWong16 authored Feb 17, 2024
    Copy the full SHA
    41f6b9c View commit details
  6. Add exception for two-factor required (#1357)

    * Add exception for two-factor required
    
    * Update tools/plex-gettoken.py with 2FA exception
    JonnyWong16 authored Feb 17, 2024
    Copy the full SHA
    26447d1 View commit details
  7. Breaking: Change regex/iregex to use re.search instead of re.match (#…

    …1358)
    
    * Change regex/iregex to use re.search instead of re.match
    
    This is BREAKING CHANGE
    
    * Also cast to bool.
    
    Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
    
    ---------
    
    Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>
    glensc and JonnyWong16 authored Feb 17, 2024
    Copy the full SHA
    7c99d4a View commit details
  8. Lowercase input of istartswith/iendswith (#1359)

    To be symmetric with other case insensitive methods
    that lowercase both operands in comparison
    glensc authored Feb 17, 2024
    Copy the full SHA
    4b67b4d View commit details
  9. Copy the full SHA
    cfce82a View commit details
  10. Copy the full SHA
    9a54177 View commit details
  11. Bump tqdm from 4.66.1 to 4.66.2 (#1364)

    Bumps [tqdm](https://github.com/tqdm/tqdm) from 4.66.1 to 4.66.2.
    - [Release notes](https://github.com/tqdm/tqdm/releases)
    - [Commits](tqdm/tqdm@v4.66.1...v4.66.2)
    
    ---
    updated-dependencies:
    - dependency-name: tqdm
      dependency-type: direct:development
      update-type: version-update:semver-patch
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    dependabot[bot] authored Feb 17, 2024
    Copy the full SHA
    67e9d37 View commit details
  12. Release 4.15.10

    JonnyWong16 committed Feb 17, 2024
    Copy the full SHA
    abcab4f View commit details
Showing with 68 additions and 34 deletions.
  1. +0 −1 .gitignore
  2. +6 −1 plexapi/audio.py
  3. +5 −5 plexapi/base.py
  4. +1 −1 plexapi/const.py
  5. +5 −0 plexapi/exceptions.py
  6. +1 −1 plexapi/mixins.py
  7. +3 −1 plexapi/myplex.py
  8. +3 −0 plexapi/photo.py
  9. +14 −0 plexapi/video.py
  10. +1 −1 requirements_dev.txt
  11. +2 −0 tests/test_audio.py
  12. +16 −21 tests/test_history.py
  13. +4 −0 tests/test_video.py
  14. +7 −2 tools/plex-gettoken.py
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
syntax: glob
*.db
*.egg-info
*.log
7 changes: 6 additions & 1 deletion plexapi/audio.py
Original file line number Diff line number Diff line change
@@ -159,7 +159,7 @@ def sonicallySimilar(

return self.fetchItems(
key,
cls=self.__class__,
cls=type(self),
**kwargs,
)

@@ -427,6 +427,7 @@ class Track(
chapterSource (str): Unknown
collections (List<:class:`~plexapi.media.Collection`>): List of collection objects.
duration (int): Length of the track in milliseconds.
genres (List<:class:`~plexapi.media.Genre`>): List of genre objects.
grandparentArt (str): URL to album artist artwork (/library/metadata/<grandparentRatingKey>/art/<artid>).
grandparentGuid (str): Plex GUID for the album artist (plex://artist/5d07bcb0403c64029053ac4c).
grandparentKey (str): API URL of the album artist (/library/metadata/<grandparentRatingKey>).
@@ -449,6 +450,8 @@ class Track(
primaryExtraKey (str) API URL for the primary extra for the track.
ratingCount (int): Number of listeners who have scrobbled this track, as reported by Last.fm.
skipCount (int): Number of times the track has been skipped.
sourceURI (str): Remote server URI (server://<machineIdentifier>/com.plexapp.plugins.library)
(remote playlist item only).
viewOffset (int): View offset in milliseconds.
year (int): Year the track was released.
"""
@@ -463,6 +466,7 @@ def _loadData(self, data):
self.chapterSource = data.attrib.get('chapterSource')
self.collections = self.findItems(data, media.Collection)
self.duration = utils.cast(int, data.attrib.get('duration'))
self.genres = self.findItems(data, media.Genre)
self.grandparentArt = data.attrib.get('grandparentArt')
self.grandparentGuid = data.attrib.get('grandparentGuid')
self.grandparentKey = data.attrib.get('grandparentKey')
@@ -483,6 +487,7 @@ def _loadData(self, data):
self.primaryExtraKey = data.attrib.get('primaryExtraKey')
self.ratingCount = utils.cast(int, data.attrib.get('ratingCount'))
self.skipCount = utils.cast(int, data.attrib.get('skipCount'))
self.sourceURI = data.attrib.get('source') # remote playlist item
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
self.year = utils.cast(int, data.attrib.get('year'))

10 changes: 5 additions & 5 deletions plexapi/base.py
Original file line number Diff line number Diff line change
@@ -22,12 +22,12 @@
'lt': lambda v, q: v < q,
'lte': lambda v, q: v <= q,
'startswith': lambda v, q: v.startswith(q),
'istartswith': lambda v, q: v.lower().startswith(q),
'istartswith': lambda v, q: v.lower().startswith(q.lower()),
'endswith': lambda v, q: v.endswith(q),
'iendswith': lambda v, q: v.lower().endswith(q),
'iendswith': lambda v, q: v.lower().endswith(q.lower()),
'exists': lambda v, q: v is not None if q else v is None,
'regex': lambda v, q: re.match(q, v),
'iregex': lambda v, q: re.match(q, v, flags=re.IGNORECASE),
'regex': lambda v, q: bool(re.search(q, v)),
'iregex': lambda v, q: bool(re.search(q, v, flags=re.IGNORECASE)),
}


@@ -440,7 +440,7 @@ def _getAttrValue(self, elem, attrstr, results=None):
attrstr = parts[1] if len(parts) == 2 else None
if attrstr:
results = [] if results is None else results
for child in [c for c in elem if c.tag.lower() == attr.lower()]:
for child in (c for c in elem if c.tag.lower() == attr.lower()):
results += self._getAttrValue(child, attrstr, results)
return [r for r in results if r is not None]
# check were looking for the tag
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 = 9
PATCH_VERSION = 10
__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__ = f"{__short_version__}.{PATCH_VERSION}"
5 changes: 5 additions & 0 deletions plexapi/exceptions.py
Original file line number Diff line number Diff line change
@@ -29,3 +29,8 @@ class Unsupported(PlexApiException):
class Unauthorized(BadRequest):
""" Invalid username/password or token. """
pass


class TwoFactorRequired(Unauthorized):
""" Two factor authentication required. """
pass
2 changes: 1 addition & 1 deletion plexapi/mixins.py
Original file line number Diff line number Diff line change
@@ -1196,7 +1196,7 @@ class AlbumEditMixins(
class TrackEditMixins(
ArtLockMixin, PosterLockMixin, ThemeLockMixin,
AddedAtMixin, TitleMixin, TrackArtistMixin, TrackNumberMixin, TrackDiscNumberMixin, UserRatingMixin,
CollectionMixin, LabelMixin, MoodMixin
CollectionMixin, GenreMixin, LabelMixin, MoodMixin
):
pass

4 changes: 3 additions & 1 deletion plexapi/myplex.py
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@
log, logfilter, utils)
from plexapi.base import PlexObject
from plexapi.client import PlexClient
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
from plexapi.exceptions import BadRequest, NotFound, Unauthorized, TwoFactorRequired
from plexapi.library import LibrarySection
from plexapi.server import PlexServer
from plexapi.sonos import PlexSonosClient
@@ -237,6 +237,8 @@ def query(self, url, method=None, headers=None, timeout=None, **kwargs):
errtext = response.text.replace('\n', ' ')
message = f'({response.status_code}) {codename}; {response.url} {errtext}'
if response.status_code == 401:
if "verification code" in response.text:
raise TwoFactorRequired(message)
raise Unauthorized(message)
elif response.status_code == 404:
raise NotFound(message)
3 changes: 3 additions & 0 deletions plexapi/photo.py
Original file line number Diff line number Diff line change
@@ -180,6 +180,8 @@ class Photo(
parentThumb (str): URL to photo album thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
parentTitle (str): Name of the photo album for the photo.
ratingKey (int): Unique key identifying the photo.
sourceURI (str): Remote server URI (server://<machineIdentifier>/com.plexapp.plugins.library)
(remote playlist item only).
summary (str): Summary of the photo.
tags (List<:class:`~plexapi.media.Tag`>): List of tag objects.
thumb (str): URL to thumbnail image (/library/metadata/<ratingKey>/thumb/<thumbid>).
@@ -218,6 +220,7 @@ def _loadData(self, data):
self.parentThumb = data.attrib.get('parentThumb')
self.parentTitle = data.attrib.get('parentTitle')
self.ratingKey = utils.cast(int, data.attrib.get('ratingKey'))
self.sourceURI = data.attrib.get('source') # remote playlist item
self.summary = data.attrib.get('summary')
self.tags = self.findItems(data, media.Tag)
self.thumb = data.attrib.get('thumb')
14 changes: 14 additions & 0 deletions plexapi/video.py
Original file line number Diff line number Diff line change
@@ -368,7 +368,10 @@ class Movie(
ratingImage (str): Key to critic rating image (rottentomatoes://image.rating.rotten).
ratings (List<:class:`~plexapi.media.Rating`>): List of rating objects.
roles (List<:class:`~plexapi.media.Role`>): List of role objects.
slug (str): The clean watch.plex.tv URL identifier for the movie.
similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects.
sourceURI (str): Remote server URI (server://<machineIdentifier>/com.plexapp.plugins.library)
(remote playlist item only).
studio (str): Studio that created movie (Di Bonaventura Pictures; 21 Laps Entertainment).
tagline (str): Movie tag line (Back 2 Work; Who says men can't change?).
theme (str): URL to theme resource (/library/metadata/<ratingkey>/theme/<themeid>).
@@ -411,7 +414,9 @@ def _loadData(self, data):
self.ratingImage = data.attrib.get('ratingImage')
self.ratings = self.findItems(data, media.Rating)
self.roles = self.findItems(data, media.Role)
self.slug = data.attrib.get('slug')
self.similar = self.findItems(data, media.Similar)
self.sourceURI = data.attrib.get('source') # remote playlist item
self.studio = data.attrib.get('studio')
self.tagline = data.attrib.get('tagline')
self.theme = data.attrib.get('theme')
@@ -531,6 +536,7 @@ class Show(
(None = Library default, tmdbAiring = The Movie Database (Aired),
aired = TheTVDB (Aired), dvd = TheTVDB (DVD), absolute = TheTVDB (Absolute)).
similar (List<:class:`~plexapi.media.Similar`>): List of Similar objects.
slug (str): The clean watch.plex.tv URL identifier for the show.
studio (str): Studio that created show (Di Bonaventura Pictures; 21 Laps Entertainment).
subtitleLanguage (str): Setting that indicates the preferred subtitle language.
subtitleMode (int): Setting that indicates the auto-select subtitle mode.
@@ -580,6 +586,7 @@ def _loadData(self, data):
self.seasonCount = utils.cast(int, data.attrib.get('seasonCount', self.childCount))
self.showOrdering = data.attrib.get('showOrdering')
self.similar = self.findItems(data, media.Similar)
self.slug = data.attrib.get('slug')
self.studio = data.attrib.get('studio')
self.subtitleLanguage = data.attrib.get('subtitleLanguage', '')
self.subtitleMode = utils.cast(int, data.attrib.get('subtitleMode', '-1'))
@@ -717,6 +724,7 @@ class Season(
parentIndex (int): Plex index number for the show.
parentKey (str): API URL of the show (/library/metadata/<parentRatingKey>).
parentRatingKey (int): Unique key identifying the show.
parentSlug (str): The clean watch.plex.tv URL identifier for the show.
parentStudio (str): Studio that created show.
parentTheme (str): URL to show theme resource (/library/metadata/<parentRatingkey>/theme/<themeid>).
parentThumb (str): URL to show thumbnail image (/library/metadata/<parentRatingKey>/thumb/<thumbid>).
@@ -746,6 +754,7 @@ def _loadData(self, data):
self.parentIndex = utils.cast(int, data.attrib.get('parentIndex'))
self.parentKey = data.attrib.get('parentKey')
self.parentRatingKey = utils.cast(int, data.attrib.get('parentRatingKey'))
self.parentSlug = data.attrib.get('parentSlug')
self.parentStudio = data.attrib.get('parentStudio')
self.parentTheme = data.attrib.get('parentTheme')
self.parentThumb = data.attrib.get('parentThumb')
@@ -877,6 +886,7 @@ class Episode(
grandparentGuid (str): Plex GUID for the show (plex://show/5d9c086fe9d5a1001f4d9fe6).
grandparentKey (str): API URL of the show (/library/metadata/<grandparentRatingKey>).
grandparentRatingKey (int): Unique key identifying the show.
grandparentSlug (str): The clean watch.plex.tv URL identifier for the show.
grandparentTheme (str): URL to show theme resource (/library/metadata/<grandparentRatingkey>/theme/<themeid>).
grandparentThumb (str): URL to show thumbnail image (/library/metadata/<grandparentRatingKey>/thumb/<thumbid>).
grandparentTitle (str): Name of the show for the episode.
@@ -898,6 +908,8 @@ class Episode(
ratings (List<:class:`~plexapi.media.Rating`>): List of rating objects.
roles (List<:class:`~plexapi.media.Role`>): List of role objects.
skipParent (bool): True if the show's seasons are set to hidden.
sourceURI (str): Remote server URI (server://<machineIdentifier>/com.plexapp.plugins.library)
(remote playlist item only).
viewOffset (int): View offset in milliseconds.
writers (List<:class:`~plexapi.media.Writer`>): List of writers objects.
year (int): Year the episode was released.
@@ -922,6 +934,7 @@ def _loadData(self, data):
self.grandparentGuid = data.attrib.get('grandparentGuid')
self.grandparentKey = data.attrib.get('grandparentKey')
self.grandparentRatingKey = utils.cast(int, data.attrib.get('grandparentRatingKey'))
self.grandparentSlug = data.attrib.get('grandparentSlug')
self.grandparentTheme = data.attrib.get('grandparentTheme')
self.grandparentThumb = data.attrib.get('grandparentThumb')
self.grandparentTitle = data.attrib.get('grandparentTitle')
@@ -940,6 +953,7 @@ def _loadData(self, data):
self.ratings = self.findItems(data, media.Rating)
self.roles = self.findItems(data, media.Role)
self.skipParent = utils.cast(bool, data.attrib.get('skipParent', '0'))
self.sourceURI = data.attrib.get('source') # remote playlist item
self.viewOffset = utils.cast(int, data.attrib.get('viewOffset', 0))
self.writers = self.findItems(data, media.Writer)
self.year = utils.cast(int, data.attrib.get('year'))
2 changes: 1 addition & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -14,5 +14,5 @@ requests==2.31.0
requests-mock==1.11.0
sphinx==7.1.2
sphinx-rtd-theme==2.0.0
tqdm==4.66.1
tqdm==4.66.2
websocket-client==1.7.0
2 changes: 2 additions & 0 deletions tests/test_audio.py
Original file line number Diff line number Diff line change
@@ -278,6 +278,7 @@ def test_audio_Track_attrs(album):
assert utils.is_art(track.art)
assert track.chapterSource is None
assert utils.is_int(track.duration)
assert track.genres == []
if track.grandparentArt:
assert utils.is_art(track.grandparentArt)
assert utils.is_metadata(track.grandparentKey)
@@ -422,6 +423,7 @@ def test_audio_Track_mixins_fields(track):

def test_audio_Track_mixins_tags(track):
test_mixins.edit_collection(track)
test_mixins.edit_genre(track)
test_mixins.edit_label(track)
test_mixins.edit_mood(track)

37 changes: 16 additions & 21 deletions tests/test_history.py
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@
def test_history_Movie(movie):
movie.markPlayed()
history = movie.history()
assert len(history)
assert not len(history)
movie.markUnplayed()


@@ -16,8 +16,7 @@ def test_history_Show(show):
show.markUnplayed()


def test_history_Season(show):
season = show.season("Season 1")
def test_history_Season(season):
season.markPlayed()
history = season.history()
assert len(history)
@@ -27,7 +26,7 @@ def test_history_Season(show):
def test_history_Episode(episode):
episode.markPlayed()
history = episode.history()
assert len(history)
assert not len(history)
episode.markUnplayed()


@@ -48,49 +47,45 @@ def test_history_Album(album):
def test_history_Track(track):
track.markPlayed()
history = track.history()
assert len(history)
assert not len(history)
track.markUnplayed()


def test_history_MyAccount(account, movie, show):
movie.markPlayed()
def test_history_MyAccount(account, show):
show.markPlayed()
history = account.history()
assert len(history)
movie.markUnplayed()
show.markUnplayed()


def test_history_MyLibrary(plex, movie, show):
movie.markPlayed()
def test_history_MyLibrary(plex, show):
show.markPlayed()
history = plex.library.history()
assert len(history)
movie.markUnplayed()
show.markUnplayed()


def test_history_MySection(plex, movie):
movie.markPlayed()
history = plex.library.section("Movies").history()
def test_history_MySection(tvshows, show):
show.markPlayed()
history = tvshows.history()
assert len(history)
movie.markUnplayed()
show.markUnplayed()


def test_history_MyServer(plex, movie):
movie.markPlayed()
def test_history_MyServer(plex, show):
show.markPlayed()
history = plex.history()
assert len(history)
movie.markUnplayed()
show.markUnplayed()


def test_history_PlexHistory(plex, movie):
movie.markPlayed()
def test_history_PlexHistory(plex, show):
show.markPlayed()
history = plex.history()
assert len(history)

hist = history[0]
assert hist.source() == movie
assert hist.source().show() == show
assert hist.accountID
assert hist.deviceID
assert hist.historyKey
4 changes: 4 additions & 0 deletions tests/test_video.py
Original file line number Diff line number Diff line change
@@ -92,6 +92,7 @@ def test_video_Movie_attrs(movies):
assert utils.is_metadata(movie.primaryExtraKey)
assert movie.ratingKey >= 1
assert movie._server._baseurl == utils.SERVER_BASEURL
assert movie.slug == "sita-sings-the-blues"
assert movie.studio == "Nina Paley"
assert utils.is_string(movie.summary, gte=100)
assert movie.tagline == "The Greatest Break-Up Story Ever Told."
@@ -788,6 +789,7 @@ def test_video_Show_attrs(show):
assert show._server._baseurl == utils.SERVER_BASEURL
assert utils.is_int(show.seasonCount)
assert show.showOrdering in (None, 'aired')
assert show.slug == "game-of-thrones"
assert show.studio == "Revolution Sun Studios"
assert utils.is_string(show.summary, gte=100)
assert show.subtitleLanguage == ''
@@ -1006,6 +1008,7 @@ def test_video_Season_attrs(show):
assert season.parentIndex == 1
assert utils.is_metadata(season.parentKey)
assert utils.is_int(season.parentRatingKey)
assert season.parentSlug == "game-of-thrones"
assert season.parentStudio == "Revolution Sun Studios"
assert utils.is_metadata(season.parentTheme)
if season.parentThumb:
@@ -1182,6 +1185,7 @@ def test_video_Episode_attrs(episode):
assert episode.grandparentGuid == "plex://show/5d9c086c46115600200aa2fe"
assert utils.is_metadata(episode.grandparentKey)
assert utils.is_int(episode.grandparentRatingKey)
assert episode.grandparentSlug == "game-of-thrones"
assert utils.is_metadata(episode.grandparentTheme)
if episode.grandparentThumb:
assert utils.is_thumb(episode.grandparentThumb)
9 changes: 7 additions & 2 deletions tools/plex-gettoken.py
Original file line number Diff line number Diff line change
@@ -4,11 +4,16 @@
Plex-GetToken is a simple method to retrieve a Plex account token.
"""
from getpass import getpass
from plexapi.exceptions import TwoFactorRequired
from plexapi.myplex import MyPlexAccount

username = input("Plex username: ")
password = getpass("Plex password: ")
code = input("Plex 2FA code (leave blank for none): ")

account = MyPlexAccount(username, password, code=code)
try:
account = MyPlexAccount(username, password)
except TwoFactorRequired:
code = input("Plex 2FA code: ")
account = MyPlexAccount(username, password, code=code)

print(account.authenticationToken)