Moved matcher plugin to core/media, moved some matcher related functions from ShowSearcher to ShowMatcher
This commit is contained in:
@@ -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
|
||||||
@@ -18,25 +18,6 @@ class ShowSearcher(Plugin):
|
|||||||
|
|
||||||
in_progress = False
|
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):
|
def __init__(self):
|
||||||
super(ShowSearcher, self).__init__()
|
super(ShowSearcher, self).__init__()
|
||||||
|
|
||||||
@@ -46,8 +27,6 @@ class ShowSearcher(Plugin):
|
|||||||
addEvent('%s.searcher.single' % type, self.single)
|
addEvent('%s.searcher.single' % type, self.single)
|
||||||
|
|
||||||
addEvent('searcher.get_search_title', self.getSearchTitle)
|
addEvent('searcher.get_search_title', self.getSearchTitle)
|
||||||
|
|
||||||
addEvent('searcher.correct_match', self.correctMatch)
|
|
||||||
addEvent('searcher.correct_release', self.correctRelease)
|
addEvent('searcher.correct_release', self.correctRelease)
|
||||||
|
|
||||||
def single(self, media, search_protocols = None, manual = False):
|
def single(self, media, search_protocols = None, manual = False):
|
||||||
@@ -234,30 +213,12 @@ class ShowSearcher(Plugin):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# TODO Matching is quite costly, maybe we should be caching release matches somehow? (also look at caper optimizations)
|
# 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:
|
if match:
|
||||||
return match.weight
|
return match.weight
|
||||||
|
|
||||||
return False
|
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):
|
def getLibraries(self, library):
|
||||||
if 'related_libraries' not in library:
|
if 'related_libraries' not in library:
|
||||||
log.warning("'related_libraries' missing from media library, unable to continue searching")
|
log.warning("'related_libraries' missing from media library, unable to continue searching")
|
||||||
|
|||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user