From 6259684487f084d79bee0c896508f15ac47d4bfb Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sat, 5 Oct 2013 14:26:49 +1300 Subject: [PATCH] Moved caper matching into a new 'matcher' plugin. --- couchpotato/core/media/show/searcher/main.py | 236 +++++++------------ couchpotato/core/plugins/matcher/__init__.py | 6 + couchpotato/core/plugins/matcher/main.py | 120 ++++++++++ 3 files changed, 217 insertions(+), 145 deletions(-) create mode 100644 couchpotato/core/plugins/matcher/__init__.py create mode 100644 couchpotato/core/plugins/matcher/main.py diff --git a/couchpotato/core/media/show/searcher/main.py b/couchpotato/core/media/show/searcher/main.py index ec9da74a..e8c06554 100644 --- a/couchpotato/core/media/show/searcher/main.py +++ b/couchpotato/core/media/show/searcher/main.py @@ -5,7 +5,6 @@ from couchpotato.core.logger import CPLog from couchpotato.core.media._base.searcher.main import SearchSetupError from couchpotato.core.plugins.base import Plugin from couchpotato.core.settings.model import Media, Library -from caper import Caper log = CPLog(__name__) @@ -27,35 +26,15 @@ class ShowSearcher(Plugin): super(ShowSearcher, self).__init__() addEvent('show.searcher.single', self.single) - addEvent('searcher.correct_release', self.correctRelease) addEvent('searcher.get_search_title', self.getSearchTitle) + + addEvent('searcher.correct_match', self.correctMatch) + addEvent('searcher.correct_release', self.correctRelease) + + addEvent('searcher.get_media_identifier', self.getMediaIdentifier) + addEvent('searcher.get_media_root', self.getMediaRoot) addEvent('searcher.get_media_searcher_id', self.getMediaSearcherId) - self.caper = Caper() - - def _lookupMedia(self, media): - db = get_session() - - media_library = db.query(Library).filter_by(id = media['library_id']).first() - - show = None - season = None - episode = None - - if media['type'] == 'episode': - show = media_library.parent.parent - season = media_library.parent - episode = media_library - - if media['type'] == 'season': - show = media_library.parent - season = media_library - - if media['type'] == 'show': - show = media_library - - return show, season, episode - def single(self, media, search_protocols = None, manual = False): if media['type'] == 'show': # TODO handle show searches (scan all seasons) @@ -87,7 +66,7 @@ class ShowSearcher(Plugin): #fireEvent('episode.delete', episode['id'], single = True) return - show, season, episode = self._lookupMedia(media) + show, season, episode = self.getMedia(media) if show is None or season is None: log.error('Unable to find show or season library in database, missing required data for searching') return @@ -149,6 +128,28 @@ class ShowSearcher(Plugin): return ret + def getSearchTitle(self, media): + show, season, episode = self.getMedia(media) + if show is None: + return None + + # TODO this misses alternative titles from the database + show_title = getTitle(show) + if not show_title: + return None + + identifier = fireEvent('searcher.get_media_identifier', media['library'], single = True) + + name = show_title + + if identifier['season']: + name += ' S%02d' % identifier['season'] + + if identifier['episode']: + name += 'E%02d' % identifier['episode'] + + return name + def correctRelease(self, release = None, media = None, quality = None, **kwargs): if media.get('type') not in ['season', 'episode']: return @@ -163,156 +164,101 @@ class ShowSearcher(Plugin): if not fireEvent('searcher.correct_words', release['name'], media, single = True): return False - show, season, episode = self._lookupMedia(media) + show, season, episode = self.getMedia(media) if show is None or season is None: log.error('Unable to find show or season library in database, missing required data for searching') return - release_info = self.caper.parse(release['name']) - if len(release_info.chains) < 1: - log.info2('Wrong: %s, unable to parse release name (no chains)', release['name']) - return False + match = fireEvent('matcher.best', release, media, quality, single = True) + if match: + return match.weight - # TODO look at all chains - chain = release_info.chains[0] + return False - if not self.correctQuality(chain, quality['identifier']): + def correctMatch(self, chain, release, media, quality): + log.info("Checking if '%s' is valid", release['name']) + + if not fireEvent('matcher.correct_quality', chain, quality, self.quality_map, single = True): log.info('Wrong: %s, quality does not match', release['name']) return False - if not self.correctIdentifier(chain, media): + if not fireEvent('matcher.correct_identifier', chain, media): log.info('Wrong: %s, identifier does not match', release['name']) return False - if 'show_name' not in chain.info or not len(chain.info['show_name']): - log.info('Wrong: %s, missing show name in parsed result', release['name']) - return False - - chain_words = [x.lower() for x in chain.info['show_name']] - chain_title = ' '.join(chain_words) - - library_title = None - - # Check show titles match - for raw_title in show.titles: - for valid_words in [x.split(' ') for x in possibleTitles(raw_title.title)]: - if not library_title: - library_title = ' '.join(valid_words) - - if valid_words == chain_words: - return chain.weight - - log.info("Wrong: title '%s', undetermined show naming. Looking for '%s (%s)'", (chain_title, library_title, media['library']['year'])) - return False - - def correctQuality(self, chain, quality_identifier): - if quality_identifier not in self.quality_map: - log.info2('Wrong: unknown preferred quality %s for TV searching', quality_identifier) - return False - - if 'video' not in chain.info: - log.info2('Wrong: no video tags found') - return False - - video_tags = self.quality_map[quality_identifier] - - if not self.chainMatches(chain, 'video', video_tags): - log.info2('Wrong: %s tags not in chain', video_tags) - return False - - return True - - def correctIdentifier(self, chain, media): - required_id = self.getMediaIdentifier(media['library']) - - if 'identifier' not in chain.info: - return False - - # TODO could be handled better? - if len(chain.info['identifier']) != 1: - return False - identifier = chain.info['identifier'][0] - - # TODO air by date episodes - release_id = self.toNumericIdentifier(identifier.get('season'), identifier.get('episode')) - - if required_id != release_id: - log.info2('Wrong: required identifier %s does not match release identifier %s', (str(required_id), str(release_id))) + if not fireEvent('matcher.correct_title', chain, media): + log.info("Wrong: '%s', undetermined naming. Looking for '%s (%s)'", ( + ' '.join(chain.info['show_name']), + 'library_title', + media['library']['year']) + ) return False return True def getMediaIdentifier(self, media_library): - identifier = None, None + if media_library['type'] not in ['show', 'season', 'episode']: + return None + + identifier = { + 'season': None, + 'episode': None + } if media_library['type'] == 'episode': map_episode = media_library['info'].get('map_episode') if map_episode and 'scene' in map_episode: - identifier = ( - map_episode['scene'].get('season'), - map_episode['scene'].get('episode') - ) + identifier['season'] = map_episode['scene'].get('season') + identifier['episode'] = map_episode['scene'].get('episode') else: # TODO xem mapping? - identifier = ( - media_library.get('season_number'), - media_library.get('episode_number') - ) + identifier['season'] = media_library.get('season_number') + identifier['episode'] = media_library.get('episode_number') if media_library['type'] == 'season': - identifier = media_library.get('season_number'), None + identifier['season'] = media_library.get('season_number') - return self.toNumericIdentifier(*identifier) + # Try cast identifier values to integers + identifier['season'] = tryInt(identifier['season'], None) + identifier['episode'] = tryInt(identifier['episode'], None) - def toNumericIdentifier(self, season, episode): - return tryInt(season, None), tryInt(episode, None) + return identifier - def chainMatches(self, chain, group, tags): - found_tags = [] - - for match in chain.info[group]: - for ck, cv in match.items(): - if ck in tags and self.cleanMatchValue(cv) in tags[ck]: - found_tags.append(ck) - - - if set(tags.keys()) == set(found_tags): - return True - - return set([key for key, value in tags.items() if None not in value]) == set(found_tags) - - def cleanMatchValue(self, value): - value = value.lower() - value = value.strip() - - for ch in [' ', '-', '.']: - value = value.replace(ch, '') - - return value - - def getSearchTitle(self, media): - show, season, episode = self._lookupMedia(media) - if show is None: + def getMediaRoot(self, media): + if media['type'] not in ['show', 'season', 'episode']: return None - # TODO this misses alternative titles from the database - show_title = getTitle(show) - if not show_title: - return None + show, season, episode = self.getMedia(media) + if show is None or season is None: + log.error('Unable to find show or season library in database, missing required data for searching') + return - season_num, episode_num = self.getMediaIdentifier(media['library']) - - name = show_title - - if season_num: - name += ' S%02d' % season_num - - if episode_num: - name += 'E%02d' % episode_num - - return name + return show.to_dict() def getMediaSearcherId(self, media_type): if media_type in ['show', 'season', 'episode']: return 'show' + + def getMedia(self, media): + db = get_session() + + media_library = db.query(Library).filter_by(id = media['library_id']).first() + + show = None + season = None + episode = None + + if media['type'] == 'episode': + show = media_library.parent.parent + season = media_library.parent + episode = media_library + + if media['type'] == 'season': + show = media_library.parent + season = media_library + + if media['type'] == 'show': + show = media_library + + return show, season, episode \ No newline at end of file diff --git a/couchpotato/core/plugins/matcher/__init__.py b/couchpotato/core/plugins/matcher/__init__.py new file mode 100644 index 00000000..46b1a8ef --- /dev/null +++ b/couchpotato/core/plugins/matcher/__init__.py @@ -0,0 +1,6 @@ +from .main import Matcher + +def start(): + return Matcher() + +config = [] diff --git a/couchpotato/core/plugins/matcher/main.py b/couchpotato/core/plugins/matcher/main.py new file mode 100644 index 00000000..dda93113 --- /dev/null +++ b/couchpotato/core/plugins/matcher/main.py @@ -0,0 +1,120 @@ +import pprint +from caper import Caper +from couchpotato import CPLog, tryInt +from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.helpers.variable import possibleTitles +from couchpotato.core.plugins.base import Plugin + +log = CPLog(__name__) + + +class Matcher(Plugin): + def __init__(self): + self.caper = Caper() + + addEvent('matcher.parse', self.parse) + addEvent('matcher.best', self.best) + + addEvent('matcher.correct_title', self.correctTitle) + addEvent('matcher.correct_identifier', self.correctIdentifier) + addEvent('matcher.correct_quality', self.correctQuality) + + def parse(self, release): + return self.caper.parse(release['name']) + + def best(self, release, media, quality): + rel_info = fireEvent('matcher.parse', release, single = True) + + if len(rel_info.chains) < 1: + log.info2('Wrong: %s, unable to parse release name (no chains)', release['name']) + return False + + for chain in rel_info.chains: + if fireEvent('searcher.correct_match', chain, release, media, quality, single = True): + return chain + + return None + + def chainMatches(self, chain, group, tags): + found_tags = [] + + for match in chain.info[group]: + for ck, cv in match.items(): + if ck in tags and self.cleanMatchValue(cv) in tags[ck]: + found_tags.append(ck) + + + if set(tags.keys()) == set(found_tags): + return True + + return set([key for key, value in tags.items() if None not in value]) == set(found_tags) + + def cleanMatchValue(self, value): + value = value.lower() + value = value.strip() + + for ch in [' ', '-', '.']: + value = value.replace(ch, '') + + return value + + def dictIsSubset(self, a, b): + return all([k in b and b[k] == v for k, v in a.items()]) + + def correctIdentifier(self, chain, media): + required_id = fireEvent('searcher.get_media_identifier', media['library'], single = True) + + if 'identifier' not in chain.info: + return False + + # TODO could be handled better? + if len(chain.info['identifier']) != 1: + return False + identifier = chain.info['identifier'][0] + + # TODO air by date episodes + + # TODO this should support identifiers with characters 'a', 'b', etc.. + for k, v in identifier.items(): + identifier[k] = tryInt(v, None) + + if not self.dictIsSubset(required_id, identifier): + log.info2('Wrong: required identifier %s does not match release identifier %s', (str(required_id), str(identifier))) + return False + + return True + + def correctTitle(self, chain, media): + root_library = fireEvent('searcher.get_media_root', media['library'], single = True) + + if 'show_name' not in chain.info or not len(chain.info['show_name']): + log.info('Wrong: missing show name in parsed result') + return False + + chain_words = [x.lower() for x in chain.info['show_name']] + + # Check show titles match + for title in root_library['info']['titles']: + for valid_words in [x.split(' ') for x in possibleTitles(title)]: + + if valid_words == chain_words: + return True + + return False + + def correctQuality(self, chain, quality, quality_map): + if quality['identifier'] not in quality_map: + log.info2('Wrong: unknown preferred quality %s', quality['identifier']) + return False + + if 'video' not in chain.info: + log.info2('Wrong: no video tags found') + return False + + video_tags = quality_map[quality['identifier']] + + if not self.chainMatches(chain, 'video', video_tags): + log.info2('Wrong: %s tags not in chain', video_tags) + return False + + return True