Moved caper matching into a new 'matcher' plugin.

This commit is contained in:
Dean Gardiner committed 2013-10-16 14:58:47 +13:00
1 parent 0a0935d635
commit 6259684487
3 files changed
+217 -145

No files matched your search

+91 -145
View File
@@ -5,7 +5,6 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.searcher.main import SearchSetupError from couchpotato.core.media._base.searcher.main import SearchSetupError
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Media, Library from couchpotato.core.settings.model import Media, Library
from caper import Caper
log = CPLog(__name__) log = CPLog(__name__)
@@ -27,35 +26,15 @@ class ShowSearcher(Plugin):
super(ShowSearcher, self).__init__() super(ShowSearcher, self).__init__()
addEvent('show.searcher.single', self.single) addEvent('show.searcher.single', self.single)
addEvent('searcher.correct_release', self.correctRelease)
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.get_media_identifier', self.getMediaIdentifier)
addEvent('searcher.get_media_root', self.getMediaRoot)
addEvent('searcher.get_media_searcher_id', self.getMediaSearcherId) addEvent('searcher.get_media_searcher_id', self.getMediaSearcherId)
self.caper = Caper()
def _lookupMedia(self, media):
db = get_session()
media_library = db.query(Library).filter_by(id = media['library_id']).first()
show = None
season = None
episode = None
if media['type'] == 'episode':
show = media_library.parent.parent
season = media_library.parent
episode = media_library
if media['type'] == 'season':
show = media_library.parent
season = media_library
if media['type'] == 'show':
show = media_library
return show, season, episode
def single(self, media, search_protocols = None, manual = False): def single(self, media, search_protocols = None, manual = False):
if media['type'] == 'show': if media['type'] == 'show':
# TODO handle show searches (scan all seasons) # TODO handle show searches (scan all seasons)
@@ -87,7 +66,7 @@ class ShowSearcher(Plugin):
#fireEvent('episode.delete', episode['id'], single = True) #fireEvent('episode.delete', episode['id'], single = True)
return return
show, season, episode = self._lookupMedia(media) show, season, episode = self.getMedia(media)
if show is None or season is None: if show is None or season is None:
log.error('Unable to find show or season library in database, missing required data for searching') log.error('Unable to find show or season library in database, missing required data for searching')
return return
@@ -149,6 +128,28 @@ class ShowSearcher(Plugin):
return ret return ret
def getSearchTitle(self, media):
show, season, episode = self.getMedia(media)
if show is None:
return None
# TODO this misses alternative titles from the database
show_title = getTitle(show)
if not show_title:
return None
identifier = fireEvent('searcher.get_media_identifier', media['library'], single = True)
name = show_title
if identifier['season']:
name += ' S%02d' % identifier['season']
if identifier['episode']:
name += 'E%02d' % identifier['episode']
return name
def correctRelease(self, release = None, media = None, quality = None, **kwargs): def correctRelease(self, release = None, media = None, quality = None, **kwargs):
if media.get('type') not in ['season', 'episode']: return if media.get('type') not in ['season', 'episode']: return
@@ -163,156 +164,101 @@ class ShowSearcher(Plugin):
if not fireEvent('searcher.correct_words', release['name'], media, single = True): if not fireEvent('searcher.correct_words', release['name'], media, single = True):
return False return False
show, season, episode = self._lookupMedia(media) show, season, episode = self.getMedia(media)
if show is None or season is None: if show is None or season is None:
log.error('Unable to find show or season library in database, missing required data for searching') log.error('Unable to find show or season library in database, missing required data for searching')
return return
release_info = self.caper.parse(release['name']) match = fireEvent('matcher.best', release, media, quality, single = True)
if len(release_info.chains) < 1: if match:
log.info2('Wrong: %s, unable to parse release name (no chains)', release['name']) return match.weight
return False
# TODO look at all chains return False
chain = release_info.chains[0]
if not self.correctQuality(chain, quality['identifier']): def correctMatch(self, chain, release, media, quality):
log.info("Checking if '%s' is valid", release['name'])
if not fireEvent('matcher.correct_quality', chain, quality, self.quality_map, single = True):
log.info('Wrong: %s, quality does not match', release['name']) log.info('Wrong: %s, quality does not match', release['name'])
return False return False
if not self.correctIdentifier(chain, media): if not fireEvent('matcher.correct_identifier', chain, media):
log.info('Wrong: %s, identifier does not match', release['name']) log.info('Wrong: %s, identifier does not match', release['name'])
return False return False
if 'show_name' not in chain.info or not len(chain.info['show_name']): if not fireEvent('matcher.correct_title', chain, media):
log.info('Wrong: %s, missing show name in parsed result', release['name']) log.info("Wrong: '%s', undetermined naming. Looking for '%s (%s)'", (
return False ' '.join(chain.info['show_name']),
'library_title',
chain_words = [x.lower() for x in chain.info['show_name']] media['library']['year'])
chain_title = ' '.join(chain_words) )
library_title = None
# Check show titles match
for raw_title in show.titles:
for valid_words in [x.split(' ') for x in possibleTitles(raw_title.title)]:
if not library_title:
library_title = ' '.join(valid_words)
if valid_words == chain_words:
return chain.weight
log.info("Wrong: title '%s', undetermined show naming. Looking for '%s (%s)'", (chain_title, library_title, media['library']['year']))
return False
def correctQuality(self, chain, quality_identifier):
if quality_identifier not in self.quality_map:
log.info2('Wrong: unknown preferred quality %s for TV searching', quality_identifier)
return False
if 'video' not in chain.info:
log.info2('Wrong: no video tags found')
return False
video_tags = self.quality_map[quality_identifier]
if not self.chainMatches(chain, 'video', video_tags):
log.info2('Wrong: %s tags not in chain', video_tags)
return False
return True
def correctIdentifier(self, chain, media):
required_id = self.getMediaIdentifier(media['library'])
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
release_id = self.toNumericIdentifier(identifier.get('season'), identifier.get('episode'))
if required_id != release_id:
log.info2('Wrong: required identifier %s does not match release identifier %s', (str(required_id), str(release_id)))
return False return False
return True return True
def getMediaIdentifier(self, media_library): def getMediaIdentifier(self, media_library):
identifier = None, None if media_library['type'] not in ['show', 'season', 'episode']:
return None
identifier = {
'season': None,
'episode': None
}
if media_library['type'] == 'episode': if media_library['type'] == 'episode':
map_episode = media_library['info'].get('map_episode') map_episode = media_library['info'].get('map_episode')
if map_episode and 'scene' in map_episode: if map_episode and 'scene' in map_episode:
identifier = ( identifier['season'] = map_episode['scene'].get('season')
map_episode['scene'].get('season'), identifier['episode'] = map_episode['scene'].get('episode')
map_episode['scene'].get('episode')
)
else: else:
# TODO xem mapping? # TODO xem mapping?
identifier = ( identifier['season'] = media_library.get('season_number')
media_library.get('season_number'), identifier['episode'] = media_library.get('episode_number')
media_library.get('episode_number')
)
if media_library['type'] == 'season': if media_library['type'] == 'season':
identifier = media_library.get('season_number'), None identifier['season'] = media_library.get('season_number')
return self.toNumericIdentifier(*identifier) # Try cast identifier values to integers
identifier['season'] = tryInt(identifier['season'], None)
identifier['episode'] = tryInt(identifier['episode'], None)
def toNumericIdentifier(self, season, episode): return identifier
return tryInt(season, None), tryInt(episode, None)
def chainMatches(self, chain, group, tags): def getMediaRoot(self, media):
found_tags = [] if media['type'] not in ['show', 'season', 'episode']:
for match in chain.info[group]:
for ck, cv in match.items():
if ck in tags and self.cleanMatchValue(cv) in tags[ck]:
found_tags.append(ck)
if set(tags.keys()) == set(found_tags):
return True
return set([key for key, value in tags.items() if None not in value]) == set(found_tags)
def cleanMatchValue(self, value):
value = value.lower()
value = value.strip()
for ch in [' ', '-', '.']:
value = value.replace(ch, '')
return value
def getSearchTitle(self, media):
show, season, episode = self._lookupMedia(media)
if show is None:
return None return None
# TODO this misses alternative titles from the database show, season, episode = self.getMedia(media)
show_title = getTitle(show) if show is None or season is None:
if not show_title: log.error('Unable to find show or season library in database, missing required data for searching')
return None return
season_num, episode_num = self.getMediaIdentifier(media['library']) return show.to_dict()
name = show_title
if season_num:
name += ' S%02d' % season_num
if episode_num:
name += 'E%02d' % episode_num
return name
def getMediaSearcherId(self, media_type): def getMediaSearcherId(self, media_type):
if media_type in ['show', 'season', 'episode']: if media_type in ['show', 'season', 'episode']:
return 'show' return 'show'
def getMedia(self, media):
db = get_session()
media_library = db.query(Library).filter_by(id = media['library_id']).first()
show = None
season = None
episode = None
if media['type'] == 'episode':
show = media_library.parent.parent
season = media_library.parent
episode = media_library
if media['type'] == 'season':
show = media_library.parent
season = media_library
if media['type'] == 'show':
show = media_library
return show, season, episode
@@ -0,0 +1,6 @@
from .main import Matcher
def start():
return Matcher()
config = []
+120
View File
@@ -0,0 +1,120 @@
import pprint
from caper import Caper
from couchpotato import CPLog, tryInt
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import possibleTitles
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):
rel_info = fireEvent('matcher.parse', release, single = True)
if len(rel_info.chains) < 1:
log.info2('Wrong: %s, unable to parse release name (no chains)', release['name'])
return False
for chain in rel_info.chains:
if fireEvent('searcher.correct_match', chain, release, media, quality, single = True):
return chain
return None
def chainMatches(self, chain, group, tags):
found_tags = []
for match in chain.info[group]:
for ck, cv in match.items():
if ck in tags and self.cleanMatchValue(cv) in tags[ck]:
found_tags.append(ck)
if set(tags.keys()) == set(found_tags):
return True
return set([key for key, value in tags.items() if None not in value]) == set(found_tags)
def cleanMatchValue(self, value):
value = value.lower()
value = value.strip()
for ch in [' ', '-', '.']:
value = value.replace(ch, '')
return value
def dictIsSubset(self, a, b):
return all([k in b and b[k] == v for k, v in a.items()])
def correctIdentifier(self, chain, media):
required_id = fireEvent('searcher.get_media_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 not self.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 = fireEvent('searcher.get_media_root', media['library'], single = True)
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
chain_words = [x.lower() for x in chain.info['show_name']]
# Check show titles match
for title in root_library['info']['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.chainMatches(chain, 'video', video_tags):
log.info2('Wrong: %s tags not in chain', video_tags)
return False
return True