diff --git a/couchpotato/core/media/show/_base/main.py b/couchpotato/core/media/show/_base/main.py index 25fc1a9..97fc520 100644 --- a/couchpotato/core/media/show/_base/main.py +++ b/couchpotato/core/media/show/_base/main.py @@ -94,6 +94,8 @@ class ShowBase(MediaBase): season_params = {'season_identifier': season_id} # 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['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['identifier'] = season_id single_season['parent_identifier'] = identifier @@ -125,6 +127,8 @@ class ShowBase(MediaBase): episode_params['absolute'] = absolute_number # 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['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['identifier'] = episode_id single_episode['parent_identifier'] = single_season['identifier'] @@ -134,9 +138,14 @@ class ShowBase(MediaBase): single_episode.get('original_title', ''))) 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 - 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") if not params.get('identifier'): diff --git a/couchpotato/core/media/show/searcher/main.py b/couchpotato/core/media/show/searcher/main.py index c33d069..1ce3034 100644 --- a/couchpotato/core/media/show/searcher/main.py +++ b/couchpotato/core/media/show/searcher/main.py @@ -1,8 +1,9 @@ from couchpotato import Env, get_session 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.media._base.searcher.main import SearchSetupError +from couchpotato.core.media.show._base import ShowBase from couchpotato.core.plugins.base import Plugin from couchpotato.core.settings.model import Media from qcond import QueryCondenser @@ -17,10 +18,20 @@ class ShowSearcher(Plugin): 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 = { + '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']}, @@ -42,8 +53,15 @@ class ShowSearcher(Plugin): def single(self, media, search_protocols = None, manual = False): show, season, episode = self.getLibraries(media['library']) + db = get_session() + 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 # 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.') return - db = get_session() - #pre_releases = fireEvent('quality.pre_releases', single = True) found_releases = [] @@ -95,7 +111,12 @@ class ShowSearcher(Plugin): # Don't search for quality lower then already available. 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) 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) else: 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 if CP wants to shut down @@ -128,6 +149,16 @@ class ShowSearcher(Plugin): if len(too_early_to_search) > 0: 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) @@ -234,17 +265,18 @@ class ShowSearcher(Plugin): 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', []) - 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', []) - if len(season) <= 1: + if library['type'] in ['season', 'episode']: season = season[0] if len(season) else None + # Episode collapses if the subject is a episode episode = libraries.get('episode', []) - if len(episode) <= 1: + if library['type'] == 'episode': episode = episode[0] if len(episode) else None return show, season, episode diff --git a/couchpotato/core/plugins/matcher/main.py b/couchpotato/core/plugins/matcher/main.py index 30f679d..fb3bfc1 100644 --- a/couchpotato/core/plugins/matcher/main.py +++ b/couchpotato/core/plugins/matcher/main.py @@ -35,29 +35,46 @@ class Matcher(Plugin): return False - def chainMatch(self, chain, group, tags): - found_tags = [] + 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) - for match in chain.info[group]: - for ck, cv in match.items(): - if ck not in tags: - continue + return flat_info - if isinstance(cv, basestring) and simplifyString(cv) in tags[ck]: - found_tags.append(ck) + def simplifyValue(self, value): + if not value: + return value - elif isinstance(cv, list): - simple_list = [simplifyString(x) for x in cv] + if isinstance(value, basestring): + return simplifyString(value) - if simple_list in tags[ck]: - found_tags.append(ck) + 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 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): required_id = fireEvent('library.identifier', media['library'], single = True) @@ -76,6 +93,14 @@ class Matcher(Plugin): 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 diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 5b7cf4e..9df0bf5 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/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': '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 {'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_480p', 'hd': True, 'size': (100, 5000), 'label': 'WEB-DL - 480p', 'width': 720, 'alternative': [], 'allow': [], 'ext':['mkv']}, # HDTV {'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']}, diff --git a/couchpotato/core/providers/torrent/iptorrents/main.py b/couchpotato/core/providers/torrent/iptorrents/main.py index f030a7e..6882196 100644 --- a/couchpotato/core/providers/torrent/iptorrents/main.py +++ b/couchpotato/core/providers/torrent/iptorrents/main.py @@ -138,13 +138,21 @@ class Movie(MovieProvider, Base): class Season(SeasonProvider, Base): + # TODO come back to this later, a better quality system needs to be created 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): + # TODO come back to this later, a better quality system needs to be created cat_ids = [ ([5], ['hdtv_720p', 'webdl_720p', 'webdl_1080p']), ([4, 78, 79], ['hdtv_sd']) diff --git a/libs/caper/__init__.py b/libs/caper/__init__.py index 2509c11..9637c06 100644 --- a/libs/caper/__init__.py +++ b/libs/caper/__init__.py @@ -19,7 +19,7 @@ from caper.parsers.anime import AnimeParser from caper.parsers.scene import SceneParser -__version_info__ = ('0', '2', '5') +__version_info__ = ('0', '2', '6') __version_branch__ = 'master' __version__ = "%s%s" % ( diff --git a/libs/caper/constraint.py b/libs/caper/constraint.py index d8f5280..96f45c3 100644 --- a/libs/caper/constraint.py +++ b/libs/caper/constraint.py @@ -53,11 +53,8 @@ class CaptureConstraint(object): elif hasattr(fragment, name): match = self.capture_group.parser.matcher.value_match(getattr(fragment, name), arg, single=True) return 1.0, match is not None - - if not hasattr(fragment, name): - raise ValueError("Unable to find fragment with name '%s'" % name) else: - raise ValueError("Unexpected argument type") + raise ValueError("Unable to find attribute with name '%s'" % name) def execute(self, fragment): results = [] diff --git a/libs/caper/parsers/scene.py b/libs/caper/parsers/scene.py index 764748d..b96967b 100644 --- a/libs/caper/parsers/scene.py +++ b/libs/caper/parsers/scene.py @@ -22,6 +22,12 @@ PATTERN_GROUPS = [ (1.0, [ # S01E01-E02 ('^S(?P\d+)E(?P\d+)$', '^E(?P\d+)$'), + # S03 E01 to E08 + ('^S(?P\d+)$', '^E(?P\d+)$', '^to$', '^E(?P\d+)$'), + + # S01-S03 + ('^S(?P\d+)$', '^S(?P\d+)$'), + # S02E13 r'^S(?P\d+)E(?P\d+)$', # S01 E13 @@ -72,25 +78,62 @@ PATTERN_GROUPS = [ '1080p' ]), + # + # Source + # + (r'(?P%s)', [ + 'DVDRiP', + # HDTV 'HDTV', 'PDTV', 'DSR', - 'DVDRiP', - 'WEBDL' + # WEB + 'WEBRip', + 'WEBDL', + # BluRay + 'BluRay', + 'B(D|R)Rip', + # DVD + 'DVDR', + 'DVD9', + 'DVD5' ]), - # For 'WEB-DL', 'WEB DL', etc... - ('(?PWEB)', '(?PDL)'), + # For multi-fragment 'WEB-DL', 'WEB-Rip', etc... matches + ('(?PWEB)', '(?PDL|Rip)'), + + # + # Codec + # (r'(?P%s)', [ 'x264', 'XViD', - 'H264' + 'H264', + 'AVC' ]), - # For 'H 264' tags + # For multi-fragment 'H 264' tags ('(?PH)', '(?P264)'), + ]), + + ('dvd', [ + r'D(ISC)?(?P\d+)', + + r'R(?P[0-8])', + + (r'(?P%s)', [ + 'PAL', + 'NTSC' + ]), + ]), + + ('audio', [ + (r'(?P%s)', [ + 'AC3', + 'TrueHD' + ]), (r'(?P%s)', [ 'GERMAN', @@ -100,6 +143,10 @@ PATTERN_GROUPS = [ 'DANiSH', 'iTALiAN' ]), + ]), + + ('scene', [ + r'(?PPROPER|REAL)', ]) ] @@ -123,11 +170,17 @@ class SceneParser(Parser): self.capture_fragment('show_name', single=False)\ .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() 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)\ .execute() diff --git a/libs/caper/result.py b/libs/caper/result.py index 4d57f89..24037cd 100644 --- a/libs/caper/result.py +++ b/libs/caper/result.py @@ -95,7 +95,7 @@ class CaperResult(object): self.chains.append(chain) 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() self.chains.sort(key=lambda chain: chain.weight, reverse=True)