diff --git a/plexapi/audio.py b/plexapi/audio.py index 1a5b3c21..c8f80106 100644 --- a/plexapi/audio.py +++ b/plexapi/audio.py @@ -4,6 +4,7 @@ from urllib.parse import quote_plus from plexapi import library, media, utils from plexapi.base import Playable, PlexPartialObject from plexapi.exceptions import BadRequest +from plexapi.mixins import SplitMergeMixin, UnmatchMatchMixin from plexapi.mixins import CollectionMixin, CountryMixin, GenreMixin, LabelMixin, MoodMixin, SimilarArtistMixin, StyleMixin @@ -124,7 +125,8 @@ class Audio(PlexPartialObject): @utils.registerPlexObject -class Artist(Audio, CollectionMixin, CountryMixin, GenreMixin, MoodMixin, SimilarArtistMixin, StyleMixin): +class Artist(Audio, SplitMergeMixin, UnmatchMatchMixin, + CollectionMixin, CountryMixin, GenreMixin, MoodMixin, SimilarArtistMixin, StyleMixin): """ Represents a single Artist. Attributes: @@ -227,7 +229,8 @@ class Artist(Audio, CollectionMixin, CountryMixin, GenreMixin, MoodMixin, Simila @utils.registerPlexObject -class Album(Audio, CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin): +class Album(Audio, UnmatchMatchMixin, + CollectionMixin, GenreMixin, LabelMixin, MoodMixin, StyleMixin): """ Represents a single Album. Attributes: diff --git a/plexapi/base.py b/plexapi/base.py index 5fb1b0f4..a736a4ce 100644 --- a/plexapi/base.py +++ b/plexapi/base.py @@ -114,7 +114,7 @@ class PlexObject(object): def _isChildOf(self, **kwargs): """ Returns True if this object is a child of the given attributes. This will search the parent objects all the way to the top. - + Parameters: **kwargs (dict): The attributes and values to search for in the parent objects. See all possible `**kwargs*` in :func:`~plexapi.base.PlexObject.fetchItem`. @@ -546,95 +546,6 @@ class PlexPartialObject(PlexObject): """ Set :class:`~plexapi.media.Poster` to :class:`~plexapi.video.Video` """ art.select() - def unmatch(self): - """ Unmatches metadata match from object. """ - key = '/library/metadata/%s/unmatch' % self.ratingKey - self._server.query(key, method=self._server._session.put) - - def matches(self, agent=None, title=None, year=None, language=None): - """ Return list of (:class:`~plexapi.media.SearchResult`) metadata matches. - - Parameters: - agent (str): Agent name to be used (imdb, thetvdb, themoviedb, etc.) - title (str): Title of item to search for - year (str): Year of item to search in - language (str) : Language of item to search in - - Examples: - 1. video.matches() - 2. video.matches(title="something", year=2020) - 3. video.matches(title="something") - 4. video.matches(year=2020) - 5. video.matches(title="something", year="") - 6. video.matches(title="", year=2020) - 7. video.matches(title="", year="") - - 1. The default behaviour in Plex Web = no params in plexapi - 2. Both title and year specified by user - 3. Year automatically filled in - 4. Title automatically filled in - 5. Explicitly searches for title with blank year - 6. Explicitly searches for blank title with year - 7. I don't know what the user is thinking... return the same result as 1 - - For 2 to 7, the agent and language is automatically filled in - """ - key = '/library/metadata/%s/matches' % self.ratingKey - params = {'manual': 1} - - if agent and not any([title, year, language]): - params['language'] = self.section().language - params['agent'] = utils.getAgentIdentifier(self.section(), agent) - else: - if any(x is not None for x in [agent, title, year, language]): - if title is None: - params['title'] = self.title - else: - params['title'] = title - - if year is None: - params['year'] = self.year - else: - params['year'] = year - - params['language'] = language or self.section().language - - if agent is None: - params['agent'] = self.section().agent - else: - params['agent'] = utils.getAgentIdentifier(self.section(), agent) - - key = key + '?' + urlencode(params) - data = self._server.query(key, method=self._server._session.get) - return self.findItems(data, initpath=key) - - def fixMatch(self, searchResult=None, auto=False, agent=None): - """ Use match result to update show metadata. - - Parameters: - auto (bool): True uses first match from matches - False allows user to provide the match - searchResult (:class:`~plexapi.media.SearchResult`): Search result from - ~plexapi.base.matches() - agent (str): Agent name to be used (imdb, thetvdb, themoviedb, etc.) - """ - key = '/library/metadata/%s/match' % self.ratingKey - if auto: - autoMatch = self.matches(agent=agent) - if autoMatch: - searchResult = autoMatch[0] - else: - raise NotFound('No matches found using this agent: (%s:%s)' % (agent, autoMatch)) - elif not searchResult: - raise NotFound('fixMatch() requires either auto=True or ' - 'searchResult=:class:`~plexapi.media.SearchResult`.') - - params = {'guid': searchResult.guid, - 'name': searchResult.name} - - data = key + '?' + urlencode(params) - self._server.query(data, method=self._server._session.put) - # The photo tag cant be built atm. TODO # def arts(self): # part = '%s/arts' % self.key @@ -712,19 +623,6 @@ class Playable(object): for part in item.parts: yield part - def split(self): - """Split a duplicate.""" - key = '%s/split' % self.key - return self._server.query(key, method=self._server._session.put) - - def merge(self, ratingKeys): - """Merge duplicate items.""" - if not isinstance(ratingKeys, list): - ratingKeys = str(ratingKeys).split(",") - - key = '%s/merge?ids=%s' % (self.key, ','.join(ratingKeys)) - return self._server.query(key, method=self._server._session.put) - def unmatch(self): """Unmatch a media file.""" key = '%s/unmatch' % self.key diff --git a/plexapi/mixins.py b/plexapi/mixins.py index 98eef805..d200a464 100644 --- a/plexapi/mixins.py +++ b/plexapi/mixins.py @@ -1,4 +1,122 @@ # -*- coding: utf-8 -*- +from urllib.parse import urlencode + +from plexapi import utils +from plexapi.exceptions import NotFound + + +class SplitMergeMixin(object): + """ Mixin for Plex objects that can be split and merged.""" + + def split(self): + """ Split duplicated Plex object into separate objects. """ + key = '/library/metadata/%s/split' % self.ratingKey + return self._server.query(key, method=self._server._session.put) + + def merge(self, ratingKeys): + """ Merge other Plex objects into the current object. + + Parameters: + ratingKeys (list): A list of rating keys to merge. + """ + if not isinstance(ratingKeys, list): + ratingKeys = str(ratingKeys).split(',') + + key = '%s/merge?ids=%s' % (self.key, ','.join([str(r) for r in ratingKeys])) + return self._server.query(key, method=self._server._session.put) + + +class UnmatchMatchMixin(object): + """ Mixin for Plex objects that can be unmatched and matched.""" + + def unmatch(self): + """ Unmatches metadata match from object. """ + key = '/library/metadata/%s/unmatch' % self.ratingKey + self._server.query(key, method=self._server._session.put) + + def matches(self, agent=None, title=None, year=None, language=None): + """ Return list of (:class:`~plexapi.media.SearchResult`) metadata matches. + + Parameters: + agent (str): Agent name to be used (imdb, thetvdb, themoviedb, etc.) + title (str): Title of item to search for + year (str): Year of item to search in + language (str) : Language of item to search in + + Examples: + 1. video.matches() + 2. video.matches(title="something", year=2020) + 3. video.matches(title="something") + 4. video.matches(year=2020) + 5. video.matches(title="something", year="") + 6. video.matches(title="", year=2020) + 7. video.matches(title="", year="") + + 1. The default behaviour in Plex Web = no params in plexapi + 2. Both title and year specified by user + 3. Year automatically filled in + 4. Title automatically filled in + 5. Explicitly searches for title with blank year + 6. Explicitly searches for blank title with year + 7. I don't know what the user is thinking... return the same result as 1 + + For 2 to 7, the agent and language is automatically filled in + """ + key = '/library/metadata/%s/matches' % self.ratingKey + params = {'manual': 1} + + if agent and not any([title, year, language]): + params['language'] = self.section().language + params['agent'] = utils.getAgentIdentifier(self.section(), agent) + else: + if any(x is not None for x in [agent, title, year, language]): + if title is None: + params['title'] = self.title + else: + params['title'] = title + + if year is None: + params['year'] = self.year + else: + params['year'] = year + + params['language'] = language or self.section().language + + if agent is None: + params['agent'] = self.section().agent + else: + params['agent'] = utils.getAgentIdentifier(self.section(), agent) + + key = key + '?' + urlencode(params) + data = self._server.query(key, method=self._server._session.get) + return self.findItems(data, initpath=key) + + def fixMatch(self, searchResult=None, auto=False, agent=None): + """ Use match result to update show metadata. + + Parameters: + auto (bool): True uses first match from matches + False allows user to provide the match + searchResult (:class:`~plexapi.media.SearchResult`): Search result from + ~plexapi.base.matches() + agent (str): Agent name to be used (imdb, thetvdb, themoviedb, etc.) + """ + key = '/library/metadata/%s/match' % self.ratingKey + if auto: + autoMatch = self.matches(agent=agent) + if autoMatch: + searchResult = autoMatch[0] + else: + raise NotFound('No matches found using this agent: (%s:%s)' % (agent, autoMatch)) + elif not searchResult: + raise NotFound('fixMatch() requires either auto=True or ' + 'searchResult=:class:`~plexapi.media.SearchResult`.') + + params = {'guid': searchResult.guid, + 'name': searchResult.name} + + data = key + '?' + urlencode(params) + self._server.query(data, method=self._server._session.put) class CollectionMixin(object): diff --git a/plexapi/video.py b/plexapi/video.py index d42acf5d..c2957b1e 100644 --- a/plexapi/video.py +++ b/plexapi/video.py @@ -5,6 +5,7 @@ from urllib.parse import quote_plus, urlencode from plexapi import library, media, settings, utils from plexapi.base import Playable, PlexPartialObject from plexapi.exceptions import BadRequest, NotFound +from plexapi.mixins import SplitMergeMixin, UnmatchMatchMixin from plexapi.mixins import CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, EditWriter @@ -260,7 +261,8 @@ class Video(PlexPartialObject): @utils.registerPlexObject -class Movie(Playable, Video, CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, EditWriter): +class Movie(Playable, Video, SplitMergeMixin, UnmatchMatchMixin, + CollectionMixin, CountryMixin, DirectorMixin, GenreMixin, LabelMixin, ProducerMixin, EditWriter): """ Represents a single Movie. Attributes: @@ -386,7 +388,8 @@ class Movie(Playable, Video, CollectionMixin, CountryMixin, DirectorMixin, Genre @utils.registerPlexObject -class Show(Video, CollectionMixin, GenreMixin, LabelMixin): +class Show(Video, SplitMergeMixin, UnmatchMatchMixin, + CollectionMixin, GenreMixin, LabelMixin): """ Represents a single Show (including all seasons and episodes). Attributes: diff --git a/tests/test_video.py b/tests/test_video.py index 51cdd2da..87c3014c 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -523,10 +523,6 @@ def test_video_Show(show): assert show.title == "Game of Thrones" -def test_video_Episode_split(episode, patched_http_call): - episode.split() - - def test_video_Episode_unmatch(episode, patched_http_call): episode.unmatch()