diff --git a/CHANGELOG.md b/CHANGELOG.md index f9f389d3d5..0b208c497a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ #### Improvements - Add show names with dashes to guessit expected titles ([#7918](https://github.com/pymedusa/Medusa/pull/7918)) - Provider YggTorrents: Add 'saison' as a season pack search keyword ([#7920](https://github.com/pymedusa/Medusa/pull/7920)) +- Show Snatched or Downloaded release name when manually picking a subtitle ([#7955](https://github.com/pymedusa/Medusa/pull/7955)) #### Fixes - Fixed root dirs not always shown on Home page ([#7921](https://github.com/pymedusa/Medusa/pull/7921)) diff --git a/dredd/api-description.yml b/dredd/api-description.yml index d1bc317811..320964a7d9 100644 --- a/dredd/api-description.yml +++ b/dredd/api-description.yml @@ -221,7 +221,6 @@ paths: The Episode endpoint returns information about the Episodes from a given Series. parameters: - $ref: '#/parameters/series-id' - name: seriesid - name: season in: query required: false @@ -995,6 +994,91 @@ paths: 400: $ref: '#/responses/error' x-disabled: true + /history: + get: + summary: Return history entries to a specific show + description: | + The history endpoint returns logged activities stored in the history table, like episodes snatched and downloaded. Or downloaded subtitles for an episode. + parameters: + - $ref: '#/parameters/page' + - $ref: '#/parameters/limit' + - $ref: '#/parameters/sort' + responses: + 200: + $ref: '#/responses/pagination_history' + x-disabled: true + 400: + $ref: '#/responses/error' + description: Invalid series id or pagination parameters + x-request: + path-params: + seriesid: asdf + x-disabled: true + 404: + $ref: '#/responses/error' + description: Series not found + x-request: + path-params: + seriesid: tvdb999999999 + x-disabled: true + /history/{seriesid}: + get: + summary: Return history entries to a specific show + description: | + The history endpoint returns logged activities stored in the history table, like episodes snatched and downloaded. Or downloaded subtitles for an episode. + parameters: + - $ref: '#/parameters/series-id' + - $ref: '#/parameters/page' + - $ref: '#/parameters/limit' + - $ref: '#/parameters/sort' + responses: + 200: + $ref: '#/responses/pagination_history' + x-disabled: true + 400: + $ref: '#/responses/error' + description: Invalid series id or pagination parameters + x-request: + path-params: + seriesid: asdf + x-disabled: true + 404: + $ref: '#/responses/error' + description: Series not found + x-request: + path-params: + seriesid: tvdb999999999 + x-disabled: true + /history/{seriesid}/episode/{episodeid}: + get: + summary: Return history entries for a specific episode + description: | + The histories episode endpoint returns history entries for a specific episode + parameters: + - $ref: '#/parameters/series-id' + - $ref: '#/parameters/episode-id' + responses: + 200: + description: Array of History entries + schema: + type: array + items: + $ref: '#/definitions/History' + x-disabled: true + 400: + $ref: '#/responses/error' + description: Invalid series id or pagination parameters + x-request: + path-params: + seriesid: asdf + x-disabled: true + 404: + $ref: '#/responses/error' + description: Series not found + x-request: + path-params: + seriesid: tvdb999999999 + x-disabled: true definitions: Series: @@ -2721,6 +2805,51 @@ definitions: overview: type: string description: Episode status/quality overview string + + History: + description: History object + type: object + properties: + id: + type: integer + format: int32 + description: Internal id for the history row + series: + type: string + description: Series slug (if available) + status: + type: integer + format: int32 + description: Status (numberic) + statusName: + type: string + description: Status description + actionDate: + type: integer + format: int32 + description: Date of when the history entrie was stored + resource: + type: string + description: Description of what was stored + example: + - The release name for a statusName of "Downloaded" or "Snatched" + - The language of a subitle downloaded fo ra statusName of "Subtitled" + size: + type: integer + description: Snatched or Downloaded filesize + season: + type: integer + description: Season number + episode: + type: integer + description: Episode number + manuallySearched: + type: boolean + description: Specifies if an episode was snatched or downloaded through a manual search + provider: + type: string + description: Provider of the history record. For example, the provider id if origin from snatch/download. + parameters: detailed: name: detailed @@ -2762,7 +2891,7 @@ parameters: x-example: tvdb301824 type: string episode-id: - name: episode-id + name: episodeid in: path required: true description: The episode id to retrieve. E.g. s02e03, e34 or 2016-12-31 @@ -2925,3 +3054,26 @@ responses: description: Unexpected error schema: $ref: '#/definitions/Error' + pagination_history: + description: A paged array of history records + headers: + X-Pagination-Page: + type: integer + format: int32 + description: The page number + X-Pagination-Limit: + type: integer + format: int32 + description: The pagination limit + X-Pagination-Count: + type: integer + format: int32 + description: The total items count + Link: + type: string + description: "The pagination links: next, last, first and previous" + schema: + type: array + items: + $ref: '#/definitions/History' + diff --git a/medusa/server/api/v2/episode_history.py b/medusa/server/api/v2/episode_history.py new file mode 100644 index 0000000000..3f3d43d2bf --- /dev/null +++ b/medusa/server/api/v2/episode_history.py @@ -0,0 +1,99 @@ +# coding=utf-8 +"""Request handler for series and episodes.""" +from __future__ import unicode_literals + +import logging +from os.path import basename + +from medusa import db +from medusa.common import statusStrings +from medusa.logger.adapters.style import BraceAdapter +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 + + +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',) + + 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._bad_request('Invalid episode slug') + + episode_number = EpisodeNumber.from_slug(episode_slug) + if not episode_number: + return self._not_found('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'] + 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'] = bool(item['manually_searched']) + d['provider'] = item['provider'] + + yield d + + if not 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())) diff --git a/medusa/server/api/v2/history.py b/medusa/server/api/v2/history.py new file mode 100644 index 0000000000..ffa807eec6 --- /dev/null +++ b/medusa/server/api/v2/history.py @@ -0,0 +1,92 @@ +# coding=utf-8 +"""Request handler for alias (scene exceptions).""" +from __future__ import unicode_literals + +from os.path import basename + +from medusa import db +from medusa.common import statusStrings +from medusa.server.api.v2.base import BaseRequestHandler +from medusa.tv.series import SeriesIdentifier + + +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): + """ + Get history records. + + History records can be specified using a show slug. + """ + sql_base = """ + SELECT rowid, date, action, quality, + provider, version, proper_tags, manually_searched, + 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 and paginate history records.""" + start = arg_limit * (arg_page - 1) + + for item in results[start:start + arg_limit]: + d = {} + d['id'] = item['rowid'] + 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'] = bool(item['manually_searched']) + d['provider'] = item['provider'] + + yield d + + if not results: + return self._not_found('History data not found') + + return self._paginate(data_generator=data_generator) + + def delete(self, identifier, **kwargs): + """Delete a history record.""" + identifier = self._parse(identifier) + if not identifier: + return self._bad_request('Invalid history id') + + main_db_con = db.DBConnection() + last_changes = main_db_con.connection.total_changes + main_db_con.action('DELETE FROM history WHERE row_id = ?', [identifier]) + if main_db_con.connection.total_changes - last_changes <= 0: + return self._not_found('History row not found') + + return self._no_content() diff --git a/medusa/server/core.py b/medusa/server/core.py index a3cd5987f4..d1e554b563 100644 --- a/medusa/server/core.py +++ b/medusa/server/core.py @@ -22,7 +22,9 @@ from medusa.server.api.v2.auth import AuthHandler from medusa.server.api.v2.base import BaseRequestHandler, NotFoundHandler from medusa.server.api.v2.config import ConfigHandler +from medusa.server.api.v2.episode_history import EpisodeHistoryHandler from medusa.server.api.v2.episodes import EpisodeHandler +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), diff --git a/medusa/tv/series.py b/medusa/tv/series.py index 8a8b3a3359..67e8c0a9cf 100644 --- a/medusa/tv/series.py +++ b/medusa/tv/series.py @@ -2103,6 +2103,12 @@ def to_json(self, detailed=False, episodes=False): data['config']['release']['blacklist'] = bw_list.blacklist data['config']['release']['whitelist'] = bw_list.whitelist + # Make sure these are at least defined + data['xemNumbering'] = [] + data['sceneAbsoluteNumbering'] = [] + data['xemAbsoluteNumbering'] = [] + data['sceneNumbering'] = [] + if detailed: data['size'] = self.size data['showQueueStatus'] = self.show_queue_status @@ -2111,9 +2117,6 @@ def to_json(self, detailed=False, episodes=False): if self.is_scene: data['xemAbsoluteNumbering'] = dict_to_array(self.xem_absolute_numbering, key='absolute', value='sceneAbsolute') data['sceneNumbering'] = numbering_tuple_to_dict(self.scene_numbering) - else: - data['xemAbsoluteNumbering'] = [] - data['sceneNumbering'] = [] if episodes: all_episodes = self.get_all_episodes() diff --git a/themes-default/slim/src/components/display-show.vue b/themes-default/slim/src/components/display-show.vue index 5a18f756ec..820b418f7e 100644 --- a/themes-default/slim/src/components/display-show.vue +++ b/themes-default/slim/src/components/display-show.vue @@ -786,7 +786,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..afd5364361 100644 --- a/themes-default/slim/src/components/subtitle-search.vue +++ b/themes-default/slim/src/components/subtitle-search.vue @@ -1,7 +1,7 @@ - {{loadingMessage}} + {{loadingMessage}} Do you want to manually pick subtitles or let us choose it for you? @@ -13,58 +13,64 @@ - - - - {{props.column.label}} - hide - - - {{props.column.label}} - - - - - - {{props.row.provider}} - - - - - - - - {{props.row.filename}} - - - - - - - - - - {{props.formattedRow[props.column.field]}} - - - + + + {{releaseName}} + + + + + {{props.column.label}} + hide + + + {{props.column.label}} + + + + + + {{props.row.provider}} + + + + + + + + {{props.row.filename}} + + + + + + + + + + {{props.formattedRow[props.column.field]}} + + + + +
Do you want to manually pick subtitles or let us choose it for you?