diff --git a/CHANGELOG.md b/CHANGELOG.md index 45dba195ce..18fd00d5b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,17 @@ #### Improvements - Shows without any episodes can now be added ([#6977](https://github.com/pymedusa/Medusa/pull/6977)) +- Vueified displayShow ([#6709](https://github.com/pymedusa/Medusa/pull/6709)) + - New subtitles search UI component + - Direct toggle of show options on displayShow page like the checks for Subtitles, Season Folders, Paused, etc. + - Mark episodes as "watched" + - Added pagination + - Added search field, that searches columns like Title, File and Episode number #### Fixes - Fixed AnimeBytes daily search, for multi-ep results ([#7190](https://github.com/pymedusa/Medusa/pull/7190)) - Fixed rare UnicodeDecodeError when parsing titles with Python 2.7 ([#7192](https://github.com/pymedusa/Medusa/pull/7192)) +- Fixed displayShow loading of large shows with many seasons e.g. daily shows ([#6977](https://github.com/pymedusa/Medusa/pull/6977)) - Fixed torrent checker for client Transmission running on python 3 ([#7250](https://github.com/pymedusa/Medusa/pull/7250)) - Fixed provider beyond-hd due to layout changes ([#7250](https://github.com/pymedusa/Medusa/pull/7250)) - Fixed provider bj-share due to layout changes ([#7250](https://github.com/pymedusa/Medusa/pull/7250)) diff --git a/medusa/__main__.py b/medusa/__main__.py index d5d3466c30..b68b03f265 100755 --- a/medusa/__main__.py +++ b/medusa/__main__.py @@ -970,7 +970,6 @@ def initialize(self, console_logging=True): app.TIMEZONE_DISPLAY = check_setting_str(app.CFG, 'GUI', 'timezone_display', 'local') app.POSTER_SORTBY = check_setting_str(app.CFG, 'GUI', 'poster_sortby', 'name') app.POSTER_SORTDIR = check_setting_int(app.CFG, 'GUI', 'poster_sortdir', 1) - app.DISPLAY_ALL_SEASONS = bool(check_setting_int(app.CFG, 'General', 'display_all_seasons', 1)) app.RECENTLY_DELETED = set() app.RELEASES_IN_PP = [] app.GIT_REMOTE_BRANCHES = [] @@ -1602,7 +1601,6 @@ def save_config(): new_config['General']['no_restart'] = int(app.NO_RESTART) new_config['General']['developer'] = int(app.DEVELOPER) new_config['General']['python_version'] = app.PYTHON_VERSION - new_config['General']['display_all_seasons'] = int(app.DISPLAY_ALL_SEASONS) new_config['General']['news_last_read'] = app.NEWS_LAST_READ new_config['General']['broken_providers'] = helpers.get_broken_providers() or app.BROKEN_PROVIDERS new_config['General']['selected_root'] = int(app.SELECTED_ROOT) diff --git a/medusa/app.py b/medusa/app.py index 0d69459004..9f0dfdbf7c 100644 --- a/medusa/app.py +++ b/medusa/app.py @@ -196,7 +196,6 @@ def __init__(self): self.SORT_ARTICLE = False self.DEBUG = False self.DBDEBUG = False - self.DISPLAY_ALL_SEASONS = True self.DEFAULT_PAGE = 'home' self.SEEDERS_LEECHERS_IN_NOTIFY = True self.SHOW_LIST_ORDER = ['Anime', 'Series'] diff --git a/medusa/common.py b/medusa/common.py index dea20cf737..096faa8b27 100644 --- a/medusa/common.py +++ b/medusa/common.py @@ -808,8 +808,8 @@ class Overview(object): overviewStrings = { SKIPPED: 'skipped', WANTED: 'wanted', - QUAL: 'qual', - GOOD: 'good', + QUAL: 'allowed', + GOOD: 'preferred', UNAIRED: 'unaired', SNATCHED: 'snatched', # we can give these a different class later, otherwise diff --git a/medusa/search/manual.py b/medusa/search/manual.py index 38f6ea19bb..500a291669 100644 --- a/medusa/search/manual.py +++ b/medusa/search/manual.py @@ -86,23 +86,31 @@ def get_episodes(search_thread, searchstatus): for ep_obj in search_thread.segment: ep = series_obj.get_episode(ep_obj.season, ep_obj.episode) results.append({ - 'indexer_id': series_obj.indexer, - 'series_id': series_obj.series_id, - 'episode': ep.episode, - 'episodeindexerid': ep.indexerid, - 'season': ep.season, - 'searchstatus': searchstatus, - 'status': statusStrings[ep.status], - # TODO: `quality_name` and `quality_style` should both be removed - # when converting forced/manual episode search to Vue (use QualityPill component directly) - 'quality_name': Quality.qualityStrings[ep.quality], - 'quality_style': Quality.quality_keys.get(ep.quality) or Quality.quality_keys[Quality.UNKNOWN], - 'overview': Overview.overviewStrings[series_obj.get_overview( - ep.status, ep.quality, - manually_searched=ep.manually_searched - )], - 'queuetime': search_thread.queue_time.isoformat(), - 'starttime': search_thread.start_time.isoformat() if search_thread.start_time else None, + 'show': { + 'indexer': series_obj.indexer, + 'series_id': series_obj.series_id, + 'slug': series_obj.slug + }, + 'episode': { + 'episode': ep.episode, + 'season': ep.season, + 'slug': ep.slug, + 'indexerid': ep.indexerid, + 'status': statusStrings[ep.status], + # TODO: `quality_name` and `quality_style` should both be removed + # when converting forced/manual episode search to Vue (use QualityPill component directly) + 'quality_name': Quality.qualityStrings[ep.quality], + 'quality_style': Quality.quality_keys.get(ep.quality) or Quality.quality_keys[Quality.UNKNOWN], + 'overview': Overview.overviewStrings[series_obj.get_overview( + ep.status, ep.quality, + manually_searched=ep.manually_searched + )] + }, + 'search': { + 'status': searchstatus, + 'queuetime': search_thread.queue_time.isoformat(), + 'starttime': search_thread.start_time.isoformat() if search_thread.start_time else None + } }) return results @@ -163,11 +171,11 @@ def collect_episodes_from_search_thread(series_obj): continue if isinstance(search_thread, ManualSearchQueueItem): - if not [x for x in episodes if x['episodeindexerid'] in [search.indexerid for search in search_thread.segment]]: + if not [x for x in episodes if x['episode']['indexerid'] in [search.indexerid for search in search_thread.segment]]: episodes += get_episodes(search_thread, searchstatus) else: # These are only Failed Downloads/Retry search thread items.. lets loop through the segment/episodes - if not [i for i, j in zip(search_thread.segment, episodes) if i.indexerid == j['episodeindexerid']]: + if not [i for i, j in zip(search_thread.segment, episodes) if i.indexerid == j['episode']['indexerid']]: episodes += get_episodes(search_thread, searchstatus) return episodes diff --git a/medusa/search/queue.py b/medusa/search/queue.py index 6b695b9a9a..8824125433 100644 --- a/medusa/search/queue.py +++ b/medusa/search/queue.py @@ -105,7 +105,7 @@ def force_daily(self): class ForcedSearchQueue(generic_queue.GenericQueue): - """Search Queueu used for Manual, Failed Search.""" + """Search Queue used for Manual, (forced) Backlog and Failed Search.""" def __init__(self): """Initialize ForcedSearch Queue.""" @@ -346,7 +346,7 @@ def __init__(self, show, segment, manual_search_type='episode'): self.manual_search_type = manual_search_type def run(self): - """Run forced search thread.""" + """Run manual search thread.""" generic_queue.QueueItem.run(self) self.started = True @@ -551,6 +551,9 @@ def run(self): self.success = False log.debug(traceback.format_exc()) + # Keep a list with the 100 last executed searches + fifo(SEARCH_HISTORY, self, SEARCH_HISTORY_SIZE) + if self.success is None: self.success = False @@ -648,7 +651,7 @@ def run(self): self.success = False log.info(traceback.format_exc()) - # ## Keep a list with the 100 last executed searches + # Keep a list with the 100 last executed searches fifo(SEARCH_HISTORY, self, SEARCH_HISTORY_SIZE) if self.success is None: diff --git a/medusa/server/api/v2/config.py b/medusa/server/api/v2/config.py index 5de436ad6e..d28175bed6 100644 --- a/medusa/server/api/v2/config.py +++ b/medusa/server/api/v2/config.py @@ -122,7 +122,6 @@ class ConfigHandler(BaseRequestHandler): 'layout.history': EnumField(app, 'HISTORY_LAYOUT', ('compact', 'detailed'), default_value='detailed'), 'layout.home': EnumField(app, 'HOME_LAYOUT', ('poster', 'small', 'banner', 'simple', 'coverflow'), default_value='poster'), - 'layout.show.allSeasons': BooleanField(app, 'DISPLAY_ALL_SEASONS'), 'layout.show.specials': BooleanField(app, 'DISPLAY_SHOW_SPECIALS'), 'layout.show.showListOrder': ListField(app, 'SHOW_LIST_ORDER'), 'theme.name': StringField(app, 'THEME_NAME', setter=theme_name_setter), @@ -558,7 +557,7 @@ def data_main(): section_data['layout']['history'] = app.HISTORY_LAYOUT section_data['layout']['home'] = app.HOME_LAYOUT section_data['layout']['show'] = {} - section_data['layout']['show']['allSeasons'] = bool(app.DISPLAY_ALL_SEASONS) + section_data['layout']['show']['specials'] = bool(app.DISPLAY_SHOW_SPECIALS) section_data['layout']['show']['showListOrder'] = app.SHOW_LIST_ORDER diff --git a/medusa/server/core.py b/medusa/server/core.py index bdb24d1c8e..0c5e68312c 100644 --- a/medusa/server/core.py +++ b/medusa/server/core.py @@ -77,6 +77,7 @@ def clean_url_path(*args, **kwargs): def get_apiv2_handlers(base): """Return api v2 handlers.""" return [ + # Order: Most specific to most generic # /api/v2/search diff --git a/medusa/server/web/config/general.py b/medusa/server/web/config/general.py index ff968a5ee4..021758833f 100644 --- a/medusa/server/web/config/general.py +++ b/medusa/server/web/config/general.py @@ -72,7 +72,7 @@ def saveGeneral(self, log_dir=None, log_nr=5, log_size=1, web_port=None, notify_ fuzzy_dating=None, trim_zero=None, date_preset=None, date_preset_na=None, time_preset=None, indexer_timeout=None, download_url=None, rootDir=None, theme_name=None, default_page=None, git_reset=None, git_reset_branches=None, git_auth_type=0, git_username=None, git_password=None, git_token=None, - display_all_seasons=None, subliminal_log=None, privacy_level='normal', fanart_background=None, fanart_background_opacity=None, + subliminal_log=None, privacy_level='normal', fanart_background=None, fanart_background_opacity=None, dbdebug=None, fallback_plex_enable=1, fallback_plex_notifications=1, fallback_plex_timeout=3, web_root=None, ssl_ca_bundle=None): results = [] @@ -119,7 +119,6 @@ def saveGeneral(self, log_dir=None, log_nr=5, log_size=1, web_port=None, notify_ app.SSL_CA_BUNDLE = ssl_ca_bundle # app.LOG_DIR is set in config.change_LOG_DIR() app.COMING_EPS_MISSED_RANGE = int(coming_eps_missed_range) - app.DISPLAY_ALL_SEASONS = config.checkbox_to_value(display_all_seasons) app.NOTIFY_ON_LOGIN = config.checkbox_to_value(notify_on_login) app.WEB_PORT = int(web_port) app.WEB_IPV6 = config.checkbox_to_value(web_ipv6) diff --git a/medusa/server/web/home/handler.py b/medusa/server/web/home/handler.py index bd2a216ff7..b42c1d210d 100644 --- a/medusa/server/web/home/handler.py +++ b/medusa/server/web/home/handler.py @@ -62,10 +62,7 @@ ) from medusa.scene_numbering import ( get_scene_absolute_numbering, - get_scene_absolute_numbering_for_show, get_scene_numbering, - get_scene_numbering_for_show, - get_xem_absolute_numbering_for_show, get_xem_numbering_for_show, set_scene_numbering, xem_refresh, @@ -763,27 +760,7 @@ def displayShow(self, indexername=None, seriesid=None, ): if series_obj is None: return self._genericMessage('Error', 'Show not in show list') - min_season = 0 if app.DISPLAY_SHOW_SPECIALS else 1 - - main_db_con = db.DBConnection() - sql_results = main_db_con.select( - 'SELECT * ' - 'FROM tv_episodes ' - 'WHERE indexer = ? AND showid = ? AND season >= ? ' - 'ORDER BY season DESC, episode DESC', - [series_obj.indexer, series_obj.series_id, min_season] - ) - - t = PageTemplate(rh=self, filename='displayShow.mako') - - ep_cats = {} - - for cur_result in sql_results: - cur_ep_cat = series_obj.get_overview(cur_result['status'], cur_result['quality'], manually_searched=cur_result['manually_searched']) - if cur_ep_cat: - ep_cats['s{season}e{episode}'.format(season=cur_result['season'], episode=cur_result['episode'])] = cur_ep_cat - - series_obj.exceptions = get_scene_exceptions(series_obj) + t = PageTemplate(rh=self, filename='index.mako') indexer_id = int(series_obj.indexer) series_id = int(series_obj.series_id) @@ -805,11 +782,6 @@ def displayShow(self, indexername=None, seriesid=None, ): }) return t.render( - show=series_obj, sql_results=sql_results, ep_cats=ep_cats, - scene_numbering=get_scene_numbering_for_show(series_obj), - xem_numbering=get_xem_numbering_for_show(series_obj, refresh_data=False), - scene_absolute_numbering=get_scene_absolute_numbering_for_show(series_obj), - xem_absolute_numbering=get_xem_absolute_numbering_for_show(series_obj), controller='home', action='displayShow', ) @@ -923,10 +895,10 @@ def manualSearchCheckCache(self, indexername, seriesid, season=None, episode=Non # Check if the requested ep is in a search thread searched_item = [ep for ep in episodes_in_search - if all([ep.get('indexer_id') == series_obj.identifier.indexer.id, - ep.get('series_id') == series_obj.identifier.id, - text_type(ep.get('season')) == season, - text_type(ep.get('episode')) == episode])] + if all([ep['show']['indexer'] == series_obj.identifier.indexer.id, + ep['show']['series_id'] == series_obj.identifier.id, + text_type(ep['episode']['season']) == season, + text_type(ep['episode']['episode']) == episode])] # # No last_prov_updates available, let's assume we need to refresh until we get some # if not last_prov_updates: @@ -959,7 +931,7 @@ def manualSearchCheckCache(self, indexername, seriesid, season=None, episode=Non # but then check if as soon as a search has finished # Move on and show results # Return a list of queues the episode has been found in - search_status = [item.get('searchstatus') for item in searched_item] + search_status = [item['search']['status'] for item in searched_item] if not searched_item or all([last_prov_updates, SEARCH_STATUS_QUEUED not in search_status, SEARCH_STATUS_SEARCHING not in search_status, @@ -974,7 +946,7 @@ def manualSearchCheckCache(self, indexername, seriesid, season=None, episode=Non if not last_prov_updates and SEARCH_STATUS_FINISHED in search_status: return {'result': refresh_results} - return {'result': searched_item[0]['searchstatus']} + return {'result': searched_item[0]['search']['status']} def snatchSelection(self, indexername, seriesid, season=None, episode=None, manual_search_type='episode', perform_search=0, down_cur_quality=0, show_all_results=0): @@ -1748,10 +1720,15 @@ def searchEpisode(self, indexername=None, seriesid=None, season=None, episode=No 'result': 'failure', }) - # ## Returns the current ep_queue_item status for the current viewed show. - # Possible status: Downloaded, Snatched, etc... - # Returns {'show': 279530, 'episodes' : ['episode' : 6, 'season' : 1, 'searchstatus' : 'queued', 'status' : 'running', 'quality': '4013'] def getManualSearchStatus(self, indexername=None, seriesid=None): + """ + Returns the current ep_queue_item status for the current viewed show. + Possible status: Downloaded, Snatched, etc... + Returns {'show': 279530, 'episodes' : ['episode' : 6, 'season' : 1, 'searchstatus' : 'queued', 'status' : 'running', 'quality': '4013'] + :param indexername: Name of indexer. For ex. 'tvdb', 'tmdb', 'tvmaze' + :param seriesid: Id of series as identified by the indexer + :return: + """ indexer_id = indexer_name_to_id(indexername) series_obj = Show.find_by_id(app.showList, indexer_id, seriesid) episodes = collect_episodes_from_search_thread(series_obj) @@ -1775,29 +1752,32 @@ def searchEpisodeSubtitles(self, indexername=None, seriesid=None, season=None, e logger.log('Manual re-downloading subtitles for {show} with language {lang}'.format (show=ep_obj.series.name, lang=lang)) new_subtitles = ep_obj.download_subtitles(lang=lang) - except Exception: + except Exception as error: return json.dumps({ 'result': 'failure', + 'description': 'Error while downloading subtitles: {error}'.format(error=error) }) if new_subtitles: new_languages = [subtitles.name_from_code(code) for code in new_subtitles] - status = 'New subtitles downloaded: {languages}'.format(languages=', '.join(new_languages)) + description = 'New subtitles downloaded: {languages}'.format(languages=', '.join(new_languages)) result = 'success' else: new_languages = [] - status = 'No subtitles downloaded' + description = 'No subtitles downloaded' result = 'failure' - ui.notifications.message(ep_obj.series.name, status) + ui.notifications.message(ep_obj.series.name, description) return json.dumps({ 'result': result, - 'subtitles': ','.join(ep_obj.subtitles), - 'new_subtitles': ','.join(new_languages), + 'subtitles': ep_obj.subtitles, + 'languages': new_languages, + 'description': description }) - def manual_search_subtitles(self, indexername=None, seriesid=None, season=None, episode=None, release_id=None, picked_id=None): + def manualSearchSubtitles(self, indexername=None, seriesid=None, season=None, episode=None, release_id=None, picked_id=None): mode = 'downloading' if picked_id else 'searching' + description = '' logger.log('Starting to manual {mode} subtitles'.format(mode=mode)) try: if release_id: @@ -1818,17 +1798,26 @@ def manual_search_subtitles(self, indexername=None, seriesid=None, season=None, except IndexError: ui.notifications.message('Outdated list', 'Please refresh page and try again') logger.log('Outdated list. Please refresh page and try again', logger.WARNING) - return json.dumps({'result': 'failure'}) + return json.dumps({ + 'result': 'failure', + 'description': 'Outdated list. Please refresh page and try again' + }) except (ValueError, TypeError) as e: ui.notifications.message('Error', 'Please check logs') logger.log('Error while manual {mode} subtitles. Error: {error_msg}'.format (mode=mode, error_msg=e), logger.ERROR) - return json.dumps({'result': 'failure'}) + return json.dumps({ + 'result': 'failure', + 'description': 'Error while manual {mode} subtitles. Error: {error_msg}'.format(mode=mode, error_msg=e) + }) if not os.path.isfile(video_path): ui.notifications.message(ep_obj.series.name, "Video file no longer exists. Can't search for subtitles") logger.log('Video file no longer exists: {video_file}'.format(video_file=video_path), logger.DEBUG) - return json.dumps({'result': 'failure'}) + return json.dumps({ + 'result': 'failure', + 'description': 'Video file no longer exists: {video_file}'.format(video_file=video_path) + }) if mode == 'searching': logger.log('Manual searching subtitles for: {0}'.format(release_name)) @@ -1837,7 +1826,11 @@ def manual_search_subtitles(self, indexername=None, seriesid=None, season=None, ui.notifications.message(ep_obj.series.name, 'Found {} subtitles'.format(len(found_subtitles))) else: ui.notifications.message(ep_obj.series.name, 'No subtitle found') - result = 'success' if found_subtitles else 'failure' + if found_subtitles: + result = 'success' + else: + result = 'failure' + description = 'No subtitles found' subtitles_result = found_subtitles else: logger.log('Manual downloading subtitles for: {0}'.format(release_name)) @@ -1848,13 +1841,19 @@ def manual_search_subtitles(self, indexername=None, seriesid=None, season=None, 'Subtitle downloaded: {0}'.format(','.join(new_manual_subtitle))) else: ui.notifications.message(ep_obj.series.name, 'Failed to download subtitle for {0}'.format(release_name)) - result = 'success' if new_manual_subtitle else 'failure' + if new_manual_subtitle: + result = 'success' + else: + result = 'failure' + description = 'Failed to download subtitle for {0}'.format(release_name) + subtitles_result = new_manual_subtitle return json.dumps({ 'result': result, 'release': release_name, - 'subtitles': subtitles_result + 'subtitles': subtitles_result, + 'description': description }) def setSceneNumbering(self, indexername=None, seriesid=None, forSeason=None, forEpisode=None, forAbsolute=None, sceneSeason=None, @@ -1871,6 +1870,12 @@ def setSceneNumbering(self, indexername=None, seriesid=None, forSeason=None, for indexer_id = indexer_name_to_id(indexername) series_obj = Show.find_by_id(app.showList, indexer_id, seriesid) + if not series_obj: + return json.dumps({ + 'success': False, + 'errorMessage': 'Could not find show {0} {1} to set scene numbering'.format(indexername, seriesid), + }) + # Check if this is an anime, because we can't set the Scene numbering for anime shows if series_obj.is_anime and forAbsolute is None: return json.dumps({ diff --git a/medusa/tv/episode.py b/medusa/tv/episode.py index 1eb6754ac1..77e2150dc9 100644 --- a/medusa/tv/episode.py +++ b/medusa/tv/episode.py @@ -243,6 +243,7 @@ def __init__(self, series, season, episode, filepath=''): self.name = '' self.season = season self.episode = episode + self.slug = 's{season:02d}e{episode:02d}'.format(season=self.season, episode=self.episode) self.absolute_number = 0 self.description = '' self.subtitles = [] @@ -1074,7 +1075,7 @@ def to_json(self, detailed=True): data = {} data['identifier'] = self.identifier data['id'] = {self.indexer_name: self.indexerid} - data['slug'] = 's{season:02d}e{episode:02d}'.format(season=self.season, episode=self.episode) + data['slug'] = self.slug data['season'] = self.season data['episode'] = self.episode @@ -1103,6 +1104,7 @@ def to_json(self, detailed=True): data['file'] = {} data['file']['location'] = self.location + data['file']['name'] = os.path.basename(self.location) if self.file_size: data['file']['size'] = self.file_size diff --git a/tests/apiv2/test_config.py b/tests/apiv2/test_config.py index 1a23b6e0c3..c6caf0df69 100644 --- a/tests/apiv2/test_config.py +++ b/tests/apiv2/test_config.py @@ -114,7 +114,6 @@ def config_main(monkeypatch, app_config): config_data['layout']['history'] = app.HISTORY_LAYOUT config_data['layout']['home'] = app.HOME_LAYOUT config_data['layout']['show'] = {} - config_data['layout']['show']['allSeasons'] = bool(app.DISPLAY_ALL_SEASONS) config_data['layout']['show']['specials'] = bool(app.DISPLAY_SHOW_SPECIALS) config_data['layout']['show']['showListOrder'] = app.SHOW_LIST_ORDER diff --git a/themes-default/slim/package.json b/themes-default/slim/package.json index 4c6dc3e577..33d49ff54b 100644 --- a/themes-default/slim/package.json +++ b/themes-default/slim/package.json @@ -35,13 +35,15 @@ "country-language": "0.1.7", "date-fns": "2.0.1", "is-visible": "2.2.0", + "javascript-time-ago": "2.0.2", "jquery": "3.4.1", "lodash": "4.17.15", "tablesorter": "2.31.1", + "v-tooltip": "2.0.2", "vue": "2.6.10", "vue-async-computed": "3.7.0", - "vue-cookie": "1.1.4", - "vue-good-table": "https://github.com/p0psicles/vue-good-table/archive/master.tar.gz", + "vue-cookies": "1.5.13", + "vue-good-table": "git+https://github.com/p0psicles/vue-good-table#25a6f282231426fbbdb44d8a6d1927e8abf21e4d", "vue-js-modal": "1.3.31", "vue-js-toggle-button": "1.3.2", "vue-meta": "2.0.5", diff --git a/themes-default/slim/src/api.js b/themes-default/slim/src/api.js index 37e899884e..355bd25a39 100644 --- a/themes-default/slim/src/api.js +++ b/themes-default/slim/src/api.js @@ -8,7 +8,7 @@ export const apiKey = document.body.getAttribute('api-key'); */ export const apiRoute = axios.create({ baseURL: webRoot + '/', - timeout: 30000, + timeout: 60000, headers: { Accept: 'application/json', 'Content-Type': 'application/json' diff --git a/themes-default/slim/src/components/backstretch.vue b/themes-default/slim/src/components/backstretch.vue index a438a46eb4..399c15f75c 100644 --- a/themes-default/slim/src/components/backstretch.vue +++ b/themes-default/slim/src/components/backstretch.vue @@ -39,7 +39,6 @@ export default { if (!this.enabled) { return; } - const { opacity, slug, offset } = this; if (slug) { const imgUrl = `${webRoot}/api/v2/series/${slug}/asset/fanart?api_key=${apiKey}`; diff --git a/themes-default/slim/src/components/display-show.vue b/themes-default/slim/src/components/display-show.vue new file mode 100644 index 0000000000..5b672896d6 --- /dev/null +++ b/themes-default/slim/src/components/display-show.vue @@ -0,0 +1,1399 @@ + + + + + diff --git a/themes-default/slim/src/components/helpers/plot-info.vue b/themes-default/slim/src/components/helpers/plot-info.vue index 5846452bbe..a5c59b771a 100644 --- a/themes-default/slim/src/components/helpers/plot-info.vue +++ b/themes-default/slim/src/components/helpers/plot-info.vue @@ -1,69 +1,25 @@ @@ -82,4 +38,114 @@ export default { top: 2px; opacity: 0.4; } + +.tooltip { + display: block !important; + z-index: 10000; +} + +.tooltip .tooltip-inner { + background: #ffef93; + color: #555; + border-radius: 16px; + padding: 5px 10px 4px; + border: 1px solid #f1d031; + -webkit-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15); + -moz-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15); + box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15); +} + +.tooltip .tooltip-arrow { + width: 0; + height: 0; + position: absolute; + margin: 5px; + border: 1px solid #ffef93; + z-index: 1; +} + +.tooltip[x-placement^="top"] { + margin-bottom: 5px; +} + +.tooltip[x-placement^="top"] .tooltip-arrow { + border-width: 5px 5px 0 5px; + border-left-color: transparent !important; + border-right-color: transparent !important; + border-bottom-color: transparent !important; + bottom: -5px; + left: calc(50% - 4px); + margin-top: 0; + margin-bottom: 0; +} + +.tooltip[x-placement^="bottom"] { + margin-top: 5px; +} + +.tooltip[x-placement^="bottom"] .tooltip-arrow { + border-width: 0 5px 5px 5px; + border-left-color: transparent !important; + border-right-color: transparent !important; + border-top-color: transparent !important; + top: -5px; + left: calc(50% - 4px); + margin-top: 0; + margin-bottom: 0; +} + +.tooltip[x-placement^="right"] { + margin-left: 5px; +} + +.tooltip[x-placement^="right"] .tooltip-arrow { + border-width: 5px 5px 5px 0; + border-left-color: transparent !important; + border-top-color: transparent !important; + border-bottom-color: transparent !important; + left: -4px; + top: calc(50% - 5px); + margin-left: 0; + margin-right: 0; +} + +.tooltip[x-placement^="left"] { + margin-right: 5px; +} + +.tooltip[x-placement^="left"] .tooltip-arrow { + border-width: 5px 0 5px 5px; + border-top-color: transparent !important; + border-right-color: transparent !important; + border-bottom-color: transparent !important; + right: -4px; + top: calc(50% - 5px); + margin-left: 0; + margin-right: 0; +} + +.tooltip.popover .popover-inner { + background: #ffef93; + color: #555; + padding: 24px; + border-radius: 5px; + box-shadow: 0 5px 30px rgba(black, 0.1); +} + +.tooltip.popover .popover-arrow { + border-color: #ffef93; +} + +.tooltip[aria-hidden='true'] { + visibility: hidden; + opacity: 0; + transition: opacity 0.15s, visibility 0.15s; +} + +.tooltip[aria-hidden='false'] { + visibility: visible; + opacity: 1; + transition: opacity 0.15s; +} + diff --git a/themes-default/slim/src/components/helpers/state-switch.vue b/themes-default/slim/src/components/helpers/state-switch.vue index 40ac89b262..9b4adda10b 100644 --- a/themes-default/slim/src/components/helpers/state-switch.vue +++ b/themes-default/slim/src/components/helpers/state-switch.vue @@ -1,5 +1,5 @@ - - diff --git a/themes-default/slim/src/components/snatch-selection.vue b/themes-default/slim/src/components/snatch-selection.vue index 8a629a7627..9fde46bb97 100644 --- a/themes-default/slim/src/components/snatch-selection.vue +++ b/themes-default/slim/src/components/snatch-selection.vue @@ -24,11 +24,13 @@ export default { }, computed: { ...mapState({ - shows: state => state.shows.shows + shows: state => state.shows.shows, + config: state => state.config }), ...mapGetters({ - getShowById: 'getShowById', - show: 'getCurrentShow' + show: 'getCurrentShow', + effectiveIgnored: 'effectiveIgnored', + effectiveRequired: 'effectiveRequired' }), indexer() { return this.$route.query.indexername; @@ -49,24 +51,46 @@ export default { }), /** * Attaches IMDB tooltip, - * Moves summary and checkbox controls backgrounds */ reflowLayout() { - this.$nextTick(() => { - this.moveSummaryBackground(); - }); - attachImdbTooltip(); // eslint-disable-line no-undef }, - /** - * Adjust the summary background position and size on page load and resize - */ - moveSummaryBackground() { - const height = $('#summary').height() + 10; - const top = $('#summary').offset().top + 5; - $('#summaryBackground').height(height); - $('#summaryBackground').offset({ top, left: 0 }); - $('#summaryBackground').show(); + getReleaseNameClasses(name) { + const { effectiveIgnored, effectiveRequired, show } = this; + const classes = []; + + if (effectiveIgnored(show).map(word => { + return name.toLowerCase().includes(word.toLowerCase()); + }).filter(x => x === true).length > 0) { + classes.push('global-ignored'); + } + + if (effectiveRequired(show).map(word => { + return name.toLowerCase().includes(word.toLowerCase()); + }).filter(x => x === true).length > 0) { + classes.push('global-required'); + } + + if (this.$store.state.search.filters.undesired.map(word => { + return name.toLowerCase().includes(word.toLowerCase()); + }).filter(x => x === true).length > 0) { + classes.push('global-undesired'); + } + + /** Disabled for now. Because global + series ignored can be concatenated or excluded. So it's not that simple to color them. */ + // if (this.show.config.release.ignoredWords.map( word => { + // return name.toLowerCase().includes(word.toLowerCase()); + // }).filter(x => x === true).length) { + // classes.push('show-ignored'); + // } + + // if (this.show.config.release.requiredWords.map( word => { + // return name.toLowerCase().includes(word.toLowerCase()); + // }).filter(x => x === true).length) { + // classes.push('show-required'); + // } + + return classes.join(' '); } }, mounted() { @@ -362,5 +386,25 @@ export default { diff --git a/themes-default/slim/src/components/subtitle-search.vue b/themes-default/slim/src/components/subtitle-search.vue index b15d685885..3556cace21 100644 --- a/themes-default/slim/src/components/subtitle-search.vue +++ b/themes-default/slim/src/components/subtitle-search.vue @@ -4,7 +4,8 @@ {{loadingMessage}}
-

Do you want to manually pick subtitles or let us choose it for you?

+

Do you want to manually pick subtitles or let us choose it for you?

+

Do you want to manually pick subtitles or search a subtitle with the language code {{lang}} for you?

@@ -86,6 +87,10 @@ export default { episode: { type: [String, Number], required: true + }, + lang: { + type: String, + required: false } }, data() { @@ -147,12 +152,17 @@ export default { }, methods: { autoSearch() { - const { subtitleParams } = this; + const { lang, subtitleParams } = this; + const params = subtitleParams; + + if (lang !== undefined) { + params.lang = lang; + } this.displayQuestion = false; this.loadingMessage = 'Searching for subtitles and downloading if available... '; this.loading = true; - apiRoute('home/searchEpisodeSubtitles', { params: subtitleParams }) + apiRoute('home/searchEpisodeSubtitles', { params }) .then(response => { if (response.data.result !== 'failure') { // Update the show, as we have new information (subtitles) @@ -191,6 +201,31 @@ export default { this.loading = false; }); }, + redownloadLang() { + const { subtitleParams } = this; + + apiRoute('home/searchEpisodeSubtitles', { params: subtitleParams }) + .then(response => { + if (response.data.result !== 'failure') { + // Update the show, as we have new information (subtitles) + // Let's emit an event, telling the displayShow component, to update the show using the api/store. + this.$emit('update', { + reason: 'new subtitles found', + codes: response.data.subtitles, + languages: response.data.languages + }); + } + }) + .catch(error => { + console.log(`Error trying to search for subtitles. Error: ${error}`); + }) + .finally(() => { + // Cleanup + this.loadingMessage = ''; + this.loading = false; + this.close(); + }); + }, pickSubtitle(subtitleId) { // Download and save this subtitle with the episode. const { subtitleParams } = this; diff --git a/themes-default/slim/src/global-vue-shim.js b/themes-default/slim/src/global-vue-shim.js index e35500f9bc..0decba34ad 100644 --- a/themes-default/slim/src/global-vue-shim.js +++ b/themes-default/slim/src/global-vue-shim.js @@ -3,6 +3,9 @@ import Vue from 'vue'; import AsyncComputed from 'vue-async-computed'; import VueMeta from 'vue-meta'; import Snotify from 'vue-snotify'; +import VueCookies from 'vue-cookies'; +import VModal from 'vue-js-modal'; +import { VTooltip } from 'v-tooltip'; import { AddShowOptions, @@ -26,7 +29,6 @@ import { RootDirs, ScrollButtons, SelectList, - Show, ShowSelector, SnatchSelection, StateSwitch, @@ -84,7 +86,6 @@ export const registerGlobalComponents = () => { components = components.concat([ Home, ManualPostProcess, - Show, SnatchSelection, Status ]); @@ -105,6 +106,9 @@ export const registerPlugins = () => { Vue.use(AsyncComputed); Vue.use(VueMeta); Vue.use(Snotify); + Vue.use(VueCookies); + Vue.use(VModal); + Vue.use(VTooltip); }; /** diff --git a/themes-default/slim/src/router/routes.js b/themes-default/slim/src/router/routes.js index b7aac383fc..6bb8ca9dfa 100644 --- a/themes-default/slim/src/router/routes.js +++ b/themes-default/slim/src/router/routes.js @@ -31,7 +31,8 @@ const homeRoutes = [ meta: { topMenu: 'home', subMenu: showSubMenu - } + }, + component: () => import('../components/display-show.vue') }, { path: '/home/snatchSelection', diff --git a/themes-default/slim/src/store/modules/config.js b/themes-default/slim/src/store/modules/config.js index 5f561c4989..d427dad3a2 100644 --- a/themes-default/slim/src/store/modules/config.js +++ b/themes-default/slim/src/store/modules/config.js @@ -1,5 +1,6 @@ import { api } from '../../api'; import { ADD_CONFIG } from '../mutation-types'; +import { arrayUnique, arrayExclude } from '../../utils/core'; const state = { wikiUrl: null, @@ -39,8 +40,7 @@ const state = { layout: { show: { specials: null, - showListOrder: [], - allSeasons: null + showListOrder: [] }, home: null, history: null, @@ -241,6 +241,23 @@ const mutations = { }; const getters = { + layout: state => layout => state.layout[layout], + effectiveIgnored: (state, _, rootState) => series => { + const seriesIgnored = series.config.release.ignoredWords.map(x => x.toLowerCase()); + const globalIgnored = rootState.search.filters.ignored.map(x => x.toLowerCase()); + if (!series.config.release.ignoredWordsExclude) { + return arrayUnique(globalIgnored.concat(seriesIgnored)); + } + return arrayExclude(globalIgnored, seriesIgnored); + }, + effectiveRequired: (state, _, rootState) => series => { + const globalRequired = rootState.search.filters.required.map(x => x.toLowerCase()); + const seriesRequired = series.config.release.requiredWords.map(x => x.toLowerCase()); + if (!series.config.release.requiredWordsExclude) { + return arrayUnique(globalRequired.concat(seriesRequired)); + } + return arrayExclude(globalRequired, seriesRequired); + }, // Get an indexer's name using its ID. indexerIdToName: state => indexerId => { if (!indexerId) { @@ -256,9 +273,6 @@ const getters = { } const { indexers } = state.indexers.config; return indexers[name].id; - }, - layout: state => layout => { - return state.layout[layout]; } }; diff --git a/themes-default/slim/src/store/modules/consts.js b/themes-default/slim/src/store/modules/consts.js index 5d46990c79..2d8cee6844 100644 --- a/themes-default/slim/src/store/modules/consts.js +++ b/themes-default/slim/src/store/modules/consts.js @@ -54,6 +54,37 @@ const getters = { } return state.statuses.find(status => key === status.key || value === status.value); }, + // Get an episode overview status using the episode status and quality + // eslint-disable-next-line no-unused-vars + getOverviewStatus: _state => (status, quality, showQualities) => { + if (['Unset', 'Unaired'].includes(status)) { + return 'Unaired'; + } + + if (['Skipped', 'Ignored'].includes(status)) { + return 'Skipped'; + } + + if (['Wanted', 'Failed'].includes(status)) { + return 'Wanted'; + } + + if (['Snatched', 'Snatched (Proper)', 'Snatched (Best)'].includes(status)) { + return 'Snatched'; + } + + if (['Downloaded'].includes(status)) { + if (showQualities.preferred.includes(quality)) { + return 'Preferred'; + } + + if (showQualities.allowed.includes(quality)) { + return 'Allowed'; + } + } + + return status; + }, splitQuality: state => { /** * Split a combined quality to allowed and preferred qualities. diff --git a/themes-default/slim/src/store/modules/shows.js b/themes-default/slim/src/store/modules/shows.js index bd97a6995a..623c9a90c0 100644 --- a/themes-default/slim/src/store/modules/shows.js +++ b/themes-default/slim/src/store/modules/shows.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import { api } from '../../api'; -import { ADD_SHOW } from '../mutation-types'; +import { ADD_SHOW, ADD_SHOW_EPISODE } from '../mutation-types'; /** * @typedef {object} ShowIdentifier @@ -43,7 +43,46 @@ const mutations = { currentShow(state, { indexer, id }) { state.currentShow.indexer = indexer; state.currentShow.id = id; + }, + [ADD_SHOW_EPISODE](state, { show, episodes }) { + // Creating a new show object (from the state one) as we want to trigger a store update + const newShow = Object.assign({}, state.shows.find(({ id, indexer }) => Number(show.id[show.indexer]) === Number(id[indexer]))); + + if (!newShow.seasons) { + newShow.seasons = []; + } + + // Recreate an Array with season objects, with each season having an episodes array. + // This format is used by vue-good-table (displayShow). + episodes.forEach(episode => { + const existingSeason = newShow.seasons.find(season => season.season === episode.season); + + if (existingSeason) { + const foundIndex = existingSeason.episodes.findIndex(element => element.slug === episode.slug); + if (foundIndex === -1) { + existingSeason.episodes.push(episode); + } else { + existingSeason.episodes.splice(foundIndex, 1, episode); + } + } else { + const newSeason = { + season: episode.season, + episodes: [], + html: false, + mode: 'span', + label: 1 + }; + newShow.seasons.push(newSeason); + newSeason.episodes.push(episode); + } + }); + + // Update state + const existingShow = state.shows.find(({ id, indexer }) => Number(show.id[show.indexer]) === Number(id[indexer])); + Vue.set(state.shows, state.shows.indexOf(existingShow), newShow); + console.log(`Storing episodes for show ${newShow.title} seasons: ${[...new Set(episodes.map(episode => episode.season))].join(', ')}`); } + }; const getters = { @@ -96,6 +135,7 @@ const actions = { if (detailed !== undefined) { params.detailed = detailed; timeout = 60000; + timeout = 60000; } if (episodes !== undefined) { @@ -113,6 +153,39 @@ const actions = { }); }); }, + /** + * Get episdoes for a specified show from API and commit it to the store. + * + * @param {*} context - The store context. + * @param {ShowParameteres} parameters - Request parameters. + * @returns {Promise} The API response. + */ + getEpisodes({ commit, getters }, { indexer, id, season }) { + return new Promise((resolve, reject) => { + const { getShowById } = getters; + const show = getShowById({ id, indexer }); + + const limit = 1000; + const params = { + limit + }; + + if (season) { + params.season = season; + } + + // Get episodes + api.get(`/series/${indexer}${id}/episodes`, { params }) + .then(response => { + commit(ADD_SHOW_EPISODE, { show, episodes: response.data }); + resolve(); + }) + .catch(error => { + console.log(`Could not retrieve a episodes for show ${indexer}${id}, error: ${error}`); + reject(error); + }); + }); + }, /** * Get shows from API and commit them to the store. * diff --git a/themes-default/slim/src/store/mutation-types.js b/themes-default/slim/src/store/mutation-types.js index 07f1fe79ad..80ec36a2f0 100644 --- a/themes-default/slim/src/store/mutation-types.js +++ b/themes-default/slim/src/store/mutation-types.js @@ -14,6 +14,7 @@ const NOTIFICATIONS_ENABLED = '🔔 Notifications Enabled'; const NOTIFICATIONS_DISABLED = '🔔 Notifications Disabled'; const ADD_CONFIG = '⚙️ Config added to store'; const ADD_SHOW = '📺 Show added to store'; +const ADD_SHOW_EPISODE = '📺 Shows season with episodes added to store'; const ADD_STATS = 'ℹ️ Statistics added to store'; export { @@ -33,5 +34,6 @@ export { NOTIFICATIONS_DISABLED, ADD_CONFIG, ADD_SHOW, + ADD_SHOW_EPISODE, ADD_STATS }; diff --git a/themes-default/slim/src/utils/jquery.js b/themes-default/slim/src/utils/jquery.js index 894ff11503..6ba9e954c9 100644 --- a/themes-default/slim/src/utils/jquery.js +++ b/themes-default/slim/src/utils/jquery.js @@ -97,26 +97,26 @@ export const updateSearchIcons = (showSlug, vm) => { const queuedImage = 'queued.png'; const searchImage = 'search16.png'; - if (`${ep.indexer_name}${ep.series_id}` !== vm.show.id.slug) { + if (ep.show.slug !== vm.show.id.slug) { return true; } // Try to get the Element - const img = vm.$refs[`search-${ep.slug}`]; + const img = vm.$refs[`search-${ep.episode.slug}`]; if (img) { - if (ep.searchstatus.toLowerCase() === 'searching') { + if (ep.search.status.toLowerCase() === 'searching') { // El=$('td#' + ep.season + 'x' + ep.episode + '.search img'); img.title = 'Searching'; img.alt = 'Searching'; img.src = 'images/' + loadingImage; disableLink(img); - } else if (ep.searchstatus.toLowerCase() === 'queued') { + } else if (ep.search.status.toLowerCase() === 'queued') { // El=$('td#' + ep.season + 'x' + ep.episode + '.search img'); img.title = 'Queued'; img.alt = 'queued'; img.src = 'images/' + queuedImage; disableLink(img); - } else if (ep.searchstatus.toLowerCase() === 'finished') { + } else if (ep.search.status.toLowerCase() === 'finished') { // El=$('td#' + ep.season + 'x' + ep.episode + '.search img'); img.title = 'Searching'; img.alt = 'searching'; diff --git a/themes-default/slim/static/css/dark.css b/themes-default/slim/static/css/dark.css index ef79f793e4..3541ec9067 100644 --- a/themes-default/slim/static/css/dark.css +++ b/themes-default/slim/static/css/dark.css @@ -663,6 +663,25 @@ body { box-shadow: 0 6px 12px rgba(0, 0, 0, 0.176); } +/* Vueified DisplayShow uses the vue component vue-good-table to display tables. */ +.vgt-dropdown-menu { + background-color: rgb(41, 41, 41); + border: 1px solid rgba(0, 0, 0, 0.15); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.176); +} + +.vgt-dropdown-menu > li > span:hover, +.vgt-dropdown-menu > li > span:focus { + color: rgb(255, 255, 255); + text-decoration: none; + background-color: rgb(119, 119, 119); +} + +.vgt-dropdown-menu > li > span { + padding: 4px 36px 4px 20px; + color: rgb(255, 255, 255); +} + /* submenu styling */ #sub-menu-container { background-color: rgb(41, 41, 41); diff --git a/themes-default/slim/static/css/style.css b/themes-default/slim/static/css/style.css index 4a410ad4f7..53540c45af 100644 --- a/themes-default/slim/static/css/style.css +++ b/themes-default/slim/static/css/style.css @@ -1383,6 +1383,10 @@ ul.tags li a { background-color: rgb(195, 227, 200); } +.archived { + background-color: rgb(195, 227, 200); +} + .qual { background-color: rgb(255, 218, 138); } @@ -2786,6 +2790,26 @@ fieldset[disabled] .navbar-default .btn-link:focus { box-shadow: 0 6px 12px rgba(0, 0, 0, 0.176); } +/* Vueified DisplayShow uses the vue component vue-good-table to display tables. */ + +.vgt-dropdown-menu { + background-color: rgb(245, 241, 228); + border: 1px solid rgba(0, 0, 0, 0.15); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.176); +} + +.vgt-dropdown-menu > li > a:hover, +.vgt-dropdown-menu > li > a:focus { + color: rgb(38, 38, 38); + text-decoration: none; + background-color: rgb(245, 245, 245); +} + +.vgt-dropdown-menu > li > span { + padding: 4px 36px 4px 20px; + color: #333; +} + .form-control { color: rgb(0, 0, 0); } diff --git a/themes-default/slim/static/images/notwatched.png b/themes-default/slim/static/images/notwatched.png new file mode 100644 index 0000000000..03e44cd495 Binary files /dev/null and b/themes-default/slim/static/images/notwatched.png differ diff --git a/themes-default/slim/static/images/watched.png b/themes-default/slim/static/images/watched.png new file mode 100644 index 0000000000..c392829573 Binary files /dev/null and b/themes-default/slim/static/images/watched.png differ diff --git a/themes-default/slim/static/js/ajax-episode-search.js b/themes-default/slim/static/js/ajax-episode-search.js index 07f898b33b..841472ada6 100644 --- a/themes-default/slim/static/js/ajax-episode-search.js +++ b/themes-default/slim/static/js/ajax-episode-search.js @@ -1,7 +1,3 @@ -const searchStatusUrl = 'home/getManualSearchStatus'; -let failedDownload = false; -let qualityDownload = false; -let selectedEpisode = ''; PNotify.prototype.options.maxonscreen = 5; $.fn.forcedSearches = []; @@ -26,25 +22,25 @@ function updateImages(data) { const searchImage = 'search16.png'; let htmlContent = ''; // Try to get the Element - const el = $('a[id=' + ep.indexer_id + 'x' + ep.series_id + 'x' + ep.season + 'x' + ep.episode + ']'); + const el = $('a[id=' + ep.show.indexer + 'x' + ep.show.series_id + 'x' + ep.episode.season + 'x' + ep.episode.episode + ']'); const img = el.children('img[data-ep-search]'); const parent = el.parent(); if (el) { - if (ep.searchstatus.toLowerCase() === 'searching') { + if (ep.search.status.toLowerCase() === 'searching') { // El=$('td#' + ep.season + 'x' + ep.episode + '.search img'); img.prop('title', 'Searching'); img.prop('alt', 'Searching'); img.prop('src', 'images/' + loadingImage); disableLink(el); - htmlContent = ep.searchstatus; - } else if (ep.searchstatus.toLowerCase() === 'queued') { + htmlContent = ep.search.status; + } else if (ep.search.status.toLowerCase() === 'queued') { // El=$('td#' + ep.season + 'x' + ep.episode + '.search img'); img.prop('title', 'Queued'); img.prop('alt', 'queued'); img.prop('src', 'images/' + queuedImage); disableLink(el); - htmlContent = ep.searchstatus; - } else if (ep.searchstatus.toLowerCase() === 'finished') { + htmlContent = ep.search.status; + } else if (ep.search.status.toLowerCase() === 'finished') { // El=$('td#' + ep.season + 'x' + ep.episode + '.search img'); img.prop('title', 'Searching'); img.prop('alt', 'searching'); @@ -54,34 +50,34 @@ function updateImages(data) { // Update Status and Quality let qualityPill = ''; - if (ep.quality_style !== 'na') { + if (ep.episode.quality_style !== 'na') { // @FIXME: (sharkykh) This is a hack to get the QualityPill's scoped style to work. const qualityPillScopeId = window.Vue.options.components['quality-pill'].options._scopeId; - qualityPill = ' ' + ep.quality_name + ''; + qualityPill = ' ' + ep.episode.quality_name + ''; } - htmlContent = ep.status + qualityPill; - parent.closest('tr').prop('class', ep.overview + ' season-' + ep.season + ' seasonstyle'); + htmlContent = ep.episode.status + qualityPill; + parent.closest('tr').prop('class', ep.episode.overview + ' season-' + ep.episode.season + ' seasonstyle'); } // Update the status column if it exists parent.siblings('.col-status').html(htmlContent); } - const elementCompleteEpisodes = $('a[id=forceUpdate-' + ep.indexer_id + 'x' + ep.series_id + 'x' + ep.season + 'x' + ep.episode + ']'); + const elementCompleteEpisodes = $('a[id=forceUpdate-' + ep.show.indexer + 'x' + ep.show.series_id + 'x' + ep.episode.season + 'x' + ep.episode.episode + ']'); const imageCompleteEpisodes = elementCompleteEpisodes.children('img'); if (elementCompleteEpisodes) { - if (ep.searchstatus.toLowerCase() === 'searching') { + if (ep.search.status.toLowerCase() === 'searching') { imageCompleteEpisodes.prop('title', 'Searching'); imageCompleteEpisodes.prop('alt', 'Searching'); imageCompleteEpisodes.prop('src', 'images/' + loadingImage); disableLink(elementCompleteEpisodes); - } else if (ep.searchstatus.toLowerCase() === 'queued') { + } else if (ep.search.status.toLowerCase() === 'queued') { imageCompleteEpisodes.prop('title', 'Queued'); imageCompleteEpisodes.prop('alt', 'queued'); imageCompleteEpisodes.prop('src', 'images/' + queuedImage); - } else if (ep.searchstatus.toLowerCase() === 'finished') { + } else if (ep.search.status.toLowerCase() === 'finished') { imageCompleteEpisodes.prop('title', 'Forced Search'); imageCompleteEpisodes.prop('alt', '[search]'); imageCompleteEpisodes.prop('src', 'images/' + searchImage); - if (ep.overview.toLowerCase() === 'snatched') { + if (ep.episode.overview.toLowerCase() === 'snatched') { elementCompleteEpisodes.closest('tr').remove(); } else { enableLink(elementCompleteEpisodes); @@ -93,6 +89,7 @@ function updateImages(data) { function checkManualSearches() { let pollInterval = 5000; + const searchStatusUrl = 'home/getManualSearchStatus'; // Try to get a indexer name and series id. If we can't get any, we request the manual search status for all shows. const indexerName = $('#indexer-name').val(); @@ -143,7 +140,7 @@ $.ajaxEpSearch = function(options) { return false; } - selectedEpisode = $(this); + $.selectedEpisode = $(this); $('#forcedSearchModalFailed').modal('show'); }); @@ -151,19 +148,21 @@ $.ajaxEpSearch = function(options) { function forcedSearch() { let imageName; let imageResult; + const failedDownload = false; + const qualityDownload = false; - const parent = selectedEpisode.parent(); + const parent = $.selectedEpisode.parent(); // Create var for anchor - const link = selectedEpisode; + const link = $.selectedEpisode; // Create var for img under anchor and set options for the loading gif - const img = selectedEpisode.children('img'); + const img = $.selectedEpisode.children('img'); img.prop('title', 'loading'); img.prop('alt', ''); img.prop('src', 'images/' + options.loadingImage); - let url = selectedEpisode.prop('href'); + let url = $.selectedEpisode.prop('href'); if (!failedDownload) { url = url.replace('retryEpisode', 'searchEpisode'); @@ -212,7 +211,7 @@ $.ajaxEpSearch = function(options) { return false; } - selectedEpisode = $(this); + $.selectedEpisode = $(this); // @TODO: Replace this with an easier to read selector if ($(this).parent().parent().children('.col-status').children('.quality').length > 0) { @@ -240,14 +239,4 @@ $.ajaxEpSearch = function(options) { window.location = url; } }); - - $('#forcedSearchModalFailed .btn-medusa').on('click', function() { - failedDownload = ($(this).text().toLowerCase() === 'yes'); - $('#forcedSearchModalQuality').modal('show'); - }); - - $('#forcedSearchModalQuality .btn-medusa').on('click', function() { - qualityDownload = ($(this).text().toLowerCase() === 'yes'); - forcedSearch(); - }); }; diff --git a/themes-default/slim/test/specs/__snapshots__/plot-info.spec.js.snap b/themes-default/slim/test/specs/__snapshots__/plot-info.spec.js.snap index f4d20fbb13..5557de38f3 100644 --- a/themes-default/slim/test/specs/__snapshots__/plot-info.spec.js.snap +++ b/themes-default/slim/test/specs/__snapshots__/plot-info.spec.js.snap @@ -3,7 +3,8 @@ exports[`PlotInfo.test.js renders 1`] = ` { localVue, store, propsData: { - showSlug: '', - season: '', - episode: '' + description: 'This is an example for an episodes plot info' } }); diff --git a/themes-default/slim/views/config_general.mako b/themes-default/slim/views/config_general.mako index 4465997f56..4fb033c0d3 100644 --- a/themes-default/slim/views/config_general.mako +++ b/themes-default/slim/views/config_general.mako @@ -328,16 +328,6 @@ window.app = new Vue({
- -
- -