Repository: pymedusa/Medusa

Issue:
Change Episode Status in /manage fails 
Display snatched episodes (/manage/episodeStatuses?whichStatus=2) and try to change the status back to wanted/failed.   Submit and white screen.    Error in the log:

2021-02-12 16:28:50 ERROR    Thread_3 :: [b21beeb] Exception generated: 'Manage' object has no attribute 'setStatus'
Traceback (most recent call last):
  File "/app/medusa/medusa/server/web/core/base.py", line 252, in async_call
    result = function(**kwargs)
  File "/app/medusa/medusa/server/web/manage/handler.py", line 175, in changeEpisodeStatuses
    self.setStatus(
AttributeError: 'Manage' object has no attribute 'setStatus'

Status change works on the individual show pages. 


Medusa Info: | Branch: master
Commit: b21beeb3be5c7e1dec8cbe4bfc13b565b1e1a0b9




Discussion:
Seeing this too.
I am experiencing this as well, tried to submit, but for some reason cannot.
Seeing this as well. Also submitted and it tied it automatically to https://github.com/pymedusa/Medusa/issues/9215. 

Resolving patch:
diff --git a/medusa/server/web/home/handler.py b/medusa/server/web/home/handler.py
--- a/medusa/server/web/home/handler.py
+++ b/medusa/server/web/home/handler.py
@@ -28,6 +28,7 @@
     ARCHIVED,
     DOWNLOADED,
     FAILED,
+    IGNORED,
     Quality,
     SKIPPED,
     SNATCHED,
@@ -36,6 +37,7 @@
     UNAIRED,
     WANTED,
     cpu_presets,
+    statusStrings
 )
 from medusa.helper.exceptions import (
     AnidbAdbaConnectionException,
@@ -64,6 +66,7 @@
 )
 from medusa.search.queue import (
     BacklogQueueItem,
+    FailedQueueItem,
     SnatchQueueItem,
 )
 from medusa.server.web.core import (
@@ -1150,6 +1153,181 @@ def updateEMBY(self, showslug=None):
         else:
             return self.redirect('/home/')
 
+    def setStatus(self, showslug=None, eps=None, status=None, direct=False):
+        # @TODO: Merge this with the other PUT commands for /api/v2/show/{id}
+        # Still used by manage/changeEpisodeStatuses (manage_episodeStatuses.mako)
+        if not all([showslug, eps, status]):
+            error_message = 'You must specify a show and at least one episode'
+            if direct:
+                ui.notifications.error('Error', error_message)
+                return json.dumps({
+                    'result': 'error',
+                })
+            else:
+                return self._genericMessage('Error', error_message)
+
+        status = int(status)
+        if status not in statusStrings:
+            error_message = 'Invalid status'
+            if direct:
+                ui.notifications.error('Error', error_message)
+                return json.dumps({
+                    'result': 'error',
+                })
+            else:
+                return self._genericMessage('Error', error_message)
+
+        identifier = SeriesIdentifier.from_slug(showslug)
+        series_obj = Series.find_by_identifier(identifier)
+
+        if not series_obj:
+            error_message = 'Error', 'Show not in show list'
+            if direct:
+                ui.notifications.error('Error', error_message)
+                return json.dumps({
+                    'result': 'error',
+                })
+            else:
+                return self._genericMessage('Error', error_message)
+
+        segments = {}
+        trakt_data = []
+        if eps:
+
+            sql_l = []
+            for cur_ep in eps.split('|'):
+
+                if not cur_ep:
+                    logger.log('Current episode was empty when trying to set status', logger.DEBUG)
+
+                logger.log('Attempting to set status for episode {series} {episode} to {status}'.format(
+                    series=series_obj.name, episode=cur_ep, status=status), logger.DEBUG)
+
+                season_no, episode_no = cur_ep.lstrip('s').split('e')
+                if not all([season_no, episode_no]):
+                    logger.log('Something went wrong when trying to set status, season: {season}, episode: {episode}'.format
+                               (season=season_no, episode=episode_no), logger.DEBUG)
+                    continue
+
+                ep_obj = series_obj.get_episode(season_no, episode_no)
+                if not ep_obj:
+                    return self._genericMessage('Error', "Episode couldn't be retrieved")
+
+                status = int(status)
+                if status in [WANTED, FAILED]:
+                    # figure out what episodes are wanted so we can backlog them
+                    if ep_obj.season in segments:
+                        segments[ep_obj.season].append(ep_obj)
+                    else:
+                        segments[ep_obj.season] = [ep_obj]
+
+                with ep_obj.lock:
+                    if ep_obj.status == UNAIRED:
+                        logger.log('Refusing to change status of {series} {episode} because it is UNAIRED'.format(
+                            series=series_obj.name, episode=cur_ep), logger.WARNING)
+                        continue
+
+                    snatched_qualities = [SNATCHED, SNATCHED_PROPER, SNATCHED_BEST]
+
+                    if status == DOWNLOADED and not (
+                            ep_obj.status in snatched_qualities + [DOWNLOADED]
+                            or os.path.isfile(ep_obj.location)):
+                        logger.log('Refusing to change status of {series} {episode} to DOWNLOADED'
+                                   " because it's not SNATCHED/DOWNLOADED or the file is missing".format(
+                                       series=series_obj.name, episode=cur_ep), logger.WARNING)
+                        continue
+
+                    if status == FAILED and ep_obj.status not in snatched_qualities + [DOWNLOADED, ARCHIVED]:
+                        logger.log('Refusing to change status of {series} {episode} to FAILED'
+                                   " because it's not SNATCHED/DOWNLOADED/ARCHIVED".format(
+                                       series=series_obj.name, episode=cur_ep), logger.WARNING)
+                        continue
+
+                    if status == WANTED:
+                        if ep_obj.status in [DOWNLOADED, ARCHIVED]:
+                            logger.log('Removing release_name of {series} {episode} as episode was changed to WANTED'.format(
+                                series=series_obj.name, episode=cur_ep), logger.DEBUG)
+                            ep_obj.release_name = ''
+
+                        if ep_obj.manually_searched:
+                            logger.log("Resetting 'manually searched' flag of {series} {episode}"
+                                       ' as episode was changed to WANTED'.format(
+                                           series=series_obj.name, episode=cur_ep), logger.DEBUG)
+                            ep_obj.manually_searched = False
+
+                    # Only in failed_history we set to FAILED.
+                    if status != FAILED:
+                        ep_obj.status = status
+
+                    # mass add to database
+                    sql_l.append(ep_obj.get_sql())
+
+                    trakt_data.append(ep_obj)
+
+            if app.USE_TRAKT and app.TRAKT_SYNC_WATCHLIST:
+                if status in [WANTED, FAILED]:
+                    upd = 'Add'
+                elif status in [IGNORED, SKIPPED, DOWNLOADED, ARCHIVED]:
+                    upd = 'Remove'
+
+                logger.log('{action} episodes, showid: indexerid {show.indexerid}, Title {show.name} to Watchlist'.format(
+                    action=upd, show=series_obj), logger.DEBUG)
+
+                if trakt_data:
+                    for ep_obj in trakt_data:
+                        notifiers.trakt_notifier.update_watchlist_episode(series_obj, ep_obj)
+
+            if sql_l:
+                main_db_con = db.DBConnection()
+                main_db_con.mass_action(sql_l)
+
+        if status == WANTED and not series_obj.paused:
+            msg = 'Backlog was automatically started for the following seasons of <b>{show}</b>:<br>'.format(show=series_obj.name)
+            msg += '<ul>'
+
+            for season, segment in iteritems(segments):
+                cur_backlog_queue_item = BacklogQueueItem(series_obj, segment)
+                app.search_queue_scheduler.action.add_item(cur_backlog_queue_item)
+
+                msg += '<li>Season {season}</li>'.format(season=season)
+                logger.log(u'Sending backlog for {show} season {season} '
+                           u'because some episodes were set to wanted'.format
+                           (show=series_obj.name, season=season))
+
+            msg += '</ul>'
+
+            if segments:
+                ui.notifications.message('Backlog started', msg)
+        elif status == WANTED and series_obj.paused:
+            logger.log(u'Some episodes were set to wanted, but {show} is paused. '
+                       u'Not adding to Backlog until show is unpaused'.format
+                       (show=series_obj.name))
+
+        if status == FAILED:
+            msg = 'Retrying Search was automatically started for the following season of <b>{show}</b>:<br>'.format(show=series_obj.name)
+            msg += '<ul>'
+
+            for season, segment in iteritems(segments):
+                cur_failed_queue_item = FailedQueueItem(series_obj, segment)
+                app.search_queue_scheduler.action.add_item(cur_failed_queue_item)
+
+                msg += '<li>Season {season}</li>'.format(season=season)
+                logger.log(u'Retrying Search for {show} season {season} '
+                           u'because some episodes were set to failed'.format
+                           (show=series_obj.name, season=season))
+
+            msg += '</ul>'
+
+            if segments:
+                ui.notifications.message('Retry Search started', msg)
+
+        if direct:
+            return json.dumps({
+                'result': 'success',
+            })
+        else:
+            return self.redirect('/home/displayShow?showslug={series_obj.slug}'.format(series_obj=series_obj))
+
     def testRename(self, showslug=None):
         if not showslug:
             return self._genericMessage('Error', 'You must specify a show')
diff --git a/medusa/server/web/manage/handler.py b/medusa/server/web/manage/handler.py
--- a/medusa/server/web/manage/handler.py
+++ b/medusa/server/web/manage/handler.py
@@ -173,8 +173,7 @@ def changeEpisodeStatuses(self, oldStatus, newStatus, *args, **kwargs):
                 to_change[cur_indexer_id, cur_series_id] = all_eps
 
             self.setStatus(
-                indexername=indexer_id_to_name(int(cur_indexer_id)),
-                seriesid=cur_series_id,
+                showslug=f'{indexer_id_to_name(int(cur_indexer_id))}{cur_series_id}',
                 eps='|'.join(to_change[(cur_indexer_id, cur_series_id)]),
                 status=newStatus,
                 direct=True


Test patch: