Skip to content

Commit

Permalink
Start on changes for changeIndexer page. (#9862)
Browse files Browse the repository at this point in the history
* Start on changes for  changeIndexer page.

* Separated new-show into new-show and new-show-search.vue.
* Added components:
  - change-indexer.vue
  - change-indexer-row.vue
  - select-indexer.vue

* Add ChangeIndexer QueueItem

* Added ChangeIndexer queueitem

* implement the api

* Conntect frontend / backend.
Enabled mass updating indexers.

* yarn dev

* Added menu item.
* Fixed merge conflicts

* Allow change episode status from SKIPPED to DOWNLOADED.
This is the case when we quickly remove/add a show with existing episodes.

* Remove a removed show from recentShows.

* Fix linting issues

* Fix tests. The specific test should result in a different new status now.

* yarn dev

* update snapshot

* Update changelog
  • Loading branch information
p0psicles authored Jan 12, 2022
1 parent 83575a7 commit fef2f5f
Show file tree
Hide file tree
Showing 36 changed files with 2,147 additions and 544 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#### New Features
- Add support for banner and background images to indexer tvmaze ([10234](https://github.com/pymedusa/Medusa/pull/10234))
- Add option for using ffprobe to validate postprocessed media ([10132](https://github.com/pymedusa/Medusa/pull/10132))
- Add change indexer page to change the current indexer for shows in bulk ([9862](https://github.com/pymedusa/Medusa/pull/9862))

#### Improvements
- Add column sorting for the add new show page search results ([10217](https://github.com/pymedusa/Medusa/pull/10217))
Expand Down
247 changes: 241 additions & 6 deletions medusa/queues/show_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
from medusa.logger.adapters.style import BraceAdapter
from medusa.name_cache import build_name_cache
from medusa.queues import generic_queue
from medusa.tv.series import SaveSeriesException, Series, SeriesIdentifier
from medusa.tv.series import ChangeIndexerException, SaveSeriesException, Series, SeriesIdentifier

from requests.exceptions import RequestException

Expand All @@ -75,6 +75,7 @@ def __init__(self):
SUBTITLE = 6
REMOVE = 7
SEASON_UPDATE = 8
CHANGE = 9

names = {
REFRESH: 'Refresh',
Expand All @@ -84,6 +85,7 @@ def __init__(self):
SUBTITLE: 'Subtitle',
REMOVE: 'Remove Show',
SEASON_UPDATE: 'Season Update',
CHANGE: 'Change Indexer'
}


Expand All @@ -95,7 +97,9 @@ class ShowQueue(generic_queue.GenericQueue):
ShowQueueActions.SEASON_UPDATE: 'The information on this page is in the process of being updated.',
ShowQueueActions.REFRESH: 'The episodes below are currently being refreshed from disk',
ShowQueueActions.SUBTITLE: 'Currently downloading subtitles for this show',
ShowQueueActions.CHANGE: "This show is in the process of changing it's indexer",
}

queue_mappings = {
ShowQueueActions.REFRESH: 'This show is queued to be refreshed.',
ShowQueueActions.UPDATE: 'This show is queued and awaiting an update.',
Expand Down Expand Up @@ -237,6 +241,12 @@ def addShow(self, indexer, indexer_id, show_dir, **options):

return queue_item_obj

def changeIndexer(self, old_slug, new_slug):
queue_item_obj = QueueItemChangeIndexer(old_slug, new_slug)
self.add_item(queue_item_obj)

return queue_item_obj

def removeShow(self, show, full=False):
if show is None:
raise CantRemoveShowException('Failed removing show: Show does not exist')
Expand Down Expand Up @@ -300,12 +310,234 @@ def _isLoading(self):
isLoading = property(_isLoading)


class QueueItemAdd(ShowQueueItem):
def __init__(self, indexer, indexer_id, show_dir, **options):
class QueueItemChangeIndexer(ShowQueueItem):
"""Queue Item for changing a shows indexer to another."""

def __init__(self, old_slug, new_slug):
"""
Initialize QueueItemChangeIndexer with an old slug and new slug.
Old slug will be used as the currently added show. Which is used to get all show options.
New slug is the to be created show.
"""
self.old_slug = old_slug
self.new_slug = new_slug
self.show_dir = None
self.root_dir = None

self.options = {}
self.old_show = None
self.new_show = None

# this will initialize self.show to None
ShowQueueItem.__init__(self, ShowQueueActions.CHANGE, self.old_show)

# Process add show in priority
self.priority = generic_queue.QueuePriorities.HIGH

def _store_options(self):
self.options = {
'default_status': None,
'quality': {'preferred': self.old_show.qualities_preferred, 'allowed': self.old_show.qualities_allowed},
'season_folders': self.old_show.season_folders,
'lang': self.old_show.lang,
'subtitles': self.old_show.subtitles,
'anime': self.old_show.anime,
'scene': self.old_show.scene,
'paused': self.old_show.paused,
'blacklist': self.old_show.release_groups.blacklist if self.old_show.release_groups else None,
'whitelist': self.old_show.release_groups.whitelist if self.old_show.release_groups else None,
'default_status_after': self.old_show.default_ep_status,
'root_dir': None,
'show_lists': self.old_show.show_lists
}

self.show_dir = self.old_show._location

def run(self):
"""Run QueueItemChangeIndexer queue item."""
step = []

# Small helper, to reduce code for messaging
def message_step(new_step):
step.append(new_step)
ws.Message('QueueItemShow', dict(
step=step, **self.to_json
)).push()

ShowQueueItem.run(self)

def get_show_from_slug(slug):
identifier = SeriesIdentifier.from_slug(slug)
if not identifier:
raise ChangeIndexerException(f'Could not create identifier with slug {slug}')

show = Series.find_by_identifier(identifier)
return show

try:
# Create reference to old show, before starting the remove it.
self.old_show = get_show_from_slug(self.old_slug)

# Store needed options.
self._store_options()

# Start of removing the old show
log.info(
'{id}: Removing {show}',
{'id': self.old_show.series_id, 'show': self.old_show.name}
)
message_step(f'Removing old show {self.old_show.name}')

# Need to first remove the episodes from the Trakt collection, because we need the list of
# Episodes from the db to know which eps to remove.
if app.USE_TRAKT:
message_step('Removing episodes from trakt collection')
try:
app.trakt_checker_scheduler.action.remove_show_trakt_library(self.old_show)
except TraktException as error:
log.warning(
'{id}: Unable to delete show {show} from Trakt.'
' Please remove manually otherwise it will be added again.'
' Error: {error_msg}',
{'id': self.old_show.series_id, 'show': self.old_show.name, 'error_msg': error}
)
except Exception as error:
log.exception('Exception occurred while trying to delete show {show}, error: {error',
{'show': self.old_show.name, 'error': error})

self.old_show.delete_show(full=False)

# Send showRemoved to frontend, so we can remove it from localStorage.
ws.Message('showRemoved', self.old_show.to_json(detailed=False)).push() # Send ws update to client

# Double check to see if the show really has been removed, else bail.
if get_show_from_slug(self.old_slug):
raise ChangeIndexerException(f'Could not create identifier with slug {self.old_slug}')

# Start adding the new show
log.info(
'Starting to add show by {0}',
('show_dir: {0}'.format(self.show_dir)
if self.show_dir else
'New slug: {0}'.format(self.new_slug))
)

self.new_show = Series.from_identifier(SeriesIdentifier.from_slug(self.new_slug))

try:
# Push an update to any open Web UIs through the WebSocket
message_step('load show from {indexer}'.format(indexer=indexerApi(self.new_show.indexer).name))

api = self.new_show.identifier.get_indexer_api(self.options)

if getattr(api[self.new_show.series_id], 'seriesname', None) is None:
log.error(
'Show in {path} has no name on {indexer}, probably searched with the wrong language.',
{'path': self.show_dir, 'indexer': indexerApi(self.new_show.indexer).name}
)

ui.notifications.error(
'Unable to add show',
'Show in {path} has no name on {indexer}, probably the wrong language.'
' Delete .nfo and manually add the correct language.'.format(
path=self.show_dir, indexer=indexerApi(self.new_show.indexer).name)
)
self._finish_early()
raise SaveSeriesException('Indexer is missing a showname in this language: {0!r}')

self.new_show.load_from_indexer(tvapi=api)

message_step('load info from imdb')
self.new_show.load_imdb_info()
except IndexerException as error:
log.warning('Unable to load series from indexer: {0!r}'.format(error))
raise SaveSeriesException('Unable to load series from indexer: {0!r}'.format(error))

try:
message_step('configure show options')
self.new_show.configure(self)
except KeyError as error:
log.error(
'Unable to add show {series_name} due to an error with one of the provided options: {error}',
{'series_name': self.new_show.name, 'error': error}
)
ui.notifications.error(
'Unable to add show {series_name} due to an error with one of the provided options: {error}'.format(
series_name=self.new_show.name, error=error
)
)
raise SaveSeriesException(
'Unable to add show {series_name} due to an error with one of the provided options: {error}'.format(
series_name=self.new_show.name, error=error
))

except Exception as error:
log.error('Error trying to configure show: {0}', error)
log.debug(traceback.format_exc())
raise

app.showList.append(self.new_show)
self.new_show.save_to_db()

try:
message_step('load episodes from {indexer}'.format(indexer=indexerApi(self.new_show.indexer).name))
self.new_show.load_episodes_from_indexer(tvapi=api)
# If we provide a default_status_after through the apiv2 series route options object.
# set it after we've added the episodes.
self.new_show.default_ep_status = self.options['default_status_after'] or app.STATUS_DEFAULT_AFTER

except IndexerException as error:
log.warning('Unable to load series episodes from indexer: {0!r}'.format(error))
raise SaveSeriesException(
'Unable to load series episodes from indexer: {0!r}'.format(error)
)

# show_dir, default_status, quality, season_folders, lang, subtitles, anime,
# scene, paused, blacklist, whitelist, default_status_after, root_dir, show_lists):
message_step('create metadata in show folder')
self.new_show.write_metadata()
self.new_show.update_metadata()
self.new_show.populate_cache()
build_name_cache(self.new_show) # update internal name cache
self.new_show.flush_episodes()
self.new_show.sync_trakt()

message_step('add scene numbering')
self.new_show.add_scene_numbering()

if self.show_dir:
# If a show dir was passed, this was added as an existing show.
# For new shows we shouldn't have any files on disk.
message_step('refresh episodes from disk')
try:
app.show_queue_scheduler.action.refreshShow(self.new_show)
except CantRefreshShowException as error:
log.warning('Unable to rescan episodes from disk: {0!r}'.format(error))

except (ChangeIndexerException, SaveSeriesException) as error:
log.warning('Unable to add series: {0!r}'.format(error))
self.success = False
self._finish_early()
log.debug(traceback.format_exc())

default_status = self.options['default_status'] or app.STATUS_DEFAULT
if statusStrings[default_status] == 'Wanted':
message_step('trigger backlog search')
app.backlog_search_scheduler.action.search_backlog([self.new_show])

self.success = True

ws.Message('showAdded', self.new_show.to_json(detailed=False)).push() # Send ws update to client
message_step('finished')
self.finish()

def _finish_early(self):
if self.new_show is not None:
app.show_queue_scheduler.action.removeShow(self.new_show)
self.finish()


class QueueItemAdd(ShowQueueItem):
def __init__(self, indexer, indexer_id, show_dir, **options):
self.indexer = indexer
self.indexer_id = indexer_id
self.show_dir = ensure_text(show_dir) if show_dir else None
Expand Down Expand Up @@ -491,7 +723,10 @@ def _finish_early(self):


class QueueItemRefresh(ShowQueueItem):
"""QueueItemRefresh class."""

def __init__(self, show=None, force=False):
"""Queue item refresh constructor."""
ShowQueueItem.__init__(self, ShowQueueActions.REFRESH, show)

# do refreshes first because they're quick
Expand All @@ -501,7 +736,7 @@ def __init__(self, show=None, force=False):
self.force = force

def run(self):

"""Run QueueItemRefresh queue item."""
ShowQueueItem.run(self)

log.info(
Expand Down
2 changes: 1 addition & 1 deletion medusa/server/api/v2/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def post(self, series_slug=None, path_param=None):
'paused': data_options.get('paused'),
'blacklist': data_options['release'].get('blacklist', []) if data_options.get('release') else None,
'whitelist': data_options['release'].get('whitelist', []) if data_options.get('release') else None,
'default_status_after': data_options.get('statusAfter'),
'default_status_after': None,
'root_dir': data_options.get('rootDir'),
'show_lists': data_options.get('showLists')
}
Expand Down
35 changes: 35 additions & 0 deletions medusa/server/api/v2/series_change_indexer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# coding=utf-8
"""Request handler for series assets."""
from __future__ import unicode_literals

from medusa import app
from medusa.server.api.v2.base import BaseRequestHandler
from medusa.tv.series import Series, SeriesIdentifier

from tornado.escape import json_decode


class SeriesChangeIndexer(BaseRequestHandler):
"""Change shows indexer."""

#: resource name
name = 'changeindexer'
#: identifier
identifier = None
#: allowed HTTP methods
allowed_methods = ('POST', )

def post(self):
"""Change an existing show's indexer to another."""
data = json_decode(self.request.body)
old_slug = data.get('oldSlug')
new_slug = data.get('newSlug')

identifier = SeriesIdentifier.from_slug(old_slug)
series_obj = Series.find_by_identifier(identifier)
if not series_obj:
return self._not_found(f'Could not find a show to change indexer with slug {old_slug}')

queue_item_obj = app.show_queue_scheduler.action.changeIndexer(old_slug, new_slug)

return self._created(data=queue_item_obj.to_json)
4 changes: 4 additions & 0 deletions medusa/server/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from medusa.server.api.v2.search import SearchHandler
from medusa.server.api.v2.series import SeriesHandler
from medusa.server.api.v2.series_asset import SeriesAssetHandler
from medusa.server.api.v2.series_change_indexer import SeriesChangeIndexer
from medusa.server.api.v2.series_legacy import SeriesLegacyHandler
from medusa.server.api.v2.series_mass_edit import SeriesMassEdit
from medusa.server.api.v2.series_mass_operation import SeriesMassOperation
Expand Down Expand Up @@ -117,6 +118,9 @@ def get_apiv2_handlers(base):
SeriesMassEdit.create_app_handler(base),
# /api/v2/massupdate
SeriesMassOperation.create_app_handler(base),

# /api/v2/series/changeindexer
SeriesChangeIndexer.create_app_handler(base),
# /api/v2/series/tvdb1234/operation
SeriesOperationHandler.create_app_handler(base),
# /api/v2/series/tvdb1234/asset
Expand Down
8 changes: 8 additions & 0 deletions medusa/server/web/manage/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ def episodeStatuses(self, status=None):
"""
return PageTemplate(rh=self, filename='index.mako').render()

def changeIndexer(self):
"""
Render manage/changeIndexer page.
[Converted to VueRouter]
"""
return PageTemplate(rh=self, filename='index.mako').render()

def subtitleMissed(self, whichSubs=None):
"""
Serve manageEpisodeStatus page.
Expand Down
Loading

0 comments on commit fef2f5f

Please sign in to comment.