diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py index 50cf91f..8e4eb8e 100755 --- a/couchpotato/core/helpers/variable.py +++ b/couchpotato/core/helpers/variable.py @@ -8,6 +8,7 @@ import re import string import sys import traceback +import time from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss, sp from couchpotato.core.logger import CPLog @@ -411,3 +412,7 @@ def find(func, iterable): return item return None + +def strtotime(string, format): + timestamp = time.strptime(string, format) + return time.mktime(timestamp) diff --git a/couchpotato/core/media/_base/searcher/main.py b/couchpotato/core/media/_base/searcher/main.py index 6d0b684..19100e7 100755 --- a/couchpotato/core/media/_base/searcher/main.py +++ b/couchpotato/core/media/_base/searcher/main.py @@ -1,12 +1,15 @@ import datetime import re +import time +from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import simplifyString -from couchpotato.core.helpers.variable import splitString, removeEmpty, removeDuplicate +from couchpotato.core.helpers.variable import splitString, removeEmpty, removeDuplicate, getTitle, tryInt from couchpotato.core.logger import CPLog from couchpotato.core.media._base.searcher.base import SearcherBase +from couchpotato.environment import Env log = CPLog(__name__) @@ -16,12 +19,6 @@ class Searcher(SearcherBase): # noinspection PyMissingConstructor def __init__(self): - addEvent('searcher.protocols', self.getSearchProtocols) - addEvent('searcher.contains_other_quality', self.containsOtherQuality) - addEvent('searcher.correct_3d', self.correct3D) - addEvent('searcher.correct_year', self.correctYear) - addEvent('searcher.correct_name', self.correctName) - addEvent('searcher.correct_words', self.correctWords) addEvent('searcher.search', self.search) addApiView('searcher.full_search', self.searchAllView, docs = { @@ -224,5 +221,168 @@ class Searcher(SearcherBase): return True + def correctRelease(self, nzb = None, media = None, quality = None, **kwargs): + raise NotImplementedError + + def couldBeReleased(self, is_pre_release, dates, media): + raise NotImplementedError + + def getTitle(self, media): + return getTitle(media) + + def getProfileId(self, media): + # Required because the profile_id for an show episode is stored with + # the show, not the episode. + raise NotImplementedError + + def single(self, media, search_protocols = None, manual = False, force_download = False, notify = True): + + # Find out search type + try: + if not search_protocols: + search_protocols = self.getSearchProtocols() + except SearchSetupError: + return + + db = get_db() + profile = db.get('id', self.getProfileId(media)) + + if not profile or (media['status'] == 'done' and not manual): + log.debug('Media does not have a profile or already done, assuming in manage tab.') + fireEvent('media.restatus', media['_id'], single = True) + return + + default_title = self.getTitle(media) + if not default_title: + log.error('No proper info found for media, removing it from library to stop it from causing more issues.') + fireEvent('media.delete', media['_id'], single = True) + return + + # Update media status and check if it is still not done (due to the stop searching after feature + if fireEvent('media.restatus', media['_id'], single = True) == 'done': + log.debug('No better quality found, marking media %s as done.', default_title) + + pre_releases = fireEvent('quality.pre_releases', single = True) + release_dates = fireEvent('media.update_release_dates', media['_id'], merge = True) + + found_releases = [] + previous_releases = media.get('releases', []) + too_early_to_search = [] + outside_eta_results = 0 + always_search = self.conf('always_search') + ignore_eta = manual + total_result_count = 0 + + if notify: + fireEvent('notify.frontend', type = '%s.searcher.started' % self._type, data = {'_id': media['_id']}, message = 'Searching for "%s"' % default_title) + + # Ignore eta once every 7 days + if not always_search: + prop_name = 'last_ignored_eta.%s' % media['_id'] + last_ignored_eta = float(Env.prop(prop_name, default = 0)) + if last_ignored_eta < time.time() - 604800: + ignore_eta = True + Env.prop(prop_name, value = time.time()) + + ret = False + + for index, q_identifier in enumerate(profile.get('qualities', [])): + quality_custom = { + 'index': index, + 'quality': q_identifier, + 'finish': profile['finish'][index], + 'wait_for': tryInt(profile['wait_for'][index]), + '3d': profile['3d'][index] if profile.get('3d') else False, + 'minimum_score': profile.get('minimum_score', 1), + } + + could_not_be_released = not self.couldBeReleased(q_identifier in pre_releases, release_dates, media) + if not always_search and could_not_be_released: + too_early_to_search.append(q_identifier) + + # Skip release, if ETA isn't ignored + if not ignore_eta: + continue + + has_better_quality = 0 + + # See if better quality is available + for release in media.get('releases', []): + 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 > 0: + log.info('Better quality (%s) already available or snatched for %s', (q_identifier, default_title)) + fireEvent('media.restatus', media['_id'], single = True) + break + + quality = fireEvent('quality.single', identifier = q_identifier, single = True) + log.info('Search for %s in %s%s', (default_title, quality['label'], ' ignoring ETA' if always_search or ignore_eta else '')) + + # Extend quality with profile customs + quality['custom'] = quality_custom + + results = fireEvent('searcher.search', search_protocols, media, quality, single = True) or [] + + # Check if media isn't deleted while searching + if not fireEvent('media.get', media.get('_id'), single = True): + break + + # Add them to this media releases list + found_releases += fireEvent('release.create_from_search', results, media, quality, single = True) + results_count = len(found_releases) + total_result_count += results_count + if results_count == 0: + log.debug('Nothing found for %s in %s', (default_title, quality['label'])) + + # Keep track of releases found outside ETA window + outside_eta_results += results_count if could_not_be_released else 0 + + # Don't trigger download, but notify user of available releases + if could_not_be_released and results_count > 0: + log.debug('Found %s releases for "%s", but ETA isn\'t correct yet.', (results_count, default_title)) + + # Try find a valid result and download it + if (force_download or not could_not_be_released or always_search) and fireEvent('release.try_download_result', results, media, quality_custom, single = True): + ret = True + + # Remove releases that aren't found anymore + temp_previous_releases = [] + for release in previous_releases: + if release.get('status') == 'available' and release.get('identifier') not in found_releases: + fireEvent('release.delete', release.get('_id'), single = True) + else: + temp_previous_releases.append(release) + previous_releases = temp_previous_releases + del temp_previous_releases + + # Break if CP wants to shut down + if self.shuttingDown() or ret: + break + + if total_result_count > 0: + fireEvent('media.tag', media['_id'], 'recent', update_edited = True, single = True) + + if len(too_early_to_search) > 0: + log.info2('Too early to search for %s, %s', (too_early_to_search, default_title)) + + if outside_eta_results > 0: + message = 'Found %s releases for "%s" before ETA. Select and download via the dashboard.' % (outside_eta_results, default_title) + log.info(message) + + if not manual: + fireEvent('media.available', message = message, data = {}) + + if notify: + fireEvent('notify.frontend', type = '%s.searcher.ended' % self._type, data = {'_id': media['_id']}) + + return ret + class SearchSetupError(Exception): pass diff --git a/couchpotato/core/media/movie/quality/main.py b/couchpotato/core/media/movie/quality/main.py index d469a33..a588233 100644 --- a/couchpotato/core/media/movie/quality/main.py +++ b/couchpotato/core/media/movie/quality/main.py @@ -3,8 +3,9 @@ import re from couchpotato import CPLog from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import ss -from couchpotato.core.helpers.variable import getExt, splitString, tryInt +from couchpotato.core.helpers.variable import getExt, splitString, tryFloat from couchpotato.core.media._base.quality.base import QualityBase +from math import ceil, fabs log = CPLog(__name__) @@ -43,7 +44,8 @@ class MovieQuality(QualityBase): # Create hash for cache cache_key = str([f.replace('.' + getExt(f), '') if len(getExt(f)) < 4 else f for f in files]) - if use_cache: + #if use_cache: + if True: cached = self.getCache(cache_key) if cached and len(extra) == 0: return cached @@ -213,11 +215,14 @@ class MovieQuality(QualityBase): size_diff = size - size_min size_proc = (size_diff / proc_range) - median_diff = quality['median_size'] - size_min - median_proc = (median_diff / proc_range) + #median_diff = quality['median_size'] - size_min + # FIXME: not sure this is the proper fix + average_diff = ((size_min + size_max) / 2) - size_min + average_proc = (average_diff / proc_range) max_points = 8 - score += ceil(max_points - (fabs(size_proc - median_proc) * max_points)) + #score += ceil(max_points - (fabs(size_proc - median_proc) * max_points)) + score += ceil(max_points - (fabs(size_proc - average_proc) * max_points)) else: score -= 5 diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index 2fd7700..e328146 100755 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -4,13 +4,13 @@ import re import time import traceback -from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.helpers.encoding import simplifyString from couchpotato.core.helpers.variable import getTitle, possibleTitles, getImdb, getIdentifier, tryInt from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.searcher.base import SearcherBase +from couchpotato.core.media._base.searcher.main import Searcher +from couchpotato.core.media._base.searcher.main import SearchSetupError from couchpotato.core.media.movie import MovieTypeBase from couchpotato.environment import Env @@ -20,7 +20,7 @@ log = CPLog(__name__) autoload = 'MovieSearcher' -class MovieSearcher(SearcherBase, MovieTypeBase): +class MovieSearcher(Searcher, MovieTypeBase): in_progress = False @@ -110,153 +110,6 @@ class MovieSearcher(SearcherBase, MovieTypeBase): self.in_progress = False - def single(self, movie, search_protocols = None, manual = False, force_download = False): - - # Find out search type - try: - if not search_protocols: - search_protocols = fireEvent('searcher.protocols', single = True) - except SearchSetupError: - return - - if not movie['profile_id'] or (movie['status'] == 'done' and not manual): - log.debug('Movie doesn\'t have a profile or already done, assuming in manage tab.') - fireEvent('media.restatus', movie['_id'], single = True) - return - - default_title = getTitle(movie) - if not default_title: - log.error('No proper info found for movie, removing it from library to stop it from causing more issues.') - fireEvent('media.delete', movie['_id'], single = True) - return - - # Update media status and check if it is still not done (due to the stop searching after feature - if fireEvent('media.restatus', movie['_id'], single = True) == 'done': - log.debug('No better quality found, marking movie %s as done.', default_title) - - pre_releases = fireEvent('quality.pre_releases', single = True) - release_dates = fireEvent('movie.update_release_dates', movie['_id'], merge = True) - - found_releases = [] - previous_releases = movie.get('releases', []) - too_early_to_search = [] - outside_eta_results = 0 - always_search = self.conf('always_search') - ignore_eta = manual - total_result_count = 0 - - fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'_id': movie['_id']}, message = 'Searching for "%s"' % default_title) - - # Ignore eta once every 7 days - if not always_search: - prop_name = 'last_ignored_eta.%s' % movie['_id'] - last_ignored_eta = float(Env.prop(prop_name, default = 0)) - if last_ignored_eta < time.time() - 604800: - ignore_eta = True - Env.prop(prop_name, value = time.time()) - - db = get_db() - - profile = db.get('id', movie['profile_id']) - ret = False - - for index, q_identifier in enumerate(profile.get('qualities', [])): - quality_custom = { - 'index': index, - 'quality': q_identifier, - 'finish': profile['finish'][index], - 'wait_for': tryInt(profile['wait_for'][index]), - '3d': profile['3d'][index] if profile.get('3d') else False, - 'minimum_score': profile.get('minimum_score', 1), - } - - could_not_be_released = not self.couldBeReleased(q_identifier in pre_releases, release_dates, movie['info']['year']) - if not always_search and could_not_be_released: - too_early_to_search.append(q_identifier) - - # Skip release, if ETA isn't ignored - if not ignore_eta: - continue - - has_better_quality = 0 - - # See if better quality is available - for release in movie.get('releases', []): - 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 > 0: - log.info('Better quality (%s) already available or snatched for %s', (q_identifier, default_title)) - fireEvent('media.restatus', movie['_id'], single = True) - break - - quality = fireEvent('quality.single', identifier = q_identifier, single = True) - log.info('Search for %s in %s%s', (default_title, quality['label'], ' ignoring ETA' if always_search or ignore_eta else '')) - - # Extend quality with profile customs - quality['custom'] = quality_custom - - results = fireEvent('searcher.search', search_protocols, movie, quality, single = True) or [] - - # Check if movie isn't deleted while searching - if not fireEvent('media.get', movie.get('_id'), single = True): - break - - # Add them to this movie releases list - found_releases += fireEvent('release.create_from_search', results, movie, quality, single = True) - results_count = len(found_releases) - total_result_count += results_count - if results_count == 0: - log.debug('Nothing found for %s in %s', (default_title, quality['label'])) - - # Keep track of releases found outside ETA window - outside_eta_results += results_count if could_not_be_released else 0 - - # Don't trigger download, but notify user of available releases - if could_not_be_released and results_count > 0: - log.debug('Found %s releases for "%s", but ETA isn\'t correct yet.', (results_count, default_title)) - - # Try find a valid result and download it - if (force_download or not could_not_be_released or always_search) and fireEvent('release.try_download_result', results, movie, quality_custom, single = True): - ret = True - - # Remove releases that aren't found anymore - temp_previous_releases = [] - for release in previous_releases: - if release.get('status') == 'available' and release.get('identifier') not in found_releases: - fireEvent('release.delete', release.get('_id'), single = True) - else: - temp_previous_releases.append(release) - previous_releases = temp_previous_releases - del temp_previous_releases - - # Break if CP wants to shut down - if self.shuttingDown() or ret: - break - - if total_result_count > 0: - fireEvent('media.tag', movie['_id'], 'recent', update_edited = True, single = True) - - if len(too_early_to_search) > 0: - log.info2('Too early to search for %s, %s', (too_early_to_search, default_title)) - - if outside_eta_results > 0: - message = 'Found %s releases for "%s" before ETA. Select and download via the dashboard.' % (outside_eta_results, default_title) - log.info(message) - - if not manual: - fireEvent('media.available', message = message, data = {}) - - fireEvent('notify.frontend', type = 'movie.searcher.ended', data = {'_id': movie['_id']}) - - return ret - def correctRelease(self, nzb = None, media = None, quality = None, **kwargs): if media.get('type') != 'movie': return @@ -271,19 +124,23 @@ class MovieSearcher(SearcherBase, MovieTypeBase): return False # Check for required and ignored words - if not fireEvent('searcher.correct_words', nzb['name'], media, single = True): + if not self.correctWords(nzb['name'], media): 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', nzb, movie_year = media['info']['year'], preferred_quality = preferred_quality, types = [self._type], single = True) + contains_other = self.containsOtherQuality( + nzb, movie_year = media['info']['year'], + preferred_quality = preferred_quality, + types = [self._type]) 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, types = [self._type], single = True): + # FIXME: media was passed instead of nzb here before + if not self.correct3D(nzb, preferred_quality = preferred_quality, types = [self._type]): log.info2('Wrong: %s, %slooking for %s in 3D', (nzb['name'], ('' if preferred_quality['custom'].get('3d') else 'NOT '), quality['label'])) return False @@ -318,23 +175,24 @@ class MovieSearcher(SearcherBase, MovieTypeBase): for movie_title in possibleTitles(raw_title): movie_words = re.split('\W+', simplifyString(movie_title)) - if fireEvent('searcher.correct_name', nzb['name'], movie_title, single = True): + if self.correctName(nzb['name'], movie_title): # if no IMDB link, at least check year range 1 - if len(movie_words) > 2 and fireEvent('searcher.correct_year', nzb['name'], media['info']['year'], 1, single = True): + if len(movie_words) > 2 and self.correctYear(nzb['name'], media['info']['year'], 1): return True # if no IMDB link, at least check year - if len(movie_words) <= 2 and fireEvent('searcher.correct_year', nzb['name'], media['info']['year'], 0, single = True): + if len(movie_words) <= 2 and self.correctYear(nzb['name'], media['info']['year'], 0): return True log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], media_title, media['info']['year'])) return False - def couldBeReleased(self, is_pre_release, dates, year = None): + def couldBeReleased(self, is_pre_release, dates, media): now = int(time.time()) now_year = date.today().year now_month = date.today().month + year = media['info']['year'] if (year is None or year < now_year - 1 or (year <= now_year - 1 and now_month > 4)) and (not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0)): return True @@ -405,9 +263,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase): if media['type'] == 'movie': return getTitle(media) -class SearchSetupError(Exception): - pass + def getProfileId(self, media): + assert media['type'] == 'movie' + return media.get('profile_id') config = [{ 'name': 'moviesearcher', diff --git a/couchpotato/core/media/show/matcher/base.py b/couchpotato/core/media/show/matcher/base.py index 488b191..c4ba00e 100755 --- a/couchpotato/core/media/show/matcher/base.py +++ b/couchpotato/core/media/show/matcher/base.py @@ -40,8 +40,21 @@ class Base(MatcherBase): if len(value) <= 1: value = value[0] else: - log.warning('Wrong: identifier contains multiple season or episode values, unsupported') - return None + # It might contain multiple season or episode values, but + # there's a chance that it contains the same identifier + # multiple times. + x, y = None, None + for y in value: + y = tryInt(y, None) + if x is None: + x = y + elif x is None or y is None or x != y: + break + if x is not None and y is not None and x == y: + value = value[0] + else: + log.warning('Wrong: identifier contains multiple season or episode values, unsupported: %s' % repr(value)) + return None identifier[key] = tryInt(value, value) diff --git a/couchpotato/core/media/show/providers/info/thetvdb.py b/couchpotato/core/media/show/providers/info/thetvdb.py index e1d749f..cef6766 100755 --- a/couchpotato/core/media/show/providers/info/thetvdb.py +++ b/couchpotato/core/media/show/providers/info/thetvdb.py @@ -129,6 +129,7 @@ class TheTVDb(ShowProvider): season_number = int(season_number) except: return None + identifier = tryInt(identifier) cache_key = 'thetvdb.cache.%s.%s.%s' % (identifier, episode_identifier, season_number) log.debug('Getting EpisodeInfo: %s', cache_key) result = self.getCache(cache_key) or {} @@ -136,7 +137,7 @@ class TheTVDb(ShowProvider): return result try: - show = self.tvdb[int(identifier)] + show = self.tvdb[identifier] except (tvdb_exceptions.tvdb_error, IOError), e: log.error('Failed parsing TheTVDB EpisodeInfo for "%s" id "%s": %s', (show, identifier, traceback.format_exc())) return False @@ -263,9 +264,12 @@ class TheTVDb(ShowProvider): except: pass + identifier = tryInt( + show['id'] if show.get('id') else show[number][1]['seasonid']) + season_data = { 'identifiers': { - 'thetvdb': show['id'] if show.get('id') else show[number][1]['seasonid'] + 'thetvdb': identifier }, 'number': tryInt(number), 'images': { diff --git a/couchpotato/core/media/show/providers/torrent/thepiratebay.py b/couchpotato/core/media/show/providers/torrent/thepiratebay.py index 2a7e084..b648fe0 100644 --- a/couchpotato/core/media/show/providers/torrent/thepiratebay.py +++ b/couchpotato/core/media/show/providers/torrent/thepiratebay.py @@ -25,7 +25,7 @@ class Season(SeasonProvider, Base): def buildUrl(self, media, page, cats): return ( - tryUrlencode('"%s"' % fireEvent('media.search_query', media, single = True)), + tryUrlencode('"%s"' % fireEvent('library.query', media, single = True)), page, ','.join(str(x) for x in cats) ) @@ -40,7 +40,7 @@ class Episode(EpisodeProvider, Base): def buildUrl(self, media, page, cats): return ( - tryUrlencode('"%s"' % fireEvent('media.search_query', media, single = True)), + tryUrlencode('"%s"' % fireEvent('library.query', media, single = True)), page, ','.join(str(x) for x in cats) ) diff --git a/couchpotato/core/media/show/searcher/episode.py b/couchpotato/core/media/show/searcher/episode.py index ade2165..9c494b0 100755 --- a/couchpotato/core/media/show/searcher/episode.py +++ b/couchpotato/core/media/show/searcher/episode.py @@ -1,17 +1,20 @@ +import time + from couchpotato import fireEvent, get_db, Env from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEventAsync from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.searcher.base import SearcherBase +from couchpotato.core.media._base.searcher.main import Searcher from couchpotato.core.media._base.searcher.main import SearchSetupError from couchpotato.core.media.show import ShowTypeBase +from couchpotato.core.helpers.variable import strtotime log = CPLog(__name__) autoload = 'EpisodeSearcher' -class EpisodeSearcher(SearcherBase, ShowTypeBase): +class EpisodeSearcher(Searcher, ShowTypeBase): type = 'episode' in_progress = False @@ -47,91 +50,6 @@ class EpisodeSearcher(SearcherBase, ShowTypeBase): 'result': fireEvent('%s.searcher.single' % self.getType(), media, single = True) } - def single(self, media, profile = None, search_protocols = None, manual = False): - db = get_db() - - related = fireEvent('library.related', media, single = True) - - # TODO search_protocols, profile, quality_order can be moved to a base method - # Find out search type - try: - if not search_protocols: - search_protocols = fireEvent('searcher.protocols', single = True) - except SearchSetupError: - return - - if not profile and related['show']['profile_id']: - profile = db.get('id', related['show']['profile_id']) - - # 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']): - # too_early_to_search.append(quality_type['quality']['identifier']) - # return - - ret = False - has_better_quality = None - found_releases = [] - too_early_to_search = [] - - releases = fireEvent('release.for_media', media['_id'], single = True) - query = fireEvent('library.query', media, condense = False, single = True) - - index = 0 - for q_identifier in profile.get('qualities'): - quality_custom = { - 'quality': q_identifier, - 'finish': profile['finish'][index], - 'wait_for': profile['wait_for'][index], - '3d': profile['3d'][index] if profile.get('3d') else False - } - - has_better_quality = 0 - - # See if better quality is available - for release in releases: - 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, types = ['show'], single = True) - quality['custom'] = quality_custom - - results = fireEvent('searcher.search', search_protocols, media, quality, single = True) - if len(results) == 0: - log.debug('Nothing found for %s in %s', (query, q_identifier)) - - # Add them to this movie releases list - found_releases += fireEvent('release.create_from_search', results, media, quality, single = True) - - # Try find a valid result and download it - if fireEvent('release.try_download_result', results, media, quality, single = True): - ret = True - - # Remove releases that aren't found anymore - for release in releases: - if release.get('status') == 'available' 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', (q_identifier, query)) - fireEvent('media.restatus', media['_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, query)) - def correctRelease(self, release = None, media = None, quality = None, **kwargs): if media.get('type') != 'show.episode': return @@ -142,13 +60,13 @@ class EpisodeSearcher(SearcherBase, ShowTypeBase): return False # Check for required and ignored words - if not fireEvent('searcher.correct_words', release['name'], media, single = True): + if not self.correctWords(release['name'], media): 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) + contains_other = self.containsOtherQuality(release, preferred_quality = preferred_quality, types= [self._type]) 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 @@ -159,3 +77,33 @@ class EpisodeSearcher(SearcherBase, ShowTypeBase): return match.weight return False + + def couldBeReleased(self, is_pre_release, dates, media): + """ + Determine if episode could have aired by now + + @param is_pre_release: True if quality is pre-release, otherwise False. Ignored for episodes. + @param dates: + @param media: media dictionary to retrieve episode air date from. + @return: dict, with media + """ + now = time.time() + released = strtotime(media.get('info', {}).get('released'), '%Y-%m-%d') + + if (released < now): + return True + + return False + + def getProfileId(self, media): + assert media and media['type'] == 'show.episode' + + profile_id = None + + related = fireEvent('library.related', media, single = True) + if related: + show = related.get('show') + if show: + profile_id = show.get('profile_id') + + return profile_id diff --git a/couchpotato/core/media/show/searcher/season.py b/couchpotato/core/media/show/searcher/season.py index 17fc170..9ca6d4d 100755 --- a/couchpotato/core/media/show/searcher/season.py +++ b/couchpotato/core/media/show/searcher/season.py @@ -2,16 +2,17 @@ from couchpotato import get_db, Env from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEventAsync, fireEvent from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.searcher.base import SearcherBase +from couchpotato.core.media._base.searcher.main import Searcher from couchpotato.core.media.movie.searcher import SearchSetupError from couchpotato.core.media.show import ShowTypeBase +from couchpotato.core.helpers.variable import getTitle log = CPLog(__name__) autoload = 'SeasonSearcher' -class SeasonSearcher(SearcherBase, ShowTypeBase): +class SeasonSearcher(Searcher, ShowTypeBase): type = 'season' in_progress = False @@ -37,120 +38,36 @@ class SeasonSearcher(SearcherBase, ShowTypeBase): def searchAll(self, manual = False): pass - def single(self, media, profile = None, search_protocols = None, manual = False): - db = get_db() + def single(self, media, search_protocols = None, manual = False, force_download = False, notify = True): + # The user can prefer episode releases over season releases. + prefer_episode_releases = self.conf('prefer_episode_releases') + + episodes = [] + all_episodes_available = self.couldBeReleased(False, [], media) + + event_type = 'show.season.searcher.started' related = fireEvent('library.related', media, single = True) + default_title = getTitle(related.get('show')) + fireEvent('notify.frontend', type = event_type, data = {'_id': media['_id']}, message = 'Searching for "%s"' % default_title) - # TODO search_protocols, profile, quality_order can be moved to a base method - # Find out search type - try: - if not search_protocols: - search_protocols = fireEvent('searcher.protocols', single = True) - except SearchSetupError: - return + result = False + if not all_episodes_available or prefer_episode_releases: + result = True + for episode in episodes: + if not fireEvent('show.episode.searcher.single', episode, search_protocols, manual, force_download, False): + result = False + break + + if not result and all_episodes_available: + # The user might have preferred episode releases over season + # releases, but that did not work out, fallback to season releases. + result = super(SeasonSearcher, self).single(media, search_protocols, manual, force_download, False) - if not profile and related['show']['profile_id']: - profile = db.get('id', related['show']['profile_id']) - - # Find 'active' episodes - episodes = related['episodes'] - episodes_active = [] - - for episode in episodes: - if episode.get('status') != 'active': - continue - - episodes_active.append(episode) - - if len(episodes_active) == len(episodes): - # All episodes are 'active', try and search for full season - if self.search(media, profile, search_protocols): - # Success, end season search - return True - else: - log.info('Unable to find season pack, searching for individual episodes...') - - # Search for each episode individually - for episode in episodes_active: - 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, 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']): - # too_early_to_search.append(quality_type['quality']['identifier']) - # return - - ret = False - has_better_quality = None - found_releases = [] - too_early_to_search = [] - - releases = fireEvent('release.for_media', media['_id'], single = True) - query = fireEvent('library.query', media, condense = False, single = True) - - index = 0 - for q_identifier in profile.get('qualities'): - quality_custom = { - 'quality': q_identifier, - 'finish': profile['finish'][index], - 'wait_for': profile['wait_for'][index], - '3d': profile['3d'][index] if profile.get('3d') else False - } - - has_better_quality = 0 - - # See if better quality is available - for release in releases: - 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['custom'] = quality_custom - - results = fireEvent('searcher.search', search_protocols, media, quality, single = True) - if len(results) == 0: - log.debug('Nothing found for %s in %s', (query, q_identifier)) - - # Add them to this movie releases list - found_releases += fireEvent('release.create_from_search', results, media, quality, single = True) - - # Try find a valid result and download it - if fireEvent('release.try_download_result', results, media, quality, single = True): - ret = True - - # Remove releases that aren't found anymore - for release in releases: - if release.get('status') == 'available' 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', (q_identifier, query)) - fireEvent('media.restatus', media['_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, query)) - - return len(found_releases) > 0 + event_type = 'show.season.searcher.ended' + fireEvent('notify.frontend', type = event_type, data = {'_id': media['_id']}) + + return result def correctRelease(self, release = None, media = None, quality = None, **kwargs): if media.get('type') != 'show.season': @@ -163,13 +80,13 @@ class SeasonSearcher(SearcherBase, ShowTypeBase): return False # Check for required and ignored words - if not fireEvent('searcher.correct_words', release['name'], media, single = True): + if not self.correctWords(release['name'], media): 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) + contains_other = self.containsOtherQuality(release, preferred_quality = preferred_quality, types = [self._type]) 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 @@ -180,3 +97,41 @@ class SeasonSearcher(SearcherBase, ShowTypeBase): return match.weight return False + + def couldBeReleased(self, is_pre_release, dates, media): + episodes = [] + all_episodes_available = True + + related = fireEvent('library.related', media, single = True) + if related: + for episode in related.get('episodes', []): + if episode.get('status') == 'active': + episodes.append(episode) + else: + all_episodes_available = False + if not episodes: + all_episodes_available = False + + return all_episodes_available + + def getTitle(self, media): + # FIXME: Season media type should have a title. + # e.g. Season + title = None + related = fireEvent('library.related', media, single = True) + if related: + title = getTitle(related.get('show')) + return title + + def getProfileId(self, media): + assert media and media['type'] == 'show.season' + + profile_id = None + + related = fireEvent('library.related', media, single = True) + if related: + show = related.get('show') + if show: + profile_id = show.get('profile_id') + + return profile_id diff --git a/couchpotato/core/media/show/searcher/show.py b/couchpotato/core/media/show/searcher/show.py index de2f436..38b81cc 100755 --- a/couchpotato/core/media/show/searcher/show.py +++ b/couchpotato/core/media/show/searcher/show.py @@ -3,7 +3,7 @@ from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, addEvent, fireEventAsync from couchpotato.core.helpers.variable import getTitle from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.searcher.base import SearcherBase +from couchpotato.core.media._base.searcher.main import Searcher from couchpotato.core.media._base.searcher.main import SearchSetupError from couchpotato.core.media.show import ShowTypeBase @@ -12,7 +12,7 @@ log = CPLog(__name__) autoload = 'ShowSearcher' -class ShowSearcher(SearcherBase, ShowTypeBase): +class ShowSearcher(Searcher, ShowTypeBase): type = 'show' in_progress = False @@ -38,50 +38,56 @@ class ShowSearcher(SearcherBase, ShowTypeBase): def searchAll(self, manual = False): pass - def single(self, media, search_protocols = None, manual = False): - # Find out search type - try: - if not search_protocols: - search_protocols = fireEvent('searcher.protocols', single = True) - except SearchSetupError: - return + def single(self, media, search_protocols = None, manual = False, force_download = False, notify = True): + + db = get_db() + profile = db.get('id', media['profile_id']) - if not media['profile_id'] or media['status'] == 'done': - log.debug('Show doesn\'t have a profile or already done, assuming in manage tab.') + if not profile or (media['status'] == 'done' and not manual): + log.debug('Media does not have a profile or already done, assuming in manage tab.') + fireEvent('media.restatus', media['_id'], single = True) return - show_title = fireEvent('media.search_query', media, condense = False, single = True) + default_title = getTitle(media) + if not default_title: + log.error('No proper info found for media, removing it from library to stop it from causing more issues.') + fireEvent('media.delete', media['_id'], single = True) + return - fireEvent('notify.frontend', type = 'show.searcher.started.%s' % media['_id'], data = True, message = 'Searching for "%s"' % show_title) + fireEvent('notify.frontend', type = 'show.searcher.started.%s' % media['_id'], data = True, message = 'Searching for "%s"' % default_title) - show_tree = fireEvent('library.tree', media, single = True) + seasons = [] - db = get_db() + tree = fireEvent('library.tree', media, single = True) + if tree: + for season in tree.get('seasons', []): + if season.get('info'): + continue - profile = db.get('id', media['profile_id']) + # Skip specials (and seasons missing 'number') for now + # TODO: set status for specials to skipped by default + if not season['info'].get('number'): + continue - for season in show_tree.get('seasons', []): - if not season.get('info'): - continue + seasons.append(season) - # Skip specials (and seasons missing 'number') for now - # TODO: set status for specials to skipped by default - if not season['info'].get('number'): - continue - - # Check if full season can be downloaded - fireEvent('show.season.searcher.single', season, profile, search_protocols, manual) - - # TODO (testing) only snatch one season - return + result = True + for season in seasons: + if not fireEvent('show.season.searcher.single', search_protocols, manual, force_download, False): + result = False + break fireEvent('notify.frontend', type = 'show.searcher.ended.%s' % media['_id'], data = True) + return result + def getSearchTitle(self, media): - if media.get('type') != 'show': + show = None + if media.get('type') == 'show': + show = media + elif media.get('type') in ('show.season', 'show.episode'): related = fireEvent('library.related', media, single = True) show = related['show'] - else: - show = media - return getTitle(show) + if show: + return getTitle(show) diff --git a/couchpotato/core/plugins/dashboard.py b/couchpotato/core/plugins/dashboard.py index 0f23b5a..b170ad5 100755 --- a/couchpotato/core/plugins/dashboard.py +++ b/couchpotato/core/plugins/dashboard.py @@ -76,9 +76,10 @@ class Dashboard(Plugin): coming_soon = False # Theater quality - if pp.get('theater') and fireEvent('movie.searcher.could_be_released', True, eta, media['info']['year'], single = True): + event = '%s.searcher.could_be_released' % (media.get('type')) + if pp.get('theater') and fireEvent(event, True, eta, media, single = True): coming_soon = 'theater' - elif pp.get('dvd') and fireEvent('movie.searcher.could_be_released', False, eta, media['info']['year'], single = True): + elif pp.get('dvd') and fireEvent(event, False, eta, media, single = True): coming_soon = 'dvd' if coming_soon: