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