diff --git a/couchpotato/core/media/_base/matcher/main.py b/couchpotato/core/media/_base/matcher/main.py old mode 100644 new mode 100755 index 64e13ae..c722368 --- a/couchpotato/core/media/_base/matcher/main.py +++ b/couchpotato/core/media/_base/matcher/main.py @@ -21,7 +21,6 @@ class Matcher(MatcherBase): addEvent('matcher.construct_from_raw', self.constructFromRaw) addEvent('matcher.correct_title', self.correctTitle) - addEvent('matcher.correct_quality', self.correctQuality) def parse(self, name, parser='scene'): return self.caper.parse(name, parser) @@ -70,20 +69,3 @@ class Matcher(MatcherBase): 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 diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 28221ba..5c2d7f4 100755 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -198,14 +198,25 @@ class MediaPlugin(MediaBase): else: yield ms - def withIdentifiers(self, identifiers, with_doc = False): + def withIdentifiers(self, identifiers, with_doc = False, types = None): + if types and not with_doc: + raise ValueError("Unable to filter types without with_doc = True") + db = get_db() for x in identifiers: - try: - return db.get('media', '%s-%s' % (x, identifiers[x]), with_doc = with_doc) - except: - pass + items = db.get_many('media', '%s-%s' % (x, identifiers[x]), with_doc = with_doc) + + if not items: + # No items found, move to next identifier + continue + + for item in items: + if types and item['doc'].get('type') not in types: + # Type doesn't match request, move to next item + continue + + return item log.debug('No media found with identifiers: %s', identifiers) return False diff --git a/couchpotato/core/media/_base/providers/base.py b/couchpotato/core/media/_base/providers/base.py old mode 100644 new mode 100755 index 642d477..39c17c7 --- a/couchpotato/core/media/_base/providers/base.py +++ b/couchpotato/core/media/_base/providers/base.py @@ -266,8 +266,8 @@ class YarrProvider(Provider): if quality.get('custom'): want_3d = quality['custom'].get('3d') - for ids, qualities in self.cat_ids: - if identifier in qualities or (want_3d and '3d' in qualities): + for ids, value in self.cat_ids: + if self.categoryMatch(value, quality, identifier, want_3d): return ids if self.cat_backup_id: @@ -275,6 +275,42 @@ class YarrProvider(Provider): return [] + def categoryMatch(self, value, quality, identifier, want_3d): + if type(value) is list: + # Basic identifier matching + if identifier in value: + return True + + if want_3d and '3d' in value: + return True + + return False + + if type(value) is dict: + if not value: + # Wildcard category + return True + + # Property matching + for key in ['codec', 'resolution', 'source']: + if key not in quality: + continue + + for required in quality.get(key): + # Ensure category contains property list + if key not in value: + return False + + # Ensure required property is in category + if required not in value[key]: + return False + + # Valid + return True + + # Unknown failure + return False + class ResultList(list): diff --git a/couchpotato/core/media/_base/quality/__init__.py b/couchpotato/core/media/_base/quality/__init__.py new file mode 100755 index 0000000..d9026ff --- /dev/null +++ b/couchpotato/core/media/_base/quality/__init__.py @@ -0,0 +1,7 @@ +from .main import Quality + + +def autoload(): + return Quality() + +config = [] diff --git a/couchpotato/core/media/_base/quality/base.py b/couchpotato/core/media/_base/quality/base.py new file mode 100755 index 0000000..df83ef8 --- /dev/null +++ b/couchpotato/core/media/_base/quality/base.py @@ -0,0 +1,185 @@ +import traceback + +from CodernityDB.database import RecordNotFound +from couchpotato import get_db +from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.helpers.encoding import toUnicode, ss +from couchpotato.core.helpers.variable import mergeDicts, getExt, tryInt, splitString +from couchpotato.core.logger import CPLog +from couchpotato.core.plugins.base import Plugin + +log = CPLog(__name__) + + +class QualityBase(Plugin): + type = None + + properties = {} + qualities = [] + + pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr'] + threed_tags = { + 'sbs': [('half', 'sbs'), 'hsbs', ('full', 'sbs'), 'fsbs'], + 'ou': [('half', 'ou'), 'hou', ('full', 'ou'), 'fou'], + '3d': ['2d3d', '3d2d', '3d'], + } + + cached_qualities = None + cached_order = None + + def __init__(self): + addEvent('quality.pre_releases', self.preReleases) + + addEvent('quality.get', self.get) + addEvent('quality.all', self.all) + addEvent('quality.reset_cache', self.resetCache) + + addEvent('quality.fill', self.fill) + addEvent('quality.isfinish', self.isFinish) + addEvent('quality.ishigher', self.isHigher) + + addEvent('app.initialize', self.fill, priority = 10) + + self.order = [] + + for q in self.qualities: + self.order.append(q.get('identifier')) + + def preReleases(self, types = None): + if types and self.type not in types: + return + + return self.pre_releases + + def get(self, identifier, types = None): + if types and self.type not in types: + return + + for q in self.qualities: + if identifier == q.get('identifier'): + return q + + def all(self, types = None): + if types and self.type not in types: + return + + if self.cached_qualities: + return self.cached_qualities + + db = get_db() + + temp = [] + for quality in self.qualities: + quality_doc = db.get('quality', quality.get('identifier'), with_doc = True)['doc'] + q = mergeDicts(quality, quality_doc) + temp.append(q) + + if len(temp) == len(self.qualities): + self.cached_qualities = temp + + return temp + + def expand(self, quality): + for key, options in self.properties.items(): + if key not in quality: + continue + + quality[key] = [self.getProperty(key, identifier) for identifier in quality[key]] + + return quality + + def getProperty(self, key, identifier): + if key not in self.properties: + return + + for item in self.properties[key]: + if item.get('identifier') == identifier: + return item + + def resetCache(self): + self.cached_qualities = None + + def fill(self): + + try: + db = get_db() + + order = 0 + for q in self.qualities: + + existing = None + try: + existing = db.get('quality', q.get('identifier')) + except RecordNotFound: + pass + + if not existing: + db.insert({ + '_t': 'quality', + 'order': order, + 'identifier': q.get('identifier'), + 'size_min': tryInt(q.get('size')[0]), + 'size_max': tryInt(q.get('size')[1]), + }) + + log.info('Creating profile: %s', q.get('label')) + db.insert({ + '_t': 'profile', + 'order': order + 20, # Make sure it goes behind other profiles + 'core': True, + 'qualities': [q.get('identifier')], + 'label': toUnicode(q.get('label')), + 'finish': [True], + 'wait_for': [0], + }) + + order += 1 + + return True + except: + log.error('Failed: %s', traceback.format_exc()) + + return False + + def isFinish(self, quality, profile, release_age = 0): + if not isinstance(profile, dict) or not profile.get('qualities'): + # No profile so anything (scanned) is good enough + return True + + try: + index = [i for i, identifier in enumerate(profile['qualities']) if identifier == quality['identifier'] and bool(profile['3d'][i] if profile.get('3d') else False) == bool(quality.get('is_3d', False))][0] + + if index == 0 or (profile['finish'][index] and int(release_age) >= int(profile.get('stop_after', [0])[0])): + return True + + return False + except: + return False + + def isHigher(self, quality, compare_with, profile = None): + if not isinstance(profile, dict) or not profile.get('qualities'): + profile = fireEvent('profile.default', single = True) + + # Try to find quality in profile, if not found: a quality we do not want is lower than anything else + try: + quality_order = [i for i, identifier in enumerate(profile['qualities']) if identifier == quality['identifier'] and bool(profile['3d'][i] if profile.get('3d') else 0) == bool(quality.get('is_3d', 0))][0] + except: + log.debug('Quality %s not found in profile identifiers %s', (quality['identifier'] + (' 3D' if quality.get('is_3d', 0) else ''), \ + [identifier + (' 3D' if (profile['3d'][i] if profile.get('3d') else 0) else '') for i, identifier in enumerate(profile['qualities'])])) + return 'lower' + + # Try to find compare quality in profile, if not found: anything is higher than a not wanted quality + try: + compare_order = [i for i, identifier in enumerate(profile['qualities']) if identifier == compare_with['identifier'] and bool(profile['3d'][i] if profile.get('3d') else 0) == bool(compare_with.get('is_3d', 0))][0] + except: + log.debug('Compare quality %s not found in profile identifiers %s', (compare_with['identifier'] + (' 3D' if compare_with.get('is_3d', 0) else ''), \ + [identifier + (' 3D' if (profile['3d'][i] if profile.get('3d') else 0) else '') for i, identifier in enumerate(profile['qualities'])])) + return 'higher' + + # Note to self: a lower number means higher quality + if quality_order > compare_order: + return 'lower' + elif quality_order == compare_order: + return 'equal' + else: + return 'higher' diff --git a/couchpotato/core/plugins/quality/index.py b/couchpotato/core/media/_base/quality/index.py old mode 100644 new mode 100755 similarity index 100% rename from couchpotato/core/plugins/quality/index.py rename to couchpotato/core/media/_base/quality/index.py diff --git a/couchpotato/core/media/_base/quality/main.py b/couchpotato/core/media/_base/quality/main.py new file mode 100755 index 0000000..950630b --- /dev/null +++ b/couchpotato/core/media/_base/quality/main.py @@ -0,0 +1,82 @@ +import traceback + +from couchpotato import fireEvent, get_db, tryInt, CPLog +from couchpotato.api import addApiView +from couchpotato.core.event import addEvent +from couchpotato.core.helpers.variable import splitString, mergeDicts +from couchpotato.core.media._base.quality.index import QualityIndex +from couchpotato.core.plugins.base import Plugin + +log = CPLog(__name__) + + +class Quality(Plugin): + _database = { + 'quality': QualityIndex + } + + def __init__(self): + addEvent('quality.single', self.single) + + addApiView('quality.list', self.allView, docs = { + 'desc': 'List all available qualities', + 'params': { + 'type': {'type': 'string', 'desc': 'Media type to filter on.'}, + }, + 'return': {'type': 'object', 'example': """{ + 'success': True, + 'list': array, qualities +}"""} + }) + + addApiView('quality.size.save', self.saveSize) + + def single(self, identifier = '', types = None): + db = get_db() + quality = db.get('quality', identifier, with_doc = True)['doc'] + + if quality: + return mergeDicts( + fireEvent( + 'quality.get', + quality['identifier'], + types = types, + single = True + ), + quality + ) + + return {} + + def allView(self, **kwargs): + + return { + 'success': True, + 'list': fireEvent( + 'quality.all', + types = splitString(kwargs.get('type')), + merge = True + ) + } + + def saveSize(self, **kwargs): + + try: + db = get_db() + quality = db.get('quality', kwargs.get('identifier'), with_doc = True) + + if quality: + quality['doc'][kwargs.get('value_type')] = tryInt(kwargs.get('value')) + db.update(quality['doc']) + + fireEvent('quality.reset_cache') + + return { + 'success': True + } + except: + log.error('Failed: %s', traceback.format_exc()) + + return { + 'success': False + } diff --git a/couchpotato/core/plugins/quality/static/quality.css b/couchpotato/core/media/_base/quality/static/quality.css old mode 100644 new mode 100755 similarity index 100% rename from couchpotato/core/plugins/quality/static/quality.css rename to couchpotato/core/media/_base/quality/static/quality.css diff --git a/couchpotato/core/plugins/quality/static/quality.js b/couchpotato/core/media/_base/quality/static/quality.js old mode 100644 new mode 100755 similarity index 100% rename from couchpotato/core/plugins/quality/static/quality.js rename to couchpotato/core/media/_base/quality/static/quality.js diff --git a/couchpotato/core/media/_base/searcher/main.py b/couchpotato/core/media/_base/searcher/main.py old mode 100644 new mode 100755 index e9ba95d..6d0b684 --- a/couchpotato/core/media/_base/searcher/main.py +++ b/couchpotato/core/media/_base/searcher/main.py @@ -84,13 +84,14 @@ class Searcher(SearcherBase): return search_protocols - def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = None): + def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = None, types = None): if not preferred_quality: preferred_quality = {} found = {} # Try guessing via quality tags - guess = fireEvent('quality.guess', files = [nzb.get('name')], size = nzb.get('size', None), single = True) + guess = fireEvent('quality.guess', files = [nzb.get('name')], size = nzb.get('size', None), types = types, single = True) + if guess: found[guess['identifier']] = True @@ -111,7 +112,7 @@ class Searcher(SearcherBase): found['dvdrip'] = True # Allow other qualities - for allowed in preferred_quality.get('allow'): + for allowed in preferred_quality.get('allow', []): if found.get(allowed): del found[allowed] @@ -120,14 +121,14 @@ class Searcher(SearcherBase): return found - def correct3D(self, nzb, preferred_quality = None): + def correct3D(self, nzb, preferred_quality = None, types = None): if not preferred_quality: preferred_quality = {} if not preferred_quality.get('custom'): return threed = preferred_quality['custom'].get('3d') # Try guessing via quality tags - guess = fireEvent('quality.guess', [nzb.get('name')], single = True) + guess = fireEvent('quality.guess', [nzb.get('name')], types = types, single = True) if guess: return threed == guess.get('is_3d') diff --git a/couchpotato/core/media/movie/quality/__init__.py b/couchpotato/core/media/movie/quality/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/media/movie/quality/main.py similarity index 54% rename from couchpotato/core/plugins/quality/main.py rename to couchpotato/core/media/movie/quality/main.py index aaaed9b..d469a33 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/media/movie/quality/main.py @@ -1,212 +1,44 @@ -from math import fabs, ceil -import traceback import re -from CodernityDB.database import RecordNotFound -from couchpotato import get_db -from couchpotato.api import addApiView +from couchpotato import CPLog from couchpotato.core.event import addEvent, fireEvent -from couchpotato.core.helpers.encoding import toUnicode, ss -from couchpotato.core.helpers.variable import mergeDicts, getExt, tryInt, splitString, tryFloat -from couchpotato.core.logger import CPLog -from couchpotato.core.plugins.base import Plugin -from couchpotato.core.plugins.quality.index import QualityIndex - +from couchpotato.core.helpers.encoding import ss +from couchpotato.core.helpers.variable import getExt, splitString, tryInt +from couchpotato.core.media._base.quality.base import QualityBase log = CPLog(__name__) +autoload = 'MovieQuality' -class QualityPlugin(Plugin): - _database = { - 'quality': QualityIndex - } +class MovieQuality(QualityBase): + type = 'movie' qualities = [ - {'identifier': 'bd50', 'hd': True, 'allow_3d': True, 'size': (20000, 60000), 'median_size': 40000, 'label': 'BR-Disk', 'alternative': ['bd25', ('br', 'disk')], 'allow': ['1080p'], 'ext':['iso', 'img'], 'tags': ['bdmv', 'certificate', ('complete', 'bluray'), 'avc', 'mvc']}, - {'identifier': '1080p', 'hd': True, 'allow_3d': True, 'size': (4000, 20000), 'median_size': 10000, 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts', 'ts'], 'tags': ['m2ts', 'x264', 'h264', '1080']}, - {'identifier': '720p', 'hd': True, 'allow_3d': True, 'size': (3000, 10000), 'median_size': 5500, 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264', '720']}, - {'identifier': 'brrip', 'hd': True, 'allow_3d': True, 'size': (700, 7000), 'median_size': 2000, 'label': 'BR-Rip', 'alternative': ['bdrip', ('br', 'rip'), 'hdtv', 'hdrip'], 'allow': ['720p', '1080p'], 'ext':['mp4', 'avi'], 'tags': ['webdl', ('web', 'dl')]}, - {'identifier': 'dvdr', 'size': (3000, 10000), 'median_size': 4500, 'label': 'DVD-R', 'alternative': ['br2dvd', ('dvd', 'r')], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r'), 'dvd9']}, - {'identifier': 'dvdrip', 'size': (600, 2400), 'median_size': 1500, 'label': 'DVD-Rip', 'width': 720, 'alternative': [('dvd', 'rip')], 'allow': [], 'ext':['avi'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]}, - {'identifier': 'scr', 'size': (600, 1600), 'median_size': 700, 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr', 'webrip', ('web', 'rip')], 'allow': ['dvdr', 'dvdrip', '720p', '1080p'], 'ext':[], 'tags': []}, - {'identifier': 'r5', 'size': (600, 1000), 'median_size': 700, 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr', '720p', '1080p'], 'ext':[]}, - {'identifier': 'tc', 'size': (600, 1000), 'median_size': 700, 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': ['720p', '1080p'], 'ext':[]}, - {'identifier': 'ts', 'size': (600, 1000), 'median_size': 700, 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': ['720p', '1080p'], 'ext':[]}, - {'identifier': 'cam', 'size': (600, 1000), 'median_size': 700, 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': ['720p', '1080p'], 'ext':[]}, - - # 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']}, + {'identifier': 'bd50', 'hd': True, 'allow_3d': True, 'size': (20000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25', ('br', 'disk')], 'allow': ['1080p'], 'ext':['iso', 'img'], 'tags': ['bdmv', 'certificate', ('complete', 'bluray'), 'avc', 'mvc']}, + {'identifier': '1080p', 'hd': True, 'allow_3d': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts', 'ts'], 'tags': ['m2ts', 'x264', 'h264']}, + {'identifier': '720p', 'hd': True, 'allow_3d': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts'], 'tags': ['x264', 'h264']}, + {'identifier': 'brrip', 'hd': True, 'allow_3d': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip', ('br', 'rip')], 'allow': ['720p', '1080p'], 'ext':['mp4', 'avi'], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]}, + {'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': ['br2dvd', ('dvd', 'r')], 'allow': [], 'ext':['iso', 'img', 'vob'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts', ('dvd', 'r'), 'dvd9']}, + {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [('dvd', 'rip')], 'allow': [], 'ext':['avi'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]}, + {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr'], 'allow': ['dvdr', 'dvdrip', '720p', '1080p'], 'ext':[], 'tags': ['webrip', ('web', 'rip')]}, + {'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr', '720p'], 'ext':[]}, + {'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': ['720p'], 'ext':[]}, + {'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': ['720p'], 'ext':[]}, + {'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': ['720p'], 'ext':[]}, ] - pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr'] - threed_tags = { - 'sbs': [('half', 'sbs'), 'hsbs', ('full', 'sbs'), 'fsbs'], - 'ou': [('half', 'ou'), 'hou', ('full', 'ou'), 'fou'], - '3d': ['2d3d', '3d2d', '3d'], - } - - cached_qualities = None - cached_order = None def __init__(self): - addEvent('quality.all', self.all) - addEvent('quality.single', self.single) + super(MovieQuality, self).__init__() + addEvent('quality.guess', self.guess) - addEvent('quality.pre_releases', self.preReleases) - addEvent('quality.order', self.getOrder) - addEvent('quality.ishigher', self.isHigher) - addEvent('quality.isfinish', self.isFinish) - addEvent('quality.fill', self.fill) - - addApiView('quality.size.save', self.saveSize) - addApiView('quality.list', self.allView, docs = { - 'desc': 'List all available qualities', - 'return': {'type': 'object', 'example': """{ - 'success': True, - 'list': array, qualities -}"""} - }) - - addEvent('app.initialize', self.fill, priority = 10) addEvent('app.test', self.doTest) - self.order = [] - self.addOrder() - - def addOrder(self): - self.order = [] - for q in self.qualities: - self.order.append(q.get('identifier')) - - def getOrder(self): - return self.order - - def preReleases(self): - return self.pre_releases - - def allView(self, **kwargs): - - return { - 'success': True, - 'list': self.all() - } - - def all(self): - - if self.cached_qualities: - return self.cached_qualities - - db = get_db() - - temp = [] - for quality in self.qualities: - quality_doc = db.get('quality', quality.get('identifier'), with_doc = True)['doc'] - q = mergeDicts(quality, quality_doc) - temp.append(q) - - if len(temp) == len(self.qualities): - self.cached_qualities = temp - - return temp - - def single(self, identifier = ''): - - db = get_db() - quality_dict = {} - - quality = db.get('quality', identifier, with_doc = True)['doc'] - if quality: - quality_dict = mergeDicts(self.getQuality(quality['identifier']), quality) - - return quality_dict - - def getQuality(self, identifier): - - for q in self.qualities: - if identifier == q.get('identifier'): - return q - - def saveSize(self, **kwargs): + def guess(self, files, extra = None, size = None, types = None): + if types and self.type not in types: + return - try: - db = get_db() - quality = db.get('quality', kwargs.get('identifier'), with_doc = True) - - if quality: - quality['doc'][kwargs.get('value_type')] = tryInt(kwargs.get('value')) - db.update(quality['doc']) - - self.cached_qualities = None - - return { - 'success': True - } - except: - log.error('Failed: %s', traceback.format_exc()) - - return { - 'success': False - } - - def fill(self): - - try: - db = get_db() - - order = 0 - for q in self.qualities: - - existing = None - try: - existing = db.get('quality', q.get('identifier')) - except RecordNotFound: - pass - - if not existing: - db.insert({ - '_t': 'quality', - 'order': order, - 'identifier': q.get('identifier'), - 'size_min': tryInt(q.get('size')[0]), - 'size_max': tryInt(q.get('size')[1]), - }) - - log.info('Creating profile: %s', q.get('label')) - db.insert({ - '_t': 'profile', - 'order': order + 20, # Make sure it goes behind other profiles - 'core': True, - 'qualities': [q.get('identifier')], - 'label': toUnicode(q.get('label')), - 'finish': [True], - 'wait_for': [0], - }) - - order += 1 - - return True - except: - log.error('Failed: %s', traceback.format_exc()) - - return False - - def guess(self, files, extra = None, size = None, use_cache = True): if not extra: extra = {} # Create hash for cache @@ -417,49 +249,6 @@ class QualityPlugin(Plugin): if quality.get('identifier') != q.get('identifier') and score.get(q.get('identifier')): score[q.get('identifier')]['score'] -= 1 - def isFinish(self, quality, profile, release_age = 0): - if not isinstance(profile, dict) or not profile.get('qualities'): - # No profile so anything (scanned) is good enough - return True - - try: - index = [i for i, identifier in enumerate(profile['qualities']) if identifier == quality['identifier'] and bool(profile['3d'][i] if profile.get('3d') else False) == bool(quality.get('is_3d', False))][0] - - if index == 0 or (profile['finish'][index] and int(release_age) >= int(profile.get('stop_after', [0])[0])): - return True - - return False - except: - return False - - def isHigher(self, quality, compare_with, profile = None): - if not isinstance(profile, dict) or not profile.get('qualities'): - profile = fireEvent('profile.default', single = True) - - # Try to find quality in profile, if not found: a quality we do not want is lower than anything else - try: - quality_order = [i for i, identifier in enumerate(profile['qualities']) if identifier == quality['identifier'] and bool(profile['3d'][i] if profile.get('3d') else 0) == bool(quality.get('is_3d', 0))][0] - except: - log.debug('Quality %s not found in profile identifiers %s', (quality['identifier'] + (' 3D' if quality.get('is_3d', 0) else ''), \ - [identifier + (' 3D' if (profile['3d'][i] if profile.get('3d') else 0) else '') for i, identifier in enumerate(profile['qualities'])])) - return 'lower' - - # Try to find compare quality in profile, if not found: anything is higher than a not wanted quality - try: - compare_order = [i for i, identifier in enumerate(profile['qualities']) if identifier == compare_with['identifier'] and bool(profile['3d'][i] if profile.get('3d') else 0) == bool(compare_with.get('is_3d', 0))][0] - except: - log.debug('Compare quality %s not found in profile identifiers %s', (compare_with['identifier'] + (' 3D' if compare_with.get('is_3d', 0) else ''), \ - [identifier + (' 3D' if (profile['3d'][i] if profile.get('3d') else 0) else '') for i, identifier in enumerate(profile['qualities'])])) - return 'higher' - - # Note to self: a lower number means higher quality - if quality_order > compare_order: - return 'lower' - elif quality_order == compare_order: - return 'equal' - else: - return 'higher' - def doTest(self): tests = { @@ -519,8 +308,8 @@ class QualityPlugin(Plugin): success = test_quality.get('identifier') == tests[name]['quality'] and test_quality.get('is_3d') == tests[name].get('is_3d', False) if not success: log.error('%s failed check, thinks it\'s "%s" expecting "%s"', (name, - test_quality.get('identifier') + (' 3D' if test_quality.get('is_3d') else ''), - tests[name]['quality'] + (' 3D' if tests[name].get('is_3d') else '') + test_quality.get('identifier') + (' 3D' if test_quality.get('is_3d') else ''), + tests[name]['quality'] + (' 3D' if tests[name].get('is_3d') else '') )) correct += success @@ -530,5 +319,3 @@ class QualityPlugin(Plugin): return True else: log.error('Quality test failed: %s out of %s succeeded', (correct, len(tests))) - - diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index 6d3ee83..2fd7700 100755 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -277,13 +277,13 @@ class MovieSearcher(SearcherBase, MovieTypeBase): preferred_quality = quality if quality else fireEvent('quality.single', identifier = quality['identifier'], single = True) # Contains lower quality string - contains_other = fireEvent('searcher.contains_other_quality', nzb, movie_year = media['info']['year'], preferred_quality = preferred_quality, single = True) - if contains_other and isinstance(contains_other, dict): + contains_other = fireEvent('searcher.contains_other_quality', nzb, movie_year = media['info']['year'], preferred_quality = preferred_quality, types = [self._type], single = True) + if contains_other != False: log.info2('Wrong: %s, looking for %s, found %s', (nzb['name'], quality['label'], [x for x in contains_other] if contains_other else 'no quality')) return False # Contains lower quality string - if not fireEvent('searcher.correct_3d', nzb, preferred_quality = preferred_quality, single = True): + if not fireEvent('searcher.correct_3d', nzb, preferred_quality = preferred_quality, types = [self._type], single = True): log.info2('Wrong: %s, %slooking for %s in 3D', (nzb['name'], ('' if preferred_quality['custom'].get('3d') else 'NOT '), quality['label'])) return False diff --git a/couchpotato/core/media/show/_base/episode.py b/couchpotato/core/media/show/_base/episode.py index 400c8e7..c4e792b 100755 --- a/couchpotato/core/media/show/_base/episode.py +++ b/couchpotato/core/media/show/_base/episode.py @@ -12,6 +12,8 @@ autoload = 'Episode' class Episode(MediaBase): + _type = 'show.episode' + def __init__(self): addEvent('show.episode.add', self.add) addEvent('show.episode.update', self.update) @@ -37,7 +39,7 @@ class Episode(MediaBase): } # Check if season already exists - existing_episode = fireEvent('media.with_identifiers', identifiers, with_doc = True, single = True) + existing_episode = fireEvent('media.with_identifiers', identifiers, with_doc = True, types = [self._type], single = True) db = get_db() diff --git a/couchpotato/core/media/show/_base/main.py b/couchpotato/core/media/show/_base/main.py index e27e489..a64aa91 100755 --- a/couchpotato/core/media/show/_base/main.py +++ b/couchpotato/core/media/show/_base/main.py @@ -105,7 +105,7 @@ class ShowBase(MediaBase): # Update media with info self.updateInfo(media, info) - existing_show = fireEvent('media.with_identifiers', params.get('identifiers'), with_doc = True) + existing_show = fireEvent('media.with_identifiers', params.get('identifiers'), with_doc = True, types = [self._type], single = True) db = get_db() @@ -233,8 +233,6 @@ class ShowBase(MediaBase): return {} def updateInfo(self, media, info): - db = get_db() - # Remove season info for later use (save separately) info.pop('in_wanted', None) info.pop('in_library', None) diff --git a/couchpotato/core/media/show/_base/season.py b/couchpotato/core/media/show/_base/season.py index e41e460..639c5db 100755 --- a/couchpotato/core/media/show/_base/season.py +++ b/couchpotato/core/media/show/_base/season.py @@ -12,6 +12,8 @@ autoload = 'Season' class Season(MediaBase): + _type = 'show.season' + def __init__(self): addEvent('show.season.add', self.add) addEvent('show.season.update', self.update) @@ -34,7 +36,7 @@ class Season(MediaBase): } # Check if season already exists - existing_season = fireEvent('media.with_identifiers', identifiers, with_doc = True, single = True) + existing_season = fireEvent('media.with_identifiers', identifiers, with_doc = True, types = [self._type], single = True) db = get_db() diff --git a/couchpotato/core/media/show/matcher/base.py b/couchpotato/core/media/show/matcher/base.py index 186334f..488b191 100755 --- a/couchpotato/core/media/show/matcher/base.py +++ b/couchpotato/core/media/show/matcher/base.py @@ -6,26 +6,6 @@ log = CPLog(__name__) class Base(MatcherBase): - - # 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(Base, self).__init__() @@ -35,10 +15,6 @@ class Base(MatcherBase): 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('%s.matcher.correct_identifier' % self.type, chain, media): log.info('Wrong: %s, identifier does not match', release['name']) return False diff --git a/couchpotato/core/media/show/providers/torrent/iptorrents.py b/couchpotato/core/media/show/providers/torrent/iptorrents.py old mode 100644 new mode 100755 index 8183675..c10183f --- a/couchpotato/core/media/show/providers/torrent/iptorrents.py +++ b/couchpotato/core/media/show/providers/torrent/iptorrents.py @@ -9,29 +9,20 @@ autoload = 'IPTorrents' class IPTorrents(MultiProvider): - def getTypes(self): return [Season, Episode] class Season(SeasonProvider, Base): - - # TODO come back to this later, a better quality system needs to be created cat_ids = [ - ([65], [ - 'bluray_1080p', 'bluray_720p', - 'bdrip_1080p', 'bdrip_720p', - 'brrip_1080p', 'brrip_720p', - 'webdl_1080p', 'webdl_720p', 'webdl_480p', - 'hdtv_720p', 'hdtv_sd' - ]), + ([65], {}), ] 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']) + ([4], {'codec': ['mp4-asp'], 'resolution': ['sd'], 'source': ['hdtv', 'web']}), + ([5], {'codec': ['mp4-avc'], 'resolution': ['720p', '1080p'], 'source': ['hdtv', 'web']}), + ([78], {'codec': ['mp4-avc'], 'resolution': ['480p'], 'source': ['hdtv', 'web']}), + ([79], {'codec': ['mp4-avc'], 'resolution': ['sd'], 'source': ['hdtv', 'web']}) ] diff --git a/couchpotato/core/media/show/quality/__init__.py b/couchpotato/core/media/show/quality/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/couchpotato/core/media/show/quality/main.py b/couchpotato/core/media/show/quality/main.py new file mode 100755 index 0000000..29a5814 --- /dev/null +++ b/couchpotato/core/media/show/quality/main.py @@ -0,0 +1,196 @@ +from caper import Caper + +from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.helpers.variable import getExt +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.quality.base import QualityBase + +log = CPLog(__name__) + +autoload = 'ShowQuality' + + +class ShowQuality(QualityBase): + type = 'show' + + properties = { + 'codec': [ + {'identifier': 'mp2', 'label': 'MPEG-2/H.262', 'value': ['mpeg2']}, + {'identifier': 'mp4-asp', 'label': 'MPEG-4 ASP', 'value': ['divx', 'xvid']}, + {'identifier': 'mp4-avc', 'label': 'MPEG-4 AVC/H.264', 'value': ['avc', 'h264', 'x264', ('h', '264')]}, + ], + 'container': [ + {'identifier': 'avi', 'label': 'AVI', 'value': ['avi']}, + {'identifier': 'mov', 'label': 'QuickTime Movie', 'value': ['mov']}, + {'identifier': 'mpeg-4', 'label': 'MPEG-4', 'value': ['m4v', 'mp4']}, + {'identifier': 'mpeg-ts', 'label': 'MPEG-TS', 'value': ['m2ts', 'ts']}, + {'identifier': 'mkv', 'label': 'Matroska', 'value': ['mkv']}, + {'identifier': 'wmv', 'label': 'Windows Media Video', 'value': ['wmv']} + ], + 'resolution': [ + # TODO interlaced resolutions (auto-fill these options?) + {'identifier': 'sd'}, + {'identifier': '480p', 'width': 853, 'height': 480}, + {'identifier': '576p', 'width': 1024, 'height': 576}, + {'identifier': '720p', 'width': 1280, 'height': 720}, + {'identifier': '1080p', 'width': 1920, 'height': 1080} + ], + 'source': [ + {'identifier': 'cam', 'label': 'Cam', 'value': ['camrip', 'hdcam']}, + {'identifier': 'hdtv', 'label': 'HDTV', 'value': ['hdtv']}, + {'identifier': 'screener', 'label': 'Screener', 'value': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr']}, + {'identifier': 'web', 'label': 'Web', 'value': ['webrip', ('web', 'rip'), 'webdl', ('web', 'dl')]} + ] + } + + qualities = [ + # TODO sizes will need to be adjusted for season packs + + # resolutions + {'identifier': '1080p', 'label': '1080p', 'size': (1000, 25000), 'codec': ['mp4-avc'], 'container': ['mpeg-ts', 'mkv'], 'resolution': ['1080p']}, + {'identifier': '720p', 'label': '720p', 'size': (1000, 5000), 'codec': ['mp4-avc'], 'container': ['mpeg-ts', 'mkv'], 'resolution': ['720p']}, + {'identifier': '480p', 'label': '480p', 'size': (800, 5000), 'codec': ['mp4-avc'], 'container': ['mpeg-ts', 'mkv'], 'resolution': ['480p']}, + + # sources + {'identifier': 'cam', 'label': 'Cam', 'size': (800, 5000), 'source': ['cam']}, + {'identifier': 'hdtv', 'label': 'HDTV', 'size': (800, 5000), 'source': ['hdtv']}, + {'identifier': 'screener', 'label': 'Screener', 'size': (800, 5000), 'source': ['screener']}, + {'identifier': 'web', 'label': 'Web', 'size': (800, 5000), 'source': ['web']}, + ] + + def __init__(self): + super(ShowQuality, self).__init__() + + addEvent('quality.guess', self.guess) + + self.caper = Caper() + + def guess(self, files, extra = None, size = None, types = None): + if types and self.type not in types: + return + + log.debug('Trying to guess quality of: %s', files) + + if not extra: extra = {} + + # Create hash for cache + cache_key = str([f.replace('.' + getExt(f), '') if len(getExt(f)) < 4 else f for f in files]) + cached = self.getCache(cache_key) + if cached and len(extra) == 0: + return cached + + qualities = self.all() + + # Score files against each quality + score = self.score(files, qualities = qualities) + + if score is None: + return None + + # Return nothing if all scores are <= 0 + has_non_zero = 0 + for s in score: + if score[s]['score'] > 0: + has_non_zero += 1 + + if not has_non_zero: + return None + + heighest_quality = max(score, key = lambda p: score[p]['score']) + if heighest_quality: + for quality in qualities: + if quality.get('identifier') == heighest_quality: + quality['is_3d'] = False + if score[heighest_quality].get('3d'): + quality['is_3d'] = True + return self.setCache(cache_key, quality) + + return None + + def score(self, files, qualities = None, types = None): + if types and self.type not in types: + return None + + if not qualities: + qualities = self.all() + + qualities_expanded = [self.expand(q.copy()) for q in qualities] + + # Start with 0 + score = {} + for quality in qualities: + score[quality.get('identifier')] = { + 'score': 0, + '3d': {} + } + + for cur_file in files: + match = self.caper.parse(cur_file, 'scene') + + if len(match.chains) < 1: + log.info2('Unable to parse "%s", ignoring file') + continue + + chain = match.chains[0] + + for quality in qualities_expanded: + property_score = self.propertyScore(quality, chain) + + self.calcScore(score, quality, property_score) + + return score + + def propertyScore(self, quality, chain): + score = 0 + + if 'video' not in chain.info: + return 0 + + info = fireEvent('matcher.flatten_info', chain.info['video'], single = True) + + for key in ['codec', 'resolution', 'source']: + if key not in quality: + # No specific property required + score += 5 + continue + + available = list(self.getInfo(info, key)) + found = False + + for property in quality[key]: + required = property['value'] if 'value' in property else [property['identifier']] + + if set(available) & set(required): + score += 10 + found = True + break + + if not found: + score -= 10 + + return score + + def getInfo(self, info, key): + for value in info.get(key, []): + if isinstance(value, list): + yield tuple([x.lower() for x in value]) + else: + yield value.lower() + + def calcScore(self, score, quality, add_score, threedscore = (0, None), penalty = True): + score[quality['identifier']]['score'] += add_score + + # Set order for allow calculation (and cache) + if not self.cached_order: + self.cached_order = {} + for q in self.qualities: + self.cached_order[q.get('identifier')] = self.qualities.index(q) + + if penalty and add_score != 0: + for allow in quality.get('allow', []): + score[allow]['score'] -= 40 if self.cached_order[allow] < self.cached_order[quality['identifier']] else 5 + + # Give panelty for all lower qualities + for q in self.qualities[self.order.index(quality.get('identifier'))+1:]: + if score.get(q.get('identifier')): + score[q.get('identifier')]['score'] -= 1 diff --git a/couchpotato/core/media/show/searcher/episode.py b/couchpotato/core/media/show/searcher/episode.py index ea3a9db..ade2165 100755 --- a/couchpotato/core/media/show/searcher/episode.py +++ b/couchpotato/core/media/show/searcher/episode.py @@ -47,7 +47,7 @@ class EpisodeSearcher(SearcherBase, ShowTypeBase): 'result': fireEvent('%s.searcher.single' % self.getType(), media, single = True) } - def single(self, media, profile = None, quality_order = None, search_protocols = None, manual = False): + def single(self, media, profile = None, search_protocols = None, manual = False): db = get_db() related = fireEvent('library.related', media, single = True) @@ -63,9 +63,6 @@ class EpisodeSearcher(SearcherBase, ShowTypeBase): if not profile and related['show']['profile_id']: profile = db.get('id', related['show']['profile_id']) - if not quality_order: - quality_order = fireEvent('quality.order', single = True) - # TODO: check episode status # TODO: check air date #if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']): @@ -93,14 +90,19 @@ class EpisodeSearcher(SearcherBase, ShowTypeBase): # See if better quality is available for release in releases: - if quality_order.index(release['quality']) <= quality_order.index(q_identifier) and release['status'] not in ['available', 'ignored', 'failed']: - has_better_quality += 1 + if release['status'] not in ['available', 'ignored', 'failed']: + is_higher = fireEvent('quality.ishigher', \ + {'identifier': q_identifier, 'is_3d': quality_custom.get('3d', 0)}, \ + {'identifier': release['quality'], 'is_3d': release.get('is_3d', 0)}, \ + profile, single = True) + if is_higher != 'higher': + has_better_quality += 1 # Don't search for quality lower then already available. if has_better_quality is 0: log.info('Searching for %s in %s', (query, q_identifier)) - quality = fireEvent('quality.single', identifier = q_identifier, single = True) + quality = fireEvent('quality.single', identifier = q_identifier, types = ['show'], single = True) quality['custom'] = quality_custom results = fireEvent('searcher.search', search_protocols, media, quality, single = True) @@ -131,8 +133,7 @@ class EpisodeSearcher(SearcherBase, ShowTypeBase): log.info2('Too early to search for %s, %s', (too_early_to_search, query)) def correctRelease(self, release = None, media = None, quality = None, **kwargs): - if media.get('type') != 'show.episode': - return + if media.get('type') != 'show.episode': return retention = Env.setting('retention', section = 'nzb') @@ -144,6 +145,14 @@ class EpisodeSearcher(SearcherBase, ShowTypeBase): if not fireEvent('searcher.correct_words', release['name'], media, single = True): return False + preferred_quality = quality if quality else fireEvent('quality.single', identifier = quality['identifier'], single = True) + + # Contains lower quality string + contains_other = fireEvent('searcher.contains_other_quality', release, preferred_quality = preferred_quality, types = [self._type], single = True) + if contains_other != False: + log.info2('Wrong: %s, looking for %s, found %s', (release['name'], quality['label'], [x for x in contains_other] if contains_other else 'no quality')) + return False + # TODO Matching is quite costly, maybe we should be caching release matches somehow? (also look at caper optimizations) match = fireEvent('matcher.match', release, media, quality, single = True) if match: diff --git a/couchpotato/core/media/show/searcher/season.py b/couchpotato/core/media/show/searcher/season.py index c51d584..17fc170 100755 --- a/couchpotato/core/media/show/searcher/season.py +++ b/couchpotato/core/media/show/searcher/season.py @@ -37,7 +37,7 @@ class SeasonSearcher(SearcherBase, ShowTypeBase): def searchAll(self, manual = False): pass - def single(self, media, profile = None, quality_order = None, search_protocols = None, manual = False): + def single(self, media, profile = None, search_protocols = None, manual = False): db = get_db() related = fireEvent('library.related', media, single = True) @@ -53,9 +53,6 @@ class SeasonSearcher(SearcherBase, ShowTypeBase): if not profile and related['show']['profile_id']: profile = db.get('id', related['show']['profile_id']) - if not quality_order: - quality_order = fireEvent('quality.order', single = True) - # Find 'active' episodes episodes = related['episodes'] episodes_active = [] @@ -68,7 +65,7 @@ class SeasonSearcher(SearcherBase, ShowTypeBase): if len(episodes_active) == len(episodes): # All episodes are 'active', try and search for full season - if self.search(media, profile, quality_order, search_protocols): + if self.search(media, profile, search_protocols): # Success, end season search return True else: @@ -76,14 +73,14 @@ class SeasonSearcher(SearcherBase, ShowTypeBase): # Search for each episode individually for episode in episodes_active: - fireEvent('show.episode.searcher.single', episode, profile, quality_order, search_protocols, manual) + fireEvent('show.episode.searcher.single', episode, profile, search_protocols, manual) # TODO (testing) only grab one episode return True return True - def search(self, media, profile, quality_order, search_protocols): + def search(self, media, profile, search_protocols): # TODO: check episode status # TODO: check air date #if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']): @@ -111,8 +108,13 @@ class SeasonSearcher(SearcherBase, ShowTypeBase): # See if better quality is available for release in releases: - if quality_order.index(release['quality']) <= quality_order.index(q_identifier) and release['status'] not in ['available', 'ignored', 'failed']: - has_better_quality += 1 + if release['status'] not in ['available', 'ignored', 'failed']: + is_higher = fireEvent('quality.ishigher', \ + {'identifier': q_identifier, 'is_3d': quality_custom.get('3d', 0)}, \ + {'identifier': release['quality'], 'is_3d': release.get('is_3d', 0)}, \ + profile, single = True) + if is_higher != 'higher': + has_better_quality += 1 # Don't search for quality lower then already available. if has_better_quality is 0: @@ -164,6 +166,14 @@ class SeasonSearcher(SearcherBase, ShowTypeBase): if not fireEvent('searcher.correct_words', release['name'], media, single = True): return False + preferred_quality = quality if quality else fireEvent('quality.single', identifier = quality['identifier'], single = True) + + # Contains lower quality string + contains_other = fireEvent('searcher.contains_other_quality', release, preferred_quality = preferred_quality, types = [self._type], single = True) + if contains_other != False: + log.info2('Wrong: %s, looking for %s, found %s', (release['name'], quality['label'], [x for x in contains_other] if contains_other else 'no quality')) + return False + # TODO Matching is quite costly, maybe we should be caching release matches somehow? (also look at caper optimizations) match = fireEvent('matcher.match', release, media, quality, single = True) if match: diff --git a/couchpotato/core/media/show/searcher/show.py b/couchpotato/core/media/show/searcher/show.py index b7619cc..de2f436 100755 --- a/couchpotato/core/media/show/searcher/show.py +++ b/couchpotato/core/media/show/searcher/show.py @@ -59,7 +59,6 @@ class ShowSearcher(SearcherBase, ShowTypeBase): db = get_db() profile = db.get('id', media['profile_id']) - quality_order = fireEvent('quality.order', single = True) for season in show_tree.get('seasons', []): if not season.get('info'): @@ -71,7 +70,7 @@ class ShowSearcher(SearcherBase, ShowTypeBase): continue # Check if full season can be downloaded - fireEvent('show.season.searcher.single', season, profile, quality_order, search_protocols, manual) + fireEvent('show.season.searcher.single', season, profile, search_protocols, manual) # TODO (testing) only snatch one season return diff --git a/couchpotato/core/plugins/quality/__init__.py b/couchpotato/core/plugins/quality/__init__.py deleted file mode 100644 index 7710251..0000000 --- a/couchpotato/core/plugins/quality/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .main import QualityPlugin - - -def autoload(): - return QualityPlugin() diff --git a/couchpotato/core/plugins/score/scores.py b/couchpotato/core/plugins/score/scores.py old mode 100644 new mode 100755 index f53f69a..994dad2 --- a/couchpotato/core/plugins/score/scores.py +++ b/couchpotato/core/plugins/score/scores.py @@ -76,7 +76,7 @@ def namePositionScore(nzb_name, movie_name): score = 0 nzb_words = re.split('\W+', simplifyString(nzb_name)) - qualities = fireEvent('quality.all', single = True) + qualities = fireEvent('quality.all', merge = True) try: nzb_name = re.search(r'([\'"])[^\1]*\1', nzb_name).group(0) @@ -108,7 +108,7 @@ def namePositionScore(nzb_name, movie_name): found_quality = quality['identifier'] # Alt in words - for alt in quality['alternative']: + for alt in quality.get('alternative', []): if alt in nzb_words: found_quality = alt break diff --git a/couchpotato/templates/index.html b/couchpotato/templates/index.html old mode 100644 new mode 100755 index 729f1e3..6970c10 --- a/couchpotato/templates/index.html +++ b/couchpotato/templates/index.html @@ -66,7 +66,7 @@ Quality.setup({ 'profiles': {{ json_encode(fireEvent('profile.all', single = True)) }}, - 'qualities': {{ json_encode(fireEvent('quality.all', single = True)) }} + 'qualities': {{ json_encode(fireEvent('quality.all', merge = True)) }} }); CategoryList.setup({{ json_encode(fireEvent('category.all', single = True)) }});