# # 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 os import traceback from lib.dateutil.parser import parser from lib.tvinfo_base.exceptions import * import exceptions_helper from exceptions_helper import ex import sickgear from . import logger, ui, db, generic_queue, name_cache from .anime import AniGroupList from .common import SKIPPED, WANTED, UNAIRED, Quality, statusStrings from .helpers import should_delete_episode, find_show_by_id from .indexermapper import clean_show_name, map_indexers_to_show from .indexers.indexer_config import TVINFO_TVDB, TVINFO_TVRAGE from .name_parser.parser import NameParser from .tv import TVidProdid, TVShow, TVSWITCH_DUPLICATE_SHOW, TVSWITCH_EP_DELETED, TVSWITCH_ID_CONFLICT, \ TVSWITCH_NO_NEW_ID, TVSWITCH_NORMAL, TVSWITCH_NOT_FOUND_ERROR, TVSWITCH_SAME_ID, TVSWITCH_SOURCE_NOT_FOUND_ERROR, \ TVSWITCH_VERIFY_ERROR from six import integer_types, iteritems, itervalues from sg_helpers import try_int # noinspection PyUnreachableCode if False: from typing import Any, AnyStr, Dict, List, Optional, Tuple, Union from lib.tvinfo_base import TVInfoShow from .tv import TVEpisode # Define special priority of tv source switch tasks, higher than anything else except newly added shows SWITCH_PRIO = generic_queue.QueuePriorities.HIGH + 5 DAILY_SHOW_UPDATE_FINISHED_EVENT = 1 def bool_none(val): # type: (Optional[int]) -> Optional[bool] if None is val: return None return bool(val) class ShowQueue(generic_queue.GenericQueue): def __init__(self): generic_queue.GenericQueue.__init__(self, cache_db_tables=['show_queue'], main_db_tables=['tv_src_switch']) self.queue_name = 'SHOWQUEUE' self.daily_update_running = False if not db.DBConnection().has_flag('kodi_nfo_uid'): self.add_event(DAILY_SHOW_UPDATE_FINISHED_EVENT, sickgear.metadata.kodi.set_nfo_uid_updated) def check_events(self): if self.daily_update_running and \ not (self.is_show_update_running() or sickgear.update_show_scheduler.is_running_job): self.execute_events(DAILY_SHOW_UPDATE_FINISHED_EVENT) self.daily_update_running = False def load_queue(self): try: my_db = db.DBConnection('cache.db') sql_result = my_db.select('SELECT * FROM show_queue') for cur_row in sql_result: show_obj = None if ShowQueueActions.ADD != cur_row['action_id']: try: show_obj = find_show_by_id({cur_row['tvid']: cur_row['prodid']}) except (BaseException, Exception): continue if not show_obj: continue if cur_row['action_id'] in (ShowQueueActions.UPDATE, ShowQueueActions.FORCEUPDATE, ShowQueueActions.WEBFORCEUPDATE): self.update_show(add_to_db=False, force=bool(cur_row['force']), pausestatus_after=bool_none(cur_row['pausestatus_after']), scheduled_update=bool(cur_row['scheduled_update']), show_obj=show_obj, skip_refresh=bool(cur_row['skip_refresh']), uid=cur_row['uid'], web=ShowQueueActions.WEBFORCEUPDATE == cur_row['action_id']) elif ShowQueueActions.REFRESH == cur_row['action_id']: self.refresh_show(add_to_db=False, force=bool(cur_row['force']), force_image_cache=bool(cur_row['force_image_cache']), priority=cur_row['priority'], scheduled_update=bool(cur_row['scheduled_update']), show_obj=show_obj, uid=cur_row['uid']) elif ShowQueueActions.RENAME == cur_row['action_id']: self.rename_show_episodes(add_to_db=False, show_obj=show_obj, uid=cur_row['uid']) elif ShowQueueActions.SUBTITLE == cur_row['action_id']: self.download_subtitles(add_to_db=False, show_obj=show_obj, uid=cur_row['uid']) elif ShowQueueActions.ADD == cur_row['action_id']: self.add_show( tvid=cur_row['tvid'], prodid=cur_row['prodid'], show_dir=cur_row['show_dir'], quality=cur_row['quality'], upgrade_once=bool(cur_row['upgrade_once']), wanted_begin=cur_row['wanted_begin'], wanted_latest=cur_row['wanted_latest'], tag=cur_row['tag'], paused=bool_none(cur_row['paused']), prune=cur_row['prune'], default_status=cur_row['default_status'], scene=bool_none(cur_row['scene']), subtitles=cur_row['subtitles'], flatten_folders=bool_none(cur_row['flatten_folders']), anime=bool_none(cur_row['anime']), blocklist=cur_row['blocklist'], allowlist=cur_row['allowlist'], show_name=cur_row['show_name'], new_show=bool(cur_row['new_show']), lang=cur_row['lang'], uid=cur_row['uid'], add_to_db=False) except (BaseException, Exception) as e: logger.error('Exception loading queue %s: %s' % (self.__class__.__name__, ex(e))) def save_item(self, item): # type: (ShowQueueItem) -> None if ShowQueueActions.SWITCH == item.action_id: my_db = db.DBConnection() my_db.action( """ REPLACE INTO tv_src_switch (old_indexer, old_indexer_id, new_indexer, new_indexer_id, action_id, status, uid, mark_wanted, set_pause, force_id) VALUES (?,?,?,?,?,?,?,?,?,?) """, [item.show_obj.tvid, item.show_obj.prodid, item.new_tvid, item.new_prodid, ShowQueueActions.SWITCH, TVSWITCH_NORMAL, item.uid, int(item.mark_wanted), int(item.set_pause), int(item.force_id)]) else: generic_queue.GenericQueue.save_item(self, item) def _clear_sql(self): # noinspection SqlWithoutWhere return [['DELETE FROM show_queue']] def _get_item_sql(self, item): # type: (ShowQueueItem) -> List[List] if isinstance(item, QueueItemUpdate): if item.switch: return [] pause_status_after = item.kwargs.get('pausestatus_after') if None is not pause_status_after: pause_status_after = int(pause_status_after) return [[ """ INSERT OR IGNORE INTO show_queue (tvid, prodid, priority, force, scheduled_update, skip_refresh, pausestatus_after, action_id, uid) VALUES (?,?,?,?,?,?,?,?,?) """, [item.show_obj.tvid, item.show_obj.prodid, item.priority, int(item.force), int(item.scheduled_update), int(item.kwargs.get('skip_refresh', False)), pause_status_after, item.action_id, item.uid] ]] elif isinstance(item, QueueItemRefresh): if item.switch: return [] pause_status_after = item.kwargs.get('pausestatus_after') if None is not pause_status_after: pause_status_after = int(pause_status_after) return [[ """ INSERT OR IGNORE INTO show_queue (tvid, prodid, priority, force, scheduled_update, force_image_cache, pausestatus_after, action_id, uid) VALUES (?,?,?,?,?,?,?,?,?) """, [item.show_obj.tvid, item.show_obj.prodid, item.priority, int(item.force), int(item.scheduled_update), int(item.force_image_cache), pause_status_after, item.action_id, item.uid] ]] elif isinstance(item, QueueItemRename): return [[ """ INSERT OR IGNORE INTO show_queue (tvid, prodid, priority, scheduled_update, action_id, uid) VALUES (?,?,?,?,?,?) """, [item.show_obj.tvid, item.show_obj.prodid, item.priority, int(item.scheduled_update), item.action_id, item.uid] ]] elif isinstance(item, QueueItemSubtitle): return [[ """ INSERT OR IGNORE INTO show_queue (tvid, prodid, priority, scheduled_update, action_id, uid) VALUES (?,?,?,?,?,?) """, [item.show_obj.tvid, item.show_obj.prodid, item.priority, int(item.scheduled_update), item.action_id, item.uid] ]] elif isinstance(item, QueueItemAdd): return [[ """ INSERT OR IGNORE INTO show_queue (tvid, prodid, priority, scheduled_update, show_dir, default_status, quality, flatten_folders, lang, subtitles, anime, scene, paused, blocklist, allowlist, wanted_begin, wanted_latest, prune, tag, new_show, show_name, upgrade_once, action_id, uid) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) """, [item.tvid, item.prodid, item.priority, int(item.scheduled_update), item.showDir, item.default_status, item.quality, try_int(item.flatten_folders, None), item.lang, item.subtitles, try_int(item.anime, None), try_int(item.scene, None), try_int(item.paused, None), item.blocklist, item.allowlist, item.default_wanted_begin, item.default_wanted_latest, item.prune, item.tag, int(item.new_show), item.show_name, int(item.upgrade_once), item.action_id, item.uid] ]] return [] def delete_item(self, item, finished_run=False): # type: (ShowQueueItem, bool) -> None if isinstance(item, QueueItemSwitchSource): try: my_db = db.DBConnection() if finished_run: my_db.action('DELETE FROM tv_src_switch WHERE uid = ? AND status = ?', [item.uid, TVSWITCH_NORMAL]) else: my_db.action('DELETE FROM tv_src_switch WHERE uid = ?', [item.uid]) except (BaseException, Exception) as e: logger.error('Exception deleting item %s from db: %s' % (item, ex(e))) else: generic_queue.GenericQueue.delete_item(self, item) def _delete_item_from_db_sql(self, item): # type: (ShowQueueItem) -> List[List] return [ ['DELETE FROM show_queue WHERE uid = ?', [item.uid]] ] def _clear_queue(self, action_types=None, excluded_types=None): # type: (integer_types, List[integer_types]) -> None generic_queue.GenericQueue._clear_queue(self, action_types=action_types) def remove_from_queue(self, to_remove=None, force=False): # type: (List[integer_types], bool) -> None generic_queue.GenericQueue._remove_from_queue(self, to_remove=to_remove, force=force) def _is_in_queue(self, show_obj, actions): # type: (TVShow, Tuple[integer_types, ...]) -> bool """ :param show_obj: show object :param actions: :return: """ with self.lock: return any(1 for x in self.queue if x.action_id in actions and show_obj == x.show_obj) def _is_being_somethinged(self, show_obj, actions): # type: (TVShow, Tuple[integer_types, ...]) -> bool """ :param show_obj: show object :type show_obj: sickgear.tv.TVShow :param actions: :return: :rtype: bool """ with self.lock: return None is not self.currentItem \ and show_obj == self.currentItem.show_obj \ and self.currentItem.action_id in actions def is_in_update_queue(self, show_obj): # type: (TVShow) -> bool """ :param show_obj: show object :type show_obj: sickgear.tv.TVShow :return: :rtype: bool """ return self._is_in_queue(show_obj, (ShowQueueActions.UPDATE, ShowQueueActions.FORCEUPDATE, ShowQueueActions.WEBFORCEUPDATE)) def is_in_refresh_queue(self, show_obj): # type: (TVShow) -> bool """ :param show_obj: show object :type show_obj: sickgear.tv.TVShow :return: :rtype: bool """ return self._is_in_queue(show_obj, (ShowQueueActions.REFRESH,)) def is_in_rename_queue(self, show_obj): # type: (TVShow) -> bool """ :param show_obj: show object :type show_obj: sickgear.tv.TVShow :return: :rtype: bool """ return self._is_in_queue(show_obj, (ShowQueueActions.RENAME,)) def is_in_subtitle_queue(self, show_obj): # type: (TVShow) -> bool """ :param show_obj: show object :type show_obj: sickgear.tv.TVShow :return: :rtype: bool """ return self._is_in_queue(show_obj, (ShowQueueActions.SUBTITLE,)) def is_being_added(self, show_obj): # type: (TVShow) -> bool """ :param show_obj: show object :type show_obj: sickgear.tv.TVShow :return: :rtype: bool """ return self._is_being_somethinged(show_obj, (ShowQueueActions.ADD,)) def is_being_updated(self, show_obj): # type: (TVShow) -> bool """ :param show_obj: show object :type show_obj: sickgear.tv.TVShow :return: :rtype: bool """ return self._is_being_somethinged(show_obj, (ShowQueueActions.UPDATE, ShowQueueActions.FORCEUPDATE, ShowQueueActions.WEBFORCEUPDATE)) def is_being_refreshed(self, show_obj): # type: (TVShow) -> bool """ :param show_obj: show object :type show_obj: sickgear.tv.TVShow :return: :rtype: bool """ return self._is_being_somethinged(show_obj, (ShowQueueActions.REFRESH,)) def is_being_renamed(self, show_obj): # type: (TVShow) -> bool """ :param show_obj: show object :type show_obj: sickgear.tv.TVShow :return: :rtype: bool """ return self._is_being_somethinged(show_obj, (ShowQueueActions.RENAME,)) def is_being_subtitled(self, show_obj): # type: (TVShow) -> bool """ :param show_obj: show object :type show_obj: sickgear.tv.TVShow :return: :rtype: bool """ return self._is_being_somethinged(show_obj, (ShowQueueActions.SUBTITLE,)) def is_show_update_running(self): """ :return: :rtype: bool """ with self.lock: return any(1 for x in self.queue + [self.currentItem] if isinstance(x, ShowQueueItem) and x.scheduled_update) def is_show_being_switched(self, show_obj): # type: (TVShow) -> bool """ check if show is being switched currently :param show_obj: show object """ return self._is_being_somethinged(show_obj, (ShowQueueActions.SWITCH,)) def is_show_switch_queued(self, show_obj): # type: (TVShow) -> bool """ check if show is in switch queue :param show_obj: show object """ return self._is_in_queue(show_obj, (ShowQueueActions.SWITCH,)) def is_switch_running(self): # type: (...) -> bool with self.lock: return any(1 for x in self.queue + [self.currentItem] if isinstance(x, QueueItemSwitchSource)) def _get_loading_showlist(self): """ :return: :rtype: List """ with self.lock: return [x for x in self.queue + [self.currentItem] if None is not x and x.is_loading] def queue_length(self): # type: (...) -> Dict[AnyStr, List[AnyStr, Dict]] """ :return: """ length = {'add': [], 'update': [], 'forceupdate': [], 'forceupdateweb': [], 'refresh': [], 'rename': [], 'subtitle': [], 'switch': []} with self.lock: for cur_item in [self.currentItem] + self.queue: # type: ShowQueueItem if not cur_item: continue result_item = {'name': cur_item.show_name, 'scheduled_update': cur_item.scheduled_update, 'uid': cur_item.uid} if isinstance(cur_item, QueueItemAdd): length['add'].append(result_item) else: result_item.update({ '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}) if isinstance(cur_item, QueueItemUpdate): update_type = 'Normal' if isinstance(cur_item, QueueItemForceUpdate): update_type = 'Forced' elif isinstance(cur_item, QueueItemForceUpdateWeb): update_type = 'Forced Web' result_item.update({'update_type': update_type}) length['update'].append(result_item) elif isinstance(cur_item, QueueItemRefresh): length['refresh'].append(result_item) elif isinstance(cur_item, QueueItemRename): length['rename'].append(result_item) elif isinstance(cur_item, QueueItemSubtitle): length['subtitle'].append(result_item) elif isinstance(cur_item, QueueItemSwitchSource): result_item.update({'new_tvid': cur_item.new_tvid, 'new_prodid': cur_item.new_prodid, 'progress': cur_item.progress}) length['switch'].append(result_item) return length loading_showlist = property(_get_loading_showlist) def update_show(self, show_obj, # type: TVShow force=False, # type: bool web=False, # type: bool scheduled_update=False, # type: bool priority=generic_queue.QueuePriorities.NORMAL, # type: integer_types uid=None, # type: integer_types add_to_db=True, # type: bool **kwargs # type: Any ): # type: (...) -> Union[QueueItemUpdate, QueueItemForceUpdate, QueueItemForceUpdateWeb] """ :param show_obj: show object :type show_obj: sickgear.tv.TVShow :param force: :type force: bool :param web: :type web: bool :param scheduled_update: :type scheduled_update: bool :param priority: :type priority: int :param uid: :param add_to_db: :param kwargs: :return: :rtype: QueueItemUpdate or QueueItemForceUpdateWeb or QueueItemForceUpdate """ with self.lock: if self.is_being_added(show_obj): raise exceptions_helper.CantUpdateException( 'Show is still being added, wait until it is finished before you update.') if self.is_being_updated(show_obj): raise exceptions_helper.CantUpdateException( 'This show is already being updated, can\'t update again until it\'s done.') if self.is_in_update_queue(show_obj): raise exceptions_helper.CantUpdateException( 'This show is already being updated, can\'t update again until it\'s done.') if self.is_show_being_switched(show_obj): raise exceptions_helper.CantUpdateException('Show is in progress of being switched') if self.is_show_switch_queued(show_obj): raise exceptions_helper.CantUpdateException('Show is already queued to be switched') if not force: queue_item_obj = QueueItemUpdate( show_obj, scheduled_update=scheduled_update, uid=uid, **kwargs) elif web: queue_item_obj = QueueItemForceUpdateWeb( show_obj, scheduled_update=scheduled_update, priority=priority, uid=uid, **kwargs) else: queue_item_obj = QueueItemForceUpdate( show_obj, scheduled_update=scheduled_update, uid=uid, **kwargs) self.add_item(queue_item_obj, add_to_db=add_to_db) return queue_item_obj def refresh_show(self, show_obj, force=False, scheduled_update=False, after_update=False, priority=generic_queue.QueuePriorities.HIGH, force_image_cache=False, uid=None, add_to_db=True, **kwargs): # type: (TVShow, bool, bool, bool, integer_types, bool, integer_types, bool, Any) -> Optional[QueueItemRefresh] """ :param show_obj: show object :type show_obj: sickgear.tv.TVShow :param force: :type force: bool :param scheduled_update: :type scheduled_update: bool :param after_update: :type after_update: :param priority: :type priority: int :param force_image_cache: :type force_image_cache: bool :param uid: :param add_to_db: :param kwargs: :return: :rtype: QueueItemRefresh """ with self.lock: if (self.is_being_refreshed(show_obj) or self.is_in_refresh_queue(show_obj)) and not force: raise exceptions_helper.CantRefreshException('This show is being refreshed, not refreshing again.') if ((not after_update and self.is_being_updated(show_obj)) or self.is_in_update_queue(show_obj)) and not force: logger.debug('Skipping this refresh as there is already an update queued or' ' in progress and a refresh is done at the end of an update anyway.') return if self.is_show_being_switched(show_obj): raise exceptions_helper.CantRefreshException('Show is in progress of being switched') if self.is_show_switch_queued(show_obj): raise exceptions_helper.CantRefreshException('Show is already queued to be switched') queue_item_obj = QueueItemRefresh(show_obj, force=force, scheduled_update=scheduled_update, priority=priority, force_image_cache=force_image_cache, uid=uid, **kwargs) self.add_item(queue_item_obj, add_to_db=add_to_db) return queue_item_obj def rename_show_episodes(self, show_obj, uid=None, add_to_db=True): # type: (TVShow, integer_types, bool) -> QueueItemRename """ :param show_obj: show object :type show_obj: sickgear.tv.TVShow :param uid: :param add_to_db: :return: :rtype: QueueItemRename """ queue_item_obj = QueueItemRename(show_obj, uid=uid) self.add_item(queue_item_obj, add_to_db=add_to_db) return queue_item_obj def switch_show(self, show_obj, # type: TVShow new_tvid, # type: integer_types new_prodid, # type: integer_types force_id=False, # type: bool uid=None, # type: AnyStr set_pause=False, # type: bool mark_wanted=False, # type: bool resume=False, # type: bool old_tvid=None, # type: integer_types old_prodid=None, # type: integer_types add_to_db=True # type: bool ): # type: (...) -> QueueItemSwitchSource """ :param show_obj: :param new_tvid: :param new_prodid: :param force_id: :param uid: :param set_pause: :param mark_wanted: :param resume: :param old_tvid: :param old_prodid: :param add_to_db: """ with self.lock: if self.is_show_being_switched(show_obj): raise exceptions_helper.CantSwitchException('Show is in progress of being switched') if self.is_show_switch_queued(show_obj): raise exceptions_helper.CantSwitchException('Show is already queued to be switched') item = QueueItemSwitchSource(show_obj=show_obj, new_tvid=new_tvid, new_prodid=new_prodid, force_id=force_id, uid=uid, set_pause=set_pause, mark_wanted=mark_wanted, resume=resume, old_tvid=old_tvid, old_prodid=old_prodid) self.add_item(item, add_to_db=add_to_db) return item def download_subtitles(self, show_obj, uid=None, add_to_db=True): # type: (TVShow, integer_types, bool) -> QueueItemSubtitle """ :param show_obj: show object :type show_obj: sickgear.tv.TVShow :param uid: :param add_to_db: :return: :rtype: QueueItemSubtitle """ if sickgear.USE_SUBTITLES: queue_item_obj = QueueItemSubtitle(show_obj, uid=uid) self.add_item(queue_item_obj, add_to_db=add_to_db) return queue_item_obj def abort_show(self, show_obj): # type: (TVShow) -> None if show_obj: with self.lock: for c in ((self.currentItem and [self.currentItem]) or []) + self.queue: if show_obj == getattr(c, 'show_obj', None): try: self.remove_from_queue([c.uid]) except (BaseException, Exception): pass try: c.stop.set() except (BaseException, Exception): pass def add_show(self, tvid, prodid, show_dir, quality=None, upgrade_once=False, wanted_begin=None, wanted_latest=None, tag=None, paused=None, prune=None, default_status=None, scene=None, subtitles=None, flatten_folders=None, anime=None, blocklist=None, allowlist=None, show_name=None, new_show=False, lang='en', uid=None, add_to_db=True): """ :param tvid: tvid :type tvid: int :param prodid: prodid :type prodid: int or long :param show_dir: show dir :type show_dir: AnyStr :param quality: :type quality: None or int :param upgrade_once: :type upgrade_once: bool or None :param wanted_begin: :type wanted_begin: int or None :param wanted_latest: :type wanted_latest: int or None :param tag: :type tag: AnyStr or None :param paused: :type paused: None or int :param prune: :type prune: int or None :param default_status: :type default_status: int or None :param scene: :type scene: int or None :param subtitles: :type subtitles: int or None :param flatten_folders: :type flatten_folders: int or None :param anime: :type anime: int or None :param blocklist: :type blocklist: AnyStr or None :param allowlist: :type allowlist: AnyStr or None :param show_name: :type show_name: AnyStr or None :param new_show: :param lang: :type lang: AnyStr :param uid: :param add_to_db: :return: :rtype: QueueItemAdd """ queue_item_obj = QueueItemAdd(tvid, prodid, show_dir, default_status, quality, flatten_folders, lang, subtitles, anime, scene, paused, blocklist, allowlist, wanted_begin, wanted_latest, prune, tag, new_show=new_show, show_name=show_name, upgrade_once=upgrade_once, uid=uid) self.add_item(queue_item_obj, add_to_db=add_to_db) return queue_item_obj class ShowQueueActions(object): REFRESH = 1 ADD = 2 UPDATE = 3 FORCEUPDATE = 4 WEBFORCEUPDATE = 7 RENAME = 5 SUBTITLE = 6 SWITCH = 10 names = {REFRESH: 'Refresh', ADD: 'Add', UPDATE: 'Update', FORCEUPDATE: 'Force Update', WEBFORCEUPDATE: 'Force Web Update', RENAME: 'Rename', SUBTITLE: 'Subtitle', SWITCH: 'Switch Source'} class ShowQueueItem(generic_queue.QueueItem): """ Represents an item in the queue waiting to be executed Can be either: - show being added (may or may not be associated with a show object) - show being refreshed - show being updated - show being force updated - show being subtitled """ def __init__(self, action_id, show_obj, scheduled_update=False, uid=None): # type: (integer_types, TVShow, bool, integer_types) -> None """ :param action_id: :type action_id: :param show_obj: show object :type show_obj: sickgear.tv.TVShow or None :param scheduled_update: :type scheduled_update: bool :param uid: """ generic_queue.QueueItem.__init__(self, ShowQueueActions.names[action_id], action_id, uid=uid) self.show_obj = show_obj # type: sickgear.tv.TVShow self.scheduled_update = scheduled_update # type: bool def is_in_queue(self): """ :rtype: bool """ return self in sickgear.show_queue_scheduler.action.queue + [ sickgear.show_queue_scheduler.action.currentItem] def _get_name(self): """ :rtype: AnyStr """ if self.show_obj: return self.show_obj.name return '' def _is_loading(self): return False def __str__(self): return '<%s (%s)>' % (self.__class__.__name__, (self.show_obj and self.show_obj.name)) def __repr__(self): return self.__str__() show_name = property(_get_name) is_loading = property(_is_loading) class QueueItemAdd(ShowQueueItem): def __init__(self, tvid, prodid, show_dir, default_status, quality, flatten_folders, lang, subtitles, anime, scene, paused, blocklist, allowlist, default_wanted_begin, default_wanted_latest, prune, tag, scheduled_update=False, new_show=False, show_name=None, upgrade_once=False, uid=None): """ :param tvid: tvid :type tvid: int :param prodid: prodid :type prodid: int or long :param show_dir: :type show_dir: AnyStr :param default_status: :type default_status: :param quality: :type quality: int :param flatten_folders: :type flatten_folders: :param lang: :type lang: :param subtitles: :type subtitles: :param anime: :type anime: :param scene: :type scene: :param paused: :type paused: :param blocklist: :type blocklist: :param allowlist: :type allowlist: :param default_wanted_begin: :type default_wanted_begin: :param default_wanted_latest: :type default_wanted_latest: :param prune: :type prune: :param tag: :type tag: :param scheduled_update: :type scheduled_update: :param new_show: :type new_show: :param show_name: :type show_name: AnyStr or None :param upgrade_once: :type upgrade_once: :param uid: """ self.tvid = tvid # type: int self.prodid = prodid # type: int self.allowlist = allowlist self.anime = anime self.blocklist = blocklist self.default_status = default_status self.default_wanted_begin = (0, default_wanted_begin)[isinstance(default_wanted_begin, integer_types)] self.default_wanted_latest = (0, default_wanted_latest)[isinstance(default_wanted_latest, integer_types)] self.flatten_folders = flatten_folders self.lang = lang self.new_show = new_show self.paused = paused self.prune = prune self.quality = quality # type: int self.scene = scene self.show_obj = None self.showDir = show_dir # type: AnyStr self.showname = show_name # type: AnyStr or None self.subtitles = subtitles self.tag = tag self.upgrade_once = upgrade_once # this will initialize self.show_obj to None ShowQueueItem.__init__(self, ShowQueueActions.ADD, self.show_obj, scheduled_update, uid=uid) self.priority = generic_queue.QueuePriorities.VERYHIGH def _get_name(self): """ :return: the show name if there is a show object created, if not returns the dir that the show is being added to. :rtype: AnyStr """ if None is not self.showname: return self.showname if None is self.show_obj: return self.showDir return self.show_obj.name show_name = property(_get_name) def _is_loading(self): """ :return: True if we've gotten far enough to have a show object, or False if we still only know the folder name. :rtype: bool """ return None is self.show_obj is_loading = property(_is_loading) # if they gave a number to start or number to end as wanted, then change those eps to it def _get_wanted(self, db_obj, wanted_max, latest): # type (...) -> int latest_season = 0 actual = 0 upgradable = 0 process_sql = True wanted_updates = [] if latest: # find season number with latest aired episode latest_result = db_obj.select( """ SELECT MAX(season) AS latest_season FROM tv_episodes WHERE season > 0 AND indexer = ? AND showid = ? AND status != ? """, [self.show_obj.tvid, self.show_obj.prodid, UNAIRED]) if latest_result and None is not latest_result[0]['latest_season']: latest_season = int(latest_result[0]['latest_season']) else: process_sql = False if process_sql: if -1 == wanted_max: compare_op = '' # equal, we only want the specific season else: compare_op = ('>', '<')[latest] # less/more then equal season base_sql = """ SELECT indexerid, season, episode, status FROM tv_episodes WHERE indexer = ? AND showid = ? AND season != 0 AND season %s= ? AND status != ? ORDER BY season%s, episode%s%s """ % (compare_op, ('', ' DESC')[latest], ('', ' DESC')[latest], (' LIMIT ?', '')[-1 == wanted_max]) selected_res = db_obj.select(base_sql, [self.show_obj.tvid, self.show_obj.prodid, (1, latest_season)[latest], UNAIRED] + ([wanted_max], [])[-1 == wanted_max]) selected_ids = [] for sr in selected_res: if sr['status'] in [SKIPPED]: selected_ids.append(sr['indexerid']) wanted_updates.append({'season': sr['season'], 'episode': sr['episode'], 'status': sr['status']}) elif sr['status'] not in [WANTED]: cur_status, cur_quality = Quality.split_composite_status(int(sr['status'])) if sickgear.WANTEDLIST_CACHE.get_wantedlist( self.quality, self.upgrade_once, cur_quality, cur_status, unaired=(sickgear.SEARCH_UNAIRED and not sickgear.UNAIRED_RECENT_SEARCH_ONLY)): upgradable += 1 if selected_ids: update = 'UPDATE [tv_episodes] SET status = ? WHERE indexer = ? AND indexerid IN (%s)' % \ ','.join(['?'] * len(selected_ids)) db_obj.action(update, [WANTED, self.show_obj.tvid] + selected_ids) # noinspection SqlResolve sql_result = db_obj.select('SELECT changes() as last FROM [tv_episodes]') for cur_row in sql_result: actual = cur_row['last'] break action_log = 'didn\'t find any episodes that need to be set wanted' if actual: action_log = ('updated %s %s episodes > %s' % ( (((('%s of %s' % (actual, wanted_max)), ('%s of max %s limited' % (actual, wanted_max)))[10 == wanted_max]), ('max %s available' % actual))[-1 == wanted_max], ('first season', 'latest')[latest], ', '.join([ ('S%02dE%02d=%s' % (a['season'], a['episode'], statusStrings[a['status']])) for a in wanted_updates ]) )) logger.log('Get wanted ' + action_log) return actual + upgradable def run(self): ShowQueueItem.run(self) logger.log('Starting to add show %s' % self.showDir) # make sure the TV info source IDs are valid try: tvinfo_config = sickgear.TVInfoAPI(self.tvid).api_params.copy() kw = {} if self.lang: tvinfo_config['language'] = self.lang kw = {'language': self.lang} logger.log(f'{sickgear.TVInfoAPI(self.tvid).name}: {repr(tvinfo_config)}') t = sickgear.TVInfoAPI(self.tvid).setup(**tvinfo_config) s = t.get_show(self.prodid, load_episodes=False, **kw) if getattr(t, 'show_not_found', False): logger.error(f'Show {self.show_name} was not found on {sickgear.TVInfoAPI(self.tvid).name},' f' maybe show was deleted') self._finish_early() return # this usually only happens if they have an NFO in their show dir # which gave us a TV info source ID that has no proper english version of the show if None is getattr(s, 'seriesname', None): logger.error(f'Show in {self.showDir} has no name on {sickgear.TVInfoAPI(self.tvid).name},' f' probably the wrong language used to search with.') ui.notifications.error('Unable to add show', 'Show in %s has no name on %s, probably the wrong language.' ' Delete .nfo and add manually in the correct language.' % (self.showDir, sickgear.TVInfoAPI(self.tvid).name)) self._finish_early() return except (BaseException, Exception): logger.error('Unable to find show ID:%s on TV info: %s' % (self.prodid, sickgear.TVInfoAPI(self.tvid).name)) ui.notifications.error('Unable to add show', 'Unable to look up the show in %s on %s using ID %s, not using the NFO.' ' Delete .nfo and try adding manually again.' % (self.showDir, sickgear.TVInfoAPI(self.tvid).name, self.prodid)) self._finish_early() return try: show_exists = find_show_by_id({self.tvid: self.prodid}, no_exceptions=True) new_show_obj = show_exists or TVShow(self.tvid, self.prodid, self.lang) result = new_show_obj.load_from_tvinfo() self.show_obj = new_show_obj # set up initial values self.show_obj.location = self.showDir self.show_obj.quality = self.quality if self.quality else sickgear.QUALITY_DEFAULT self.show_obj.upgrade_once = self.upgrade_once self.show_obj.tag = self.tag if None is not self.tag else 'Show List' self.show_obj.paused = self.paused if None is not self.paused else sickgear.PAUSE_DEFAULT self.show_obj.prune = self.prune if None is not self.prune else 0 self.show_obj.scene = self.scene if None is not self.scene else sickgear.SCENE_DEFAULT self.show_obj.subtitles = self.subtitles if None is not self.subtitles else sickgear.SUBTITLES_DEFAULT self.show_obj.flatten_folders = self.flatten_folders if None is not self.flatten_folders \ else sickgear.FLATTEN_FOLDERS_DEFAULT self.show_obj.anime = self.anime if None is not self.anime else sickgear.ANIME_DEFAULT if self.show_obj.anime: self.show_obj.release_groups = AniGroupList(self.show_obj.tvid, self.show_obj.prodid, self.show_obj.tvid_prodid) if self.allowlist: self.show_obj.release_groups.set_allow_keywords(self.allowlist) if self.blocklist: self.show_obj.release_groups.set_block_keywords(self.blocklist) # be smartish about this if self.show_obj.genre and 'talk show' in self.show_obj.genre.lower(): self.show_obj.air_by_date = 1 if self.show_obj.genre and 'documentary' in self.show_obj.genre.lower(): self.show_obj.air_by_date = 0 if self.show_obj.classification and 'sports' in self.show_obj.classification.lower(): self.show_obj.sports = 1 except BaseTVinfoException as e: logger.error(f'Unable to add show due to an error with {sickgear.TVInfoAPI(self.tvid).name}: {ex(e)}') if self.show_obj: ui.notifications.error('Unable to add %s due to an error with %s' % (self.show_obj.unique_name, sickgear.TVInfoAPI(self.tvid).name)) else: ui.notifications.error( 'Unable to add show due to an error with %s' % sickgear.TVInfoAPI(self.tvid).name) self._finish_early() return except exceptions_helper.MultipleShowObjectsException: logger.error('The show in %s is already in your show list, skipping' % self.showDir) ui.notifications.error('Show skipped', 'The show in %s is already in your show list' % self.showDir) self._finish_early() return except (BaseException, Exception) as e: logger.error('Error trying to add show: %s' % ex(e)) logger.error(traceback.format_exc()) self._finish_early() raise self.show_obj.load_imdb_info() try: self.show_obj.save_to_db() except (BaseException, Exception) as e: logger.error('Error saving the show to the database: %s' % ex(e)) logger.error(traceback.format_exc()) self._finish_early() raise if not show_exists: # add it to the show list if not already in it sickgear.showList.append(self.show_obj) sickgear.showDict[self.show_obj.sid_int] = self.show_obj sickgear.webserve.Home.make_showlist_unique_names() sickgear.MEMCACHE['history_tab'] = sickgear.webserve.History.menu_tab( sickgear.MEMCACHE['history_tab_limit']) try: self.show_obj.load_episodes_from_tvinfo(tvinfo_data=(None, result)[ self.show_obj.prodid == getattr(result, 'id', None)]) except (BaseException, Exception) as e: logger.error(f'Error with {sickgear.TVInfoAPI(self.show_obj.tvid).name},' f' not creating episode list: {ex(e)}') logger.error(traceback.format_exc()) try: self.show_obj.load_episodes_from_dir() except (BaseException, Exception) as e: logger.error('Error searching directory for episodes: %s' % ex(e)) logger.error(traceback.format_exc()) # if they gave a custom status then change all the eps to it my_db = db.DBConnection() if self.default_status != SKIPPED: logger.log('Setting all episodes to the specified default status: %s' % sickgear.common.statusStrings[self.default_status]) my_db.action( """ UPDATE tv_episodes SET status = ? WHERE status = ? AND indexer = ? AND showid = ? AND season != 0 """, [self.default_status, SKIPPED, self.show_obj.tvid, self.show_obj.prodid]) items_wanted = self._get_wanted(my_db, self.default_wanted_begin, latest=False) items_wanted += self._get_wanted(my_db, self.default_wanted_latest, latest=True) self.show_obj.write_metadata() self.show_obj.update_metadata() self.show_obj.populate_cache() self.show_obj.flush_episodes() # load ids _ = self.show_obj.ids # if sickgear.USE_TRAKT: # # if there are specific episodes that need to be added by trakt # sickgear.trakt_checker_scheduler.action.manageNewShow(self.show_obj) # # # add show to trakt.tv library # if sickgear.TRAKT_SYNC: # sickgear.trakt_checker_scheduler.action.addShowToTraktLibrary(self.show_obj) # Load XEM data to DB for show sickgear.scene_numbering.xem_refresh(self.show_obj.tvid, self.show_obj.prodid, force=True) if self.show_obj.scene: # enable/disable scene flag based on if show has an explicit _scene_ mapping at XEM self.show_obj.scene = sickgear.scene_numbering.has_xem_scene_mapping( self.show_obj.tvid, self.show_obj.prodid) # if "scene" numbering is disabled during add show, output availability to log if None is not self.scene and not self.show_obj.scene and \ self.show_obj.prodid in sickgear.scene_exceptions.MEMCACHE['release_map_xem'][self.show_obj.tvid]: logger.log('No scene number mappings found at TheXEM. Therefore, episode scene numbering disabled, ' 'edit show and enable it to manually add custom numbers for search and media processing.') try: self.show_obj.save_to_db() except (BaseException, Exception) as e: logger.error('Error saving the show to the database: %s' % ex(e)) logger.error(traceback.format_exc()) self._finish_early() raise # update internal name cache name_cache.build_name_cache(self.show_obj) self.show_obj.load_episodes_from_db() map_indexers_to_show(self.show_obj, recheck=True) if self.show_obj.tvid in (TVINFO_TVRAGE, TVINFO_TVDB): # noinspection SqlResolve oh = my_db.select('SELECT resource FROM history WHERE indexer = 0 AND showid = ?', [self.show_obj.prodid]) if oh: found = False for o in oh: np = NameParser(file_name=True, indexer_lookup=False) try: pr = np.parse(o['resource']) except (BaseException, Exception): continue if pr.show_obj.tvid == self.show_obj.tvid and pr.show_obj.prodid == self.show_obj.prodid: found = True break if found: my_db.action('UPDATE history SET indexer = ? WHERE indexer = 0 AND showid = ?', [self.show_obj.tvid, self.show_obj.prodid]) msg = ' the specified show into ' + self.showDir # if started with WANTED eps then run the backlog if WANTED == self.default_status or items_wanted: logger.log('Launching backlog for this show since episodes are WANTED') sickgear.search_backlog_scheduler.action.search_backlog([self.show_obj], prevent_same=True) ui.notifications.message('Show added/search', 'Adding and searching for episodes of' + msg) else: ui.notifications.message('Show added', 'Adding' + msg) self.finish() def _finish_early(self): if None is not self.show_obj: self.show_obj.delete_show() if self.new_show: # if adding a new show, delete the empty folder that was already created try: os.rmdir(self.showDir) except (BaseException, Exception): pass self.finish() def __str__(self): return '<%s (%s)>' % (self.__class__.__name__, '%s:%s%s' % (self.tvid, self.prodid, ('', ' - %s ' % self.show_name)[None is not self.show_name])) def __repr__(self): return self.__str__() class QueueItemRefresh(ShowQueueItem): def __init__(self, show_obj=None, force=False, scheduled_update=False, priority=generic_queue.QueuePriorities.HIGH, force_image_cache=False, uid=None, switch=False, **kwargs): # type: (TVShow, bool, bool, integer_types, bool, integer_types, bool, Any) -> None """ :param show_obj: show object :type show_obj: sickgear.tv.TVShow :param force: :type force: bool :param scheduled_update: :type scheduled_update: bool :param priority: :type priority: int :param force_image_cache: :type force_image_cache: bool :param uid: :param switch: switching show :param kwargs: """ ShowQueueItem.__init__(self, ShowQueueActions.REFRESH, show_obj, scheduled_update, uid=uid) # do refreshes first because they're quick self.priority = priority # type: int # force refresh certain items self.force = force # type: bool self.force_image_cache = force_image_cache # type: bool self.switch = switch # type: bool self.kwargs = kwargs def run(self): ShowQueueItem.run(self) logger.log('Performing refresh on %s' % self.show_obj.unique_name) self.show_obj.refresh_dir() self.show_obj.write_metadata(force=self.force) # if self.force: # self.show_obj.update_metadata() self.show_obj.populate_cache(self.force_image_cache) # Load XEM data to DB for show if self.show_obj.prodid in sickgear.scene_exceptions.MEMCACHE['release_map_xem'][self.show_obj.tvid]: sickgear.scene_numbering.xem_refresh(self.show_obj.tvid, self.show_obj.prodid) if 'pausestatus_after' in self.kwargs and None is not self.kwargs['pausestatus_after']: self.show_obj.paused = self.kwargs['pausestatus_after'] self.show_obj.save_to_db() self.inProgress = False class QueueItemRename(ShowQueueItem): def __init__(self, show_obj=None, scheduled_update=False, uid=None): # type: (TVShow, bool, integer_types) -> None """ :param show_obj: show object :type show_obj: sickgear.tv.TVShow :param scheduled_update: :type scheduled_update: bool :param uid: """ ShowQueueItem.__init__(self, ShowQueueActions.RENAME, show_obj, scheduled_update, uid=uid) def run(self): ShowQueueItem.run(self) logger.log('Performing rename on %s' % self.show_obj.unique_name) try: _ = self.show_obj.location except exceptions_helper.ShowDirNotFoundException: logger.warning(f'Can\'t perform rename on {self.show_obj.unique_name} when the show directory is missing.') return ep_obj_rename_list = [] ep_obj_list = self.show_obj.get_all_episodes(has_location=True) for cur_ep_obj in ep_obj_list: # Only want to rename if we have a location if cur_ep_obj.location: if cur_ep_obj.related_ep_obj: # do we have one of multi-episodes in the rename list already have_already = False for cur_related_ep_obj in cur_ep_obj.related_ep_obj + [cur_ep_obj]: if cur_related_ep_obj in ep_obj_rename_list: have_already = True break if not have_already: ep_obj_rename_list.append(cur_ep_obj) else: ep_obj_rename_list.append(cur_ep_obj) for cur_ep_obj in ep_obj_rename_list: cur_ep_obj.rename() self.inProgress = False class QueueItemSubtitle(ShowQueueItem): def __init__(self, show_obj=None, scheduled_update=False, uid=None): # type: (TVShow, bool, integer_types) -> None """ :param show_obj: show object :type show_obj: sickgear.tv.TVShow :param scheduled_update: :type scheduled_update: bool :param uid: """ ShowQueueItem.__init__(self, ShowQueueActions.SUBTITLE, show_obj, scheduled_update, uid=uid) def run(self): ShowQueueItem.run(self) if not sickgear.USE_SUBTITLES: self.finish() return logger.log('Downloading subtitles for %s' % self.show_obj.unique_name) self.show_obj.download_subtitles() self.inProgress = False class QueueItemUpdate(ShowQueueItem): def __init__(self, show_obj=None, scheduled_update=False, uid=None, switch=False, tvinfo_data=None, old_tvid=None, old_prodid=None, **kwargs): # type: (TVShow, bool, AnyStr, bool, Optional[TVInfoShow], int, integer_types, Any) -> None """ :param show_obj: show object :type show_obj: sickgear.tv.TVShow :param scheduled_update: :type scheduled_update: bool :param skip_refresh: skip queuing refresh task at the end :param switch: switching show :param tvinfo_data: tvinfo data for the show :param old_tvid: old tvid when switching :param old_prodid: old prodid when switching :param kwargs: :param uid: """ ShowQueueItem.__init__(self, ShowQueueActions.UPDATE, show_obj, scheduled_update, uid=uid) self.force = False # type: bool self.force_web = False # type: bool self.skip_refresh = kwargs.get('skip_refresh', False) # type: bool self.switch = switch # type: bool self.tvinfo_data = tvinfo_data # type: Optional[TVInfoShow] self.old_tvid = old_tvid # type: Optional[int] self.old_prodid = old_prodid # type: Optional[integer_types] self.kwargs = kwargs def run(self): ShowQueueItem.run(self) last_update = datetime.date.fromordinal(self.show_obj.last_update_indexer) old_name = self.show_obj.name if not sickgear.TVInfoAPI(self.show_obj.tvid).config['active']: logger.log('TV info source %s is marked inactive, aborting update for show %s and continue with refresh.' % (sickgear.TVInfoAPI(self.show_obj.tvid).config['name'], self.show_obj.name)) sickgear.show_queue_scheduler.action.refresh_show(self.show_obj, self.force, self.scheduled_update, after_update=True) return logger.log('Beginning update of %s' % self.show_obj.unique_name) logger.debug('Retrieving show info from %s' % sickgear.TVInfoAPI(self.show_obj.tvid).name) try: result = self.show_obj.load_from_tvinfo(cache=not self.force, tvinfo_data=self.tvinfo_data, scheduled_update=self.scheduled_update, switch=self.switch) if result in (None, False): return elif not self.show_obj.prodid == getattr(self.tvinfo_data, 'id', None): self.tvinfo_data = result except BaseTVinfoAttributenotfound as e: logger.error(f'Data retrieved from {sickgear.TVInfoAPI(self.show_obj.tvid).name} was incomplete,' f' aborting: {ex(e)}') return except BaseTVinfoError as e: logger.warning('Unable to contact %s, aborting: %s' % (sickgear.TVInfoAPI(self.show_obj.tvid).name, ex(e))) return if self.force_web: self.show_obj.load_imdb_info() try: self.show_obj.save_to_db() except (BaseException, Exception) as e: logger.error('Error saving the show to the database: %s' % ex(e)) logger.error(traceback.format_exc()) # get episode list from DB logger.debug('Loading all episodes from the database') db_ep_obj_list = self.show_obj.load_episodes_from_db(update=True) # get episode list from TVDB logger.debug('Loading all episodes from %s' % sickgear.TVInfoAPI(self.show_obj.tvid).name) try: tvinfo_ep_list = self.show_obj.load_episodes_from_tvinfo(cache=not self.force, update=True, tvinfo_data=self.tvinfo_data, switch=self.switch, old_tvid=self.old_tvid, old_prodid=self.old_prodid) except BaseTVinfoException as e: logger.error(f'Unable to get info from {sickgear.TVInfoAPI(self.show_obj.tvid).name},' f' the show info will not be refreshed: {ex(e)}') tvinfo_ep_list = None if None is tvinfo_ep_list: logger.error('No data returned from %s, unable to update episodes for show: %s' % (sickgear.TVInfoAPI(self.show_obj.tvid).name, self.show_obj.unique_name)) elif not tvinfo_ep_list or 0 == len(tvinfo_ep_list): logger.warning('No episodes returned from %s for show: %s' % (sickgear.TVInfoAPI(self.show_obj.tvid).name, self.show_obj.unique_name)) else: # for each ep we found on TVDB delete it from the DB list for cur_season in tvinfo_ep_list: for cur_episode in tvinfo_ep_list[cur_season]: logger.debug('Removing %sx%s from the DB list' % (cur_season, cur_episode)) if cur_season in db_ep_obj_list and cur_episode in db_ep_obj_list[cur_season]: del db_ep_obj_list[cur_season][cur_episode] cl = [] # for the remaining episodes in the DB list just delete them from the DB for cur_season in db_ep_obj_list: for cur_episode in db_ep_obj_list[cur_season]: ep_obj = self.show_obj.get_episode(cur_season, cur_episode) # type: Optional[TVEpisode] status = sickgear.common.Quality.split_composite_status(ep_obj.status)[0] if self.switch or should_delete_episode(status): if self.switch: cl.append(self.show_obj.switch_ep_change_sql( self.old_tvid, self.old_prodid, cur_season, cur_episode, TVSWITCH_EP_DELETED)) logger.log(f'Permanently deleting episode {cur_season}x{cur_episode} from the database') try: cl.extend(ep_obj.delete_episode(return_sql=True)) except exceptions_helper.EpisodeDeletedException: pass else: logger.log(f'Not deleting episode {cur_season}x{cur_episode} from the database' f' because status is: {statusStrings[status]}') if cl: my_db = db.DBConnection() my_db.mass_action(cl) # update indexer mapper once a month (using the day of the first ep as random date) update_over_month = (datetime.date.today() - last_update).days > 31 try: if (self.scheduled_update or update_over_month) and tvinfo_ep_list.get(1, {}).get(1, False): first_ep_airdate = self.show_obj.first_aired_regular_episode.airdate day = (first_ep_airdate.day, 28)[28 < first_ep_airdate.day] if datetime.date.today().day == day or update_over_month or \ -8 < (datetime.date.today() - first_ep_airdate).days < 31: map_indexers_to_show(self.show_obj, force=True) except (BaseException, Exception): pass if self.priority != generic_queue.QueuePriorities.NORMAL: self.kwargs['priority'] = self.priority if self.kwargs.get('switch_src', False) or old_name != self.show_obj.name: sickgear.webserve.Home.make_showlist_unique_names() sickgear.MEMCACHE['history_tab'] = sickgear.webserve.History.menu_tab( sickgear.MEMCACHE['history_tab_limit']) if not getattr(self, 'skip_refresh', False): sickgear.show_queue_scheduler.action.refresh_show(self.show_obj, self.force, self.scheduled_update, after_update=True, force_image_cache=self.force_web, **self.kwargs) def finish(self): self.tvinfo_data = None super(QueueItemUpdate, self).finish() class QueueItemForceUpdate(QueueItemUpdate): def __init__(self, show_obj=None, scheduled_update=False, **kwargs): # type: (TVShow, bool, Any) -> None """ :param show_obj: show object :type show_obj: sickgear.tv.TVShow :param scheduled_update: :type scheduled_update: bool :param kwargs: :param uid: """ QueueItemUpdate.__init__(self, show_obj, scheduled_update, **kwargs) self.action_id = ShowQueueActions.FORCEUPDATE self.force = True # type: bool self.force_web = False # type: bool self.kwargs = kwargs class QueueItemForceUpdateWeb(QueueItemUpdate): def __init__(self, show_obj=None, scheduled_update=False, priority=generic_queue.QueuePriorities.NORMAL, **kwargs): # type: (TVShow, bool, integer_types, Any) -> None """ :param show_obj: show object :type show_obj: sickgear.tv.TVShow :param scheduled_update: :type scheduled_update: bool :param priority: :type priority: int :param kwargs: :param uid: """ QueueItemUpdate.__init__(self, show_obj, scheduled_update, **kwargs) self.action_id = ShowQueueActions.WEBFORCEUPDATE self.force = True # type: bool self.force_web = True # type: bool self.priority = priority # type: int self.kwargs = kwargs class QueueItemSwitchSource(ShowQueueItem): def __init__(self, show_obj, # type: TVShow new_tvid, # type: integer_types new_prodid, # type: integer_types force_id=False, # type: bool uid=None, # type: integer_types set_pause=False, # type: bool mark_wanted=False, # type: bool resume=False, # type: bool old_tvid=None, # type: integer_types old_prodid=None, # type: integer_types **kwargs): """ :param show_obj: TV Show object :param new_tvid: new tvid :param new_prodid: new prodid :param force_id: skip verification and forcibly use new id :param uid: uid :param set_pause: set pause :param mark_wanted: mark wanted after switch :param resume: resume unfinished switch (id already switched) :param old_tvid: old tvid if resume set :param old_prodid: old prodid if resume set :param kwargs: """ ShowQueueItem.__init__(self, ShowQueueActions.SWITCH, show_obj, uid=uid) self.new_tvid = new_tvid # type: int self.new_prodid = new_prodid # type: integer_types self.old_tvid = old_tvid or show_obj.tvid # type: int self.old_prodid = old_prodid or show_obj.prodid # type: integer_types self.force_id = force_id # type: bool self.priority = SWITCH_PRIO # type: int self.progress = 'Not Started' # type: AnyStr self.set_pause = set_pause # type: bool self.mark_wanted = mark_wanted # type: bool self.resume = resume # type: bool self.kwargs = kwargs # type: Dict def _set_switch_tbl_status(self, status=TVSWITCH_NORMAL): # type: (integer_types) -> None """ sets status in table or deletes the entry if status: TVSWITCH_NORMAL :param status: """ my_db = db.DBConnection() if 0 == status: my_db.action('DELETE FROM tv_src_switch WHERE uid = ?', [self.uid]) else: my_db.action('UPDATE tv_src_switch SET status = ? WHERE uid = ?', [status, self.uid]) def _set_switch_id(self, new_id): # type: (integer_types) -> None """ set the new prodid of the show in db :param new_id: """ my_db = db.DBConnection() my_db.action('UPDATE tv_src_switch SET new_indexer_id = ? WHERE uid = ?', [new_id, self.uid]) def _check_same_id(self, new_prodid): # type: (integer_types) -> bool if new_prodid and self.old_tvid == self.new_tvid and new_prodid == self.old_prodid: if self.show_obj: which_show = self.show_obj.unique_name else: which_show = '%s:%s' % (self.old_tvid, self.old_prodid) self._set_switch_tbl_status(TVSWITCH_SAME_ID) logger.error('Unchanged ids given, nothing to do for %s' % which_show) return True return False def run(self): ShowQueueItem.run(self) td = None if self.resume: logger.log('Resume switching show: %s' % self.show_obj.unique_name) self.progress = 'Resume switching show' with self.show_obj.lock: pausestatus_after = None if not self.set_pause: self.show_obj.paused = False if not self.mark_wanted: self.show_obj.paused = True pausestatus_after = False elif not self.show_obj.paused: self.show_obj.paused = True else: logger.log('Start switching show: %s' % self.show_obj.unique_name) # verify show before switching self.progress = 'Verifying validity of new id' new_prodid = (self.new_prodid or self.show_obj.ids.get(self.new_tvid, {}).get('id'), self.new_prodid)[self.force_id and self.new_prodid not in (None, 0)] if self._check_same_id(new_prodid): return if not new_prodid: if not self.force_id: map_indexers_to_show(self.show_obj, recheck=True) if self.show_obj.ids.get(self.new_tvid, {}).get('id') not in (None, 0): new_prodid = self.show_obj.ids.get(self.new_tvid)['id'] if not new_prodid: if self.show_obj: which_show = self.show_obj.unique_name else: which_show = '%s:%s' % (self.old_tvid, self.old_prodid) ui.notifications.message('TV info source switch: %s' % which_show, 'Error: could not find a id for show on new tv info source') logger.warning('Error: could not find a id for show on new tv info source: %s' % which_show) self._set_switch_tbl_status(TVSWITCH_NO_NEW_ID) return if self._check_same_id(new_prodid): return try: m_show_obj = find_show_by_id({self.new_tvid: new_prodid}, no_mapped_ids=False, check_multishow=True) except exceptions_helper.MultipleShowObjectsException: msg = 'Duplicate shows in DB' if self.show_obj: which_show = self.show_obj.unique_name else: which_show = '%s:%s' % (self.old_tvid, self.old_prodid) logger.warning('Duplicate shows in DB for show: %s' % which_show) ui.notifications.message('TV info source switch: %s' % which_show, 'Error: %s' % msg) self._set_switch_tbl_status(TVSWITCH_DUPLICATE_SHOW) return if not self.show_obj or (m_show_obj and self.show_obj is not m_show_obj): msg = 'Unable to find the specified show' if self.show_obj: which_show = self.show_obj.unique_name else: which_show = '%s:%s' % (self.old_tvid, self.old_prodid) ui.notifications.message('TV info source switch: %s' % which_show, 'Error: %s' % msg) self._set_switch_tbl_status(TVSWITCH_SOURCE_NOT_FOUND_ERROR) logger.warning('Unable to find the specified show: %s' % which_show) return tvinfo_config = sickgear.TVInfoAPI(self.new_tvid).api_params.copy() tvinfo_config['cache'] = False tvinfo_config['language'] = self.show_obj._lang tvinfo_config['dvdorder'] = 0 != self.show_obj._dvdorder t = sickgear.TVInfoAPI(self.new_tvid).setup(**tvinfo_config) try: td = t.get_show(show_id=new_prodid, actors=True, language=self.show_obj._lang) except (BaseException, Exception): td = None if not self.force_id: map_indexers_to_show(self.show_obj, recheck=True) if new_prodid != self.show_obj.ids.get(self.new_tvid, {}).get('id') is not None: new_prodid = self.show_obj.ids.get(self.new_tvid, {}).get('id') try: td = t.get_show(show_id=new_prodid, actors=True, language=self.show_obj._lang) except (BaseException, Exception): td = None logger.warning(f'Failed to get new tv show id ({new_prodid})' f' from source {sickgear.TVInfoAPI(self.new_tvid).name}') if None is td: self._set_switch_tbl_status(TVSWITCH_NOT_FOUND_ERROR) msg = 'Show not found on new tv source' if self.show_obj: which_show = self.show_obj.unique_name else: which_show = '%s:%s' % (self.old_tvid, self.old_prodid) ui.notifications.message('TV info source switch: %s' % which_show, 'Error: %s' % msg) logger.warning('show: %s not found on new tv source' % self.show_obj.tvid_prodid) return try: new_show_startyear = parser().parse(td[1][1].firstaired).year if 1900 > new_show_startyear: new_show_startyear = None except (BaseException, Exception): new_show_startyear = None if not new_show_startyear: try: new_show_startyear = parser().parse(td.firstaired).year except (BaseException, Exception): new_show_startyear = None try: first_ep = self.show_obj.first_aired_regular_episode existing_show_startyear = first_ep and first_ep.airdate and first_ep.airdate.year if existing_show_startyear and 1900 > existing_show_startyear: existing_show_startyear = None except (BaseException, Exception): existing_show_startyear = None if not existing_show_startyear: existing_show_startyear = self.show_obj.startyear if not self.force_id \ and not ((clean_show_name(td.seriesname.lower()) == clean_show_name(self.show_obj.name.lower()) or any(1 for s, v in td.ids if v and v == self.show_obj.ids.get(s, {}).get('id'))) and (str(existing_show_startyear) == str(new_show_startyear) or abs(try_int(existing_show_startyear, 10) - try_int(new_show_startyear, 1)) <= 1)): self._set_switch_tbl_status(TVSWITCH_VERIFY_ERROR) logger.warning('Failed to verify new ids for show %s' % self.show_obj.unique_name) msg = 'Failed to verify the show on new source' if self.show_obj: which_show = self.show_obj.unique_name else: which_show = '%s:%s' % (self.old_tvid, self.old_prodid) ui.notifications.message('TV info source switch: %s' % which_show, 'Error: %s' % msg) return # switch show to new id with self.show_obj.lock: try: new_show_obj = find_show_by_id({self.new_tvid: new_prodid}) except (BaseException, Exception): new_show_obj = None if new_show_obj: self._set_switch_tbl_status(TVSWITCH_ID_CONFLICT) msg = 'Show %s new id conflicts with existing show: %s' % \ ('[%s (%s)]' % (self.show_obj.unique_name, self.show_obj.tvid_prodid), '[%s (%s)]' % (new_show_obj.unique_name, new_show_obj.tvid_prodid)) logger.warning(msg) return self.progress = 'Switching to new source' self._set_switch_id(new_prodid) self.show_obj.remove_character_images() self.show_obj._tvid = self.new_tvid self.show_obj._prodid = new_prodid new_tvid_prodid = TVidProdid({self.new_tvid: new_prodid})() old_tvid_prodid = TVidProdid({self.old_tvid: self.old_prodid})() for tp in (old_tvid_prodid, new_tvid_prodid): try: if tp in sickgear.switched_shows: sickgear.switched_shows.pop(tp) except (BaseException, Exception): pass try: if tp in itervalues(sickgear.switched_shows): sickgear.switched_shows = {k: v for k, v in iteritems(sickgear.switched_shows) if tp != v} except (BaseException, Exception): pass sickgear.switched_shows[TVidProdid({self.old_tvid: self.old_prodid})()] = \ TVidProdid({self.new_tvid: new_prodid})() pausestatus_after = None if not self.set_pause: self.show_obj.paused = False if not self.mark_wanted: self.show_obj.paused = True pausestatus_after = False elif not self.show_obj.paused: self.show_obj.paused = True self.show_obj.switch_infosrc(self.old_tvid, self.old_prodid, update_show=False, pausestatus_after=pausestatus_after) # we directly update and refresh the show without queue as part of the switch self.progress = 'Updating from new source' update_show = QueueItemUpdate(show_obj=self.show_obj, skip_refresh=True, pausestatus_after=pausestatus_after, switch=True, tvinfo_data=td, old_tvid=self.old_tvid, old_prodid=self.old_prodid, switch_src=True) update_show.run() self.progress = 'Refreshing from disk' refresh_show = QueueItemRefresh(show_obj=self.show_obj, force_image_cache=True, pausestatus_after=pausestatus_after, switch=True, force=True) refresh_show.run() self.progress = 'Finished Switch' # now remove from switch tbl self._set_switch_tbl_status() logger.log('Finished switching show: %s' % self.show_obj.unique_name) ui.notifications.message('TV info source switch: %s' % self.show_obj.unique_name, 'Finished switching show') def __str__(self): return '<Show Switch Queue Item: %s (%s to %s)>' % \ (self.show_obj.unique_name, sickgear.TVInfoAPI(self.old_tvid).name, (sickgear.TVInfoAPI(self.new_tvid).name, self.new_prodid)[self.old_tvid == self.new_tvid]) def __repr__(self): return self.__str__()