Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 121 additions & 1 deletion plexapi/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/<librarySectionID>/all?<filter>).
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
Expand Down Expand Up @@ -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')
3 changes: 3 additions & 0 deletions plexapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
'mood': 300,
'style': 301,
'format': 302,
'subformat': 303,
'similar': 305,
'concert': 306,
'banner': 311,
Expand All @@ -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()}

Expand Down
16 changes: 13 additions & 3 deletions plexapi/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.).
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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).
Expand Down
47 changes: 47 additions & 0 deletions tests/test_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down