Skip to content

Commit 83a292a

Browse files
authored
Merge pull request #436 from pkkid/unmatch_match
unmatch_match
2 parents 0a77c74 + bef40a7 commit 83a292a

File tree

6 files changed

+198
-7
lines changed

6 files changed

+198
-7
lines changed

plexapi/base.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,90 @@ def history(self, maxresults=9999999, mindate=None):
433433
"""
434434
return self._server.history(maxresults=maxresults, mindate=mindate, ratingKey=self.ratingKey)
435435

436+
def unmatch(self):
437+
""" Unmatches metadata match from object. """
438+
key = '/library/metadata/%s/unmatch' % self.ratingKey
439+
self._server.query(key, method=self._server._session.put)
440+
441+
def matches(self, auto=False, agent=None, title=None, year=None, language=None):
442+
""" Return list of (:class:`~plexapi.media.SearchResult) metadata matches.
443+
444+
Parameters:
445+
auto (bool): True searches for matches automatically
446+
False allows for user to provide additional parameters to search
447+
*Auto searching
448+
agent (str): Agent name to be used (imdb, thetvdb, themoviedb, etc.)
449+
title (str): Title of item to search for
450+
year (str): Year of item to search in
451+
language (str) : Language of item to search in
452+
453+
Examples:
454+
1. video.matches()
455+
2. video.matches(title="something", year=2020)
456+
3. video.matches(title="something")
457+
4. video.matches(year=2020)
458+
5. video.matches(title="something", year="")
459+
6. video.matches(title="", year=2020)
460+
7. video.matches(title="", year="")
461+
462+
1. The default behaviour in Plex Web = no params in plexapi
463+
2. Both title and year specified by user
464+
3. Year automatically filled in
465+
4. Title automatically filled in
466+
5. Explicitly searches for title with blank year
467+
6. Explicitly searches for blank title with year
468+
7. I don't know what the user is thinking... return the same result as 1
469+
470+
For 2 to 7, the agent and language is automatically filled in
471+
"""
472+
key = '/library/metadata/%s/matches' % self.ratingKey
473+
params = {'manual': 1}
474+
475+
if any([agent, title, year, language]):
476+
if title is None:
477+
params['title'] = self.title
478+
else:
479+
params['title'] = title
480+
481+
if year is None:
482+
params['year'] = self.year
483+
else:
484+
params['year'] = year
485+
486+
params['language'] = language or self.section().language
487+
488+
if agent is None:
489+
params['agent'] = self.section().agent
490+
else:
491+
params['agent'] = utils.getAgentIdentifier(self.section(), agent)
492+
493+
key = key + '?' + urlencode(params)
494+
data = self._server.query(key, method=self._server._session.get)
495+
return self.findItems(data)
496+
497+
def fixMatch(self, searchResult=None, auto=False):
498+
""" Use match result to update show metadata.
499+
500+
Parameters:
501+
auto (bool): True uses first match from matches
502+
False allows user to provide the match
503+
*Auto matching
504+
searchResult (:class:`~plexapi.media.SearchResult): Search result from
505+
~plexapi.base.matches()
506+
"""
507+
key = '/library/metadata/%s/match' % self.ratingKey
508+
if auto:
509+
searchResult = self.matches()[0]
510+
elif not searchResult:
511+
raise NotFound('fixMatch() requires either auto=True or '
512+
'searchResult=:class:`~plexapi.media.SearchResult`.')
513+
514+
params = {'guid': searchResult.guid,
515+
'name': searchResult.name}
516+
517+
data = key + '?' + urlencode(params)
518+
self._server.query(data, method=self._server._session.put)
519+
436520
# The photo tag cant be built atm. TODO
437521
# def arts(self):
438522
# part = '%s/arts' % self.key

plexapi/library.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from plexapi.compat import unquote, urlencode, quote_plus
55
from plexapi.media import MediaTag
66
from plexapi.exceptions import BadRequest, NotFound
7+
from plexapi.settings import Setting
78

89

910
class Library(PlexObject):
@@ -365,13 +366,15 @@ def delete(self):
365366
log.error(msg)
366367
raise
367368

368-
def edit(self, **kwargs):
369+
def edit(self, agent=None, **kwargs):
369370
""" Edit a library (Note: agent is required). See :class:`~plexapi.library.Library` for example usage.
370371
371372
Parameters:
372373
kwargs (dict): Dict of settings to edit.
373374
"""
374-
part = '/library/sections/%s?%s' % (self.key, urlencode(kwargs))
375+
if not agent:
376+
agent = self.agent
377+
part = '/library/sections/%s?agent=%s&%s' % (self.key, agent, urlencode(kwargs))
375378
self._server.query(part, method=self._server._session.put)
376379

377380
# Reload this way since the self.key dont have a full path, but is simply a id.
@@ -401,6 +404,17 @@ def all(self, sort=None, **kwargs):
401404
key = '/library/sections/%s/all%s' % (self.key, sortStr)
402405
return self.fetchItems(key, **kwargs)
403406

407+
def agents(self):
408+
""" Returns a list of available `:class:`~plexapi.media.Agent` for this library section.
409+
"""
410+
return self._server.agents(utils.searchType(self.type))
411+
412+
def settings(self):
413+
""" Returns a list of all library settings. """
414+
key = '/library/sections/%s/prefs' % self.key
415+
data = self._server.query(key)
416+
return self.findItems(data, cls=Setting)
417+
404418
def onDeck(self):
405419
""" Returns a list of media items on deck from this library section. """
406420
key = '/library/sections/%s/onDeck' % self.key

plexapi/media.py

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -*- coding: utf-8 -*-
2-
from plexapi import log, utils
2+
from plexapi import log, utils, settings
33
from plexapi.base import PlexObject
44
from plexapi.exceptions import BadRequest
55
from plexapi.utils import cast
@@ -699,3 +699,74 @@ def _loadData(self, data):
699699
self._data = data
700700
self.name = data.attrib.get('name')
701701
self.locked = cast(bool, data.attrib.get('locked'))
702+
703+
704+
@utils.registerPlexObject
705+
class SearchResult(PlexObject):
706+
""" Represents a single SearchResult.
707+
708+
Attributes:
709+
TAG (str): 'SearchResult'
710+
"""
711+
TAG = 'SearchResult'
712+
713+
def __repr__(self):
714+
name = self._clean(self.firstAttr('name'))
715+
score = self._clean(self.firstAttr('score'))
716+
return '<%s>' % ':'.join([p for p in [self.__class__.__name__, name, score] if p])
717+
718+
def _loadData(self, data):
719+
self._data = data
720+
self.guid = data.attrib.get('guid')
721+
self.lifespanEnded = data.attrib.get('lifespanEnded')
722+
self.name = data.attrib.get('name')
723+
self.score = cast(int, data.attrib.get('score'))
724+
self.year = data.attrib.get('year')
725+
726+
727+
@utils.registerPlexObject
728+
class Agent(PlexObject):
729+
""" Represents a single Agent.
730+
731+
Attributes:
732+
TAG (str): 'Agent'
733+
"""
734+
TAG = 'Agent'
735+
736+
def __repr__(self):
737+
uid = self._clean(self.firstAttr('shortIdentifier'))
738+
return '<%s>' % ':'.join([p for p in [self.__class__.__name__, uid] if p])
739+
740+
def _loadData(self, data):
741+
self._data = data
742+
self.hasAttribution = data.attrib.get('hasAttribution')
743+
self.hasPrefs = data.attrib.get('hasPrefs')
744+
self.identifier = data.attrib.get('identifier')
745+
self.primary = data.attrib.get('primary')
746+
self.shortIdentifier = self.identifier.rsplit('.', 1)[1]
747+
if 'mediaType' in self._initpath:
748+
self.name = data.attrib.get('name')
749+
self.languageCode = []
750+
for code in data:
751+
self.languageCode += [code.attrib.get('code')]
752+
else:
753+
self.mediaTypes = [AgentMediaType(server=self._server, data=d) for d in data]
754+
755+
def _settings(self):
756+
key = '/:/plugins/%s/prefs' % self.identifier
757+
data = self._server.query(key)
758+
return self.findItems(data, cls=settings.Setting)
759+
760+
761+
class AgentMediaType(Agent):
762+
763+
def __repr__(self):
764+
uid = self._clean(self.firstAttr('name'))
765+
return '<%s>' % ':'.join([p for p in [self.__class__.__name__, uid] if p])
766+
767+
def _loadData(self, data):
768+
self.mediaType = cast(int, data.attrib.get('mediaType'))
769+
self.name = data.attrib.get('name')
770+
self.languageCode = []
771+
for code in data:
772+
self.languageCode += [code.attrib.get('code')]

plexapi/server.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,13 @@ def account(self):
184184
data = self.query(Account.key)
185185
return Account(self, data)
186186

187+
def agents(self, mediaType=None):
188+
""" Returns the `:class:`~plexapi.media.Agent` objects this server has available. """
189+
key = '/system/agents'
190+
if mediaType:
191+
key += '?mediaType=%s' % mediaType
192+
return self.fetchItems(key)
193+
187194
def createToken(self, type='delegation', scope='all'):
188195
"""Create a temp access token for the server."""
189196
q = self.query('/security/token?type=%s&scope=%s' % (type, scope))

plexapi/settings.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,8 @@ def _loadData(self, data):
124124
self.enumValues = self._getEnumValues(data)
125125

126126
def _cast(self, value):
127-
""" Cast the specifief value to the type of this setting. """
128-
if self.type != 'text':
127+
""" Cast the specific value to the type of this setting. """
128+
if self.type != 'enum':
129129
value = utils.cast(self.TYPES.get(self.type)['cast'], value)
130130
return value
131131

plexapi/utils.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,18 @@ def registerPlexObject(cls):
5959

6060
def cast(func, value):
6161
""" Cast the specified value to the specified type (returned by func). Currently this
62-
only support int, float, bool. Should be extended if needed.
62+
only support str, int, float, bool. Should be extended if needed.
6363
6464
Parameters:
6565
func (func): Calback function to used cast to type (int, bool, float).
6666
value (any): value to be cast and returned.
6767
"""
6868
if value is not None:
6969
if func == bool:
70-
return bool(int(value))
70+
try:
71+
return bool(int(value))
72+
except ValueError:
73+
return bool(value)
7174
elif func in (int, float):
7275
try:
7376
return func(value)
@@ -375,3 +378,15 @@ def choose(msg, items, attr): # pragma: no cover
375378

376379
except (ValueError, IndexError):
377380
pass
381+
382+
383+
def getAgentIdentifier(section, agent):
384+
""" Return the full agent identifier from a short identifier, name, or confirm full identifier. """
385+
agents = []
386+
for ag in section.agents():
387+
identifiers = [ag.identifier, ag.shortIdentifier, ag.name]
388+
if agent in identifiers:
389+
return ag.identifier
390+
agents += identifiers
391+
raise NotFound('Couldnt find "%s" in agents list (%s)' %
392+
(agent, ', '.join(agents)))

0 commit comments

Comments
 (0)