From a199e803ca7902b349159f4a392916771c360d4e Mon Sep 17 00:00:00 2001 From: P0psicles Date: Sat, 11 Apr 2020 18:58:51 +0200 Subject: [PATCH 01/16] Add history api route. (python apiv2 changes) This was already prepared for another PR, where it will be used by the snatch-selection (vue) component and the history (vue) component. For this addition to the subtitle-search.vue component. Only one of the actions in history.js is used. --- medusa/server/api/v2/episode_history.py | 140 ++++++++++++++++++++++++ medusa/server/api/v2/history.py | 99 +++++++++++++++++ medusa/server/core.py | 8 ++ 3 files changed, 247 insertions(+) create mode 100644 medusa/server/api/v2/episode_history.py create mode 100644 medusa/server/api/v2/history.py diff --git a/medusa/server/api/v2/episode_history.py b/medusa/server/api/v2/episode_history.py new file mode 100644 index 0000000000..eb88f8e0c7 --- /dev/null +++ b/medusa/server/api/v2/episode_history.py @@ -0,0 +1,140 @@ +# coding=utf-8 +"""Request handler for series and episodes.""" +from __future__ import unicode_literals + +import logging + +from medusa import db + +from medusa.common import statusStrings +from medusa.helper.exceptions import EpisodeDeletedException +from medusa.logger.adapters.style import BraceAdapter +from medusa.providers.generic_provider import GenericProvider +from medusa.providers import get_provider_class +from medusa.server.api.v2.base import BaseRequestHandler +from medusa.server.api.v2.history import HistoryHandler +from medusa.tv.episode import Episode, EpisodeNumber +from medusa.tv.series import Series, SeriesIdentifier + +from os.path import basename + +log = BraceAdapter(logging.getLogger(__name__)) +log.logger.addHandler(logging.NullHandler()) + + +class EpisodeHistoryHandler(BaseRequestHandler): + """Episode history request handler.""" + + #: parent resource handler + parent_handler = HistoryHandler + #: resource name + name = 'episode' + #: identifier + identifier = ('episode_slug', r'[\w-]+') + #: path param + path_param = ('path_param', r'\w+') + #: allowed HTTP methods + allowed_methods = ('GET', 'DELETE',) + + def get(self, series_slug, episode_slug, path_param): + """Query episode's history information. + + :param series_slug: series slug. E.g.: tvdb1234 + :param episode_slug: episode slug. E.g.: s01e01 + :param path_param: + """ + series_identifier = SeriesIdentifier.from_slug(series_slug) + if not series_identifier: + return self._bad_request('Invalid series slug') + + series = Series.find_by_identifier(series_identifier) + if not series: + return self._not_found('Series not found') + + if not episode_slug: + return self._not_found('Invalid episode slug') + + episode_number = EpisodeNumber.from_slug(episode_slug) + if not episode_number: + return self._bad_request('Invalid episode number') + + episode = Episode.find_by_series_and_episode(series, episode_number) + if not episode: + return self._not_found('Episode not found') + + sql_base = ''' + SELECT rowid, date, action, quality, + provider, version, resource, size, proper_tags, + indexer_id, showid, season, episode, manually_searched + FROM history + WHERE showid = ? AND indexer_id = ? AND season = ? AND episode = ? + ''' + + params = [series.series_id, series.indexer, episode.season, episode.episode] + + sql_base += ' ORDER BY date DESC' + results = db.DBConnection().select(sql_base, params) + + def data_generator(): + """Read history data and normalize key/value pairs.""" + for item in results: + d = {} + d['id'] = item['rowid'] + + if item['indexer_id'] and item['showid']: + d['series'] = SeriesIdentifier.from_id(item['indexer_id'], item['showid']).slug + + d['status'] = item['action'] + d['actionDate'] = item['date'] + + d['resource'] = basename(item['resource']) + d['size'] = item['size'] + d['properTags'] = item['proper_tags'] + d['statusName'] = statusStrings.get(item['action']) + d['season'] = item['season'] + d['episode'] = item['episode'] + d['manuallySearched'] = item['manually_searched'] + + provider = get_provider_class(GenericProvider.make_id(item['provider'])) + d['provider'] = {} + if provider: + d['provider']['id'] = provider.get_id() + d['provider']['name'] = provider.name + d['provider']['imageName'] = provider.image_name() + + yield d + + if not len(results): + return self._not_found('History data not found for show {show} and episode {episode}'.format( + show=series.identifier.slug, episode=episode.slug + )) + + return self._ok(data=list(data_generator())) + + def delete(self, series_slug, episode_slug, **kwargs): + """Delete the episode.""" + if not series_slug: + return self._method_not_allowed('Deleting multiple series are not allowed') + + identifier = SeriesIdentifier.from_slug(series_slug) + if not identifier: + return self._bad_request('Invalid series identifier') + + series = Series.find_by_identifier(identifier) + if not series: + return self._not_found('Series not found') + + episode_number = EpisodeNumber.from_slug(episode_slug) + if not episode_number: + return self._bad_request('Invalid episode number') + + episode = Episode.find_by_series_and_episode(series, episode_number) + if not episode: + return self._not_found('Episode not found') + + try: + episode.delete_episode() + except EpisodeDeletedException: + return self._no_content() + else: + return self._conflict('Unable to delete episode') diff --git a/medusa/server/api/v2/history.py b/medusa/server/api/v2/history.py new file mode 100644 index 0000000000..d2d14b89ab --- /dev/null +++ b/medusa/server/api/v2/history.py @@ -0,0 +1,99 @@ +# coding=utf-8 +"""Request handler for alias (scene exceptions).""" +from __future__ import unicode_literals + +from medusa import db + +from medusa.server.api.v2.base import BaseRequestHandler +from medusa.providers.generic_provider import GenericProvider +from medusa.providers import get_provider_class +from medusa.tv.series import SeriesIdentifier +from os.path import basename +from medusa.common import statusStrings + + +class HistoryHandler(BaseRequestHandler): + """History request handler.""" + + #: resource name + name = 'history' + #: identifier + identifier = ('series_slug', r'\w+') + #: path param + path_param = ('path_param', r'\w+') + #: allowed HTTP methods + allowed_methods = ('GET', 'POST', 'PUT', 'DELETE') + + def get(self, series_slug, path_param): + """Query search history information.""" + + sql_base = ''' + SELECT rowid, date, action, quality, + provider, version, resource, size, + indexer_id, showid, season, episode + FROM history + ''' + params = [] + + arg_page = self._get_page() + arg_limit = self._get_limit(default=50) + + if series_slug is not None: + series_identifier = SeriesIdentifier.from_slug(series_slug) + if not series_identifier: + return self._bad_request('Invalid series') + + sql_base += ' WHERE indexer_id = ? AND showid = ?' + params += [series_identifier.indexer.id, series_identifier.id] + + sql_base += ' ORDER BY date DESC' + results = db.DBConnection().select(sql_base, params) + + def data_generator(): + """Read log lines based on the specified criteria.""" + start = arg_limit * (arg_page - 1) + 1 + + for item in results[start - 1:start - 1 + arg_limit]: + d = {} + d['id'] = item['rowid'] + + if item['indexer_id'] and item['showid']: + d['series'] = SeriesIdentifier.from_id(item['indexer_id'], item['showid']).slug + + d['status'] = item['action'] + d['actionDate'] = item['date'] + + d['resource'] = basename(item['resource']) + d['size'] = item['size'] + d['statusName'] = statusStrings.get(item['action']) + d['season'] = item['season'] + d['episode'] = item['episode'] + + provider = get_provider_class(GenericProvider.make_id(item['provider'])) + d['provider'] = {} + if provider: + d['provider']['id'] = provider.get_id() + d['provider']['name'] = provider.name + d['provider']['imageName'] = provider.image_name() + + yield d + + if not len(results): + return self._not_found('History data not found') + + return self._paginate(data_generator=data_generator) + + + def delete(self, identifier, **kwargs): + """Delete an alias.""" + identifier = self._parse(identifier) + if not identifier: + return self._bad_request('Invalid history id') + + cache_db_con = db.DBConnection('cache.db') + last_changes = cache_db_con.connection.total_changes + cache_db_con.action('DELETE FROM history WHERE row_id = ?', [identifier]) + if cache_db_con.connection.total_changes - last_changes <= 0: + return self._not_found('Alias not found') + + return self._no_content() diff --git a/medusa/server/core.py b/medusa/server/core.py index a3cd5987f4..f28a5023a5 100644 --- a/medusa/server/core.py +++ b/medusa/server/core.py @@ -23,6 +23,8 @@ from medusa.server.api.v2.base import BaseRequestHandler, NotFoundHandler from medusa.server.api.v2.config import ConfigHandler from medusa.server.api.v2.episodes import EpisodeHandler +from medusa.server.api.v2.episode_history import EpisodeHistoryHandler +from medusa.server.api.v2.history import HistoryHandler from medusa.server.api.v2.internal import InternalHandler from medusa.server.api.v2.log import LogHandler from medusa.server.api.v2.search import SearchHandler @@ -80,6 +82,12 @@ def get_apiv2_handlers(base): # Order: Most specific to most generic + # /api/v2/history/tvdb1234/episode + EpisodeHistoryHandler.create_app_handler(base), + + # /api/v2/history + HistoryHandler.create_app_handler(base), + # /api/v2/search SearchHandler.create_app_handler(base), From c9a8c8bcc3ebd84fde54a146de465dba56f55e41 Mon Sep 17 00:00:00 2001 From: P0psicles Date: Sat, 11 Apr 2020 19:00:18 +0200 Subject: [PATCH 02/16] Add the last release name from the history table, to the subtitle-search.vue manual search table (if available) --- .../slim/src/components/display-show.vue | 2 +- .../slim/src/components/subtitle-search.vue | 127 +++++++++++------- themes-default/slim/src/store/index.js | 2 + .../slim/src/store/modules/history.js | 120 +++++++++++++++++ .../slim/src/store/modules/index.js | 1 + .../slim/src/store/mutation-types.js | 8 +- themes/dark/assets/js/medusa-runtime.js | 30 +++-- themes/light/assets/js/medusa-runtime.js | 30 +++-- 8 files changed, 252 insertions(+), 68 deletions(-) create mode 100644 themes-default/slim/src/store/modules/history.js diff --git a/themes-default/slim/src/components/display-show.vue b/themes-default/slim/src/components/display-show.vue index ecd27d53fb..cab36343e0 100644 --- a/themes-default/slim/src/components/display-show.vue +++ b/themes-default/slim/src/components/display-show.vue @@ -785,7 +785,7 @@ export default { const { id, indexer, getEpisodes, show, subtitleSearchComponents } = this; const SubtitleSearchClass = Vue.extend(SubtitleSearch); // eslint-disable-line no-undef const instance = new SubtitleSearchClass({ - propsData: { show, season: episode.season, episode: episode.episode, key: episode.originalIndex, lang }, + propsData: { show, episode, key: episode.originalIndex, lang }, parent: this }); diff --git a/themes-default/slim/src/components/subtitle-search.vue b/themes-default/slim/src/components/subtitle-search.vue index 1ed2fb732f..55472a8c8d 100644 --- a/themes-default/slim/src/components/subtitle-search.vue +++ b/themes-default/slim/src/components/subtitle-search.vue @@ -1,7 +1,7 @@