SickGear/sickbeard/properFinder.py

327 lines
14 KiB
Python
Raw Normal View History

# Author: Nic Wolfe <nic@wolfeden.ca>
# URL: http://code.google.com/p/sickbeard/
#
# This file is part of SickGear.
#
# SickGear is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# SickGear is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SickGear. If not, see <http://www.gnu.org/licenses/>.
import datetime
import operator
import os
import threading
import traceback
import re
import sickbeard
from sickbeard import db, exceptions, helpers, history, logger, search, show_name_helpers
from sickbeard import encodingKludge as ek
from sickbeard.common import DOWNLOADED, SNATCHED, SNATCHED_PROPER, Quality, ARCHIVED, SNATCHED_BEST
from sickbeard.exceptions import ex
from name_parser.parser import NameParser, InvalidNameException, InvalidShowException
def search_propers():
if not sickbeard.DOWNLOAD_PROPERS:
return
logger.log(u'Beginning search for new propers')
age_shows, age_anime = sickbeard.BACKLOG_DAYS + 2, 14
aired_since_shows = datetime.datetime.today() - datetime.timedelta(days=age_shows)
aired_since_anime = datetime.datetime.today() - datetime.timedelta(days=age_anime)
recent_shows, recent_anime = _recent_history(aired_since_shows, aired_since_anime)
if recent_shows or recent_anime:
propers = _get_proper_list(aired_since_shows, recent_shows, recent_anime)
if propers:
_download_propers(propers)
else:
logger.log(u'No downloads or snatches found for the last %s%s days to use for a propers search' %
(age_shows, ('', ' (%s for anime)' % age_anime)[helpers.has_anime()]))
_set_last_proper_search(datetime.datetime.today().toordinal())
run_at = ''
proper_sch = sickbeard.properFinderScheduler
if None is proper_sch.start_time:
run_in = proper_sch.lastRun + proper_sch.cycleTime - datetime.datetime.now()
run_at = u', next check '
if datetime.timedelta() > run_in:
run_at += u'imminent'
else:
hours, remainder = divmod(run_in.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
run_at += u'in approx. ' + ('%dh, %dm' % (hours, minutes) if 0 < hours else '%dm, %ds' % (minutes, seconds))
logger.log(u'Completed the search for new propers%s' % run_at)
def _get_proper_list(aired_since_shows, recent_shows, recent_anime):
propers = {}
# for each provider get a list of the
orig_thread_name = threading.currentThread().name
providers = [x for x in sickbeard.providers.sortedProviderList() if x.is_active()]
np = NameParser(False, try_scene_exceptions=True)
for cur_provider in providers:
if not recent_anime and cur_provider.anime_only:
continue
threading.currentThread().name = orig_thread_name + ' :: [' + cur_provider.name + ']'
logger.log(u'Searching for new PROPER releases')
try:
found_propers = cur_provider.find_propers(search_date=aired_since_shows, shows=recent_shows,
anime=recent_anime)
except exceptions.AuthException as e:
logger.log(u'Authentication error: ' + ex(e), logger.ERROR)
continue
except Exception as e:
logger.log(u'Error while searching ' + cur_provider.name + ', skipping: ' + ex(e), logger.ERROR)
logger.log(traceback.format_exc(), logger.ERROR)
continue
finally:
threading.currentThread().name = orig_thread_name
# if they haven't been added by a different provider than add the proper to the list
count = 0
for x in found_propers:
name = _generic_name(x.name)
if name not in propers:
try:
Add smart logic to reduce api hits to newznab server types and improve how nzbs are downloaded. Add newznab smart logic to avoid missing releases when there are a great many recent releases. Change improve performance by using newznab server advertised capabilities. Change config/providers newznab to display only non-default categories. Change use scene season for wanted segment in backlog if show is scene numbering. Change combine Manage Searches / Backlog Search / Limited and Full to Force. Change consolidate limited and full backlog. Change config / Search / Backlog search frequency to instead spread backlog searches over a number of days. Change migrate minimum used value for search frequency into new minimum 7 for search spread. Change restrict nzb providers to 1 backlog batch run per day. Add to Config/Search/Unaired episodes/Allow episodes that are released early. Add to Config/Search/Unaired episodes/Use specific api requests to search for early episode releases. Add use related ids for newznab searches to increase search efficiency. Add periodic update of related show ids. Change terminology Edit Show/"Post processing" tab name to "Other". Add advanced feature "Related show IDs" to Edit Show/Other used for finding episodes and TV info. Add search info source image links to those that have zero id under Edit Show/Other/"Related show IDs". Add "set master" button to Edit Show/Other/"Related show IDs" for info source that can be changed. Change terminology displayShow "Indexers" to "Links" to cover internal and web links. Change add related show info sources on displayShow page. Change don't display "temporarily" defunct TVRage image link on displayShow pages unless it is master info source. Change if a defunct info source is the master of a show then present a link on displayShow to edit related show IDs. Change simplify the next backlog search run time display in the page footer. Change try ssl when fetching data thetvdb, imdb, trakt, scene exception. Change improve reliability to Trakt notifier by using show related id support. Change improve config/providers newznab categories layout. Change show loaded log message at start up and include info source. Change if episode has no airdate then set status to unaired (was skipped). Technical Change move scene_exceptions table from cache.db to sickbeard.db. Add related ids to show obj. Add use of mapped indexer ids for newznab. Add indexer to sql in wanted_eps. Add aired in (scene) season for wanted episodes. Add need_anime, need_sports, need_sd, need_hd, need_uhd to wanted episodes and added as parameter to update_providers. Add fix for lib lockfile/mkdirlockfile. Add set master TV info source logic. Change harden ui input validation. Add per action dialog confirmation. Change to reload page under more events. Change implement "Mark all added episodes Wanted to search for releases" when setting new info source.
2016-09-04 20:00:44 +00:00
np = NameParser(False, try_scene_exceptions=True, showObj=x.parsed_show)
parse_result = np.parse(x.name)
if parse_result.series_name and parse_result.episode_numbers and \
parse_result.show.indexerid in recent_shows + recent_anime:
logger.log(u'Found new proper: ' + x.name, logger.DEBUG)
x.show = parse_result.show.indexerid
x.provider = cur_provider
propers[name] = x
count += 1
except (InvalidNameException, InvalidShowException):
continue
except (StandardError, Exception):
continue
cur_provider.log_result('Propers', count, '%s' % cur_provider.name)
# take the list of unique propers and get it sorted by
sorted_propers = sorted(propers.values(), key=operator.attrgetter('date'), reverse=True)
verified_propers = []
for cur_proper in sorted_propers:
parse_result = np.parse(cur_proper.name)
# set the indexerid in the db to the show's indexerid
cur_proper.indexerid = parse_result.show.indexerid
# set the indexer in the db to the show's indexer
cur_proper.indexer = parse_result.show.indexer
# populate our Proper instance
cur_proper.season = parse_result.season_number if None is not parse_result.season_number else 1
cur_proper.episode = parse_result.episode_numbers[0]
cur_proper.release_group = parse_result.release_group
cur_proper.version = parse_result.version
cur_proper.quality = Quality.nameQuality(cur_proper.name, parse_result.is_anime)
# only get anime proper if it has release group and version
if parse_result.is_anime:
if not cur_proper.release_group and -1 == cur_proper.version:
logger.log(u'Proper %s doesn\'t have a release group and version, ignoring it' % cur_proper.name,
logger.DEBUG)
continue
if not show_name_helpers.pass_wordlist_checks(cur_proper.name, parse=False):
logger.log(u'Proper %s isn\'t a valid scene release that we want, ignoring it' % cur_proper.name,
logger.DEBUG)
continue
re_extras = dict(re_prefix='.*', re_suffix='.*')
result = show_name_helpers.contains_any(cur_proper.name, parse_result.show.rls_ignore_words, **re_extras)
if None is not result and result:
logger.log(u'Ignored: %s for containing ignore word' % cur_proper.name)
continue
result = show_name_helpers.contains_any(cur_proper.name, parse_result.show.rls_require_words, **re_extras)
if None is not result and not result:
logger.log(u'Ignored: %s for not containing any required word match' % cur_proper.name)
continue
# check if we actually want this proper (if it's the right quality)
my_db = db.DBConnection()
sql_results = my_db.select(
'SELECT release_group, status, version, release_name FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ?',
[cur_proper.indexerid, cur_proper.season, cur_proper.episode])
if not sql_results:
continue
# only keep the proper if we have already retrieved the same quality ep (don't get better/worse ones)
old_status, old_quality = Quality.splitCompositeStatus(int(sql_results[0]['status']))
if old_status not in (DOWNLOADED, SNATCHED, SNATCHED_BEST, ARCHIVED) \
or cur_proper.quality != old_quality:
continue
old_release_group = sql_results[0]['release_group']
log_same_grp = 'Skipping proper from release group: [%s], does not match existing release group: [%s] for [%s]'\
% (cur_proper.release_group, old_release_group, cur_proper.name)
# for webldls, prevent propers from different groups
if sickbeard.PROPERS_WEBDL_ONEGRP and \
(old_quality in (Quality.HDWEBDL, Quality.FULLHDWEBDL, Quality.UHD4KWEB) or
(old_quality == Quality.SDTV and re.search(r'\Wweb.?(dl|rip|.[hx]26[45])\W', str(sql_results[0]['release_name']), re.I))) and \
cur_proper.release_group != old_release_group:
logger.log(log_same_grp, logger.DEBUG)
continue
# check if we actually want this proper (if it's the right release group and a higher version)
if parse_result.is_anime:
old_version = int(sql_results[0]['version'])
if -1 < old_version < cur_proper.version:
logger.log(u'Found new anime v%s to replace existing v%s' % (cur_proper.version, old_version))
else:
continue
if cur_proper.release_group != old_release_group:
logger.log(log_same_grp, logger.DEBUG)
continue
2014-07-22 04:53:32 +00:00
# if the show is in our list and there hasn't been a proper already added for that particular episode
# then add it to our list of propers
if cur_proper.indexerid != -1 and (cur_proper.indexerid, cur_proper.season, cur_proper.episode) not in map(
operator.attrgetter('indexerid', 'season', 'episode'), verified_propers):
logger.log(u'Found a proper that may be useful: %s' % cur_proper.name)
verified_propers.append(cur_proper)
return verified_propers
2014-07-22 04:53:32 +00:00
def _download_propers(proper_list):
2014-07-22 04:53:32 +00:00
for cur_proper in proper_list:
history_limit = datetime.datetime.today() - datetime.timedelta(days=30)
# make sure the episode has been downloaded before
my_db = db.DBConnection()
history_results = my_db.select(
'SELECT resource FROM history ' +
'WHERE showid = ? AND season = ? AND episode = ? AND quality = ? AND date >= ? ' +
'AND (' + ' OR '.join("action LIKE '%%%02d'" % x for x in (SNATCHED, DOWNLOADED, SNATCHED_PROPER,
SNATCHED_BEST, ARCHIVED)) + ')',
[cur_proper.indexerid, cur_proper.season, cur_proper.episode, cur_proper.quality,
history_limit.strftime(history.dateFormat)])
# if we didn't download this episode in the first place we don't know what quality to use for the proper = skip
if 0 == len(history_results):
logger.log(u'Skipping download because cannot find an original history entry for proper ' + cur_proper.name)
continue
else:
# get the show object
show_obj = helpers.findCertainShow(sickbeard.showList, cur_proper.indexerid)
if None is show_obj:
logger.log(u'Unable to find the show with indexerid ' + str(
cur_proper.indexerid) + ' so unable to download the proper', logger.ERROR)
continue
# make sure that none of the existing history downloads are the same proper we're trying to download
clean_proper_name = _generic_name(helpers.remove_non_release_groups(cur_proper.name, show_obj.is_anime))
is_same = False
for result in history_results:
# if the result exists in history already we need to skip it
if clean_proper_name == _generic_name(helpers.remove_non_release_groups(
ek.ek(os.path.basename, result['resource']))):
is_same = True
break
if is_same:
logger.log(u'This proper is already in history, skipping it', logger.DEBUG)
continue
ep_obj = show_obj.getEpisode(cur_proper.season, cur_proper.episode)
# make the result object
result = cur_proper.provider.get_result([ep_obj], cur_proper.url)
if None is result:
continue
result.name = cur_proper.name
result.quality = cur_proper.quality
result.version = cur_proper.version
# snatch it
search.snatch_episode(result, SNATCHED_PROPER)
def _recent_history(aired_since_shows, aired_since_anime):
recent_shows, recent_anime = [], []
aired_since_shows = aired_since_shows.toordinal()
aired_since_anime = aired_since_anime.toordinal()
my_db = db.DBConnection()
sql_results = my_db.select(
'SELECT s.show_name, e.showid, e.season, e.episode, e.status, e.airdate FROM tv_episodes AS e' +
' INNER JOIN tv_shows AS s ON (e.showid = s.indexer_id)' +
' WHERE e.airdate >= %s' % min(aired_since_shows, aired_since_anime) +
' AND (e.status IN (%s))' % ','.join([str(x) for x in Quality.DOWNLOADED + Quality.SNATCHED])
)
for sqlshow in sql_results:
show = helpers.findCertainShow(sickbeard.showList, sqlshow['showid'])
if show:
if sqlshow['airdate'] >= aired_since_shows and not show.is_anime:
sqlshow['showid'] not in recent_shows and recent_shows.append(sqlshow['showid'])
else:
sqlshow['showid'] not in recent_anime and show.is_anime and recent_anime.append(sqlshow['showid'])
return recent_shows, recent_anime
def _generic_name(name):
return name.replace('.', ' ').replace('-', ' ').replace('_', ' ').lower()
def _set_last_proper_search(when):
logger.log(u'Setting the last Proper search in the DB to %s' % when, logger.DEBUG)
my_db = db.DBConnection()
sql_results = my_db.select('SELECT * FROM info')
if 0 == len(sql_results):
my_db.action('INSERT INTO info (last_backlog, last_indexer, last_proper_search) VALUES (?,?,?)',
[0, 0, str(when)])
else:
my_db.action('UPDATE info SET last_proper_search=%s' % when)
def _get_last_proper_search():
my_db = db.DBConnection()
sql_results = my_db.select('SELECT * FROM info')
try:
last_proper_search = datetime.date.fromordinal(int(sql_results[0]['last_proper_search']))
except (StandardError, Exception):
return datetime.date.fromordinal(1)
return last_proper_search