diff --git a/lib/plexapi/audio.py b/lib/plexapi/audio.py index 0eb397cc..8f84f3be 100644 --- a/lib/plexapi/audio.py +++ b/lib/plexapi/audio.py @@ -181,6 +181,7 @@ class Artist( TYPE (str): 'artist' albumSort (int): Setting that indicates how albums are sorted for the artist (-1 = Library default, 0 = Newest first, 1 = Oldest first, 2 = By name). + audienceRating (float): Audience rating. collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. countries (List<:class:`~plexapi.media.Country`>): List country objects. genres (List<:class:`~plexapi.media.Genre`>): List of genre objects. @@ -188,6 +189,7 @@ class Artist( key (str): API URL (/library/metadata/). labels (List<:class:`~plexapi.media.Label`>): List of label objects. locations (List): List of folder paths where the artist is found on disk. + rating (float): Artist rating (7.9; 9.8; 8.1). similar (List<:class:`~plexapi.media.Similar`>): List of similar objects. styles (List<:class:`~plexapi.media.Style`>): List of style objects. theme (str): URL to theme resource (/library/metadata//theme/). @@ -199,6 +201,7 @@ class Artist( """ Load attribute values from Plex XML response. """ Audio._loadData(self, data) self.albumSort = utils.cast(int, data.attrib.get('albumSort', '-1')) + self.audienceRating = utils.cast(float, data.attrib.get('audienceRating')) self.collections = self.findItems(data, media.Collection) self.countries = self.findItems(data, media.Country) self.genres = self.findItems(data, media.Genre) @@ -206,6 +209,7 @@ class Artist( self.key = self.key.replace('/children', '') # FIX_BUG_50 self.labels = self.findItems(data, media.Label) self.locations = self.listAttrs(data, 'path', etag='Location') + self.rating = utils.cast(float, data.attrib.get('rating')) self.similar = self.findItems(data, media.Similar) self.styles = self.findItems(data, media.Style) self.theme = data.attrib.get('theme') @@ -301,6 +305,7 @@ class Album( Attributes: TAG (str): 'Directory' TYPE (str): 'album' + audienceRating (float): Audience rating. collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. formats (List<:class:`~plexapi.media.Format`>): List of format objects. genres (List<:class:`~plexapi.media.Genre`>): List of genre objects. @@ -329,6 +334,7 @@ class Album( def _loadData(self, data): """ Load attribute values from Plex XML response. """ Audio._loadData(self, data) + self.audienceRating = utils.cast(float, data.attrib.get('audienceRating')) self.collections = self.findItems(data, media.Collection) self.formats = self.findItems(data, media.Format) self.genres = self.findItems(data, media.Genre) @@ -426,6 +432,7 @@ class Track( Attributes: TAG (str): 'Directory' TYPE (str): 'track' + audienceRating (float): Audience rating. chapters (List<:class:`~plexapi.media.Chapter`>): List of Chapter objects. chapterSource (str): Unknown collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. @@ -451,6 +458,7 @@ class Track( parentThumb (str): URL to album thumbnail image (/library/metadata//thumb/). parentTitle (str): Name of the album for the track. primaryExtraKey (str) API URL for the primary extra for the track. + rating (float): Track rating (7.9; 9.8; 8.1). 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:///com.plexapp.plugins.library) @@ -465,6 +473,7 @@ class Track( """ Load attribute values from Plex XML response. """ Audio._loadData(self, data) Playable._loadData(self, data) + self.audienceRating = utils.cast(float, data.attrib.get('audienceRating')) self.chapters = self.findItems(data, media.Chapter) self.chapterSource = data.attrib.get('chapterSource') self.collections = self.findItems(data, media.Collection) @@ -488,6 +497,7 @@ class Track( self.parentThumb = data.attrib.get('parentThumb') self.parentTitle = data.attrib.get('parentTitle') self.primaryExtraKey = data.attrib.get('primaryExtraKey') + self.rating = utils.cast(float, data.attrib.get('rating')) 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 diff --git a/lib/plexapi/base.py b/lib/plexapi/base.py index c08334f9..9c735373 100644 --- a/lib/plexapi/base.py +++ b/lib/plexapi/base.py @@ -253,7 +253,7 @@ class PlexObject: fetchItem(ekey, viewCount__gte=0) fetchItem(ekey, Media__container__in=["mp4", "mkv"]) - fetchItem(ekey, guid__regex=r"com\\.plexapp\\.agents\\.(imdb|themoviedb)://|tt\d+") + 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") diff --git a/lib/plexapi/collection.py b/lib/plexapi/collection.py index 5f591a4a..d71ddf2f 100644 --- a/lib/plexapi/collection.py +++ b/lib/plexapi/collection.py @@ -29,6 +29,7 @@ class Collection( addedAt (datetime): Datetime the collection was added to the library. art (str): URL to artwork image (/library/metadata//art/). artBlurHash (str): BlurHash string for artwork image. + audienceRating (float): Audience rating. childCount (int): Number of items in the collection. collectionFilterBasedOnUser (int): Which user's activity is used for the collection filtering. collectionMode (int): How the items in the collection are displayed. @@ -47,6 +48,7 @@ class Collection( librarySectionTitle (str): :class:`~plexapi.library.LibrarySection` title. maxYear (int): Maximum year for the items in the collection. minYear (int): Minimum year for the items in the collection. + rating (float): Collection rating (7.9; 9.8; 8.1). ratingCount (int): The number of ratings. ratingKey (int): Unique key identifying the collection. smart (bool): True if the collection is a smart collection. @@ -69,6 +71,7 @@ class Collection( self.addedAt = utils.toDatetime(data.attrib.get('addedAt')) self.art = data.attrib.get('art') self.artBlurHash = data.attrib.get('artBlurHash') + self.audienceRating = utils.cast(float, data.attrib.get('audienceRating')) self.childCount = utils.cast(int, data.attrib.get('childCount')) self.collectionFilterBasedOnUser = utils.cast(int, data.attrib.get('collectionFilterBasedOnUser', '0')) self.collectionMode = utils.cast(int, data.attrib.get('collectionMode', '-1')) @@ -87,6 +90,7 @@ class Collection( self.librarySectionTitle = data.attrib.get('librarySectionTitle') self.maxYear = utils.cast(int, data.attrib.get('maxYear')) self.minYear = utils.cast(int, data.attrib.get('minYear')) + self.rating = utils.cast(float, data.attrib.get('rating')) self.ratingCount = utils.cast(int, data.attrib.get('ratingCount')) self.ratingKey = utils.cast(int, data.attrib.get('ratingKey')) self.smart = utils.cast(bool, data.attrib.get('smart', '0')) diff --git a/lib/plexapi/const.py b/lib/plexapi/const.py index c987e305..7568344e 100644 --- a/lib/plexapi/const.py +++ b/lib/plexapi/const.py @@ -4,6 +4,6 @@ # Library version MAJOR_VERSION = 4 MINOR_VERSION = 15 -PATCH_VERSION = 12 +PATCH_VERSION = 13 __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" diff --git a/lib/plexapi/library.py b/lib/plexapi/library.py index 662b5462..f1bf5375 100644 --- a/lib/plexapi/library.py +++ b/lib/plexapi/library.py @@ -226,7 +226,7 @@ class Library(PlexObject): section.deleteMediaPreviews() return self - def add(self, name='', type='', agent='', scanner='', location='', language='en', *args, **kwargs): + def add(self, name='', type='', agent='', scanner='', location='', language='en-US', *args, **kwargs): """ Simplified add for the most common options. Parameters: @@ -234,7 +234,7 @@ class Library(PlexObject): agent (str): Example com.plexapp.agents.imdb type (str): movie, show, # check me location (str or list): /path/to/files, ["/path/to/files", "/path/to/morefiles"] - language (str): Two letter language fx en + language (str): Four letter language code (e.g. en-US) kwargs (dict): Advanced options should be passed as a dict. where the id is the key. **Photo Preferences** diff --git a/lib/plexapi/mixins.py b/lib/plexapi/mixins.py index 60c24e26..8571ba63 100644 --- a/lib/plexapi/mixins.py +++ b/lib/plexapi/mixins.py @@ -567,6 +567,19 @@ class AddedAtMixin(EditFieldMixin): return self.editField('addedAt', addedAt, locked=locked) +class AudienceRatingMixin(EditFieldMixin): + """ Mixin for Plex objects that can have an audience rating. """ + + def editAudienceRating(self, audienceRating, locked=True): + """ Edit the audience rating. + + Parameters: + audienceRating (float): The new value. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editField('audienceRating', audienceRating, locked=locked) + + class ContentRatingMixin(EditFieldMixin): """ Mixin for Plex objects that can have a content rating. """ @@ -580,6 +593,19 @@ class ContentRatingMixin(EditFieldMixin): return self.editField('contentRating', contentRating, locked=locked) +class CriticRatingMixin(EditFieldMixin): + """ Mixin for Plex objects that can have a critic rating. """ + + def editCriticRating(self, criticRating, locked=True): + """ Edit the critic rating. + + Parameters: + criticRating (float): The new value. + locked (bool): True (default) to lock the field, False to unlock the field. + """ + return self.editField('rating', criticRating, locked=locked) + + class EditionTitleMixin(EditFieldMixin): """ Mixin for Plex objects that can have an edition title. """ @@ -751,7 +777,7 @@ class UserRatingMixin(EditFieldMixin): """ Edit the user rating. Parameters: - userRating (int): The new value. + userRating (float): The new value. locked (bool): True (default) to lock the field, False to unlock the field. """ return self.editField('userRating', userRating, locked=locked) @@ -1145,7 +1171,8 @@ class WatchlistMixin: class MovieEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, ContentRatingMixin, EditionTitleMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, + AddedAtMixin, AudienceRatingMixin, ContentRatingMixin, CriticRatingMixin, EditionTitleMixin, + OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin, SummaryMixin, TaglineMixin, TitleMixin, UserRatingMixin, CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, WriterMixin ): @@ -1154,7 +1181,8 @@ class MovieEditMixins( class ShowEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, ContentRatingMixin, OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin, + AddedAtMixin, AudienceRatingMixin, ContentRatingMixin, CriticRatingMixin, + OriginallyAvailableMixin, OriginalTitleMixin, SortTitleMixin, StudioMixin, SummaryMixin, TaglineMixin, TitleMixin, UserRatingMixin, CollectionMixin, GenreMixin, LabelMixin, ): @@ -1163,7 +1191,8 @@ class ShowEditMixins( class SeasonEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, SummaryMixin, TitleMixin, UserRatingMixin, + AddedAtMixin, AudienceRatingMixin, CriticRatingMixin, + SummaryMixin, TitleMixin, UserRatingMixin, CollectionMixin, LabelMixin ): pass @@ -1171,7 +1200,8 @@ class SeasonEditMixins( class EpisodeEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, ContentRatingMixin, OriginallyAvailableMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, + AddedAtMixin, AudienceRatingMixin, ContentRatingMixin, CriticRatingMixin, + OriginallyAvailableMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, CollectionMixin, DirectorMixin, LabelMixin, WriterMixin ): pass @@ -1179,7 +1209,8 @@ class EpisodeEditMixins( class ArtistEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, + AddedAtMixin, AudienceRatingMixin, CriticRatingMixin, + SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin ): pass @@ -1187,7 +1218,8 @@ class ArtistEditMixins( class AlbumEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin, UserRatingMixin, + AddedAtMixin, AudienceRatingMixin, CriticRatingMixin, + OriginallyAvailableMixin, SortTitleMixin, StudioMixin, SummaryMixin, TitleMixin, UserRatingMixin, CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin ): pass @@ -1195,7 +1227,8 @@ class AlbumEditMixins( class TrackEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, TitleMixin, TrackArtistMixin, TrackNumberMixin, TrackDiscNumberMixin, UserRatingMixin, + AddedAtMixin, AudienceRatingMixin, CriticRatingMixin, + TitleMixin, TrackArtistMixin, TrackNumberMixin, TrackDiscNumberMixin, UserRatingMixin, CollectionMixin, GenreMixin, LabelMixin, MoodMixin ): pass @@ -1218,7 +1251,8 @@ class PhotoEditMixins( class CollectionEditMixins( ArtLockMixin, PosterLockMixin, ThemeLockMixin, - AddedAtMixin, ContentRatingMixin, SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, + AddedAtMixin, AudienceRatingMixin, ContentRatingMixin, CriticRatingMixin, + SortTitleMixin, SummaryMixin, TitleMixin, UserRatingMixin, LabelMixin ): pass diff --git a/lib/plexapi/myplex.py b/lib/plexapi/myplex.py index 8d697924..740b2398 100644 --- a/lib/plexapi/myplex.py +++ b/lib/plexapi/myplex.py @@ -99,7 +99,7 @@ class MyPlexAccount(PlexObject): EXISTINGUSER = 'https://plex.tv/api/home/users?invitedEmail={username}' # post with data FRIENDSERVERS = 'https://plex.tv/api/servers/{machineId}/shared_servers/{serverId}' # put with data PLEXSERVERS = 'https://plex.tv/api/servers/{machineId}' # get - FRIENDUPDATE = 'https://plex.tv/api/friends/{userId}' # put with args, delete + FRIENDUPDATE = 'https://plex.tv/api/v2/sharings/{userId}' # put with args, delete HOMEUSER = 'https://plex.tv/api/home/users/{userId}' # delete, put MANAGEDHOMEUSER = 'https://plex.tv/api/v2/home/users/restricted/{userId}' # put SIGNIN = 'https://plex.tv/api/v2/users/signin' # post with auth diff --git a/lib/plexapi/playlist.py b/lib/plexapi/playlist.py index a50b6200..dda3bdf5 100644 --- a/lib/plexapi/playlist.py +++ b/lib/plexapi/playlist.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import re +from itertools import groupby from pathlib import Path from urllib.parse import quote_plus, unquote @@ -212,19 +213,23 @@ class Playlist( if items and not isinstance(items, (list, tuple)): items = [items] - ratingKeys = [] - for item in items: - if item.listType != self.playlistType: # pragma: no cover - raise BadRequest(f'Can not mix media types when building a playlist: ' - f'{self.playlistType} and {item.listType}') - ratingKeys.append(str(item.ratingKey)) + # Group items by server to maintain order when adding items from multiple servers + for server, _items in groupby(items, key=lambda item: item._server): - ratingKeys = ','.join(ratingKeys) - uri = f'{self._server._uriRoot()}/library/metadata/{ratingKeys}' + ratingKeys = [] + for item in _items: + if item.listType != self.playlistType: # pragma: no cover + raise BadRequest(f'Can not mix media types when building a playlist: ' + f'{self.playlistType} and {item.listType}') + ratingKeys.append(str(item.ratingKey)) + + ratingKeys = ','.join(ratingKeys) + uri = f'{server._uriRoot()}/library/metadata/{ratingKeys}' + + args = {'uri': uri} + key = f"{self.key}/items{utils.joinArgs(args)}" + self._server.query(key, method=self._server._session.put) - args = {'uri': uri} - key = f"{self.key}/items{utils.joinArgs(args)}" - self._server.query(key, method=self._server._session.put) return self @deprecated('use "removeItems" instead') diff --git a/lib/plexapi/video.py b/lib/plexapi/video.py index 727ba0f8..609f57f6 100644 --- a/lib/plexapi/video.py +++ b/lib/plexapi/video.py @@ -713,6 +713,7 @@ class Season( Attributes: TAG (str): 'Directory' TYPE (str): 'season' + audienceRating (float): Audience rating. audioLanguage (str): Setting that indicates the preferred audio language. collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. guids (List<:class:`~plexapi.media.Guid`>): List of guid objects. @@ -729,6 +730,7 @@ class Season( parentTheme (str): URL to show theme resource (/library/metadata//theme/). parentThumb (str): URL to show thumbnail image (/library/metadata//thumb/). parentTitle (str): Name of the show for the season. + rating (float): Season rating (7.9; 9.8; 8.1). ratings (List<:class:`~plexapi.media.Rating`>): List of rating objects. subtitleLanguage (str): Setting that indicates the preferred subtitle language. subtitleMode (int): Setting that indicates the auto-select subtitle mode. @@ -743,6 +745,7 @@ class Season( def _loadData(self, data): """ Load attribute values from Plex XML response. """ Video._loadData(self, data) + self.audienceRating = utils.cast(float, data.attrib.get('audienceRating')) self.audioLanguage = data.attrib.get('audioLanguage', '') self.collections = self.findItems(data, media.Collection) self.guids = self.findItems(data, media.Guid) @@ -759,6 +762,7 @@ class Season( self.parentTheme = data.attrib.get('parentTheme') self.parentThumb = data.attrib.get('parentThumb') self.parentTitle = data.attrib.get('parentTitle') + self.rating = utils.cast(float, data.attrib.get('rating')) self.ratings = self.findItems(data, media.Rating) self.subtitleLanguage = data.attrib.get('subtitleLanguage', '') self.subtitleMode = utils.cast(int, data.attrib.get('subtitleMode', '-1')) diff --git a/requirements.txt b/requirements.txt index 3d624c3a..6edced4e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,7 +26,7 @@ musicbrainzngs==0.7.1 packaging==24.0 paho-mqtt==2.1.0 platformdirs==4.2.2 -plexapi==4.15.12 +plexapi==4.15.13 portend==3.2.0 profilehooks==1.12.0 PyJWT==2.8.0