diff --git a/couchpotato/core/plugins/matcher/__init__.py b/couchpotato/core/media/_base/matcher/__init__.py similarity index 100% rename from couchpotato/core/plugins/matcher/__init__.py rename to couchpotato/core/media/_base/matcher/__init__.py diff --git a/couchpotato/core/media/_base/matcher/base.py b/couchpotato/core/media/_base/matcher/base.py new file mode 100644 index 0000000..399d1fe --- /dev/null +++ b/couchpotato/core/media/_base/matcher/base.py @@ -0,0 +1,48 @@ +from couchpotato.core.helpers.encoding import simplifyString +from couchpotato.core.logger import CPLog +from couchpotato.core.plugins.base import Plugin + +log = CPLog(__name__) + + +class MatcherBase(Plugin): + def flattenInfo(self, info): + flat_info = {} + + for match in info: + for key, value in match.items(): + if key not in flat_info: + flat_info[key] = [] + + flat_info[key].append(value) + + return flat_info + + def simplifyValue(self, value): + if not value: + return value + + if isinstance(value, basestring): + return simplifyString(value) + + if isinstance(value, list): + return [self.simplifyValue(x) for x in value] + + raise ValueError("Unsupported value type") + + def chainMatch(self, chain, group, tags): + info = self.flattenInfo(chain.info[group]) + + found_tags = [] + for tag, accepted in tags.items(): + values = [self.simplifyValue(x) for x in info.get(tag, [None])] + + if any([val in accepted for val in values]): + found_tags.append(tag) + + log.debug('tags found: %s, required: %s' % (found_tags, tags.keys())) + + if set(tags.keys()) == set(found_tags): + return True + + return all([key in found_tags for key, value in tags.items()]) diff --git a/couchpotato/core/media/_base/matcher/main.py b/couchpotato/core/media/_base/matcher/main.py new file mode 100644 index 0000000..9cc8c1a --- /dev/null +++ b/couchpotato/core/media/_base/matcher/main.py @@ -0,0 +1,85 @@ +from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.helpers.variable import possibleTitles +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.matcher.base import MatcherBase +from caper import Caper + +log = CPLog(__name__) + + +class Matcher(MatcherBase): + def __init__(self): + super(Matcher, self).__init__() + + self.caper = Caper() + + addEvent('matcher.parse', self.parse) + addEvent('matcher.match', self.match) + + addEvent('matcher.correct_title', self.correctTitle) + addEvent('matcher.correct_quality', self.correctQuality) + + def parse(self, name, parser='scene'): + return self.caper.parse(name, parser) + + def match(self, release, media, quality): + match = fireEvent('matcher.parse', release['name'], single = True) + + if len(match.chains) < 1: + log.info2('Wrong: %s, unable to parse release name (no chains)', release['name']) + return False + + for chain in match.chains: + if fireEvent('%s.matcher.correct' % media['type'], chain, release, media, quality, single = True): + return chain + + return False + + def correctTitle(self, chain, media): + root_library = media['library']['root_library'] + + 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 + + # Get the lower-case parsed show name from the chain + chain_words = [x.lower() for x in chain.info['show_name']] + + # Build a list of possible titles of the media we are searching for + titles = root_library['info']['titles'] + + # Add year suffix titles (will result in ['', ' ', '', ...]) + suffixes = [None, root_library['info']['year']] + + titles = [ + title + ((' %s' % suffix) if suffix else '') + for title in titles + for suffix in suffixes + ] + + # Check show titles match + # TODO check xem names + for title in 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.chainMatch(chain, 'video', video_tags): + log.info2('Wrong: %s tags not in chain', video_tags) + return False + + return True diff --git a/couchpotato/core/media/show/matcher/__init__.py b/couchpotato/core/media/show/matcher/__init__.py new file mode 100644 index 0000000..489ef67 --- /dev/null +++ b/couchpotato/core/media/show/matcher/__init__.py @@ -0,0 +1,6 @@ +from .main import ShowMatcher + +def start(): + return ShowMatcher() + +config = [] diff --git a/couchpotato/core/media/show/matcher/main.py b/couchpotato/core/media/show/matcher/main.py new file mode 100644 index 0000000..c9a5453 --- /dev/null +++ b/couchpotato/core/media/show/matcher/main.py @@ -0,0 +1,86 @@ +from couchpotato import CPLog +from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.helpers.variable import dictIsSubset, tryInt, toIterable +from couchpotato.core.media._base.matcher.base import MatcherBase + +log = CPLog(__name__) + + +class ShowMatcher(MatcherBase): + + type = ['show', 'season', 'episode'] + + # TODO come back to this later, think this could be handled better, this is starting to get out of hand.... + quality_map = { + 'bluray_1080p': {'resolution': ['1080p'], 'source': ['bluray']}, + 'bluray_720p': {'resolution': ['720p'], 'source': ['bluray']}, + + 'bdrip_1080p': {'resolution': ['1080p'], 'source': ['BDRip']}, + 'bdrip_720p': {'resolution': ['720p'], 'source': ['BDRip']}, + + 'brrip_1080p': {'resolution': ['1080p'], 'source': ['BRRip']}, + 'brrip_720p': {'resolution': ['720p'], 'source': ['BRRip']}, + + 'webdl_1080p': {'resolution': ['1080p'], 'source': ['webdl', ['web', 'dl']]}, + 'webdl_720p': {'resolution': ['720p'], 'source': ['webdl', ['web', 'dl']]}, + 'webdl_480p': {'resolution': ['480p'], 'source': ['webdl', ['web', 'dl']]}, + + 'hdtv_720p': {'resolution': ['720p'], 'source': ['hdtv']}, + 'hdtv_sd': {'resolution': ['480p', None], 'source': ['hdtv']}, + } + + def __init__(self): + super(ShowMatcher, self).__init__() + + for type in toIterable(self.type): + addEvent('%s.matcher.correct' % type, self.correct) + addEvent('%s.matcher.correct_identifier' % type, self.correctIdentifier) + + def correct(self, chain, release, media, quality): + log.info("Checking if '%s' is valid", release['name']) + log.info2('Release parsed as: %s', chain.info) + + 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 fireEvent('show.matcher.correct_identifier', chain, media): + log.info('Wrong: %s, identifier does not match', release['name']) + return False + + if not fireEvent('matcher.correct_title', chain, media): + log.info("Wrong: '%s', undetermined naming.", (' '.join(chain.info['show_name']))) + return False + + return True + + def correctIdentifier(self, chain, media): + required_id = fireEvent('library.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 any([x in identifier for x in ['episode_from', 'episode_to']]): + log.info2('Wrong: releases with identifier ranges are not supported yet') + return False + + # 'episode' is required in identifier for subset matching + if 'episode' not in identifier: + identifier['episode'] = None + + if not 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 diff --git a/couchpotato/core/media/show/searcher/main.py b/couchpotato/core/media/show/searcher/main.py index 1ce3034..c91a855 100644 --- a/couchpotato/core/media/show/searcher/main.py +++ b/couchpotato/core/media/show/searcher/main.py @@ -18,25 +18,6 @@ class ShowSearcher(Plugin): in_progress = False - # TODO come back to this later, think this could be handled better, this is starting to get out of hand.... - quality_map = { - 'bluray_1080p': {'resolution': ['1080p'], 'source': ['bluray']}, - 'bluray_720p': {'resolution': ['720p'], 'source': ['bluray']}, - - 'bdrip_1080p': {'resolution': ['1080p'], 'source': ['BDRip']}, - 'bdrip_720p': {'resolution': ['720p'], 'source': ['BDRip']}, - - 'brrip_1080p': {'resolution': ['1080p'], 'source': ['BRRip']}, - 'brrip_720p': {'resolution': ['720p'], 'source': ['BRRip']}, - - 'webdl_1080p': {'resolution': ['1080p'], 'source': ['webdl', ['web', 'dl']]}, - 'webdl_720p': {'resolution': ['720p'], 'source': ['webdl', ['web', 'dl']]}, - 'webdl_480p': {'resolution': ['480p'], 'source': ['webdl', ['web', 'dl']]}, - - 'hdtv_720p': {'resolution': ['720p'], 'source': ['hdtv']}, - 'hdtv_sd': {'resolution': ['480p', None], 'source': ['hdtv']}, - } - def __init__(self): super(ShowSearcher, self).__init__() @@ -46,8 +27,6 @@ class ShowSearcher(Plugin): addEvent('%s.searcher.single' % type, self.single) addEvent('searcher.get_search_title', self.getSearchTitle) - - addEvent('searcher.correct_match', self.correctMatch) addEvent('searcher.correct_release', self.correctRelease) def single(self, media, search_protocols = None, manual = False): @@ -234,30 +213,12 @@ class ShowSearcher(Plugin): return False # TODO Matching is quite costly, maybe we should be caching release matches somehow? (also look at caper optimizations) - match = fireEvent('matcher.best', release, media, quality, single = True) + match = fireEvent('matcher.match', release, media, quality, single = True) if match: return match.weight return False - def correctMatch(self, chain, release, media, quality): - log.info("Checking if '%s' is valid", release['name']) - log.info2('Release parsed as: %s', chain.info) - - 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 fireEvent('matcher.correct_identifier', chain, media): - log.info('Wrong: %s, identifier does not match', release['name']) - return False - - if not fireEvent('matcher.correct_title', chain, media): - log.info("Wrong: '%s', undetermined naming.", (' '.join(chain.info['show_name']))) - return False - - return True - def getLibraries(self, library): if 'related_libraries' not in library: log.warning("'related_libraries' missing from media library, unable to continue searching") diff --git a/couchpotato/core/plugins/matcher/main.py b/couchpotato/core/plugins/matcher/main.py deleted file mode 100644 index fb3bfc1..0000000 --- a/couchpotato/core/plugins/matcher/main.py +++ /dev/null @@ -1,157 +0,0 @@ -from caper import Caper -from couchpotato import CPLog, tryInt -from couchpotato.core.event import addEvent, fireEvent -from couchpotato.core.helpers.encoding import simplifyString -from couchpotato.core.helpers.variable import possibleTitles, dictIsSubset -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): - match = fireEvent('matcher.parse', release, single = True) - - if len(match.chains) < 1: - log.info2('Wrong: %s, unable to parse release name (no chains)', release['name']) - return False - - for chain in match.chains: - if fireEvent('searcher.correct_match', chain, release, media, quality, single = True): - return chain - - return False - - def flattenInfo(self, info): - flat_info = {} - - for match in info: - for key, value in match.items(): - if key not in flat_info: - flat_info[key] = [] - - flat_info[key].append(value) - - return flat_info - - def simplifyValue(self, value): - if not value: - return value - - if isinstance(value, basestring): - return simplifyString(value) - - if isinstance(value, list): - return [self.simplifyValue(x) for x in value] - - raise ValueError("Unsupported value type") - - def chainMatch(self, chain, group, tags): - info = self.flattenInfo(chain.info[group]) - - found_tags = [] - for tag, accepted in tags.items(): - values = [self.simplifyValue(x) for x in info.get(tag, [None])] - - if any([val in accepted for val in values]): - found_tags.append(tag) - - log.debug('tags found: %s, required: %s' % (found_tags, tags.keys())) - - if set(tags.keys()) == set(found_tags): - return True - - return all([key in found_tags for key, value in tags.items()]) - - def correctIdentifier(self, chain, media): - required_id = fireEvent('library.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 any([x in identifier for x in ['episode_from', 'episode_to']]): - log.info2('Wrong: releases with identifier ranges are not supported yet') - return False - - # 'episode' is required in identifier for subset matching - if 'episode' not in identifier: - identifier['episode'] = None - - if not 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 = media['library']['root_library'] - - 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 - - # Get the lower-case parsed show name from the chain - chain_words = [x.lower() for x in chain.info['show_name']] - - # Build a list of possible titles of the media we are searching for - titles = root_library['info']['titles'] - - # Add year suffix titles (will result in ['', ' ', '', ...]) - suffixes = [None, root_library['info']['year']] - - titles = [ - title + ((' %s' % suffix) if suffix else '') - for title in titles - for suffix in suffixes - ] - - # Check show titles match - # TODO check xem names - for title in 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.chainMatch(chain, 'video', video_tags): - log.info2('Wrong: %s tags not in chain', video_tags) - return False - - return True