Browse Source

[TV][Searcher] Merge show and movie searcher code

pull/5188/head
Jeroen Koekkoek 10 years ago
parent
commit
c9faf31ee8
  1. 5
      couchpotato/core/helpers/variable.py
  2. 174
      couchpotato/core/media/_base/searcher/main.py
  3. 15
      couchpotato/core/media/movie/quality/main.py
  4. 177
      couchpotato/core/media/movie/searcher.py
  5. 17
      couchpotato/core/media/show/matcher/base.py
  6. 8
      couchpotato/core/media/show/providers/info/thetvdb.py
  7. 4
      couchpotato/core/media/show/providers/torrent/thepiratebay.py
  8. 126
      couchpotato/core/media/show/searcher/episode.py
  9. 183
      couchpotato/core/media/show/searcher/season.py
  10. 72
      couchpotato/core/media/show/searcher/show.py
  11. 5
      couchpotato/core/plugins/dashboard.py

5
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)

174
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

15
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

177
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',

17
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)

8
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': {

4
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)
)

126
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

183
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. <Show> Season <Number>
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

72
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)

5
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:

Loading…
Cancel
Save