#
# 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 .
import copy
import datetime
import re
import threading
import traceback
# noinspection PyPep8Naming
from exceptions_helper import ex
import sickgear
from . import common, db, failed_history, generic_queue, helpers, \
history, logger, network_timezones, properFinder, search, ui
from .classes import Proper, SimpleNamespace
from .search import wanted_episodes, get_aired_in_season, set_wanted_aired
from .tv import TVEpisode
# noinspection PyUnreachableCode
if False:
from typing import Any, AnyStr, Dict, List, Optional, Union
from .tv import TVShow
search_queue_lock = threading.Lock()
BACKLOG_SEARCH = 10
RECENT_SEARCH = 20
FAILED_SEARCH = 30
MANUAL_SEARCH = 40
PROPER_SEARCH = 50
MANUAL_SEARCH_HISTORY = []
MANUAL_SEARCH_HISTORY_SIZE = 100
class SearchQueue(generic_queue.GenericQueue):
def __init__(self):
generic_queue.GenericQueue.__init__(self, cache_db_tables=['search_queue'])
self.queue_name = 'SEARCHQUEUE' # type: AnyStr
def load_queue(self):
try:
my_db = db.DBConnection('cache.db')
queue_sql = my_db.select('SELECT * FROM search_queue')
for q in queue_sql:
if q['action_id'] in (BACKLOG_SEARCH, FAILED_SEARCH, MANUAL_SEARCH):
show_obj = helpers.find_show_by_id({q['indexer']: q['indexer_id']})
if not show_obj:
continue
if BACKLOG_SEARCH == q['action_id']:
segments = [show_obj.get_episode(*tuple([int(x) for x in cur_ep.split('x')]))
for cur_ep in q['segment'].split(',')]
item = BacklogQueueItem(show_obj=show_obj, segment=segments,
standard_backlog=bool(q['standard_backlog']),
limited_backlog=bool(q['limited_backlog']),
forced=bool(q['forced']), torrent_only=bool(q['torrent_only']),
uid=q['uid'])
elif FAILED_SEARCH == q['action_id']:
segments = [show_obj.get_episode(*tuple([int(x) for x in cur_ep.split('x')]))
for cur_ep in q['segment'].split(',')]
item = FailedQueueItem(show_obj=show_obj, segment=segments, uid=q['uid'])
elif MANUAL_SEARCH == q['action_id']:
segment = show_obj.get_episode(*tuple([int(x) for x in q['segment'].split('x')]))
item = ManualSearchQueueItem(show_obj=show_obj, segment=segment, uid=q['uid'])
else:
continue
self.add_item(item, add_to_db=False)
except (BaseException, Exception) as e:
logger.error('Exception loading queue %s: %s' % (self.__class__.__name__, ex(e)))
def _clear_sql(self):
return [
['DELETE FROM search_queue']
]
def _get_item_sql(self, item):
# type: (BaseSearchQueueItem) -> List[List]
if isinstance(item, BacklogQueueItem):
return [
['INSERT OR IGNORE INTO search_queue (indexer, indexer_id, segment, standard_backlog, limited_backlog,'
' forced, torrent_only, action_id, uid) VALUES (?,?,?,?,?,?,?,?,?)',
[item.show_obj.tvid, item.show_obj.prodid,
','.join('%sx%s' % (i.season, i.episode) for i in item.segment), int(item.standard_backlog),
int(item.limited_backlog), int(item.forced), int(item.torrent_only), BACKLOG_SEARCH, item.uid]]
]
elif isinstance(item, FailedQueueItem):
return [
['INSERT OR IGNORE INTO search_queue (indexer, indexer_id, segment, action_id, uid)'
' VALUES (?,?,?,?,?)',
[item.show_obj.tvid, item.show_obj.prodid,
','.join('%sx%s' % (i.season, i.episode) for i in item.segment), FAILED_SEARCH, item.uid]]
]
elif isinstance(item, ManualSearchQueueItem):
return [
['INSERT OR IGNORE INTO search_queue (indexer, indexer_id, segment, action_id, uid)'
' VALUES (?,?,?,?,?)',
[item.show_obj.tvid, item.show_obj.prodid, '%sx%s' % (item.segment.season, item.segment.episode),
MANUAL_SEARCH, item.uid]]
]
return []
def _delete_item_from_db_sql(self, item):
# type: (BaseSearchQueueItem) -> List[List]
if isinstance(item, (BacklogQueueItem, FailedQueueItem, ManualSearchQueueItem)):
return [
['DELETE FROM search_queue WHERE uid = ?', [item.uid]]
]
def _clear_queue(self, action_types=None, excluded_types=None):
generic_queue.GenericQueue._clear_queue(self, action_types=action_types,
excluded_types=[RECENT_SEARCH, PROPER_SEARCH])
def remove_from_queue(self, to_remove=None, force=False):
generic_queue.GenericQueue._remove_from_queue(self, to_remove=to_remove,
excluded_types=[RECENT_SEARCH, PROPER_SEARCH], force=force)
def is_in_queue(self, show_obj, segment):
# type: (sickgear.tv.TVShow, List[sickgear.tv.TVEpisode]) -> bool
with self.lock:
return any(1 for cur_item in self.queue
if isinstance(cur_item, BacklogQueueItem) and show_obj == cur_item.show_obj
and segment == cur_item.segment)
def is_ep_in_queue(self, segment):
# type: (List[sickgear.tv.TVEpisode]) -> bool
with self.lock:
return any(1 for cur_item in self.queue
if isinstance(cur_item, (ManualSearchQueueItem, FailedQueueItem))
and cur_item.segment == segment)
def is_show_in_queue(self, tvid_prodid):
# type: (AnyStr) -> bool
with self.lock:
return any(1 for cur_item in self.queue
if isinstance(cur_item, (ManualSearchQueueItem, FailedQueueItem))
and tvid_prodid == cur_item.show_obj.tvid_prodid)
def pause_backlog(self):
# type: (...) -> None
with self.lock:
self.min_priority = generic_queue.QueuePriorities.HIGH
def unpause_backlog(self):
# type: (...) -> None
with self.lock:
self.min_priority = 0
def is_backlog_paused(self):
# type: (...) -> bool
# backlog priorities are NORMAL, this should be done properly somewhere
with self.lock:
return self.min_priority >= generic_queue.QueuePriorities.NORMAL
def _is_in_progress(self, item_type):
# type: (Any) -> bool
with self.lock:
return any(1 for cur_item in self.queue + [self.currentItem] if isinstance(cur_item, item_type))
def get_queued_manual(self, tvid_prodid):
# type: (Optional[AnyStr]) -> List[BaseSearchQueueItem]
"""
Returns None or List of base info items of all show related items in manual or failed queue
:param tvid_prodid: show tvid_prodid or None for all q items
:return: List with 0 or more items
"""
ep_ns_list = []
with self.lock:
for cur_item in self.queue:
if (isinstance(cur_item, (ManualSearchQueueItem, FailedQueueItem)) and
(not tvid_prodid or tvid_prodid == str(cur_item.show_obj.tvid_prodid))):
ep_ns_list.append(cur_item.base_info())
return ep_ns_list
def get_current_manual_item(self, tvid_prodid):
# type: (Optional[AnyStr]) -> Union[ManualSearchQueueItem, FailedQueueItem]
"""
Returns a base info item of the currently active manual search item
:param tvid_prodid: show tvid_prodid or None for all q items
:type tvid_prodid: String or None
:return: base info item of ManualSearchQueueItem or FailedQueueItem or None
"""
with self.lock:
if self.currentItem and isinstance(self.currentItem, (ManualSearchQueueItem, FailedQueueItem)) \
and (not tvid_prodid or tvid_prodid == str(self.currentItem.show_obj.tvid_prodid)):
return self.currentItem.base_info()
def is_backlog_in_progress(self):
# type: (...) -> bool
return self._is_in_progress(BacklogQueueItem)
def is_recentsearch_in_progress(self):
# type: (...) -> bool
return self._is_in_progress(RecentSearchQueueItem)
def is_propersearch_in_progress(self):
# type: (...) -> bool
with self.lock:
return any(1 for cur_item in self.queue + [self.currentItem]
if isinstance(cur_item, ProperSearchQueueItem) and None is cur_item.propers)
def is_standard_backlog_in_progress(self):
# type: (...) -> bool
with self.lock:
return any(1 for cur_item in self.queue + [self.currentItem]
if isinstance(cur_item, BacklogQueueItem) and cur_item.standard_backlog)
def type_of_backlog_in_progress(self):
# type: (...) -> AnyStr
limited = full = other = False
with self.lock:
for cur_item in self.queue + [self.currentItem]:
if isinstance(cur_item, BacklogQueueItem):
if cur_item.standard_backlog:
if cur_item.limited_backlog:
limited = True
else:
full = True
else:
other = True
types = []
for msg, variant in ['Limited', limited], ['Full', full], ['On Demand', other]:
if variant:
types.append(msg)
message = 'None'
if types:
message = ', '.join(types)
return message
def queue_length(self):
# type: (...) -> Dict[List]
length = dict(backlog=[], recent=0, manual=[], failed=[], proper=[])
with self.lock:
for cur_item in [self.currentItem] + self.queue:
if not cur_item:
continue
if isinstance(cur_item, RecentSearchQueueItem):
length['recent'] += 1
elif isinstance(cur_item, ProperSearchQueueItem):
length['proper'] += [dict(recent=None is not cur_item.propers)]
else:
result_item = dict(
name=cur_item.show_obj.name, segment=cur_item.segment,
tvid=cur_item.show_obj.tvid, prodid=cur_item.show_obj.prodid,
tvid_prodid=cur_item.show_obj.tvid_prodid,
# legacy keys for api responses
indexer=cur_item.show_obj.tvid, indexerid=cur_item.show_obj.prodid,
uid=cur_item.uid
)
if isinstance(cur_item, BacklogQueueItem):
result_item.update(dict(
standard_backlog=cur_item.standard_backlog, limited_backlog=cur_item.limited_backlog,
forced=cur_item.forced, torrent_only=cur_item.torrent_only))
length['backlog'] += [result_item]
elif isinstance(cur_item, FailedQueueItem):
length['failed'] += [result_item]
elif isinstance(cur_item, ManualSearchQueueItem):
length['manual'] += [result_item]
return length
def abort_show(self, show_obj):
# type: (TVShow) -> None
if show_obj:
with self.lock:
to_remove = []
for c in ((self.currentItem and [self.currentItem]) or []) + self.queue:
if show_obj == getattr(c, 'show_obj', None):
try:
to_remove.append(c.uid)
except (BaseException, Exception):
pass
try:
c.stop.set()
except (BaseException, Exception):
pass
if to_remove:
try:
self.remove_from_queue(to_remove)
except (BaseException, Exception):
pass
def add_item(
self,
item, # type: Union[RecentSearchQueueItem, ProperSearchQueueItem, BacklogQueueItem, ManualSearchQueueItem, FailedQueueItem]
add_to_db=True # type: bool
):
# type: (...) -> None
"""
:param item:
:param add_to_db:
:type item: RecentSearchQueueItem or ProperSearchQueueItem or BacklogQueueItem or ManualSearchQueueItem or
FailedQueueItem
"""
if isinstance(item, (RecentSearchQueueItem, ProperSearchQueueItem)):
# recent and proper searches
generic_queue.GenericQueue.add_item(self, item, add_to_db=add_to_db)
elif isinstance(item, BacklogQueueItem) and not self.is_in_queue(item.show_obj, item.segment):
# backlog searches
generic_queue.GenericQueue.add_item(self, item, add_to_db=add_to_db)
elif isinstance(item, (ManualSearchQueueItem, FailedQueueItem)) and not self.is_ep_in_queue(item.segment):
# manual and failed searches
generic_queue.GenericQueue.add_item(self, item, add_to_db=add_to_db)
else:
logger.debug("Not adding item, it's already in the queue")
class RecentSearchQueueItem(generic_queue.QueueItem):
def __init__(self):
self.success = None
self.ep_obj_list = [] # type: List
generic_queue.QueueItem.__init__(self, 'Recent Search', RECENT_SEARCH)
self.snatched_eps = set([]) # type: set
def run(self):
generic_queue.QueueItem.run(self)
try:
self._change_missing_episodes()
show_list = sickgear.showList
from_date = datetime.date.fromordinal(1)
needed = common.NeededQualities()
for cur_show_obj in show_list:
if cur_show_obj.paused:
continue
wanted_eps = wanted_episodes(cur_show_obj, from_date, unaired=sickgear.SEARCH_UNAIRED)
if wanted_eps:
if not needed.all_needed:
if not needed.all_types_needed:
needed.check_needed_types(cur_show_obj)
if not needed.all_qualities_needed:
for w in wanted_eps:
if needed.all_qualities_needed:
break
if not w.show_obj.is_anime and not w.show_obj.is_sports:
needed.check_needed_qualities(w.wanted_quality)
self.ep_obj_list.extend(wanted_eps)
if sickgear.DOWNLOAD_PROPERS:
properFinder.get_needed_qualites(needed)
self.update_providers(needed=needed)
self._check_for_propers(needed)
if not self.ep_obj_list:
logger.log('No search of cache for episodes required')
self.success = True
else:
num_shows = len(set([ep_obj.show_obj.name for ep_obj in self.ep_obj_list]))
logger.log(f'Found {len(self.ep_obj_list):d} needed episode{helpers.maybe_plural(self.ep_obj_list)}'
f' spanning {num_shows:d} show{helpers.maybe_plural(num_shows)}')
try:
logger.log('Beginning recent search for episodes')
# noinspection PyTypeChecker
search_results = search.search_for_needed_episodes(self.ep_obj_list)
if not len(search_results):
logger.log('No needed episodes found')
else:
for result in search_results:
logger.log(f'Downloading {result.name} from {result.provider.name}')
self.success = search.snatch_episode(result)
if self.success:
for ep_obj in result.ep_obj_list:
self.snatched_eps.add(SimpleNamespace(tvid_prodid=ep_obj.show_obj.tvid_prodid,
tvid=ep_obj.show_obj.tvid,
prodid=ep_obj.show_obj.prodid,
season=ep_obj.season,
episode=ep_obj.episode,
show=ep_obj.show_obj,
ep_obj=ep_obj))
helpers.cpu_sleep()
except (BaseException, Exception):
logger.error(traceback.format_exc())
if None is self.success:
self.success = False
finally:
self.finish()
@staticmethod
def _check_for_propers(needed):
# type: (sickgear.common.NeededQualities) -> None
if not sickgear.DOWNLOAD_PROPERS:
return
propers = {}
my_db = db.DBConnection('cache.db')
sql_result = my_db.select('SELECT * FROM provider_cache')
re_p = r'\brepack|proper|real%s\b' % ('', '|v[2-9]')[needed.need_anime]
proper_regex = re.compile(re_p, flags=re.I)
for cur_result in sql_result:
if proper_regex.search(cur_result['name']):
try:
show_obj = helpers.find_show_by_id({int(cur_result['indexer']): int(cur_result['indexerid'])})
except (BaseException, Exception):
continue
if show_obj:
propers.setdefault(cur_result['provider'], []).append(
Proper(cur_result['name'], cur_result['url'],
datetime.datetime.fromtimestamp(cur_result['time']), show_obj, parsed_show_obj=show_obj))
if propers:
logger.log('Found Proper/Repack/Real in recent search, sending data to properfinder')
propersearch_queue_item = sickgear.search_queue.ProperSearchQueueItem(provider_proper_obj=propers)
sickgear.search_queue_scheduler.action.add_item(propersearch_queue_item)
@staticmethod
def _change_missing_episodes():
if not network_timezones.network_dict:
network_timezones.update_network_dict()
if network_timezones.network_dict:
cur_date = (datetime.date.today() + datetime.timedelta(days=1)).toordinal()
else:
cur_date = (datetime.date.today() - datetime.timedelta(days=2)).toordinal()
cur_time = datetime.datetime.now(network_timezones.SG_TIMEZONE)
my_db = db.DBConnection()
sql_result = my_db.select(
'SELECT indexer AS tvid, showid AS prodid, airdate, season, episode, timestamp,'
' timezone, network, airtime, runtime'
' FROM tv_episodes'
' WHERE status = ? AND season > 0 AND airdate <= ? AND airdate > 1'
' ORDER BY indexer, showid', [common.UNAIRED, cur_date])
sql_l = []
show_obj = None
wanted = False
for cur_result in sql_result:
tvid, prodid = int(cur_result['tvid']), int(cur_result['prodid'])
if not show_obj or not (show_obj.tvid == tvid and show_obj.prodid == prodid):
show_obj = helpers.find_show_by_id({tvid: prodid})
# for when there is orphaned series in the database but not loaded into our showlist
if not show_obj:
continue
try:
end_time = network_timezones.get_episode_time(d=cur_result['airdate'],
t=show_obj.airs,
show_network=show_obj.network,
show_airtime=show_obj.airtime,
show_timezone=show_obj.timezone,
ep_timestamp=cur_result['timestamp'],
ep_network=cur_result['network'],
ep_airtime=cur_result['airtime'],
ep_timezone=cur_result['timezone']
)
end_time += datetime.timedelta(minutes=helpers.try_int(cur_result['runtime'] or show_obj.runtime, 60))
# filter out any episodes that haven't aired yet
if end_time > cur_time:
continue
except (BaseException, Exception):
# if an error occurred assume the episode hasn't aired yet
continue
ep_obj = show_obj.get_episode(int(cur_result['season']), int(cur_result['episode']))
with ep_obj.lock:
# Now that it is time, change state of UNAIRED show into expected or skipped
ep_obj.status = (common.WANTED, common.SKIPPED)[ep_obj.show_obj.paused]
result = ep_obj.get_sql()
if None is not result:
sql_l.append(result)
wanted |= (False, True)[common.WANTED == ep_obj.status]
if not wanted:
logger.log('No unaired episodes marked wanted')
if 0 < len(sql_l):
my_db = db.DBConnection()
my_db.mass_action(sql_l)
if wanted:
logger.log('Found new episodes marked wanted')
@staticmethod
def update_providers(needed=common.NeededQualities(need_all=True)):
# type: (sickgear.common.NeededQualities) -> None
"""
:param needed: needed class
:type needed: common.NeededQualities
"""
orig_thread_name = threading.current_thread().name
threads = []
providers = list(filter(lambda x: x.is_active() and x.enable_recentsearch,
sickgear.providers.sorted_sources()))
for cur_provider in providers:
if not cur_provider.cache.should_update():
continue
if not threads:
logger.log('Updating provider caches with recent upload data')
# spawn a thread for each provider to save time waiting for slow response providers
threads.append(threading.Thread(target=cur_provider.cache.update_cache,
kwargs={'needed': needed},
name='%s :: [%s]' % (orig_thread_name, cur_provider.name)))
# start the thread we just created
threads[-1].start()
if not len(providers):
logger.warning('No NZB/Torrent providers in Media Providers/Options are enabled to match recent episodes')
if threads:
# wait for all threads to finish
for t in threads:
t.join()
logger.log('Finished updating provider caches')
class ProperSearchQueueItem(generic_queue.QueueItem):
def __init__(self, provider_proper_obj=None):
# type: (Optional[Dict]) -> None
generic_queue.QueueItem.__init__(self, 'Proper Search', PROPER_SEARCH)
self.priority = (generic_queue.QueuePriorities.VERYHIGH,
generic_queue.QueuePriorities.HIGH)[None is provider_proper_obj]
self.propers = provider_proper_obj # type: Optional[Dict]
self.success = None
def run(self):
generic_queue.QueueItem.run(self)
try:
properFinder.search_propers(self.propers)
finally:
self.finish()
def __str__(self):
return '<%s - %s>' % (self.__class__.__name__, ('recent', 'native')[None is self.propers])
def __repr__(self):
return self.__str__()
class BaseSearchQueueItem(generic_queue.QueueItem):
def __init__(self, show_obj, segment, name, action_id=0, uid=None):
# type: (sickgear.tv.TVShow, Union[TVEpisode, List[TVEpisode]], AnyStr, int, AnyStr) -> None
"""
:param show_obj: show object
:param segment: segment
:param name: name
:param action_id:
:param uid:
"""
super(BaseSearchQueueItem, self).__init__(name, action_id, uid=uid)
self.segment = segment # type: Union[TVEpisode, List[TVEpisode]]
self.show_obj = show_obj
self.added_dt = None
self.success = None
self.snatched_eps = set([])
def base_info(self):
return SimpleNamespace(
success=self.success,
added_dt=self.added_dt,
snatched_eps=copy.deepcopy(self.snatched_eps),
show_ns=SimpleNamespace(
tvid=self.show_obj.tvid, prodid=self.show_obj.prodid, tvid_prodid=self.show_obj.tvid_prodid,
quality=self.show_obj.quality, upgrade_once=self.show_obj.upgrade_once),
segment_ns=[SimpleNamespace(
season=s.season, episode=s.episode, status=s.status, ep_obj=s,
show_ns=SimpleNamespace(
tvid=s.show_obj.tvid, prodid=s.show_obj.prodid, tvid_prodid=self.show_obj.tvid_prodid,
quality=s.show_obj.quality, upgrade_once=s.show_obj.upgrade_once
)) for s in ([self.segment], self.segment)[isinstance(self.segment, list)]])
def __str__(self):
if self.segment:
if isinstance(self.segment, list):
show_name = self.segment[0].show_obj and self.segment[0].show_obj.name
else:
show_name = self.segment.show_obj and self.segment.show_obj.name
segment_str = ' - %s (%s)' % \
(show_name,
','.join(['%sx%s' % (s.season, s.episode)
for s in ([self.segment], self.segment)[isinstance(self.segment, list)]]))
else:
segment_str = ''
return '<%s%s>' % (self.__class__.__name__, segment_str)
def __repr__(self):
return self.__str__()
class ManualSearchQueueItem(BaseSearchQueueItem):
def __init__(self, show_obj, segment, uid=None):
# type: (sickgear.tv.TVShow, sickgear.tv.TVEpisode, AnyStr) -> None
"""
:param show_obj: show object
:param segment: segment
:param uid:
"""
super(ManualSearchQueueItem, self).__init__(show_obj, segment, 'Manual Search', MANUAL_SEARCH, uid=uid)
self.priority = generic_queue.QueuePriorities.HIGH # type: int
self.name = 'MANUAL-%s' % show_obj.tvid_prodid # type: AnyStr
self.started = None
def run(self):
generic_queue.QueueItem.run(self)
try:
logger.log(f'Beginning manual search for: [{self.segment.pretty_name()}]')
self.started = True
ep_count, ep_count_scene = get_aired_in_season(self.show_obj)
set_wanted_aired(self.segment, True, ep_count, ep_count_scene, manual=True)
if not getattr(self.segment, 'wanted_quality', None):
ep_status, ep_quality = common.Quality.split_composite_status(self.segment.status)
self.segment.wanted_quality = search.get_wanted_qualities(self.segment, ep_status, ep_quality,
unaired=True, manual=True)
if not self.segment.wanted_quality:
logger.log('No qualities wanted for episode, exiting manual search')
self.success = False
self.finish()
return
search_result = search.search_providers(self.show_obj, [self.segment], True, try_other_searches=True)
if search_result:
# sort results by season, episode number
try:
search_result.sort(key=lambda a: (a.ep_obj_list[0].season or 0, a.ep_obj_list[0].episode or 0))
except (BaseException, Exception):
pass
for result in search_result: # type: sickgear.classes.NZBSearchResult
logger.log(f'Downloading {result.name} from {result.provider.name}')
self.success = search.snatch_episode(result)
for ep_obj in result.ep_obj_list: # type: sickgear.tv.TVEpisode
self.snatched_eps.add(SimpleNamespace(tvid_prodid=ep_obj.show_obj.tvid_prodid,
tvid=ep_obj.show_obj.tvid,
prodid=ep_obj.show_obj.prodid,
season=ep_obj.season,
episode=ep_obj.episode,
show=ep_obj.show_obj,
ep_obj=ep_obj))
helpers.cpu_sleep()
# just use the first result for now
break
else:
ui.notifications.message('No downloads found',
f'Could not find a download for {self.segment.pretty_name()}')
logger.log(f'Unable to find a download for: [{self.segment.pretty_name()}]')
except (BaseException, Exception):
logger.error(traceback.format_exc())
finally:
# Keep a list with the last executed searches
fifo(MANUAL_SEARCH_HISTORY, self.base_info())
if None is self.success:
self.success = False
self.finish()
class BacklogQueueItem(BaseSearchQueueItem):
def __init__(
self,
show_obj, # type: sickgear.tv.TVShow
segment, # type: List[sickgear.tv.TVEpisode]
standard_backlog=False, # type: bool
limited_backlog=False, # type: bool
forced=False, # type: bool
torrent_only=False, # type: bool
uid=None # type: AnyStr
):
"""
:param show_obj: show object
:param segment: segment
:param standard_backlog: is standard backlog
:param limited_backlog: is limited backlog
:param forced: forced
:param torrent_only: torrent only
:param uid:
"""
super(BacklogQueueItem, self).__init__(show_obj, segment, 'Backlog', BACKLOG_SEARCH, uid=uid)
self.priority = generic_queue.QueuePriorities.LOW # type: int
self.name = 'BACKLOG-%s' % show_obj.tvid_prodid # type: AnyStr
self.standard_backlog = standard_backlog # type: bool
self.limited_backlog = limited_backlog # type: bool
self.forced = forced # type: bool
self.torrent_only = torrent_only # type: bool
def run(self):
generic_queue.QueueItem.run(self)
is_error = False
try:
if not self.standard_backlog:
ep_count, ep_count_scene = get_aired_in_season(self.show_obj)
for ep_obj in self.segment: # type: sickgear.tv.TVEpisode
set_wanted_aired(ep_obj, True, ep_count, ep_count_scene)
logger.log(f'Beginning backlog search for: [{self.show_obj.unique_name}]')
search_result = search.search_providers(
self.show_obj, self.segment, False,
try_other_searches=(not self.standard_backlog or not self.limited_backlog),
scheduled=self.standard_backlog)
if search_result:
# sort results by season, episode number
try:
search_result.sort(key=lambda a: (a.ep_obj_list[0].season or 0, a.ep_obj_list[0].episode or 0))
except (BaseException, Exception):
pass
for result in search_result: # type: sickgear.classes.NZBSearchResult
logger.log(f'Downloading {result.name} from {result.provider.name}')
if search.snatch_episode(result):
for ep_obj in result.ep_obj_list: # type: sickgear.tv.TVEpisode
self.snatched_eps.add(SimpleNamespace(tvid_prodid=ep_obj.show_obj.tvid_prodid,
tvid=ep_obj.show_obj.tvid,
prodid=ep_obj.show_obj.prodid,
season=ep_obj.season,
episode=ep_obj.episode,
show=ep_obj.show_obj,
ep_obj=ep_obj))
helpers.cpu_sleep()
else:
logger.log(f'No needed episodes found during backlog search for: [{self.show_obj.unique_name}]')
except (BaseException, Exception):
is_error = True
logger.error(traceback.format_exc())
finally:
logger.log('Completed backlog search %sfor: [%s]'
% (('', 'with a debug error ')[is_error], self.show_obj.unique_name))
self.finish()
class FailedQueueItem(BaseSearchQueueItem):
def __init__(self, show_obj, segment, uid=None):
# type: (sickgear.tv.TVShow, List[sickgear.tv.TVEpisode], AnyStr) -> None
"""
:param show_obj: show object
:param segment: segment
:param uid:
"""
super(FailedQueueItem, self).__init__(show_obj, segment, 'Retry', FAILED_SEARCH, uid=uid)
self.priority = generic_queue.QueuePriorities.HIGH # type: int
self.name = 'RETRY-%s' % show_obj.tvid_prodid # type: AnyStr
self.started = None
def run(self):
generic_queue.QueueItem.run(self)
self.started = True
try:
ep_count, ep_count_scene = get_aired_in_season(self.show_obj)
for ep_obj in self.segment: # type: sickgear.tv.TVEpisode
logger.log(f'Marking episode as bad: [{ep_obj.pretty_name()}]')
failed_history.set_episode_failed(ep_obj)
(release, provider) = failed_history.find_release(ep_obj)
failed_history.revert_episode(ep_obj)
if release:
failed_history.add_failed(release)
history.log_failed(ep_obj, release, provider)
logger.log(f'Beginning failed download search for: [{ep_obj.pretty_name()}]')
set_wanted_aired(ep_obj, True, ep_count, ep_count_scene, manual=True)
search_result = search.search_providers(self.show_obj, self.segment, True, try_other_searches=True) or []
for result in search_result: # type: sickgear.classes.NZBSearchResult
logger.log(f'Downloading {result.name} from {result.provider.name}')
if search.snatch_episode(result):
for ep_obj in result.ep_obj_list: # type: sickgear.tv.TVEpisode
self.snatched_eps.add(SimpleNamespace(tvid_prodid=ep_obj.show_obj.tvid_prodid,
tvid=ep_obj.show_obj.tvid,
prodid=ep_obj.show_obj.prodid,
season=ep_obj.season,
episode=ep_obj.episode,
show=ep_obj.show_obj,
ep_obj=ep_obj))
helpers.cpu_sleep()
else:
pass
# logger.log(f'No valid episode found to retry for: [{self.segment.pretty_name()}]')
except (BaseException, Exception):
logger.error(traceback.format_exc())
finally:
# Keep a list with the last executed searches
fifo(MANUAL_SEARCH_HISTORY, self.base_info())
if None is self.success:
self.success = False
self.finish()
def fifo(my_list, item):
# type: (List, Any) -> None
remove_old_fifo(my_list)
item.added_dt = datetime.datetime.now()
if len(my_list) >= MANUAL_SEARCH_HISTORY_SIZE:
my_list.pop(0)
my_list.append(item)
def remove_old_fifo(my_list, age=datetime.timedelta(minutes=30)):
# type: (List, datetime.timedelta) -> None
try:
now = datetime.datetime.now()
my_list[:] = [i for i in my_list if not isinstance(getattr(i, 'added_dt', None), datetime.datetime)
or now - i.added_dt < age]
except (BaseException, Exception):
pass