8 changed files with 232 additions and 203 deletions
@ -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()]) |
@ -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 ['<name_one>', '<name_one> <suffix_one>', '<name_two>', ...]) |
||||
|
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 |
@ -0,0 +1,6 @@ |
|||||
|
from .main import ShowMatcher |
||||
|
|
||||
|
def start(): |
||||
|
return ShowMatcher() |
||||
|
|
||||
|
config = [] |
@ -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 |
@ -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 ['<name_one>', '<name_one> <suffix_one>', '<name_two>', ...]) |
|
||||
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 |
|
Loading…
Reference in new issue