From 874655846c5814e3ba32f8e01e180a84d93dad1c Mon Sep 17 00:00:00 2001 From: Ruud Date: Thu, 15 Aug 2013 23:38:14 +0200 Subject: [PATCH] Move movie plugin to media folder --- couchpotato/core/media/_base/__init__.py | 0 couchpotato/core/media/_base/searcher/__init__.py | 76 +++ couchpotato/core/media/_base/searcher/main.py | 207 +++++++ couchpotato/core/media/movie/_base/static/movie.js | 6 +- couchpotato/core/media/movie/searcher/__init__.py | 60 ++ couchpotato/core/media/movie/searcher/main.py | 500 ++++++++++++++++ couchpotato/core/plugins/automation/main.py | 2 +- couchpotato/core/plugins/dashboard/main.py | 4 +- couchpotato/core/plugins/renamer/main.py | 2 +- couchpotato/core/plugins/searcher/__init__.py | 124 ---- couchpotato/core/plugins/searcher/main.py | 644 --------------------- couchpotato/core/providers/base.py | 2 +- couchpotato/static/scripts/page/wanted.js | 4 +- 13 files changed, 853 insertions(+), 778 deletions(-) create mode 100644 couchpotato/core/media/_base/__init__.py create mode 100644 couchpotato/core/media/_base/searcher/__init__.py create mode 100644 couchpotato/core/media/_base/searcher/main.py create mode 100644 couchpotato/core/media/movie/searcher/__init__.py create mode 100644 couchpotato/core/media/movie/searcher/main.py delete mode 100644 couchpotato/core/plugins/searcher/__init__.py delete mode 100644 couchpotato/core/plugins/searcher/main.py diff --git a/couchpotato/core/media/_base/__init__.py b/couchpotato/core/media/_base/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/couchpotato/core/media/_base/searcher/__init__.py b/couchpotato/core/media/_base/searcher/__init__.py new file mode 100644 index 0000000..f3d764d --- /dev/null +++ b/couchpotato/core/media/_base/searcher/__init__.py @@ -0,0 +1,76 @@ +from .main import Searcher +import random + +def start(): + return Searcher() + +config = [{ + 'name': 'searcher', + 'order': 20, + 'groups': [ + { + 'tab': 'searcher', + 'name': 'searcher', + 'label': 'Basics', + 'description': 'General search options', + 'options': [ + { + 'name': 'preferred_method', + 'label': 'First search', + 'description': 'Which of the methods do you prefer', + 'default': 'both', + 'type': 'dropdown', + 'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')], + }, + ], + }, { + 'tab': 'searcher', + 'subtab': 'category', + 'subtab_label': 'Categories', + 'name': 'filter', + 'label': 'Global filters', + 'description': 'Prefer, ignore & required words in release names', + 'options': [ + { + 'name': 'preferred_words', + 'label': 'Preferred', + 'default': '', + 'placeholder': 'Example: CtrlHD, Amiable, Wiki', + 'description': 'Words that give the releases a higher score.' + }, + { + 'name': 'required_words', + 'label': 'Required', + 'default': '', + 'placeholder': 'Example: DTS, AC3 & English', + 'description': 'Release should contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"' + }, + { + 'name': 'ignored_words', + 'label': 'Ignored', + 'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs', + 'description': 'Ignores releases that match any of these sets. (Works like explained above)' + }, + ], + }, + ], +}, { + 'name': 'nzb', + 'groups': [ + { + 'tab': 'searcher', + 'name': 'searcher', + 'label': 'NZB', + 'wizard': True, + 'options': [ + { + 'name': 'retention', + 'label': 'Usenet Retention', + 'default': 1500, + 'type': 'int', + 'unit': 'days' + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/_base/searcher/main.py b/couchpotato/core/media/_base/searcher/main.py new file mode 100644 index 0000000..772a8ac --- /dev/null +++ b/couchpotato/core/media/_base/searcher/main.py @@ -0,0 +1,207 @@ +from couchpotato import get_session +from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.helpers.encoding import simplifyString, toUnicode +from couchpotato.core.helpers.variable import md5, getTitle +from couchpotato.core.logger import CPLog +from couchpotato.core.plugins.base import Plugin +from couchpotato.core.settings.model import Movie, Release, ReleaseInfo +from couchpotato.environment import Env +from inspect import ismethod, isfunction +import datetime +import re +import time +import traceback + +log = CPLog(__name__) + + +class Searcher(Plugin): + + def __init__(self): + addEvent('searcher.get_types', self.getSearchTypes) + addEvent('searcher.contains_other_quality', self.containsOtherQuality) + addEvent('searcher.correct_year', self.correctYear) + addEvent('searcher.correct_name', self.correctName) + addEvent('searcher.download', self.download) + + def download(self, data, movie, manual = False): + + # Test to see if any downloaders are enabled for this type + downloader_enabled = fireEvent('download.enabled', manual, data, single = True) + + if downloader_enabled: + + snatched_status = fireEvent('status.get', 'snatched', single = True) + + # Download movie to temp + filedata = None + if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))): + filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id')) + if filedata == 'try_next': + return filedata + + download_result = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True) + log.debug('Downloader result: %s', download_result) + + if download_result: + try: + # Mark release as snatched + db = get_session() + rls = db.query(Release).filter_by(identifier = md5(data['url'])).first() + if rls: + renamer_enabled = Env.setting('enabled', 'renamer') + + done_status = fireEvent('status.get', 'done', single = True) + rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id') + + # Save download-id info if returned + if isinstance(download_result, dict): + for key in download_result: + rls_info = ReleaseInfo( + identifier = 'download_%s' % key, + value = toUnicode(download_result.get(key)) + ) + rls.info.append(rls_info) + db.commit() + + log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label) + snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie) + log.info(snatch_message) + fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict()) + + # If renamer isn't used, mark movie done + if not renamer_enabled: + active_status = fireEvent('status.get', 'active', single = True) + done_status = fireEvent('status.get', 'done', single = True) + try: + if movie['status_id'] == active_status.get('id'): + for profile_type in movie['profile']['types']: + if profile_type['quality_id'] == rls.quality.id and profile_type['finish']: + log.info('Renamer disabled, marking movie as finished: %s', log_movie) + + # Mark release done + rls.status_id = done_status.get('id') + rls.last_edit = int(time.time()) + db.commit() + + # Mark movie done + mvie = db.query(Movie).filter_by(id = movie['id']).first() + mvie.status_id = done_status.get('id') + mvie.last_edit = int(time.time()) + db.commit() + except: + log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc()) + + except: + log.error('Failed marking movie finished: %s', traceback.format_exc()) + + return True + + log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', (data.get('type', ''))) + + return False + + def getSearchTypes(self): + + download_types = fireEvent('download.enabled_types', merge = True) + provider_types = fireEvent('provider.enabled_types', merge = True) + + if download_types and len(list(set(provider_types) & set(download_types))) == 0: + log.error('There aren\'t any providers enabled for your downloader (%s). Check your settings.', ','.join(download_types)) + return [] + + for useless_provider in list(set(provider_types) - set(download_types)): + log.debug('Provider for "%s" enabled, but no downloader.', useless_provider) + + search_types = download_types + + if len(search_types) == 0: + log.error('There aren\'t any downloaders enabled. Please pick one in settings.') + return [] + + return search_types + + def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}): + + name = nzb['name'] + size = nzb.get('size', 0) + nzb_words = re.split('\W+', simplifyString(name)) + + qualities = fireEvent('quality.all', single = True) + + found = {} + for quality in qualities: + # Main in words + if quality['identifier'] in nzb_words: + found[quality['identifier']] = True + + # Alt in words + if list(set(nzb_words) & set(quality['alternative'])): + found[quality['identifier']] = True + + # Try guessing via quality tags + guess = fireEvent('quality.guess', [nzb.get('name')], single = True) + if guess: + found[guess['identifier']] = True + + # Hack for older movies that don't contain quality tag + year_name = fireEvent('scanner.name_year', name, single = True) + if len(found) == 0 and movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None): + if size > 3000: # Assume dvdr + log.info('Quality was missing in name, assuming it\'s a DVD-R based on the size: %s', (size)) + found['dvdr'] = True + else: # Assume dvdrip + log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', (size)) + found['dvdrip'] = True + + # Allow other qualities + for allowed in preferred_quality.get('allow'): + if found.get(allowed): + del found[allowed] + + return not (found.get(preferred_quality['identifier']) and len(found) == 1) + + def correctYear(self, haystack, year, year_range): + + if not isinstance(haystack, (list, tuple, set)): + haystack = [haystack] + + for string in haystack: + + year_name = fireEvent('scanner.name_year', string, single = True) + + if year_name and ((year - year_range) <= year_name.get('year') <= (year + year_range)): + log.debug('Movie year matches range: %s looking for %s', (year_name.get('year'), year)) + return True + + log.debug('Movie year doesn\'t matche range: %s looking for %s', (year_name.get('year'), year)) + return False + + def correctName(self, check_name, movie_name): + + check_names = [check_name] + + # Match names between " + try: check_names.append(re.search(r'([\'"])[^\1]*\1', check_name).group(0)) + except: pass + + # Match longest name between [] + try: check_names.append(max(check_name.split('['), key = len)) + except: pass + + for check_name in list(set(check_names)): + check_movie = fireEvent('scanner.name_year', check_name, single = True) + + try: + check_words = filter(None, re.split('\W+', check_movie.get('name', ''))) + movie_words = filter(None, re.split('\W+', simplifyString(movie_name))) + + if len(check_words) > 0 and len(movie_words) > 0 and len(list(set(check_words) - set(movie_words))) == 0: + return True + except: + pass + + return False + +class SearchSetupError(Exception): + pass diff --git a/couchpotato/core/media/movie/_base/static/movie.js b/couchpotato/core/media/movie/_base/static/movie.js index f5b5a2d..20956a0 100644 --- a/couchpotato/core/media/movie/_base/static/movie.js +++ b/couchpotato/core/media/movie/_base/static/movie.js @@ -29,14 +29,14 @@ var Movie = new Class({ self.update.delay(2000, self, notification); }); - ['movie.busy', 'searcher.started'].each(function(listener){ + ['movie.busy', 'movie.searcher.started'].each(function(listener){ App.addEvent(listener+'.'+self.data.id, function(notification){ if(notification.data) self.busy(true) }); }) - App.addEvent('searcher.ended.'+self.data.id, function(notification){ + App.addEvent('movie.searcher.ended.'+self.data.id, function(notification){ if(notification.data) self.busy(false) }); @@ -53,7 +53,7 @@ var Movie = new Class({ // Remove events App.removeEvents('movie.update.'+self.data.id); - ['movie.busy', 'searcher.started'].each(function(listener){ + ['movie.busy', 'movie.searcher.started'].each(function(listener){ App.removeEvents(listener+'.'+self.data.id); }) }, diff --git a/couchpotato/core/media/movie/searcher/__init__.py b/couchpotato/core/media/movie/searcher/__init__.py new file mode 100644 index 0000000..2962caa --- /dev/null +++ b/couchpotato/core/media/movie/searcher/__init__.py @@ -0,0 +1,60 @@ +from .main import Searcher +import random + +def start(): + return Searcher() + +config = [{ + 'name': 'searcher', + 'order': 20, + 'groups': [ + { + 'tab': 'searcher', + 'name': 'movie_searcher', + 'label': 'Movie search', + 'description': 'Search options for movies', + 'advanced': True, + 'options': [ + { + 'name': 'always_search', + 'default': False, + 'type': 'bool', + 'label': 'Always search', + 'description': 'Search for movies even before there is a ETA. Enabling this will probably get you a lot of fakes.', + }, + { + 'name': 'run_on_launch', + 'label': 'Run on launch', + 'advanced': True, + 'default': 0, + 'type': 'bool', + 'description': 'Force run the searcher after (re)start.', + }, + { + 'name': 'cron_day', + 'label': 'Day', + 'advanced': True, + 'default': '*', + 'type': 'string', + 'description': '*: Every day, */2: Every 2 days, 1: Every first of the month. See APScheduler for details.', + }, + { + 'name': 'cron_hour', + 'label': 'Hour', + 'advanced': True, + 'default': random.randint(0, 23), + 'type': 'string', + 'description': '*: Every hour, */8: Every 8 hours, 3: At 3, midnight.', + }, + { + 'name': 'cron_minute', + 'label': 'Minute', + 'advanced': True, + 'default': random.randint(0, 59), + 'type': 'string', + 'description': "Just keep it random, so the providers don't get DDOSed by every CP user on a 'full' hour." + }, + ], + }, + ], +}] diff --git a/couchpotato/core/media/movie/searcher/main.py b/couchpotato/core/media/movie/searcher/main.py new file mode 100644 index 0000000..dbfd679 --- /dev/null +++ b/couchpotato/core/media/movie/searcher/main.py @@ -0,0 +1,500 @@ +from couchpotato import get_session +from couchpotato.api import addApiView +from couchpotato.core.event import addEvent, fireEvent, fireEventAsync +from couchpotato.core.helpers.encoding import simplifyString, toUnicode +from couchpotato.core.helpers.variable import md5, getTitle, splitString, \ + possibleTitles, getImdb +from couchpotato.core.logger import CPLog +from couchpotato.core.plugins.base import Plugin +from couchpotato.core.settings.model import Movie, Release, ReleaseInfo +from couchpotato.environment import Env +from datetime import date +from sqlalchemy.exc import InterfaceError +import datetime +import random +import re +import time +import traceback + +log = CPLog(__name__) + + +class Searcher(Plugin): + + in_progress = False + + def __init__(self): + addEvent('movie.searcher.all', self.searchAll) + addEvent('movie.searcher.single', self.single) + addEvent('movie.searcher.correct_movie', self.correctMovie) + addEvent('movie.searcher.try_next_release', self.tryNextRelease) + addEvent('movie.searcher.could_be_released', self.couldBeReleased) + + addApiView('movie.searcher.try_next', self.tryNextReleaseView, docs = { + 'desc': 'Marks the snatched results as ignored and try the next best release', + 'params': { + 'id': {'desc': 'The id of the movie'}, + }, + }) + + addApiView('movie.searcher.full_search', self.searchAllView, docs = { + 'desc': 'Starts a full search for all wanted movies', + }) + + addApiView('movie.searcher.progress', self.getProgress, docs = { + 'desc': 'Get the progress of current full search', + 'return': {'type': 'object', 'example': """{ + 'progress': False || object, total & to_go, +}"""}, + }) + + if self.conf('run_on_launch'): + addEvent('app.load', self.searchAll) + + addEvent('app.load', self.setCrons) + addEvent('setting.save.searcher.cron_day.after', self.setCrons) + addEvent('setting.save.searcher.cron_hour.after', self.setCrons) + addEvent('setting.save.searcher.cron_minute.after', self.setCrons) + + def setCrons(self): + fireEvent('schedule.cron', 'movie.searcher.all', self.searchAll, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) + + def searchAllView(self, **kwargs): + + in_progress = self.in_progress + if not in_progress: + fireEventAsync('movie.searcher.all') + fireEvent('notify.frontend', type = 'movie.searcher.started', data = True, message = 'Full search started') + else: + fireEvent('notify.frontend', type = 'movie.searcher.already_started', data = True, message = 'Full search already in progress') + + return { + 'success': not in_progress + } + + def getProgress(self, **kwargs): + + return { + 'progress': self.in_progress + } + + def searchAll(self): + + if self.in_progress: + log.info('Search already in progress') + return + + self.in_progress = True + + db = get_session() + + movies = db.query(Movie).filter( + Movie.status.has(identifier = 'active') + ).all() + random.shuffle(movies) + + self.in_progress = { + 'total': len(movies), + 'to_go': len(movies), + } + + try: + search_types = fireEvent('searcher.get_types', single = True) + + for movie in movies: + movie_dict = movie.to_dict({ + 'category': {}, + 'profile': {'types': {'quality': {}}}, + 'releases': {'status': {}, 'quality': {}}, + 'library': {'titles': {}, 'files':{}}, + 'files': {} + }) + + try: + self.single(movie_dict, search_types) + except IndexError: + log.error('Forcing library update for %s, if you see this often, please report: %s', (movie_dict['library']['identifier'], traceback.format_exc())) + fireEvent('library.update', movie_dict['library']['identifier'], force = True) + except: + log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc())) + + self.in_progress['to_go'] -= 1 + + # Break if CP wants to shut down + if self.shuttingDown(): + break + + except SearchSetupError: + pass + + self.in_progress = False + + def single(self, movie, search_types = None): + + # Find out search type + try: + if not search_types: + search_types = fireEvent('searcher.get_types', single = True) + except SearchSetupError: + return + + done_status = fireEvent('status.get', 'done', single = True) + + if not movie['profile'] or movie['status_id'] == done_status.get('id'): + log.debug('Movie doesn\'t have a profile or already done, assuming in manage tab.') + return + + db = get_session() + + pre_releases = fireEvent('quality.pre_releases', single = True) + release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True) + available_status, ignored_status, failed_status = fireEvent('status.get', ['available', 'ignored', 'failed'], single = True) + + found_releases = [] + too_early_to_search = [] + + default_title = getTitle(movie['library']) + if not default_title: + log.error('No proper info found for movie, removing it from library to cause it from having more issues.') + fireEvent('movie.delete', movie['id'], single = True) + return + + fireEvent('notify.frontend', type = 'movie.searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title) + + + ret = False + for quality_type in movie['profile']['types']: + if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']): + too_early_to_search.append(quality_type['quality']['identifier']) + continue + + has_better_quality = 0 + + # See if better quality is available + for release in movie['releases']: + if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id'), failed_status.get('id')]: + has_better_quality += 1 + + # Don't search for quality lower then already available. + if has_better_quality is 0: + + log.info('Search for %s in %s', (default_title, quality_type['quality']['label'])) + quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True) + + results = [] + for search_type in search_types: + type_results = fireEvent('%s.search' % search_type, movie, quality, merge = True) + if type_results: + results += type_results + + sorted_results = sorted(results, key = lambda k: k['score'], reverse = True) + if len(sorted_results) == 0: + log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label'])) + + download_preference = self.conf('preferred_method') + if download_preference != 'both': + sorted_results = sorted(sorted_results, key = lambda k: k['type'][:3], reverse = (download_preference == 'torrent')) + + # Check if movie isn't deleted while searching + if not db.query(Movie).filter_by(id = movie.get('id')).first(): + break + + # Add them to this movie releases list + for nzb in sorted_results: + + nzb_identifier = md5(nzb['url']) + found_releases.append(nzb_identifier) + + rls = db.query(Release).filter_by(identifier = nzb_identifier).first() + if not rls: + rls = Release( + identifier = nzb_identifier, + movie_id = movie.get('id'), + quality_id = quality_type.get('quality_id'), + status_id = available_status.get('id') + ) + db.add(rls) + else: + [db.delete(old_info) for old_info in rls.info] + rls.last_edit = int(time.time()) + + db.commit() + + for info in nzb: + try: + if not isinstance(nzb[info], (str, unicode, int, long, float)): + continue + + rls_info = ReleaseInfo( + identifier = info, + value = toUnicode(nzb[info]) + ) + rls.info.append(rls_info) + except InterfaceError: + log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc())) + + db.commit() + + nzb['status_id'] = rls.status_id + + + for nzb in sorted_results: + if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and nzb.get('age') <= quality_type.get('wait_for', 0): + log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name'])) + continue + + if nzb['status_id'] in [ignored_status.get('id'), failed_status.get('id')]: + log.info('Ignored: %s', nzb['name']) + continue + + if nzb['score'] <= 0: + log.info('Ignored, score to low: %s', nzb['name']) + continue + + downloaded = fireEvent('searcher.download', data = nzb, movie = movie, single = True) + if downloaded is True: + ret = True + break + elif downloaded != 'try_next': + break + + # Remove releases that aren't found anymore + for release in movie.get('releases', []): + if release.get('status_id') == available_status.get('id') and release.get('identifier') not in found_releases: + 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', movie['id']) + break + + # Break if CP wants to shut down + if self.shuttingDown() or ret: + break + + if len(too_early_to_search) > 0: + log.info2('Too early to search for %s, %s', (too_early_to_search, default_title)) + + fireEvent('notify.frontend', type = 'movie.searcher.ended.%s' % movie['id'], data = True) + + return ret + + def correctMovie(self, nzb = None, movie = None, quality = None, **kwargs): + + imdb_results = kwargs.get('imdb_results', False) + retention = Env.setting('retention', section = 'nzb') + + if nzb.get('seeders') is None and 0 < retention < nzb.get('age', 0): + log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name'])) + return False + + movie_name = getTitle(movie['library']) + movie_words = re.split('\W+', simplifyString(movie_name)) + nzb_name = simplifyString(nzb['name']) + nzb_words = re.split('\W+', nzb_name) + + # Make sure it has required words + required_words = splitString(self.conf('required_words').lower()) + try: required_words = list(set(required_words + splitString(movie['category']['required'].lower()))) + except: pass + + req_match = 0 + for req_set in required_words: + req = splitString(req_set, '&') + req_match += len(list(set(nzb_words) & set(req))) == len(req) + + if len(required_words) > 0 and req_match == 0: + log.info2('Wrong: Required word missing: %s', nzb['name']) + return False + + # Ignore releases + ignored_words = splitString(self.conf('ignored_words').lower()) + try: ignored_words = list(set(ignored_words + splitString(movie['category']['ignored'].lower()))) + except: pass + + ignored_match = 0 + for ignored_set in ignored_words: + ignored = splitString(ignored_set, '&') + ignored_match += len(list(set(nzb_words) & set(ignored))) == len(ignored) + + if len(ignored_words) > 0 and ignored_match: + log.info2("Wrong: '%s' contains 'ignored words'", (nzb['name'])) + return False + + # Ignore porn stuff + pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic'] + pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words)) + if pron_words: + log.info('Wrong: %s, probably pr0n', (nzb['name'])) + return False + + preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True) + + # Contains lower quality string + if fireEvent('searcher.contains_other_quality', nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality, single = True): + log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label'])) + return False + + + # File to small + if nzb['size'] and preferred_quality['size_min'] > nzb['size']: + log.info2('Wrong: "%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min'])) + return False + + # File to large + if nzb['size'] and preferred_quality.get('size_max') < nzb['size']: + log.info2('Wrong: "%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max'])) + return False + + + # Provider specific functions + get_more = nzb.get('get_more_info') + if get_more: + get_more(nzb) + + extra_check = nzb.get('extra_check') + if extra_check and not extra_check(nzb): + return False + + + if imdb_results: + return True + + # Check if nzb contains imdb link + if getImdb(nzb.get('description', '')) == movie['library']['identifier']: + return True + + for raw_title in movie['library']['titles']: + for movie_title in possibleTitles(raw_title['title']): + movie_words = re.split('\W+', simplifyString(movie_title)) + + if fireEvent('searcher.correct_name', nzb['name'], movie_title, single = True): + # if no IMDB link, at least check year range 1 + if len(movie_words) > 2 and fireEvent('searcher.correct_year', nzb['name'], movie['library']['year'], 1, single = True): + return True + + # if no IMDB link, at least check year + if len(movie_words) <= 2 and fireEvent('searcher.correct_year', nzb['name'], movie['library']['year'], 0, single = True): + return True + + log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], movie_name, movie['library']['year'])) + return False + + def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}): + + name = nzb['name'] + size = nzb.get('size', 0) + nzb_words = re.split('\W+', simplifyString(name)) + + qualities = fireEvent('quality.all', single = True) + + found = {} + for quality in qualities: + # Main in words + if quality['identifier'] in nzb_words: + found[quality['identifier']] = True + + # Alt in words + if list(set(nzb_words) & set(quality['alternative'])): + found[quality['identifier']] = True + + # Try guessing via quality tags + guess = fireEvent('quality.guess', [nzb.get('name')], single = True) + if guess: + found[guess['identifier']] = True + + # Hack for older movies that don't contain quality tag + year_name = fireEvent('scanner.name_year', name, single = True) + if len(found) == 0 and movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None): + if size > 3000: # Assume dvdr + log.info('Quality was missing in name, assuming it\'s a DVD-R based on the size: %s', (size)) + found['dvdr'] = True + else: # Assume dvdrip + log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', (size)) + found['dvdrip'] = True + + # Allow other qualities + for allowed in preferred_quality.get('allow'): + if found.get(allowed): + del found[allowed] + + return not (found.get(preferred_quality['identifier']) and len(found) == 1) + + def checkIMDB(self, haystack, imdbId): + + for string in haystack: + if 'imdb.com/title/' + imdbId in string: + return True + + return False + + def couldBeReleased(self, is_pre_release, dates, year = None): + + now = int(time.time()) + now_year = date.today().year + + if (year is None or year < now_year - 1) and (not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0)): + return True + else: + + # For movies before 1972 + if dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0: + return True + + if is_pre_release: + # Prerelease 1 week before theaters + if dates.get('theater') - 604800 < now: + return True + else: + # 12 weeks after theater release + if dates.get('theater') > 0 and dates.get('theater') + 7257600 < now: + return True + + if dates.get('dvd') > 0: + + # 4 weeks before dvd release + if dates.get('dvd') - 2419200 < now: + return True + + # Dvd should be released + if dates.get('dvd') < now: + return True + + + return False + + def tryNextReleaseView(self, id = None, **kwargs): + + trynext = self.tryNextRelease(id) + + return { + 'success': trynext + } + + def tryNextRelease(self, movie_id, manual = False): + + snatched_status, ignored_status = fireEvent('status.get', ['snatched', 'ignored'], single = True) + + try: + db = get_session() + rels = db.query(Release).filter_by( + status_id = snatched_status.get('id'), + movie_id = movie_id + ).all() + + for rel in rels: + rel.status_id = ignored_status.get('id') + db.commit() + + movie_dict = fireEvent('movie.get', movie_id, single = True) + log.info('Trying next release for: %s', getTitle(movie_dict['library'])) + fireEvent('movie.searcher.single', movie_dict) + + return True + + except: + log.error('Failed searching for next release: %s', traceback.format_exc()) + return False + +class SearchSetupError(Exception): + pass diff --git a/couchpotato/core/plugins/automation/main.py b/couchpotato/core/plugins/automation/main.py index 67bae1d..80e1285 100644 --- a/couchpotato/core/plugins/automation/main.py +++ b/couchpotato/core/plugins/automation/main.py @@ -36,4 +36,4 @@ class Automation(Plugin): for movie_id in movie_ids: movie_dict = fireEvent('movie.get', movie_id, single = True) - fireEvent('searcher.single', movie_dict) + fireEvent('movie.searcher.single', movie_dict) diff --git a/couchpotato/core/plugins/dashboard/main.py b/couchpotato/core/plugins/dashboard/main.py index 939b415..df6f975 100644 --- a/couchpotato/core/plugins/dashboard/main.py +++ b/couchpotato/core/plugins/dashboard/main.py @@ -70,9 +70,9 @@ class Dashboard(Plugin): coming_soon = False # Theater quality - if pp.get('theater') and fireEvent('searcher.could_be_released', True, eta, movie.library.year, single = True): + if pp.get('theater') and fireEvent('movie.searcher.could_be_released', True, eta, movie.library.year, single = True): coming_soon = True - if pp.get('dvd') and fireEvent('searcher.could_be_released', False, eta, movie.library.year, single = True): + if pp.get('dvd') and fireEvent('movie.searcher.could_be_released', False, eta, movie.library.year, single = True): coming_soon = True # Skip if movie is snatched/downloaded/available diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index 30ce408..8d1a818 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -715,7 +715,7 @@ Remove it if you want it to be renamed (again, or at least let it try again) db.commit() if self.conf('next_on_failed'): - fireEvent('searcher.try_next_release', movie_id = rel.movie_id) + fireEvent('movie.searcher.try_next_release', movie_id = rel.movie_id) elif item['status'] == 'completed': log.info('Download of %s completed!', item['name']) if self.statusInfoComplete(item): diff --git a/couchpotato/core/plugins/searcher/__init__.py b/couchpotato/core/plugins/searcher/__init__.py deleted file mode 100644 index d925ae7..0000000 --- a/couchpotato/core/plugins/searcher/__init__.py +++ /dev/null @@ -1,124 +0,0 @@ -from .main import Searcher -import random - -def start(): - return Searcher() - -config = [{ - 'name': 'searcher', - 'order': 20, - 'groups': [ - { - 'tab': 'searcher', - 'name': 'searcher', - 'label': 'Search', - 'description': 'Options for the searchers', - 'options': [ - { - 'name': 'preferred_method', - 'label': 'First search', - 'description': 'Which of the methods do you prefer', - 'default': 'both', - 'type': 'dropdown', - 'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')], - }, - { - 'name': 'always_search', - 'default': False, - 'advanced': True, - 'type': 'bool', - 'label': 'Always search', - 'description': 'Search for movies even before there is a ETA. Enabling this will probably get you a lot of fakes.', - }, - ], - }, { - 'tab': 'searcher', - 'subtab': 'category', - 'subtab_label': 'Categories', - 'name': 'filter', - 'label': 'Global filters', - 'description': 'Prefer, ignore & required words in release names', - 'options': [ - { - 'name': 'preferred_words', - 'label': 'Preferred', - 'default': '', - 'placeholder': 'Example: CtrlHD, Amiable, Wiki', - 'description': 'Words that give the releases a higher score.' - }, - { - 'name': 'required_words', - 'label': 'Required', - 'default': '', - 'placeholder': 'Example: DTS, AC3 & English', - 'description': 'Release should contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"' - }, - { - 'name': 'ignored_words', - 'label': 'Ignored', - 'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs', - 'description': 'Ignores releases that match any of these sets. (Works like explained above)' - }, - ], - }, { - 'tab': 'searcher', - 'name': 'cronjob', - 'label': 'Cronjob', - 'advanced': True, - 'description': 'Cron settings for the searcher see: APScheduler for details.', - 'options': [ - { - 'name': 'run_on_launch', - 'label': 'Run on launch', - 'advanced': True, - 'default': 0, - 'type': 'bool', - 'description': 'Force run the searcher after (re)start.', - }, - { - 'name': 'cron_day', - 'label': 'Day', - 'advanced': True, - 'default': '*', - 'type': 'string', - 'description': '*: Every day, */2: Every 2 days, 1: Every first of the month.', - }, - { - 'name': 'cron_hour', - 'label': 'Hour', - 'advanced': True, - 'default': random.randint(0, 23), - 'type': 'string', - 'description': '*: Every hour, */8: Every 8 hours, 3: At 3, midnight.', - }, - { - 'name': 'cron_minute', - 'label': 'Minute', - 'advanced': True, - 'default': random.randint(0, 59), - 'type': 'string', - 'description': "Just keep it random, so the providers don't get DDOSed by every CP user on a 'full' hour." - }, - ], - }, - ], -}, { - 'name': 'nzb', - 'groups': [ - { - 'tab': 'searcher', - 'name': 'searcher', - 'label': 'NZB', - 'wizard': True, - 'options': [ - { - 'name': 'retention', - 'label': 'Usenet Retention', - 'default': 1500, - 'type': 'int', - 'unit': 'days' - }, - ], - }, - ], -}] diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py deleted file mode 100644 index b55e720..0000000 --- a/couchpotato/core/plugins/searcher/main.py +++ /dev/null @@ -1,644 +0,0 @@ -from couchpotato import get_session -from couchpotato.api import addApiView -from couchpotato.core.event import addEvent, fireEvent, fireEventAsync -from couchpotato.core.helpers.encoding import simplifyString, toUnicode -from couchpotato.core.helpers.variable import md5, getTitle, splitString, \ - possibleTitles -from couchpotato.core.logger import CPLog -from couchpotato.core.plugins.base import Plugin -from couchpotato.core.settings.model import Movie, Release, ReleaseInfo -from couchpotato.environment import Env -from datetime import date -from inspect import ismethod, isfunction -from sqlalchemy.exc import InterfaceError -import datetime -import random -import re -import time -import traceback - -log = CPLog(__name__) - - -class Searcher(Plugin): - - in_progress = False - - def __init__(self): - addEvent('searcher.all', self.allMovies) - addEvent('searcher.single', self.single) - addEvent('searcher.correct_movie', self.correctMovie) - addEvent('searcher.download', self.download) - addEvent('searcher.try_next_release', self.tryNextRelease) - addEvent('searcher.could_be_released', self.couldBeReleased) - - addApiView('searcher.try_next', self.tryNextReleaseView, docs = { - 'desc': 'Marks the snatched results as ignored and try the next best release', - 'params': { - 'id': {'desc': 'The id of the movie'}, - }, - }) - - addApiView('searcher.full_search', self.allMoviesView, docs = { - 'desc': 'Starts a full search for all wanted movies', - }) - - addApiView('searcher.progress', self.getProgress, docs = { - 'desc': 'Get the progress of current full search', - 'return': {'type': 'object', 'example': """{ - 'progress': False || object, total & to_go, -}"""}, - }) - - if self.conf('run_on_launch'): - addEvent('app.load', self.allMovies) - - addEvent('app.load', self.setCrons) - addEvent('setting.save.searcher.cron_day.after', self.setCrons) - addEvent('setting.save.searcher.cron_hour.after', self.setCrons) - addEvent('setting.save.searcher.cron_minute.after', self.setCrons) - - def setCrons(self): - fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) - - def allMoviesView(self, **kwargs): - - in_progress = self.in_progress - if not in_progress: - fireEventAsync('searcher.all') - fireEvent('notify.frontend', type = 'searcher.started', data = True, message = 'Full search started') - else: - fireEvent('notify.frontend', type = 'searcher.already_started', data = True, message = 'Full search already in progress') - - return { - 'success': not in_progress - } - - def getProgress(self, **kwargs): - - return { - 'progress': self.in_progress - } - - def allMovies(self): - - if self.in_progress: - log.info('Search already in progress') - return - - self.in_progress = True - - db = get_session() - - movies = db.query(Movie).filter( - Movie.status.has(identifier = 'active') - ).all() - random.shuffle(movies) - - self.in_progress = { - 'total': len(movies), - 'to_go': len(movies), - } - - try: - search_types = self.getSearchTypes() - - for movie in movies: - movie_dict = movie.to_dict({ - 'category': {}, - 'profile': {'types': {'quality': {}}}, - 'releases': {'status': {}, 'quality': {}}, - 'library': {'titles': {}, 'files':{}}, - 'files': {} - }) - - try: - self.single(movie_dict, search_types) - except IndexError: - log.error('Forcing library update for %s, if you see this often, please report: %s', (movie_dict['library']['identifier'], traceback.format_exc())) - fireEvent('library.update', movie_dict['library']['identifier'], force = True) - except: - log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc())) - - self.in_progress['to_go'] -= 1 - - # Break if CP wants to shut down - if self.shuttingDown(): - break - - except SearchSetupError: - pass - - self.in_progress = False - - def single(self, movie, search_types = None): - - # Find out search type - try: - if not search_types: - search_types = self.getSearchTypes() - except SearchSetupError: - return - - done_status = fireEvent('status.get', 'done', single = True) - - if not movie['profile'] or movie['status_id'] == done_status.get('id'): - log.debug('Movie doesn\'t have a profile or already done, assuming in manage tab.') - return - - db = get_session() - - pre_releases = fireEvent('quality.pre_releases', single = True) - release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True) - available_status, ignored_status, failed_status = fireEvent('status.get', ['available', 'ignored', 'failed'], single = True) - - found_releases = [] - too_early_to_search = [] - - default_title = getTitle(movie['library']) - if not default_title: - log.error('No proper info found for movie, removing it from library to cause it from having more issues.') - fireEvent('movie.delete', movie['id'], single = True) - return - - fireEvent('notify.frontend', type = 'searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title) - - - ret = False - for quality_type in movie['profile']['types']: - if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']): - too_early_to_search.append(quality_type['quality']['identifier']) - continue - - has_better_quality = 0 - - # See if better quality is available - for release in movie['releases']: - if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id'), failed_status.get('id')]: - has_better_quality += 1 - - # Don't search for quality lower then already available. - if has_better_quality is 0: - - log.info('Search for %s in %s', (default_title, quality_type['quality']['label'])) - quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True) - - results = [] - for search_type in search_types: - type_results = fireEvent('%s.search' % search_type, movie, quality, merge = True) - if type_results: - results += type_results - - sorted_results = sorted(results, key = lambda k: k['score'], reverse = True) - if len(sorted_results) == 0: - log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label'])) - - download_preference = self.conf('preferred_method') - if download_preference != 'both': - sorted_results = sorted(sorted_results, key = lambda k: k['type'][:3], reverse = (download_preference == 'torrent')) - - # Check if movie isn't deleted while searching - if not db.query(Movie).filter_by(id = movie.get('id')).first(): - break - - # Add them to this movie releases list - for nzb in sorted_results: - - nzb_identifier = md5(nzb['url']) - found_releases.append(nzb_identifier) - - rls = db.query(Release).filter_by(identifier = nzb_identifier).first() - if not rls: - rls = Release( - identifier = nzb_identifier, - movie_id = movie.get('id'), - quality_id = quality_type.get('quality_id'), - status_id = available_status.get('id') - ) - db.add(rls) - else: - [db.delete(old_info) for old_info in rls.info] - rls.last_edit = int(time.time()) - - db.commit() - - for info in nzb: - try: - if not isinstance(nzb[info], (str, unicode, int, long, float)): - continue - - rls_info = ReleaseInfo( - identifier = info, - value = toUnicode(nzb[info]) - ) - rls.info.append(rls_info) - except InterfaceError: - log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc())) - - db.commit() - - nzb['status_id'] = rls.status_id - - - for nzb in sorted_results: - if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and nzb.get('age') <= quality_type.get('wait_for', 0): - log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name'])) - continue - - if nzb['status_id'] in [ignored_status.get('id'), failed_status.get('id')]: - log.info('Ignored: %s', nzb['name']) - continue - - if nzb['score'] <= 0: - log.info('Ignored, score to low: %s', nzb['name']) - continue - - downloaded = self.download(data = nzb, movie = movie) - if downloaded is True: - ret = True - break - elif downloaded != 'try_next': - break - - # Remove releases that aren't found anymore - for release in movie.get('releases', []): - if release.get('status_id') == available_status.get('id') and release.get('identifier') not in found_releases: - 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', movie['id']) - break - - # Break if CP wants to shut down - if self.shuttingDown() or ret: - break - - if len(too_early_to_search) > 0: - log.info2('Too early to search for %s, %s', (too_early_to_search, default_title)) - - fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True) - - return ret - - def download(self, data, movie, manual = False): - - # Test to see if any downloaders are enabled for this type - downloader_enabled = fireEvent('download.enabled', manual, data, single = True) - - if downloader_enabled: - - snatched_status = fireEvent('status.get', 'snatched', single = True) - - # Download movie to temp - filedata = None - if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))): - filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id')) - if filedata == 'try_next': - return filedata - - download_result = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True) - log.debug('Downloader result: %s', download_result) - - if download_result: - try: - # Mark release as snatched - db = get_session() - rls = db.query(Release).filter_by(identifier = md5(data['url'])).first() - if rls: - renamer_enabled = Env.setting('enabled', 'renamer') - - done_status = fireEvent('status.get', 'done', single = True) - rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id') - - # Save download-id info if returned - if isinstance(download_result, dict): - for key in download_result: - rls_info = ReleaseInfo( - identifier = 'download_%s' % key, - value = toUnicode(download_result.get(key)) - ) - rls.info.append(rls_info) - db.commit() - - log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label) - snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie) - log.info(snatch_message) - fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict()) - - # If renamer isn't used, mark movie done - if not renamer_enabled: - active_status = fireEvent('status.get', 'active', single = True) - done_status = fireEvent('status.get', 'done', single = True) - try: - if movie['status_id'] == active_status.get('id'): - for profile_type in movie['profile']['types']: - if profile_type['quality_id'] == rls.quality.id and profile_type['finish']: - log.info('Renamer disabled, marking movie as finished: %s', log_movie) - - # Mark release done - rls.status_id = done_status.get('id') - rls.last_edit = int(time.time()) - db.commit() - - # Mark movie done - mvie = db.query(Movie).filter_by(id = movie['id']).first() - mvie.status_id = done_status.get('id') - mvie.last_edit = int(time.time()) - db.commit() - except: - log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc()) - - except: - log.error('Failed marking movie finished: %s', traceback.format_exc()) - - return True - - log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', (data.get('type', ''))) - - return False - - def getSearchTypes(self): - - download_types = fireEvent('download.enabled_types', merge = True) - provider_types = fireEvent('provider.enabled_types', merge = True) - - if download_types and len(list(set(provider_types) & set(download_types))) == 0: - log.error('There aren\'t any providers enabled for your downloader (%s). Check your settings.', ','.join(download_types)) - raise NoProviders - - for useless_provider in list(set(provider_types) - set(download_types)): - log.debug('Provider for "%s" enabled, but no downloader.', useless_provider) - - search_types = download_types - - if len(search_types) == 0: - log.error('There aren\'t any downloaders enabled. Please pick one in settings.') - raise NoDownloaders - - return search_types - - def correctMovie(self, nzb = None, movie = None, quality = None, **kwargs): - - imdb_results = kwargs.get('imdb_results', False) - retention = Env.setting('retention', section = 'nzb') - - if nzb.get('seeders') is None and 0 < retention < nzb.get('age', 0): - log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name'])) - return False - - movie_name = getTitle(movie['library']) - movie_words = re.split('\W+', simplifyString(movie_name)) - nzb_name = simplifyString(nzb['name']) - nzb_words = re.split('\W+', nzb_name) - - # Make sure it has required words - required_words = splitString(self.conf('required_words').lower()) - try: required_words = list(set(required_words + splitString(movie['category']['required'].lower()))) - except: pass - - req_match = 0 - for req_set in required_words: - req = splitString(req_set, '&') - req_match += len(list(set(nzb_words) & set(req))) == len(req) - - if len(required_words) > 0 and req_match == 0: - log.info2('Wrong: Required word missing: %s', nzb['name']) - return False - - # Ignore releases - ignored_words = splitString(self.conf('ignored_words').lower()) - try: ignored_words = list(set(ignored_words + splitString(movie['category']['ignored'].lower()))) - except: pass - - ignored_match = 0 - for ignored_set in ignored_words: - ignored = splitString(ignored_set, '&') - ignored_match += len(list(set(nzb_words) & set(ignored))) == len(ignored) - - if len(ignored_words) > 0 and ignored_match: - log.info2("Wrong: '%s' contains 'ignored words'", (nzb['name'])) - return False - - # Ignore porn stuff - pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic'] - pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words)) - if pron_words: - log.info('Wrong: %s, probably pr0n', (nzb['name'])) - return False - - preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True) - - # Contains lower quality string - if self.containsOtherQuality(nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality): - log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label'])) - return False - - - # File to small - if nzb['size'] and preferred_quality['size_min'] > nzb['size']: - log.info2('Wrong: "%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min'])) - return False - - # File to large - if nzb['size'] and preferred_quality.get('size_max') < nzb['size']: - log.info2('Wrong: "%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max'])) - return False - - - # Provider specific functions - get_more = nzb.get('get_more_info') - if get_more: - get_more(nzb) - - extra_check = nzb.get('extra_check') - if extra_check and not extra_check(nzb): - return False - - - if imdb_results: - return True - - # Check if nzb contains imdb link - if self.checkIMDB([nzb.get('description', '')], movie['library']['identifier']): - return True - - for raw_title in movie['library']['titles']: - for movie_title in possibleTitles(raw_title['title']): - movie_words = re.split('\W+', simplifyString(movie_title)) - - if self.correctName(nzb['name'], movie_title): - # if no IMDB link, at least check year range 1 - if len(movie_words) > 2 and self.correctYear([nzb['name']], movie['library']['year'], 1): - return True - - # if no IMDB link, at least check year - if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0): - return True - - log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], movie_name, movie['library']['year'])) - return False - - def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}): - - name = nzb['name'] - size = nzb.get('size', 0) - nzb_words = re.split('\W+', simplifyString(name)) - - qualities = fireEvent('quality.all', single = True) - - found = {} - for quality in qualities: - # Main in words - if quality['identifier'] in nzb_words: - found[quality['identifier']] = True - - # Alt in words - if list(set(nzb_words) & set(quality['alternative'])): - found[quality['identifier']] = True - - # Try guessing via quality tags - guess = fireEvent('quality.guess', [nzb.get('name')], single = True) - if guess: - found[guess['identifier']] = True - - # Hack for older movies that don't contain quality tag - year_name = fireEvent('scanner.name_year', name, single = True) - if len(found) == 0 and movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None): - if size > 3000: # Assume dvdr - log.info('Quality was missing in name, assuming it\'s a DVD-R based on the size: %s', (size)) - found['dvdr'] = True - else: # Assume dvdrip - log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', (size)) - found['dvdrip'] = True - - # Allow other qualities - for allowed in preferred_quality.get('allow'): - if found.get(allowed): - del found[allowed] - - return not (found.get(preferred_quality['identifier']) and len(found) == 1) - - def checkIMDB(self, haystack, imdbId): - - for string in haystack: - if 'imdb.com/title/' + imdbId in string: - return True - - return False - - def correctYear(self, haystack, year, year_range): - - for string in haystack: - - year_name = fireEvent('scanner.name_year', string, single = True) - - if year_name and ((year - year_range) <= year_name.get('year') <= (year + year_range)): - log.debug('Movie year matches range: %s looking for %s', (year_name.get('year'), year)) - return True - - log.debug('Movie year doesn\'t matche range: %s looking for %s', (year_name.get('year'), year)) - return False - - def correctName(self, check_name, movie_name): - - check_names = [check_name] - - # Match names between " - try: check_names.append(re.search(r'([\'"])[^\1]*\1', check_name).group(0)) - except: pass - - # Match longest name between [] - try: check_names.append(max(check_name.split('['), key = len)) - except: pass - - for check_name in list(set(check_names)): - check_movie = fireEvent('scanner.name_year', check_name, single = True) - - try: - check_words = filter(None, re.split('\W+', check_movie.get('name', ''))) - movie_words = filter(None, re.split('\W+', simplifyString(movie_name))) - - if len(check_words) > 0 and len(movie_words) > 0 and len(list(set(check_words) - set(movie_words))) == 0: - return True - except: - pass - - return False - - def couldBeReleased(self, is_pre_release, dates, year = None): - - now = int(time.time()) - now_year = date.today().year - - if (year is None or year < now_year - 1) and (not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0)): - return True - else: - - # For movies before 1972 - if dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0: - return True - - if is_pre_release: - # Prerelease 1 week before theaters - if dates.get('theater') - 604800 < now: - return True - else: - # 12 weeks after theater release - if dates.get('theater') > 0 and dates.get('theater') + 7257600 < now: - return True - - if dates.get('dvd') > 0: - - # 4 weeks before dvd release - if dates.get('dvd') - 2419200 < now: - return True - - # Dvd should be released - if dates.get('dvd') < now: - return True - - - return False - - def tryNextReleaseView(self, id = None, **kwargs): - - trynext = self.tryNextRelease(id) - - return { - 'success': trynext - } - - def tryNextRelease(self, movie_id, manual = False): - - snatched_status, ignored_status = fireEvent('status.get', ['snatched', 'ignored'], single = True) - - try: - db = get_session() - rels = db.query(Release).filter_by( - status_id = snatched_status.get('id'), - movie_id = movie_id - ).all() - - for rel in rels: - rel.status_id = ignored_status.get('id') - db.commit() - - movie_dict = fireEvent('movie.get', movie_id, single = True) - log.info('Trying next release for: %s', getTitle(movie_dict['library'])) - fireEvent('searcher.single', movie_dict) - - return True - - except: - log.error('Failed searching for next release: %s', traceback.format_exc()) - return False - -class SearchSetupError(Exception): - pass - -class NoDownloaders(SearchSetupError): - pass - -class NoProviders(SearchSetupError): - pass diff --git a/couchpotato/core/providers/base.py b/couchpotato/core/providers/base.py index cb7b16d..d7ac7d1 100644 --- a/couchpotato/core/providers/base.py +++ b/couchpotato/core/providers/base.py @@ -257,7 +257,7 @@ class ResultList(list): new_result = self.fillResult(result) - is_correct_movie = fireEvent('searcher.correct_movie', + is_correct_movie = fireEvent('movie.searcher.correct_movie', nzb = new_result, movie = self.movie, quality = self.quality, imdb_results = self.kwargs.get('imdb_results', False), single = True) diff --git a/couchpotato/static/scripts/page/wanted.js b/couchpotato/static/scripts/page/wanted.js index eabd146..41bd4bc 100644 --- a/couchpotato/static/scripts/page/wanted.js +++ b/couchpotato/static/scripts/page/wanted.js @@ -40,7 +40,7 @@ Page.Wanted = new Class({ if(!self.search_in_progress){ - Api.request('searcher.full_search'); + Api.request('movie.searcher.full_search'); self.startProgressInterval(); } @@ -53,7 +53,7 @@ Page.Wanted = new Class({ var start_text = self.manual_search.get('text'); self.progress_interval = setInterval(function(){ if(self.search_progress && self.search_progress.running) return; - self.search_progress = Api.request('searcher.progress', { + self.search_progress = Api.request('movie.searcher.progress', { 'onComplete': function(json){ self.search_in_progress = true; if(!json.progress){