SickGear/sickgear/show_queue.py
JackDandy fce8878fa9 Add menu Shows/"TMDB Cards".
Add top rated, popular, trending today, trending this week, to TMDB cards.
Change Shows/Add show... Trakt and IMDb card descriptions to be dynamic.
Add network data to trakt cards.
Add fallback for show cast when no suitable items are found in tvdb_api.
Change refactor for trakt lib update.
Change sanitise api response dates for cards (an invalid trakt date caused a UI glitch).
Fix issue where trakt can return no tvdb id causing a card to not display.
Change view-show TVDb genre links to use anon link setting.
Change remove displayShow.tmpl code for '|' sep as it is a fixed list string at data source.
Change add TVINFO_FANSITE placeholder icon.
Change refactor api sg.`CMD_SickGearShowsBrowseTrakt`.
---
Refactor date handling to make it's usage consistent from indexerapi
Split when_past out to to started_past and return_past for UI.
Fix trakt returning dates.
Change bring properly into use, the proper new season returning date.
Change remove PY2 unused import statements.
---
Fix issue with Trakt cards where an incorrect cache image obscures the default card placeholder image.
Simplify airtime for cards.
Remove nonsense TMDB api overview text.
Rearrange cards order on top menu.
---
Remove unused vars and simplify regex use cases.
Fix tv cards issue with Trakt and Tvmaze where show_info does not carry a first episode_info.
Instantiate with TVInfoEpisode() instead of None, so that expected properties exist without a need for Nonetype tests.
Change simplify `overview` and `airtime` use after lib output type is made trustable.
Change cards view to not allow items with no seriesname.
2023-05-03 00:43:59 +01:00

1807 lines
77 KiB
Python

#
# 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__()