Browse Source

Moved matcher plugin to core/media, moved some matcher related functions from ShowSearcher to ShowMatcher

pull/2599/head
Dean Gardiner 12 years ago
parent
commit
f648af66a6
  1. 0
      couchpotato/core/media/_base/matcher/__init__.py
  2. 48
      couchpotato/core/media/_base/matcher/base.py
  3. 85
      couchpotato/core/media/_base/matcher/main.py
  4. 6
      couchpotato/core/media/show/matcher/__init__.py
  5. 86
      couchpotato/core/media/show/matcher/main.py
  6. 41
      couchpotato/core/media/show/searcher/main.py
  7. 157
      couchpotato/core/plugins/matcher/main.py

0
couchpotato/core/plugins/matcher/__init__.py → couchpotato/core/media/_base/matcher/__init__.py

48
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()])

85
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 ['<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

6
couchpotato/core/media/show/matcher/__init__.py

@ -0,0 +1,6 @@
from .main import ShowMatcher
def start():
return ShowMatcher()
config = []

86
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

41
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")

157
couchpotato/core/plugins/matcher/main.py

@ -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…
Cancel
Save