# Author: Nic Wolfe # URL: http://code.google.com/p/sickbeard/ # # This file is part of SickGear. # # SickGear is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # SickGear is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . from __future__ import with_statement import traceback import sickbeard from sickbeard.common import SKIPPED, WANTED, UNAIRED from sickbeard.tv import TVShow from sickbeard import exceptions, logger, ui, db from sickbeard import generic_queue from sickbeard import name_cache from sickbeard.exceptions import ex from sickbeard.blackandwhitelist import BlackAndWhiteList class ShowQueue(generic_queue.GenericQueue): def __init__(self): generic_queue.GenericQueue.__init__(self) self.queue_name = "SHOWQUEUE" def _isInQueue(self, show, actions): return show in [x.show for x in self.queue if x.action_id in actions] def _isBeingSomethinged(self, show, actions): return self.currentItem != None and show == self.currentItem.show and \ self.currentItem.action_id in actions def isInUpdateQueue(self, show): return self._isInQueue(show, (ShowQueueActions.UPDATE, ShowQueueActions.FORCEUPDATE)) def isInRefreshQueue(self, show): return self._isInQueue(show, (ShowQueueActions.REFRESH,)) def isInRenameQueue(self, show): return self._isInQueue(show, (ShowQueueActions.RENAME,)) def isInSubtitleQueue(self, show): return self._isInQueue(show, (ShowQueueActions.SUBTITLE,)) def isBeingAdded(self, show): return self._isBeingSomethinged(show, (ShowQueueActions.ADD,)) def isBeingUpdated(self, show): return self._isBeingSomethinged(show, (ShowQueueActions.UPDATE, ShowQueueActions.FORCEUPDATE)) def isBeingRefreshed(self, show): return self._isBeingSomethinged(show, (ShowQueueActions.REFRESH,)) def isBeingRenamed(self, show): return self._isBeingSomethinged(show, (ShowQueueActions.RENAME,)) def isBeingSubtitled(self, show): return self._isBeingSomethinged(show, (ShowQueueActions.SUBTITLE,)) def _getLoadingShowList(self): return [x for x in self.queue + [self.currentItem] if x != None and x.isLoading] loadingShowList = property(_getLoadingShowList) def updateShow(self, show, force=False, web=False): if self.isBeingAdded(show): raise exceptions.CantUpdateException( "Show is still being added, wait until it is finished before you update.") if self.isBeingUpdated(show): raise exceptions.CantUpdateException( "This show is already being updated, can't update again until it's done.") if self.isInUpdateQueue(show): raise exceptions.CantUpdateException( "This show is already being updated, can't update again until it's done.") if not force: queueItemObj = QueueItemUpdate(show) elif web: queueItemObj = QueueItemForceUpdateWeb(show) else: queueItemObj = QueueItemForceUpdate(show) self.add_item(queueItemObj) return queueItemObj def refreshShow(self, show, force=False): if self.isBeingRefreshed(show) and not force: raise exceptions.CantRefreshException("This show is already being refreshed, not refreshing again.") if (self.isBeingUpdated(show) or self.isInUpdateQueue(show)) and not force: logger.log( u"A refresh was attempted but there is already an update queued or in progress. Since updates do a refresh at the end anyway I'm skipping this request.", logger.DEBUG) return queueItemObj = QueueItemRefresh(show, force=force) self.add_item(queueItemObj) return queueItemObj def renameShowEpisodes(self, show, force=False): queueItemObj = QueueItemRename(show) self.add_item(queueItemObj) return queueItemObj def downloadSubtitles(self, show, force=False): queueItemObj = QueueItemSubtitle(show) self.add_item(queueItemObj) return queueItemObj def addShow(self, indexer, indexer_id, showDir, default_status=None, quality=None, flatten_folders=None, lang="en", subtitles=None, anime=None, scene=None, paused=None, blacklist=None, whitelist=None, wanted_begin=None, wanted_latest=None): queueItemObj = QueueItemAdd(indexer, indexer_id, showDir, default_status, quality, flatten_folders, lang, subtitles, anime, scene, paused, blacklist, whitelist, wanted_begin, wanted_latest) self.add_item(queueItemObj) return queueItemObj class ShowQueueActions: REFRESH = 1 ADD = 2 UPDATE = 3 FORCEUPDATE = 4 RENAME = 5 SUBTITLE = 6 names = {REFRESH: 'Refresh', ADD: 'Add', UPDATE: 'Update', FORCEUPDATE: 'Force Update', RENAME: 'Rename', SUBTITLE: 'Subtitle'} 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): generic_queue.QueueItem.__init__(self, ShowQueueActions.names[action_id], action_id) self.show = show def isInQueue(self): return self in sickbeard.showQueueScheduler.action.queue + [ sickbeard.showQueueScheduler.action.currentItem] #@UndefinedVariable def _getName(self): return str(self.show.indexerid) def _isLoading(self): return False show_name = property(_getName) isLoading = property(_isLoading) class QueueItemAdd(ShowQueueItem): def __init__(self, indexer, indexer_id, showDir, default_status, quality, flatten_folders, lang, subtitles, anime, scene, paused, blacklist, whitelist, default_wanted_begin, default_wanted_latest): self.indexer = indexer self.indexer_id = indexer_id self.showDir = showDir self.default_status = default_status self.default_wanted_begin = default_wanted_begin self.default_wanted_latest = default_wanted_latest self.quality = quality self.flatten_folders = flatten_folders self.lang = lang self.subtitles = subtitles self.anime = anime self.scene = scene self.paused = paused self.blacklist = blacklist self.whitelist = whitelist self.show = None # this will initialize self.show to None ShowQueueItem.__init__(self, ShowQueueActions.ADD, self.show) def _getName(self): """ Returns the show name if there is a show object created, if not returns the dir that the show is being added to. """ if self.show == None: return self.showDir return self.show.name show_name = property(_getName) def _isLoading(self): """ Returns True if we've gotten far enough to have a show object, or False if we still only know the folder name. """ if self.show == None: return True return False isLoading = property(_isLoading) def run(self): ShowQueueItem.run(self) logger.log(u"Starting to add show " + self.showDir) # make sure the Indexer IDs are valid try: lINDEXER_API_PARMS = sickbeard.indexerApi(self.indexer).api_params.copy() if self.lang: lINDEXER_API_PARMS['language'] = self.lang logger.log(u"" + str(sickbeard.indexerApi(self.indexer).name) + ": " + repr(lINDEXER_API_PARMS)) t = sickbeard.indexerApi(self.indexer).indexer(**lINDEXER_API_PARMS) s = t[self.indexer_id] # this usually only happens if they have an NFO in their show dir which gave us a Indexer ID that has no proper english version of the show if getattr(s, 'seriesname', None) is None: logger.log(u"Show in " + self.showDir + " has no name on " + str( sickbeard.indexerApi(self.indexer).name) + ", probably the wrong language used to search with.", logger.ERROR) ui.notifications.error("Unable to add show", "Show in " + self.showDir + " has no name on " + str(sickbeard.indexerApi( self.indexer).name) + ", probably the wrong language. Delete .nfo and add manually in the correct language.") self._finishEarly() return # if the show has no episodes/seasons if not s: logger.log(u"Show " + str(s['seriesname']) + " is on " + str( sickbeard.indexerApi(self.indexer).name) + " but contains no season/episode data.", logger.ERROR) ui.notifications.error("Unable to add show", "Show " + str(s['seriesname']) + " is on " + str(sickbeard.indexerApi( self.indexer).name) + " but contains no season/episode data.") self._finishEarly() return except Exception, e: logger.log(u"Unable to find show ID:" + str(self.indexer_id) + " on Indexer: " + str( sickbeard.indexerApi(self.indexer).name), logger.ERROR) ui.notifications.error("Unable to add show", "Unable to look up the show in " + self.showDir + " on " + str(sickbeard.indexerApi( self.indexer).name) + " using ID " + str( self.indexer_id) + ", not using the NFO. Delete .nfo and try adding manually again.") self._finishEarly() return try: newShow = TVShow(self.indexer, self.indexer_id, self.lang) newShow.loadFromIndexer() self.show = newShow # set up initial values self.show.location = self.showDir self.show.subtitles = self.subtitles if self.subtitles != None else sickbeard.SUBTITLES_DEFAULT self.show.quality = self.quality if self.quality else sickbeard.QUALITY_DEFAULT self.show.flatten_folders = self.flatten_folders if self.flatten_folders != None else sickbeard.FLATTEN_FOLDERS_DEFAULT self.show.anime = self.anime if self.anime != None else sickbeard.ANIME_DEFAULT self.show.scene = self.scene if self.scene != None else sickbeard.SCENE_DEFAULT self.show.paused = self.paused if self.paused != None else False if self.show.anime: self.show.release_groups = BlackAndWhiteList(self.show.indexerid) if self.blacklist: self.show.release_groups.set_black_keywords(self.blacklist) if self.whitelist: self.show.release_groups.set_white_keywords(self.whitelist) # be smartish about this if self.show.genre and "talk show" in self.show.genre.lower(): self.show.air_by_date = 1 if self.show.genre and "documentary" in self.show.genre.lower(): self.show.air_by_date = 0 if self.show.classification and "sports" in self.show.classification.lower(): self.show.sports = 1 except sickbeard.indexer_exception, e: logger.log( u"Unable to add show due to an error with " + sickbeard.indexerApi(self.indexer).name + ": " + ex(e), logger.ERROR) if self.show: ui.notifications.error( "Unable to add " + str(self.show.name) + " due to an error with " + sickbeard.indexerApi( self.indexer).name + "") else: ui.notifications.error( "Unable to add show due to an error with " + sickbeard.indexerApi(self.indexer).name + "") self._finishEarly() return except exceptions.MultipleShowObjectsException: logger.log(u"The show in " + self.showDir + " is already in your show list, skipping", logger.ERROR) ui.notifications.error('Show skipped', "The show in " + self.showDir + " is already in your show list") self._finishEarly() return except Exception, e: logger.log(u"Error trying to add show: " + ex(e), logger.ERROR) logger.log(traceback.format_exc(), logger.DEBUG) self._finishEarly() raise self.show.load_imdb_info() try: self.show.saveToDB() except Exception, e: logger.log(u"Error saving the show to the database: " + ex(e), logger.ERROR) logger.log(traceback.format_exc(), logger.DEBUG) self._finishEarly() raise # add it to the show list sickbeard.showList.append(self.show) try: self.show.loadEpisodesFromIndexer() except Exception, e: logger.log( u"Error with " + sickbeard.indexerApi(self.show.indexer).name + ", not creating episode list: " + ex(e), logger.ERROR) logger.log(traceback.format_exc(), logger.DEBUG) # update internal name cache name_cache.buildNameCache() try: self.show.loadEpisodesFromDir() except Exception, e: logger.log(u"Error searching directory for episodes: " + ex(e), logger.ERROR) logger.log(traceback.format_exc(), logger.DEBUG) # if they gave a custom status then change all the eps to it my_db = db.DBConnection() if self.default_status != SKIPPED: logger.log(u"Setting all episodes to the specified default status: " + str(self.default_status)) my_db.action("UPDATE tv_episodes SET status = ? WHERE status = ? AND showid = ? AND season != 0", [self.default_status, SKIPPED, self.show.indexerid]) # if they gave a number to start or number to end as wanted, then change those eps to it def get_wanted(db_obj, wanted_max, latest): actual = 0 if wanted_max: select_id = 'FROM [tv_episodes] t5 JOIN (SELECT t3.indexerid, t3.status, t3.season*1000000+t3.episode AS t3_se, t2.start_season FROM [tv_episodes] t3'\ + ' JOIN (SELECT t1.showid, M%s(t1.season) AS start_season' % ('IN', 'AX')[latest]\ + ', MAX(t1.airdate) AS airdate, t1.episode, t1.season*1000000+t1.episode AS se FROM [tv_episodes] t1'\ + ' WHERE %s=t1.showid' % self.show.indexerid\ + ' AND 0=t3_se ORDER BY t3_se %sSC' % ('A', 'DE')[latest]\ + ' %s) as t4' % (' LIMIT %s' % wanted_max, '')[-1 == wanted_max]\ + ' ON t4.indexerid=t5.indexerid'\ + '%s' % ('', ' AND t4.start_season=t5.season')[-1 == wanted_max]\ + ' AND t4.status NOT IN (%s)' % ','.join([str(x) for x in sickbeard.common.Quality.DOWNLOADED + [WANTED]]) select = 'SELECT t5.indexerid as indexerid, t5.season as season, t5.episode as episode, t5.status as status ' + select_id update = 'UPDATE [tv_episodes] SET status=%s WHERE indexerid IN (SELECT t5.indexerid %s)' % (WANTED, select_id) wanted_updates = db_obj.select(select) db_obj.action(update) result = db_obj.select("SELECT changes() as last FROM [tv_episodes]") for cur_result in result: actual = cur_result['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=%d' % (a['season'], a['episode'], a['status'])) for a in wanted_updates]))) logger.log('Get wanted ' + action_log) return actual items_wanted = get_wanted(my_db, self.default_wanted_begin, latest=False) items_wanted += get_wanted(my_db, self.default_wanted_latest, latest=True) 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(u'Launching backlog for this show since episodes are WANTED') sickbeard.backlogSearchScheduler.action.searchBacklog([self.show]) #@UndefinedVariable ui.notifications.message('Show added/search', 'Adding and searching for episodes of' + msg) else: ui.notifications.message('Show added', 'Adding' + msg) self.show.writeMetadata() self.show.updateMetadata() self.show.populateCache() self.show.flushEpisodes() if sickbeard.USE_TRAKT: # if there are specific episodes that need to be added by trakt sickbeard.traktCheckerScheduler.action.manageNewShow(self.show) # add show to trakt.tv library if sickbeard.TRAKT_SYNC: sickbeard.traktCheckerScheduler.action.addShowToTraktLibrary(self.show) # Load XEM data to DB for show sickbeard.scene_numbering.xem_refresh(self.show.indexerid, self.show.indexer, force=True) # check if show has XEM mapping so we can determine if searches should go by scene numbering or indexer numbering. if not self.scene and sickbeard.scene_numbering.get_xem_numbering_for_show(self.show.indexerid, self.show.indexer): self.show.scene = 1 self.finish() def _finishEarly(self): if self.show != None: self.show.deleteShow() self.finish() class QueueItemRefresh(ShowQueueItem): def __init__(self, show=None, force=False): ShowQueueItem.__init__(self, ShowQueueActions.REFRESH, show) # do refreshes first because they're quick self.priority = generic_queue.QueuePriorities.HIGH # force refresh certain items self.force = force def run(self): ShowQueueItem.run(self) logger.log(u"Performing refresh on " + self.show.name) self.show.refreshDir() self.show.writeMetadata() #if self.force: # self.show.updateMetadata() self.show.populateCache() # Load XEM data to DB for show sickbeard.scene_numbering.xem_refresh(self.show.indexerid, self.show.indexer) self.inProgress = False class QueueItemRename(ShowQueueItem): def __init__(self, show=None): ShowQueueItem.__init__(self, ShowQueueActions.RENAME, show) def run(self): ShowQueueItem.run(self) logger.log(u"Performing rename on " + self.show.name) try: show_loc = self.show.location except exceptions.ShowDirNotFoundException: logger.log(u"Can't perform rename on " + self.show.name + " when the show directory is missing.", logger.WARNING) return ep_obj_rename_list = [] ep_obj_list = self.show.getAllEpisodes(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.relatedEps: # do we have one of multi-episodes in the rename list already have_already = False for cur_related_ep in cur_ep_obj.relatedEps + [cur_ep_obj]: if cur_related_ep 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=None): ShowQueueItem.__init__(self, ShowQueueActions.SUBTITLE, show) def run(self): ShowQueueItem.run(self) logger.log(u"Downloading subtitles for " + self.show.name) self.show.downloadSubtitles() self.inProgress = False class QueueItemUpdate(ShowQueueItem): def __init__(self, show=None): ShowQueueItem.__init__(self, ShowQueueActions.UPDATE, show) self.force = False self.force_web = False def run(self): ShowQueueItem.run(self) logger.log(u"Beginning update of " + self.show.name) logger.log(u"Retrieving show info from " + sickbeard.indexerApi(self.show.indexer).name + "", logger.DEBUG) try: result = self.show.loadFromIndexer(cache=not self.force) if None is not result: return except sickbeard.indexer_error, e: logger.log(u"Unable to contact " + sickbeard.indexerApi(self.show.indexer).name + ", aborting: " + ex(e), logger.WARNING) return except sickbeard.indexer_attributenotfound, e: logger.log(u"Data retrieved from " + sickbeard.indexerApi( self.show.indexer).name + " was incomplete, aborting: " + ex(e), logger.ERROR) return if self.force_web: self.show.load_imdb_info() try: self.show.saveToDB() except Exception, e: logger.log(u"Error saving the episode to the database: " + ex(e), logger.ERROR) logger.log(traceback.format_exc(), logger.DEBUG) # get episode list from DB logger.log(u"Loading all episodes from the database", logger.DEBUG) DBEpList = self.show.loadEpisodesFromDB() # get episode list from TVDB logger.log(u"Loading all episodes from " + sickbeard.indexerApi(self.show.indexer).name + "", logger.DEBUG) try: IndexerEpList = self.show.loadEpisodesFromIndexer(cache=not self.force) except sickbeard.indexer_exception, e: logger.log(u"Unable to get info from " + sickbeard.indexerApi( self.show.indexer).name + ", the show info will not be refreshed: " + ex(e), logger.ERROR) IndexerEpList = None if IndexerEpList == None: logger.log(u"No data returned from " + sickbeard.indexerApi( self.show.indexer).name + ", unable to update this show", logger.ERROR) else: # for each ep we found on TVDB delete it from the DB list for curSeason in IndexerEpList: for curEpisode in IndexerEpList[curSeason]: logger.log(u"Removing " + str(curSeason) + "x" + str(curEpisode) + " from the DB list", logger.DEBUG) if curSeason in DBEpList and curEpisode in DBEpList[curSeason]: del DBEpList[curSeason][curEpisode] # for the remaining episodes in the DB list just delete them from the DB for curSeason in DBEpList: for curEpisode in DBEpList[curSeason]: logger.log(u"Permanently deleting episode " + str(curSeason) + "x" + str( curEpisode) + " from the database", logger.MESSAGE) curEp = self.show.getEpisode(curSeason, curEpisode) try: curEp.deleteEpisode() except exceptions.EpisodeDeletedException: pass sickbeard.showQueueScheduler.action.refreshShow(self.show, self.force) class QueueItemForceUpdate(QueueItemUpdate): def __init__(self, show=None): ShowQueueItem.__init__(self, ShowQueueActions.FORCEUPDATE, show) self.force = True self.force_web = False class QueueItemForceUpdateWeb(QueueItemUpdate): def __init__(self, show=None): ShowQueueItem.__init__(self, ShowQueueActions.FORCEUPDATE, show) self.force = True self.force_web = True