From ffe6da00e63de03d7f2efe504781b74a00d64142 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Wed, 17 Sep 2025 15:51:07 -0700 Subject: [PATCH 1/6] Add support for Common Sense Media --- plexapi/media.py | 122 ++++++++++++++++++++++++++++++++++++++++++++++- plexapi/video.py | 16 +++++-- 2 files changed, 134 insertions(+), 4 deletions(-) diff --git a/plexapi/media.py b/plexapi/media.py index 718b5b7c6..d831cb5e4 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[0] + 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 hub. """ + 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/video.py b/plexapi/video.py index 597bbca7f..ab5f8dfa2 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 (List<:class:`~plexapi.media.CommonSenseMedia`>): List of Common Sense Media objects. 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.findItems(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 (List<:class:`~plexapi.media.CommonSenseMedia`>): List of Common Sense Media objects. 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.findItems(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). From 64ef12d924900c95a1592e367354a858555a9a09 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Thu, 18 Sep 2025 11:17:23 -0700 Subject: [PATCH 2/6] Update tag types --- plexapi/utils.py | 3 +++ 1 file changed, 3 insertions(+) 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()} From 1305cb616d204f85879ada7c8e8e7e6315998a8b Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Thu, 18 Sep 2025 11:49:12 -0700 Subject: [PATCH 3/6] Add tests for Common Senses Media --- tests/test_video.py | 47 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/test_video.py b/tests/test_video.py index 2b10ff0b6..7d86f271c 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[0] + 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[0] + 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 From 94961beeb8d04b1584956c7c583b047ccb44d318 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Tue, 21 Oct 2025 22:14:21 -0700 Subject: [PATCH 4/6] Single commonSenseMedia object --- plexapi/media.py | 2 +- plexapi/video.py | 8 ++++---- tests/test_video.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plexapi/media.py b/plexapi/media.py index d831cb5e4..79d8348e7 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -1395,7 +1395,7 @@ class CommonSenseMedia(PlexObject): # Retrieve the Common Sense Media info for a movie movie = plex.library.section('Movies').get('Cars') - commonSenseMedia = movie.commonSenseMedia[0] + commonSenseMedia = movie.commonSenseMedia ageRating = commonSenseMedia.ageRatings[0].age # Load the Common Sense Media info from Plex Discover (Plex Pass required) diff --git a/plexapi/video.py b/plexapi/video.py index ab5f8dfa2..4226cc3c9 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -352,7 +352,7 @@ class Movie( 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 (List<:class:`~plexapi.media.CommonSenseMedia`>): List of Common Sense Media 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 country objects. directors (List<:class:`~plexapi.media.Director`>): List of director objects. @@ -429,7 +429,7 @@ def collections(self): @cached_data_property def commonSenseMedia(self): - return self.findItems(self._data, media.CommonSenseMedia) + return self.findItem(self._data, media.CommonSenseMedia) @cached_data_property def countries(self): @@ -571,7 +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 (List<:class:`~plexapi.media.CommonSenseMedia`>): List of Common Sense Media 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. @@ -659,7 +659,7 @@ def collections(self): @cached_data_property def commonSenseMedia(self): - return self.findItems(self._data, media.CommonSenseMedia) + return self.findItem(self._data, media.CommonSenseMedia) @cached_data_property def genres(self): diff --git a/tests/test_video.py b/tests/test_video.py index 7d86f271c..4ea386d79 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -1020,7 +1020,7 @@ def test_video_Show_streamingServices(show): def test_video_Show_commonSenseMedia(show): - commonSenseMedia = show.commonSenseMedia[0] + commonSenseMedia = show.commonSenseMedia assert utils.is_int(commonSenseMedia.id) assert commonSenseMedia.oneLiner @@ -1032,7 +1032,7 @@ def test_video_Show_commonSenseMedia(show): @pytest.mark.authenticated def test_video_Show_commonSenseMedia_full(account_plexpass, show): - commonSenseMedia = show.commonSenseMedia[0] + commonSenseMedia = show.commonSenseMedia commonSenseMedia.reload() assert commonSenseMedia.anyGood assert commonSenseMedia.key From 8ad6ebf7254a6e749d982cce1309f5f03a2b855d Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Tue, 21 Oct 2025 22:52:16 -0700 Subject: [PATCH 5/6] Fix docstring example indent Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- plexapi/media.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/plexapi/media.py b/plexapi/media.py index 79d8348e7..6de3a7f01 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -1386,22 +1386,22 @@ class CommonSenseMedia(PlexObject): 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: + Example: - .. code-block:: python + .. code-block:: python - from plexapi.server import PlexServer - plex = PlexServer('http://localhost:32400', token='xxxxxxxxxxxxxxxxxxxx') + 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 + # 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 + # Load the Common Sense Media info from Plex Discover (Plex Pass required) + commonSenseMedia.reload() + parentalAdvisoryTopics = commonSenseMedia.parentalAdvisoryTopics + talkingPoints = commonSenseMedia.talkingPoints """ TAG = 'CommonSenseMedia' From 52372b26a6ee62bc02adc6bc48e92d7095f04d46 Mon Sep 17 00:00:00 2001 From: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> Date: Tue, 21 Oct 2025 22:52:37 -0700 Subject: [PATCH 6/6] Fix Common Sense Media reload docstring Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- plexapi/media.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plexapi/media.py b/plexapi/media.py index 6de3a7f01..572b3175b 100644 --- a/plexapi/media.py +++ b/plexapi/media.py @@ -1417,7 +1417,7 @@ def _loadData(self, data): self.talkingPoints = self.findItems(data, TalkingPoint) def _reload(self, **kwargs): - """ Reload the data for the hub. """ + """ Reload the data for the Common Sense Media object. """ guid = self._parent().guid if not guid.startswith('plex://'): return self