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 string
import sys import sys
import traceback import traceback
import time
from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss, sp from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss, sp
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
@ -411,3 +412,7 @@ def find(func, iterable):
return item return item
return None 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 datetime
import re import re
import time
from couchpotato import get_db
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import simplifyString 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.logger import CPLog
from couchpotato.core.media._base.searcher.base import SearcherBase from couchpotato.core.media._base.searcher.base import SearcherBase
from couchpotato.environment import Env
log = CPLog(__name__) log = CPLog(__name__)
@ -16,12 +19,6 @@ class Searcher(SearcherBase):
# noinspection PyMissingConstructor # noinspection PyMissingConstructor
def __init__(self): 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) addEvent('searcher.search', self.search)
addApiView('searcher.full_search', self.searchAllView, docs = { addApiView('searcher.full_search', self.searchAllView, docs = {
@ -224,5 +221,168 @@ class Searcher(SearcherBase):
return True 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): class SearchSetupError(Exception):
pass pass

15
couchpotato/core/media/movie/quality/main.py

@ -3,8 +3,9 @@ import re
from couchpotato import CPLog from couchpotato import CPLog
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import ss 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 couchpotato.core.media._base.quality.base import QualityBase
from math import ceil, fabs
log = CPLog(__name__) log = CPLog(__name__)
@ -43,7 +44,8 @@ class MovieQuality(QualityBase):
# Create hash for cache # Create hash for cache
cache_key = str([f.replace('.' + getExt(f), '') if len(getExt(f)) < 4 else f for f in files]) 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) cached = self.getCache(cache_key)
if cached and len(extra) == 0: if cached and len(extra) == 0:
return cached return cached
@ -213,11 +215,14 @@ class MovieQuality(QualityBase):
size_diff = size - size_min size_diff = size - size_min
size_proc = (size_diff / proc_range) size_proc = (size_diff / proc_range)
median_diff = quality['median_size'] - size_min #median_diff = quality['median_size'] - size_min
median_proc = (median_diff / proc_range) # 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 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: else:
score -= 5 score -= 5

177
couchpotato/core/media/movie/searcher.py

@ -4,13 +4,13 @@ import re
import time import time
import traceback import traceback
from couchpotato import get_db
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import simplifyString from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.helpers.variable import getTitle, possibleTitles, getImdb, getIdentifier, tryInt from couchpotato.core.helpers.variable import getTitle, possibleTitles, getImdb, getIdentifier, tryInt
from couchpotato.core.logger import CPLog 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.core.media.movie import MovieTypeBase
from couchpotato.environment import Env from couchpotato.environment import Env
@ -20,7 +20,7 @@ log = CPLog(__name__)
autoload = 'MovieSearcher' autoload = 'MovieSearcher'
class MovieSearcher(SearcherBase, MovieTypeBase): class MovieSearcher(Searcher, MovieTypeBase):
in_progress = False in_progress = False
@ -110,153 +110,6 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
self.in_progress = False 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): def correctRelease(self, nzb = None, media = None, quality = None, **kwargs):
if media.get('type') != 'movie': return if media.get('type') != 'movie': return
@ -271,19 +124,23 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
return False return False
# Check for required and ignored words # 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 return False
preferred_quality = quality if quality else fireEvent('quality.single', identifier = quality['identifier'], single = True) preferred_quality = quality if quality else fireEvent('quality.single', identifier = quality['identifier'], single = True)
# Contains lower quality string # 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: 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')) 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 return False
# Contains lower quality string # 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'])) log.info2('Wrong: %s, %slooking for %s in 3D', (nzb['name'], ('' if preferred_quality['custom'].get('3d') else 'NOT '), quality['label']))
return False return False
@ -318,23 +175,24 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
for movie_title in possibleTitles(raw_title): for movie_title in possibleTitles(raw_title):
movie_words = re.split('\W+', simplifyString(movie_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 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 return True
# if no IMDB link, at least check year # 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 return True
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], media_title, media['info']['year'])) log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], media_title, media['info']['year']))
return False return False
def couldBeReleased(self, is_pre_release, dates, year = None): def couldBeReleased(self, is_pre_release, dates, media):
now = int(time.time()) now = int(time.time())
now_year = date.today().year now_year = date.today().year
now_month = date.today().month 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)): 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 return True
@ -405,9 +263,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
if media['type'] == 'movie': if media['type'] == 'movie':
return getTitle(media) return getTitle(media)
class SearchSetupError(Exception): def getProfileId(self, media):
pass assert media['type'] == 'movie'
return media.get('profile_id')
config = [{ config = [{
'name': 'moviesearcher', 'name': 'moviesearcher',

17
couchpotato/core/media/show/matcher/base.py

@ -40,8 +40,21 @@ class Base(MatcherBase):
if len(value) <= 1: if len(value) <= 1:
value = value[0] value = value[0]
else: else:
log.warning('Wrong: identifier contains multiple season or episode values, unsupported') # It might contain multiple season or episode values, but
return None # 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) 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) season_number = int(season_number)
except: return None except: return None
identifier = tryInt(identifier)
cache_key = 'thetvdb.cache.%s.%s.%s' % (identifier, episode_identifier, season_number) cache_key = 'thetvdb.cache.%s.%s.%s' % (identifier, episode_identifier, season_number)
log.debug('Getting EpisodeInfo: %s', cache_key) log.debug('Getting EpisodeInfo: %s', cache_key)
result = self.getCache(cache_key) or {} result = self.getCache(cache_key) or {}
@ -136,7 +137,7 @@ class TheTVDb(ShowProvider):
return result return result
try: try:
show = self.tvdb[int(identifier)] show = self.tvdb[identifier]
except (tvdb_exceptions.tvdb_error, IOError), e: except (tvdb_exceptions.tvdb_error, IOError), e:
log.error('Failed parsing TheTVDB EpisodeInfo for "%s" id "%s": %s', (show, identifier, traceback.format_exc())) log.error('Failed parsing TheTVDB EpisodeInfo for "%s" id "%s": %s', (show, identifier, traceback.format_exc()))
return False return False
@ -263,9 +264,12 @@ class TheTVDb(ShowProvider):
except: except:
pass pass
identifier = tryInt(
show['id'] if show.get('id') else show[number][1]['seasonid'])
season_data = { season_data = {
'identifiers': { 'identifiers': {
'thetvdb': show['id'] if show.get('id') else show[number][1]['seasonid'] 'thetvdb': identifier
}, },
'number': tryInt(number), 'number': tryInt(number),
'images': { 'images': {

4
couchpotato/core/media/show/providers/torrent/thepiratebay.py

@ -25,7 +25,7 @@ class Season(SeasonProvider, Base):
def buildUrl(self, media, page, cats): def buildUrl(self, media, page, cats):
return ( return (
tryUrlencode('"%s"' % fireEvent('media.search_query', media, single = True)), tryUrlencode('"%s"' % fireEvent('library.query', media, single = True)),
page, page,
','.join(str(x) for x in cats) ','.join(str(x) for x in cats)
) )
@ -40,7 +40,7 @@ class Episode(EpisodeProvider, Base):
def buildUrl(self, media, page, cats): def buildUrl(self, media, page, cats):
return ( return (
tryUrlencode('"%s"' % fireEvent('media.search_query', media, single = True)), tryUrlencode('"%s"' % fireEvent('library.query', media, single = True)),
page, page,
','.join(str(x) for x in cats) ','.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 import fireEvent, get_db, Env
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEventAsync
from couchpotato.core.logger import CPLog 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._base.searcher.main import SearchSetupError
from couchpotato.core.media.show import ShowTypeBase from couchpotato.core.media.show import ShowTypeBase
from couchpotato.core.helpers.variable import strtotime
log = CPLog(__name__) log = CPLog(__name__)
autoload = 'EpisodeSearcher' autoload = 'EpisodeSearcher'
class EpisodeSearcher(SearcherBase, ShowTypeBase): class EpisodeSearcher(Searcher, ShowTypeBase):
type = 'episode' type = 'episode'
in_progress = False in_progress = False
@ -47,91 +50,6 @@ class EpisodeSearcher(SearcherBase, ShowTypeBase):
'result': fireEvent('%s.searcher.single' % self.getType(), media, single = True) '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): def correctRelease(self, release = None, media = None, quality = None, **kwargs):
if media.get('type') != 'show.episode': return if media.get('type') != 'show.episode': return
@ -142,13 +60,13 @@ class EpisodeSearcher(SearcherBase, ShowTypeBase):
return False return False
# Check for required and ignored words # 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 return False
preferred_quality = quality if quality else fireEvent('quality.single', identifier = quality['identifier'], single = True) preferred_quality = quality if quality else fireEvent('quality.single', identifier = quality['identifier'], single = True)
# Contains lower quality string # 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: 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')) 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 return False
@ -159,3 +77,33 @@ class EpisodeSearcher(SearcherBase, ShowTypeBase):
return match.weight return match.weight
return False 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.api import addApiView
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.logger import CPLog 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.movie.searcher import SearchSetupError
from couchpotato.core.media.show import ShowTypeBase from couchpotato.core.media.show import ShowTypeBase
from couchpotato.core.helpers.variable import getTitle
log = CPLog(__name__) log = CPLog(__name__)
autoload = 'SeasonSearcher' autoload = 'SeasonSearcher'
class SeasonSearcher(SearcherBase, ShowTypeBase): class SeasonSearcher(Searcher, ShowTypeBase):
type = 'season' type = 'season'
in_progress = False in_progress = False
@ -37,120 +38,36 @@ class SeasonSearcher(SearcherBase, ShowTypeBase):
def searchAll(self, manual = False): def searchAll(self, manual = False):
pass pass
def single(self, media, profile = None, search_protocols = None, manual = False): def single(self, media, search_protocols = None, manual = False, force_download = False, notify = True):
db = get_db()
# 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) 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 result = False
# Find out search type if not all_episodes_available or prefer_episode_releases:
try: result = True
if not search_protocols: for episode in episodes:
search_protocols = fireEvent('searcher.protocols', single = True) if not fireEvent('show.episode.searcher.single', episode, search_protocols, manual, force_download, False):
except SearchSetupError: result = False
return 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']: event_type = 'show.season.searcher.ended'
profile = db.get('id', related['show']['profile_id']) fireEvent('notify.frontend', type = event_type, data = {'_id': media['_id']})
# Find 'active' episodes return result
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
def correctRelease(self, release = None, media = None, quality = None, **kwargs): def correctRelease(self, release = None, media = None, quality = None, **kwargs):
if media.get('type') != 'show.season': if media.get('type') != 'show.season':
@ -163,13 +80,13 @@ class SeasonSearcher(SearcherBase, ShowTypeBase):
return False return False
# Check for required and ignored words # 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 return False
preferred_quality = quality if quality else fireEvent('quality.single', identifier = quality['identifier'], single = True) preferred_quality = quality if quality else fireEvent('quality.single', identifier = quality['identifier'], single = True)
# Contains lower quality string # 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: 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')) 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 return False
@ -180,3 +97,41 @@ class SeasonSearcher(SearcherBase, ShowTypeBase):
return match.weight return match.weight
return False 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.event import fireEvent, addEvent, fireEventAsync
from couchpotato.core.helpers.variable import getTitle from couchpotato.core.helpers.variable import getTitle
from couchpotato.core.logger import CPLog 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._base.searcher.main import SearchSetupError
from couchpotato.core.media.show import ShowTypeBase from couchpotato.core.media.show import ShowTypeBase
@ -12,7 +12,7 @@ log = CPLog(__name__)
autoload = 'ShowSearcher' autoload = 'ShowSearcher'
class ShowSearcher(SearcherBase, ShowTypeBase): class ShowSearcher(Searcher, ShowTypeBase):
type = 'show' type = 'show'
in_progress = False in_progress = False
@ -38,50 +38,56 @@ class ShowSearcher(SearcherBase, ShowTypeBase):
def searchAll(self, manual = False): def searchAll(self, manual = False):
pass pass
def single(self, media, search_protocols = None, manual = False): def single(self, media, search_protocols = None, manual = False, force_download = False, notify = True):
# Find out search type
try: db = get_db()
if not search_protocols: profile = db.get('id', media['profile_id'])
search_protocols = fireEvent('searcher.protocols', single = True)
except SearchSetupError:
return
if not media['profile_id'] or media['status'] == 'done': if not profile or (media['status'] == 'done' and not manual):
log.debug('Show doesn\'t have a profile or already done, assuming in manage tab.') log.debug('Media does not have a profile or already done, assuming in manage tab.')
fireEvent('media.restatus', media['_id'], single = True)
return 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', []): seasons.append(season)
if not season.get('info'):
continue
# Skip specials (and seasons missing 'number') for now result = True
# TODO: set status for specials to skipped by default for season in seasons:
if not season['info'].get('number'): if not fireEvent('show.season.searcher.single', search_protocols, manual, force_download, False):
continue result = False
break
# Check if full season can be downloaded
fireEvent('show.season.searcher.single', season, profile, search_protocols, manual)
# TODO (testing) only snatch one season
return
fireEvent('notify.frontend', type = 'show.searcher.ended.%s' % media['_id'], data = True) fireEvent('notify.frontend', type = 'show.searcher.ended.%s' % media['_id'], data = True)
return result
def getSearchTitle(self, media): 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) related = fireEvent('library.related', media, single = True)
show = related['show'] 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 coming_soon = False
# Theater quality # 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' 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' coming_soon = 'dvd'
if coming_soon: if coming_soon:

Loading…
Cancel
Save