diff --git a/plexapi/media.py b/plexapi/media.py index 718b5b7c6..572b3175b 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -741,7 +741,7 @@ class MediaTag(PlexObject): Attributes: filter (str): The library filter for the tag. - id (id): Tag ID (This seems meaningless except to use it as a unique id). + id (int): Tag ID (This seems meaningless except to use it as a unique id). key (str): API URL (/library/section//all?). role (str): The name of the character role for :class:`~plexapi.media.Role` only. tag (str): Name of the tag. This will be Animation, SciFi etc for Genres. The name of @@ -1366,3 +1366,123 @@ class Level(PlexObject): def _loadData(self, data): """ Load attribute values from Plex XML response. """ self.loudness = utils.cast(float, data.attrib.get('v')) + + +@utils.registerPlexObject +class CommonSenseMedia(PlexObject): + """ Represents a single CommonSenseMedia media tag. + Note: This object is only loaded with partial data from a Plex Media Server. + Call `reload()` to load the full data from Plex Discover (Plex Pass required). + + Attributes: + TAG (str): 'CommonSenseMedia' + ageRatings (List<:class:`~plexapi.media.AgeRating`>): List of AgeRating objects. + anyGood (str): A brief description of the media's quality. + id (int): The ID of the CommonSenseMedia tag. + key (str): The unique key for the CommonSenseMedia tag. + oneLiner (str): A brief description of the CommonSenseMedia tag. + parentalAdvisoryTopics (List<:class:`~plexapi.media.ParentalAdvisoryTopic`>): + List of ParentalAdvisoryTopic objects. + parentsNeedToKnow (str): A brief description of what parents need to know about the media. + talkingPoints (List<:class:`~plexapi.media.TalkingPoint`>): List of TalkingPoint objects. + + Example: + + .. code-block:: python + + from plexapi.server import PlexServer + plex = PlexServer('http://localhost:32400', token='xxxxxxxxxxxxxxxxxxxx') + + # Retrieve the Common Sense Media info for a movie + movie = plex.library.section('Movies').get('Cars') + commonSenseMedia = movie.commonSenseMedia + ageRating = commonSenseMedia.ageRatings[0].age + + # Load the Common Sense Media info from Plex Discover (Plex Pass required) + commonSenseMedia.reload() + parentalAdvisoryTopics = commonSenseMedia.parentalAdvisoryTopics + talkingPoints = commonSenseMedia.talkingPoints + + """ + TAG = 'CommonSenseMedia' + + def _loadData(self, data): + self.ageRatings = self.findItems(data, AgeRating) + self.anyGood = data.attrib.get('anyGood') + self.id = utils.cast(int, data.attrib.get('id')) + self.key = data.attrib.get('key') + self.oneLiner = data.attrib.get('oneLiner') + self.parentalAdvisoryTopics = self.findItems(data, ParentalAdvisoryTopic) + self.parentsNeedToKnow = data.attrib.get('parentsNeedToKnow') + self.talkingPoints = self.findItems(data, TalkingPoint) + + def _reload(self, **kwargs): + """ Reload the data for the Common Sense Media object. """ + guid = self._parent().guid + if not guid.startswith('plex://'): + return self + + ratingKey = guid.rsplit('/', 1)[-1] + account = self._server.myPlexAccount() + key = f'{account.METADATA}/library/metadata/{ratingKey}/commonsensemedia' + data = account.query(key) + self._findAndLoadElem(data) + return self + + +@utils.registerPlexObject +class AgeRating(PlexObject): + """ Represents a single AgeRating for a Common Sense Media tag. + + Attributes: + TAG (str): 'AgeRating' + age (float): The age rating (e.g. 13, 17). + ageGroup (str): The age group for the rating (e.g. Little Kids, Teens, etc.). + rating (float): The star rating (out of 5). + ratingCount (int): The number of ratings contributing to the star rating. + type (str): The type of rating (official, adult, child). + """ + TAG = 'AgeRating' + + def _loadData(self, data): + self.age = utils.cast(float, data.attrib.get('age')) + self.ageGroup = data.attrib.get('ageGroup') + self.rating = utils.cast(float, data.attrib.get('rating')) + self.ratingCount = utils.cast(int, data.attrib.get('ratingCount')) + self.type = data.attrib.get('type') + + +@utils.registerPlexObject +class TalkingPoint(PlexObject): + """ Represents a single TalkingPoint for a Common Sense Media tag. + + Attributes: + TAG (str): 'TalkingPoint' + tag (str): The description of the talking point. + """ + TAG = 'TalkingPoint' + + def _loadData(self, data): + self.tag = data.attrib.get('tag') + + +@utils.registerPlexObject +class ParentalAdvisoryTopic(PlexObject): + """ Represents a single ParentalAdvisoryTopic for a Common Sense Media tag. + + Attributes: + TAG (str): 'ParentalAdvisoryTopic' + id (str): The ID of the topic (e.g. violence, language, etc.). + label (str): The label for the topic (e.g. Violence & Scariness, Language, etc.). + positive (bool): Whether the topic is considered positive. + rating (float): The rating of the topic (out of 5). + tag (str): The description of the parental advisory topic. + """ + TAG = 'ParentalAdvisoryTopic' + + def _loadData(self, data): + self.id = data.attrib.get('id') + self.label = data.attrib.get('label') + self.positive = utils.cast(bool, data.attrib.get('positive')) + self.rating = utils.cast(float, data.attrib.get('rating')) + self.tag = data.attrib.get('tag') diff --git a/plexapi/utils.py b/plexapi/utils.py index bbff6a8e0..b85186c39 100644 --- a/plexapi/utils.py +++ b/plexapi/utils.py @@ -80,6 +80,7 @@ 'mood': 300, 'style': 301, 'format': 302, + 'subformat': 303, 'similar': 305, 'concert': 306, 'banner': 311, @@ -92,7 +93,9 @@ 'network': 319, 'showOrdering': 322, 'clearLogo': 323, + 'commonSenseMedia': 324, 'place': 400, + 'sharedWidth': 500, } REVERSETAGTYPES = {v: k for k, v in TAGTYPES.items()} diff --git a/plexapi/video.py b/plexapi/video.py index 597bbca7f..4226cc3c9 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -349,11 +349,12 @@ class Movie( TYPE (str): 'movie' audienceRating (float): Audience rating (usually from Rotten Tomatoes). audienceRatingImage (str): Key to audience rating image (rottentomatoes://image.rating.spilled). - chapters (List<:class:`~plexapi.media.Chapter`>): List of Chapter objects. + chapters (List<:class:`~plexapi.media.Chapter`>): List of chapter objects. chapterSource (str): Chapter source (agent; media; mixed). collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. + commonSenseMedia (:class:`~plexapi.media.CommonSenseMedia`): Common Sense Media object. contentRating (str) Content rating (PG-13; NR; TV-G). - countries (List<:class:`~plexapi.media.Country`>): List of countries objects. + countries (List<:class:`~plexapi.media.Country`>): List of country objects. directors (List<:class:`~plexapi.media.Director`>): List of director objects. duration (int): Duration of the movie in milliseconds. editionTitle (str): The edition title of the movie (e.g. Director's Cut, Extended Edition, etc.). @@ -426,6 +427,10 @@ def chapters(self): def collections(self): return self.findItems(self._data, media.Collection) + @cached_data_property + def commonSenseMedia(self): + return self.findItem(self._data, media.CommonSenseMedia) + @cached_data_property def countries(self): return self.findItems(self._data, media.Country) @@ -566,6 +571,7 @@ class Show( 100 = On next refresh). childCount (int): Number of seasons (including Specials) in the show. collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. + commonSenseMedia (:class:`~plexapi.media.CommonSenseMedia`): Common Sense Media object. contentRating (str) Content rating (PG-13; NR; TV-G). duration (int): Typical duration of the show episodes in milliseconds. enableCreditsMarkerGeneration (int): Setting that indicates if credits markers detection is enabled. @@ -651,6 +657,10 @@ def _loadData(self, data): def collections(self): return self.findItems(self._data, media.Collection) + @cached_data_property + def commonSenseMedia(self): + return self.findItem(self._data, media.CommonSenseMedia) + @cached_data_property def genres(self): return self.findItems(self._data, media.Genre) @@ -984,7 +994,7 @@ class Episode( TYPE (str): 'episode' audienceRating (float): Audience rating (TMDB or TVDB). audienceRatingImage (str): Key to audience rating image (tmdb://image.rating). - chapters (List<:class:`~plexapi.media.Chapter`>): List of Chapter objects. + chapters (List<:class:`~plexapi.media.Chapter`>): List of chapter objects. chapterSource (str): Chapter source (agent; media; mixed). collections (List<:class:`~plexapi.media.Collection`>): List of collection objects. contentRating (str) Content rating (PG-13; NR; TV-G). diff --git a/tests/test_video.py b/tests/test_video.py index 2b10ff0b6..4ea386d79 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -1019,6 +1019,53 @@ def test_video_Show_streamingServices(show): assert show.streamingServices() +def test_video_Show_commonSenseMedia(show): + commonSenseMedia = show.commonSenseMedia + assert utils.is_int(commonSenseMedia.id) + assert commonSenseMedia.oneLiner + + ageRating = commonSenseMedia.ageRatings[0] + assert ageRating.type == 'official' + assert utils.is_float(ageRating.age, gte=0.0) + assert utils.is_float(ageRating.rating, gte=0.0) + + +@pytest.mark.authenticated +def test_video_Show_commonSenseMedia_full(account_plexpass, show): + commonSenseMedia = show.commonSenseMedia + commonSenseMedia.reload() + assert commonSenseMedia.anyGood + assert commonSenseMedia.key + assert commonSenseMedia.oneLiner + assert commonSenseMedia.parentsNeedToKnow + + ageRatings = commonSenseMedia.ageRatings + assert len(ageRatings) == 3 + types = {r.type for r in ageRatings} + assert types == {'official', 'child', 'adult'} + ageRating = next(r for r in ageRatings if r.type == 'official') + assert utils.is_float(ageRating.age, gte=0.0) + if ageRating.ageGroup is not None: + assert ageRating.ageGroup + assert utils.is_float(ageRating.rating, gte=0.0) + if ageRating.ratingCount is not None: + assert utils.is_int(ageRating.ratingCount, gte=0) + + talkingPoints = commonSenseMedia.talkingPoints + assert len(talkingPoints) + talkingPoint = talkingPoints[0] + assert talkingPoint.tag + + parentalAdvisoryTopics = commonSenseMedia.parentalAdvisoryTopics + assert len(parentalAdvisoryTopics) + parentalAdvisoryTopic = parentalAdvisoryTopics[0] + assert parentalAdvisoryTopic.id + assert parentalAdvisoryTopic.label + assert utils.is_bool(parentalAdvisoryTopic.positive) + assert utils.is_float(parentalAdvisoryTopic.rating, gte=0.0) + assert parentalAdvisoryTopic.tag + + def test_video_Season(show): seasons = show.seasons() assert len(seasons) == 2