Browse Source

Merge pull request #2577 from fuzeman/tv_searcher

[TV] Season pack matching, better show search triggering
pull/2602/head
Joel Kåberg 12 years ago
parent
commit
0fb06a3fd3
  1. 11
      couchpotato/core/media/show/_base/main.py
  2. 54
      couchpotato/core/media/show/searcher/main.py
  3. 51
      couchpotato/core/plugins/matcher/main.py
  4. 12
      couchpotato/core/plugins/quality/main.py
  5. 10
      couchpotato/core/providers/torrent/iptorrents/main.py
  6. 2
      libs/caper/__init__.py
  7. 5
      libs/caper/constraint.py
  8. 69
      libs/caper/parsers/scene.py
  9. 2
      libs/caper/result.py

11
couchpotato/core/media/show/_base/main.py

@ -94,6 +94,8 @@ class ShowBase(MediaBase):
season_params = {'season_identifier': season_id} season_params = {'season_identifier': season_id}
# Calling all info providers; merge your info now for individual season # Calling all info providers; merge your info now for individual season
single_season = fireEvent('season.info', merge = True, identifier = identifier, params = season_params) single_season = fireEvent('season.info', merge = True, identifier = identifier, params = season_params)
single_season['category_id'] = params.get('category_id')
single_season['profile_id'] = params.get('profile_id')
single_season['title'] = single_season.get('original_title', None) single_season['title'] = single_season.get('original_title', None)
single_season['identifier'] = season_id single_season['identifier'] = season_id
single_season['parent_identifier'] = identifier single_season['parent_identifier'] = identifier
@ -125,6 +127,8 @@ class ShowBase(MediaBase):
episode_params['absolute'] = absolute_number episode_params['absolute'] = absolute_number
# Calling all info providers; merge your info now for individual episode # Calling all info providers; merge your info now for individual episode
single_episode = fireEvent('episode.info', merge = True, identifier = identifier, params = episode_params) single_episode = fireEvent('episode.info', merge = True, identifier = identifier, params = episode_params)
single_episode['category_id'] = params.get('category_id')
single_episode['profile_id'] = params.get('profile_id')
single_episode['title'] = single_episode.get('original_title', None) single_episode['title'] = single_episode.get('original_title', None)
single_episode['identifier'] = episode_id single_episode['identifier'] = episode_id
single_episode['parent_identifier'] = single_season['identifier'] single_episode['parent_identifier'] = single_season['identifier']
@ -134,9 +138,14 @@ class ShowBase(MediaBase):
single_episode.get('original_title', ''))) single_episode.get('original_title', '')))
e = self.addToDatabase(params = single_episode, type = "episode") e = self.addToDatabase(params = single_episode, type = "episode")
# Start searching now that all the media has been added
if search_after:
onComplete = self.createOnComplete(parent['id'])
onComplete()
return parent return parent
def addToDatabase(self, params = {}, type = "show", force_readd = True, search_after = True, update_library = False, status_id = None): def addToDatabase(self, params = {}, type = "show", force_readd = True, search_after = False, update_library = False, status_id = None):
log.debug("show.addToDatabase") log.debug("show.addToDatabase")
if not params.get('identifier'): if not params.get('identifier'):

54
couchpotato/core/media/show/searcher/main.py

@ -1,8 +1,9 @@
from couchpotato import Env, get_session from couchpotato import Env, get_session
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import getTitle, tryInt, toIterable from couchpotato.core.helpers.variable import getTitle, toIterable
from couchpotato.core.logger import CPLog 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.media.show._base import ShowBase
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Media from couchpotato.core.settings.model import Media
from qcond import QueryCondenser from qcond import QueryCondenser
@ -17,10 +18,20 @@ class ShowSearcher(Plugin):
in_progress = False in_progress = False
# TODO come back to this later, think this could be handled better # TODO come back to this later, think this could be handled better, this is starting to get out of hand....
quality_map = { 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_1080p': {'resolution': ['1080p'], 'source': ['webdl', ['web', 'dl']]},
'webdl_720p': {'resolution': ['720p'], '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_720p': {'resolution': ['720p'], 'source': ['hdtv']},
'hdtv_sd': {'resolution': ['480p', None], 'source': ['hdtv']}, 'hdtv_sd': {'resolution': ['480p', None], 'source': ['hdtv']},
@ -42,8 +53,15 @@ class ShowSearcher(Plugin):
def single(self, media, search_protocols = None, manual = False): def single(self, media, search_protocols = None, manual = False):
show, season, episode = self.getLibraries(media['library']) show, season, episode = self.getLibraries(media['library'])
db = get_session()
if media['type'] == 'show': if media['type'] == 'show':
# TODO handle show searches (scan all seasons) for library in season:
# TODO ideally we shouldn't need to fetch the media for each season library here
m = db.query(Media).filter_by(library_id = library['library_id']).first()
fireEvent('season.searcher.single', m.to_dict(ShowBase.search_dict))
return return
# Find out search type # Find out search type
@ -59,8 +77,6 @@ class ShowSearcher(Plugin):
log.debug('Episode doesn\'t have a profile or already done, assuming in manage tab.') log.debug('Episode doesn\'t have a profile or already done, assuming in manage tab.')
return return
db = get_session()
#pre_releases = fireEvent('quality.pre_releases', single = True) #pre_releases = fireEvent('quality.pre_releases', single = True)
found_releases = [] found_releases = []
@ -95,7 +111,12 @@ class ShowSearcher(Plugin):
# Don't search for quality lower then already available. # Don't search for quality lower then already available.
if has_better_quality is 0: if has_better_quality is 0:
log.info('Search for %s S%02d%s in %s', (getTitle(show), season['season_number'], "E%02d" % episode['episode_number'] if episode else "", quality_type['quality']['label'])) log.info('Search for %s S%02d%s in %s', (
getTitle(show),
season['season_number'],
"E%02d" % episode['episode_number'] if episode and len(episode) == 1 else "",
quality_type['quality']['label'])
)
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True) quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
results = fireEvent('searcher.search', search_protocols, media, quality, single = True) results = fireEvent('searcher.search', search_protocols, media, quality, single = True)
@ -119,7 +140,7 @@ class ShowSearcher(Plugin):
fireEvent('release.delete', release.get('id'), single = True) fireEvent('release.delete', release.get('id'), single = True)
else: else:
log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title)) log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title))
fireEvent('movie.restatus', media['id']) fireEvent('media.restatus', media['id'])
break break
# Break if CP wants to shut down # Break if CP wants to shut down
@ -128,6 +149,16 @@ class ShowSearcher(Plugin):
if len(too_early_to_search) > 0: if len(too_early_to_search) > 0:
log.info2('Too early to search for %s, %s', (too_early_to_search, default_title)) log.info2('Too early to search for %s, %s', (too_early_to_search, default_title))
elif media['type'] == 'season' and not ret:
# If nothing was found, start searching for episodes individually
log.info('No season pack found, starting individual episode search')
for library in episode:
# TODO ideally we shouldn't need to fetch the media for each episode library here
m = db.query(Media).filter_by(library_id = library['library_id']).first()
fireEvent('episode.searcher.single', m.to_dict(ShowBase.search_dict))
fireEvent('notify.frontend', type = 'show.searcher.ended.%s' % media['id'], data = True) fireEvent('notify.frontend', type = 'show.searcher.ended.%s' % media['id'], data = True)
@ -234,17 +265,18 @@ class ShowSearcher(Plugin):
libraries = library['related_libraries'] libraries = library['related_libraries']
# Get libraries and return lists only if there is multiple items # Show always collapses as there can never be any multiples
show = libraries.get('show', []) show = libraries.get('show', [])
if len(show) <= 1:
show = show[0] if len(show) else None show = show[0] if len(show) else None
# Season collapses if the subject is a season or episode
season = libraries.get('season', []) season = libraries.get('season', [])
if len(season) <= 1: if library['type'] in ['season', 'episode']:
season = season[0] if len(season) else None season = season[0] if len(season) else None
# Episode collapses if the subject is a episode
episode = libraries.get('episode', []) episode = libraries.get('episode', [])
if len(episode) <= 1: if library['type'] == 'episode':
episode = episode[0] if len(episode) else None episode = episode[0] if len(episode) else None
return show, season, episode return show, season, episode

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

@ -35,29 +35,46 @@ class Matcher(Plugin):
return False return False
def chainMatch(self, chain, group, tags): def flattenInfo(self, info):
found_tags = [] 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)
for match in chain.info[group]: return flat_info
for ck, cv in match.items():
if ck not in tags:
continue
if isinstance(cv, basestring) and simplifyString(cv) in tags[ck]: def simplifyValue(self, value):
found_tags.append(ck) if not value:
return value
elif isinstance(cv, list): if isinstance(value, basestring):
simple_list = [simplifyString(x) for x in cv] return simplifyString(value)
if simple_list in tags[ck]: if isinstance(value, list):
found_tags.append(ck) 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())) log.debug('tags found: %s, required: %s' % (found_tags, tags.keys()))
if set(tags.keys()) == set(found_tags): if set(tags.keys()) == set(found_tags):
return True return True
return set([key for key, value in tags.items() if None not in value]) == set(found_tags) return all([key in found_tags for key, value in tags.items()])
def correctIdentifier(self, chain, media): def correctIdentifier(self, chain, media):
required_id = fireEvent('library.identifier', media['library'], single = True) required_id = fireEvent('library.identifier', media['library'], single = True)
@ -76,6 +93,14 @@ class Matcher(Plugin):
for k, v in identifier.items(): for k, v in identifier.items():
identifier[k] = tryInt(v, None) 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): if not dictIsSubset(required_id, identifier):
log.info2('Wrong: required identifier %s does not match release identifier %s', (str(required_id), str(identifier))) log.info2('Wrong: required identifier %s does not match release identifier %s', (str(required_id), str(identifier)))
return False return False

12
couchpotato/core/plugins/quality/main.py

@ -28,10 +28,20 @@ class QualityPlugin(Plugin):
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':[]}, {'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':[]},
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':[]}, {'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':[]},
# TODO come back to this later, think this could be handled better # TODO come back to this later, think this could be handled better, this is starting to get out of hand....
# BluRay
{'identifier': 'bluray_1080p', 'hd': True, 'size': (800, 5000), 'label': 'BluRay - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']},
{'identifier': 'bluray_720p', 'hd': True, 'size': (800, 5000), 'label': 'BluRay - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
# BDRip
{'identifier': 'bdrip_1080p', 'hd': True, 'size': (800, 5000), 'label': 'BDRip - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']},
{'identifier': 'bdrip_720p', 'hd': True, 'size': (800, 5000), 'label': 'BDRip - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
# BRRip
{'identifier': 'brrip_1080p', 'hd': True, 'size': (800, 5000), 'label': 'BRRip - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']},
{'identifier': 'brrip_720p', 'hd': True, 'size': (800, 5000), 'label': 'BRRip - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
# WEB-DL # WEB-DL
{'identifier': 'webdl_1080p', 'hd': True, 'size': (800, 5000), 'label': 'WEB-DL - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']}, {'identifier': 'webdl_1080p', 'hd': True, 'size': (800, 5000), 'label': 'WEB-DL - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']},
{'identifier': 'webdl_720p', 'hd': True, 'size': (800, 5000), 'label': 'WEB-DL - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']}, {'identifier': 'webdl_720p', 'hd': True, 'size': (800, 5000), 'label': 'WEB-DL - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
{'identifier': 'webdl_480p', 'hd': True, 'size': (100, 5000), 'label': 'WEB-DL - 480p', 'width': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
# HDTV # HDTV
{'identifier': 'hdtv_720p', 'hd': True, 'size': (800, 5000), 'label': 'HDTV - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']}, {'identifier': 'hdtv_720p', 'hd': True, 'size': (800, 5000), 'label': 'HDTV - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']},
{'identifier': 'hdtv_sd', 'hd': False, 'size': (100, 1000), 'label': 'HDTV - SD', 'width': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'mp4', 'avi']}, {'identifier': 'hdtv_sd', 'hd': False, 'size': (100, 1000), 'label': 'HDTV - SD', 'width': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'mp4', 'avi']},

10
couchpotato/core/providers/torrent/iptorrents/main.py

@ -138,13 +138,21 @@ class Movie(MovieProvider, Base):
class Season(SeasonProvider, Base): class Season(SeasonProvider, Base):
# TODO come back to this later, a better quality system needs to be created
cat_ids = [ cat_ids = [
([65], ['hdtv_sd', 'hdtv_720p', 'webdl_720p', 'webdl_1080p']), ([65], [
'bluray_1080p', 'bluray_720p',
'bdrip_1080p', 'bdrip_720p',
'brrip_1080p', 'brrip_720p',
'webdl_1080p', 'webdl_720p', 'webdl_480p',
'hdtv_720p', 'hdtv_sd'
]),
] ]
class Episode(EpisodeProvider, Base): class Episode(EpisodeProvider, Base):
# TODO come back to this later, a better quality system needs to be created
cat_ids = [ cat_ids = [
([5], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']), ([5], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']),
([4, 78, 79], ['hdtv_sd']) ([4, 78, 79], ['hdtv_sd'])

2
libs/caper/__init__.py

@ -19,7 +19,7 @@ from caper.parsers.anime import AnimeParser
from caper.parsers.scene import SceneParser from caper.parsers.scene import SceneParser
__version_info__ = ('0', '2', '5') __version_info__ = ('0', '2', '6')
__version_branch__ = 'master' __version_branch__ = 'master'
__version__ = "%s%s" % ( __version__ = "%s%s" % (

5
libs/caper/constraint.py

@ -53,11 +53,8 @@ class CaptureConstraint(object):
elif hasattr(fragment, name): elif hasattr(fragment, name):
match = self.capture_group.parser.matcher.value_match(getattr(fragment, name), arg, single=True) match = self.capture_group.parser.matcher.value_match(getattr(fragment, name), arg, single=True)
return 1.0, match is not None return 1.0, match is not None
if not hasattr(fragment, name):
raise ValueError("Unable to find fragment with name '%s'" % name)
else: else:
raise ValueError("Unexpected argument type") raise ValueError("Unable to find attribute with name '%s'" % name)
def execute(self, fragment): def execute(self, fragment):
results = [] results = []

69
libs/caper/parsers/scene.py

@ -22,6 +22,12 @@ PATTERN_GROUPS = [
(1.0, [ (1.0, [
# S01E01-E02 # S01E01-E02
('^S(?P<season>\d+)E(?P<episode_from>\d+)$', '^E(?P<episode_to>\d+)$'), ('^S(?P<season>\d+)E(?P<episode_from>\d+)$', '^E(?P<episode_to>\d+)$'),
# S03 E01 to E08
('^S(?P<season>\d+)$', '^E(?P<episode_from>\d+)$', '^to$', '^E(?P<episode_to>\d+)$'),
# S01-S03
('^S(?P<season_from>\d+)$', '^S(?P<season_to>\d+)$'),
# S02E13 # S02E13
r'^S(?P<season>\d+)E(?P<episode>\d+)$', r'^S(?P<season>\d+)E(?P<episode>\d+)$',
# S01 E13 # S01 E13
@ -72,25 +78,62 @@ PATTERN_GROUPS = [
'1080p' '1080p'
]), ]),
#
# Source
#
(r'(?P<source>%s)', [ (r'(?P<source>%s)', [
'DVDRiP',
# HDTV
'HDTV', 'HDTV',
'PDTV', 'PDTV',
'DSR', 'DSR',
'DVDRiP', # WEB
'WEBDL' 'WEBRip',
'WEBDL',
# BluRay
'BluRay',
'B(D|R)Rip',
# DVD
'DVDR',
'DVD9',
'DVD5'
]), ]),
# For 'WEB-DL', 'WEB DL', etc... # For multi-fragment 'WEB-DL', 'WEB-Rip', etc... matches
('(?P<source>WEB)', '(?P<source>DL)'), ('(?P<source>WEB)', '(?P<source>DL|Rip)'),
#
# Codec
#
(r'(?P<codec>%s)', [ (r'(?P<codec>%s)', [
'x264', 'x264',
'XViD', 'XViD',
'H264' 'H264',
'AVC'
]), ]),
# For 'H 264' tags # For multi-fragment 'H 264' tags
('(?P<codec>H)', '(?P<codec>264)'), ('(?P<codec>H)', '(?P<codec>264)'),
]),
('dvd', [
r'D(ISC)?(?P<disc>\d+)',
r'R(?P<region>[0-8])',
(r'(?P<encoding>%s)', [
'PAL',
'NTSC'
]),
]),
('audio', [
(r'(?P<codec>%s)', [
'AC3',
'TrueHD'
]),
(r'(?P<language>%s)', [ (r'(?P<language>%s)', [
'GERMAN', 'GERMAN',
@ -100,6 +143,10 @@ PATTERN_GROUPS = [
'DANiSH', 'DANiSH',
'iTALiAN' 'iTALiAN'
]), ]),
]),
('scene', [
r'(?P<proper>PROPER|REAL)',
]) ])
] ]
@ -123,11 +170,17 @@ class SceneParser(Parser):
self.capture_fragment('show_name', single=False)\ self.capture_fragment('show_name', single=False)\
.until(fragment__re='identifier')\ .until(fragment__re='identifier')\
.until(fragment__re='video')\ .until(fragment__re='video') \
.until(fragment__re='dvd') \
.until(fragment__re='audio') \
.until(fragment__re='scene') \
.execute() .execute()
self.capture_fragment('identifier', regex='identifier', single=False)\ self.capture_fragment('identifier', regex='identifier', single=False)\
.capture_fragment('video', regex='video', single=False)\ .capture_fragment('video', regex='video', single=False) \
.capture_fragment('dvd', regex='dvd', single=False) \
.capture_fragment('audio', regex='audio', single=False) \
.capture_fragment('scene', regex='scene', single=False) \
.until(left_sep__eq='-', right__eq=None)\ .until(left_sep__eq='-', right__eq=None)\
.execute() .execute()

2
libs/caper/result.py

@ -95,7 +95,7 @@ class CaperResult(object):
self.chains.append(chain) self.chains.append(chain)
for chain in self.chains: for chain in self.chains:
chain.weights.append(chain.num_matched / float(max_matched or chain.num_matched)) chain.weights.append(chain.num_matched / float(max_matched or chain.num_matched or 1))
chain.finish() chain.finish()
self.chains.sort(key=lambda chain: chain.weight, reverse=True) self.chains.sort(key=lambda chain: chain.weight, reverse=True)

Loading…
Cancel
Save