Source code for flexget.components.series.api

import copy
from math import ceil

from flask import jsonify, request
from flask_restx import inputs
from sqlalchemy.orm.exc import NoResultFound

from flexget import plugin
from flexget.api import APIClient, APIResource, api
from flexget.api.app import (
    BadRequest,
    Conflict,
    NotFoundError,
    base_message_schema,
    etag,
    pagination_headers,
    success_response,
)
from flexget.event import fire_event
from flexget.plugin import PluginError

from . import db
from .utils import normalize_series_name

try:
    # NOTE: Importing other plugins is discouraged!
    from flexget.components.thetvdb.api import ObjectsContainer as TvdbOC
except ImportError:
    raise plugin.DependencyError(issued_by=__name__, missing='tvdb_lookup')


try:
    # NOTE: Importing other plugins is discouraged!
    from flexget.components.tvmaze.api import ObjectsContainer as TvmazeOC
except ImportError:
    raise plugin.DependencyError(issued_by=__name__, missing='tvmaze_lookup')

series_api = api.namespace('series', description='FlexGet Series operations')


[docs] def series_details(show, begin=False, latest=False): series_dict = { 'id': show.id, 'name': show.name, 'alternate_names': [n.alt_name for n in show.alternate_names], 'in_tasks': [_show.name for _show in show.in_tasks], } if begin: series_dict['begin_episode'] = show.begin.to_dict() if show.begin else None if latest: latest_entity = db.get_latest_release(show) series_dict['latest_entity'] = latest_entity.to_dict() if latest_entity else None if latest_entity: series_dict['latest_entity']['latest_release'] = latest_entity.latest_release.to_dict() return series_dict
[docs] class ObjectsContainer: episode_release_object = { 'type': 'object', 'properties': { 'id': {'type': 'integer'}, 'title': {'type': 'string'}, 'downloaded': {'type': 'boolean'}, 'quality': {'type': 'string'}, 'proper_count': {'type': 'integer'}, 'first_seen': {'type': 'string', 'format': 'date-time'}, 'episode_id': {'type': 'integer'}, }, 'required': [ 'id', 'title', 'downloaded', 'quality', 'proper_count', 'first_seen', 'episode_id', ], } season_release_object = copy.deepcopy(episode_release_object) del season_release_object['properties']['episode_id'] season_release_object['properties']['season_id'] = {'type': 'integer'} season_release_object['required'].remove('episode_id') season_release_object['required'].append('season_id') episode_release_list_schema = {'type': 'array', 'items': episode_release_object} season_release_list_schema = {'type': 'array', 'items': season_release_object} episode_object = { 'type': ['object', 'null'], 'properties': { 'first_seen': {'type': ['string', 'null'], 'format': 'date-time'}, 'id': {'type': 'integer'}, 'identified_by': {'type': 'string'}, 'identifier': {'type': 'string'}, 'premiere': {'type': ['string', 'boolean']}, 'number': {'type': 'integer'}, 'season': {'type': 'integer'}, 'series_id': {'type': 'integer'}, 'number_of_releases': {'type': 'integer'}, 'latest_release': episode_release_object, }, 'required': [ 'first_seen', 'id', 'identified_by', 'identifier', 'premiere', 'number', 'season', 'series_id', 'number_of_releases', ], } season_object = copy.deepcopy(episode_object) del season_object['properties']['number'] season_object['required'].remove('number') season_object['required'].remove('premiere') single_series_object = { 'type': 'object', 'properties': { 'id': {'type': 'integer'}, 'name': {'type': 'string'}, 'alternate_names': {'type': 'array', 'items': {'type': 'string'}}, 'in_tasks': {'type': 'array', 'items': {'type': 'string'}}, 'lookup': { 'type': 'object', 'properties': { 'tvmaze': TvmazeOC.tvmaze_series_object, 'tvdb': TvdbOC.tvdb_series_object, }, }, 'latest_episode': episode_object, 'begin_episode': episode_object, }, 'required': ['id', 'name', 'alternate_names', 'in_tasks'], } series_list_schema = {'type': 'array', 'items': single_series_object} episode_list_schema = {'type': 'array', 'items': episode_object} seasons_list_schema = {'type': 'array', 'items': season_object} series_edit_object = { 'type': 'object', 'properties': { 'begin_episode': {'type': ['string', 'integer'], 'format': 'episode_or_season_id'}, 'alternate_names': {'type': 'array', 'items': {'type': 'string'}}, }, 'anyOf': [{'required': ['begin_episode']}, {'required': ['alternate_names']}], 'additionalProperties:': False, } series_input_object = copy.deepcopy(series_edit_object) series_input_object['properties']['name'] = {'type': 'string'} del series_input_object['anyOf'] series_input_object['required'] = ['name']
series_list_schema = api.schema_model('list_series', ObjectsContainer.series_list_schema) series_edit_schema = api.schema_model('series_edit_schema', ObjectsContainer.series_edit_object) series_input_schema = api.schema_model('series_input_schema', ObjectsContainer.series_input_object) show_details_schema = api.schema_model('show_details', ObjectsContainer.single_series_object) episode_list_schema = api.schema_model('episode_list', ObjectsContainer.episode_list_schema) episode_schema = api.schema_model('episode_item', ObjectsContainer.episode_object) season_list_schema = api.schema_model('season_list', ObjectsContainer.seasons_list_schema) season_schema = api.schema_model('episode_item', ObjectsContainer.season_object) episode_release_schema = api.schema_model( 'release_schema', ObjectsContainer.episode_release_object ) episode_release_list_schema = api.schema_model( 'release_list_schema', ObjectsContainer.episode_release_list_schema ) season_release_schema = api.schema_model('release_schema', ObjectsContainer.season_release_object) season_release_list_schema = api.schema_model( 'release_list_schema', ObjectsContainer.season_release_list_schema ) base_series_parser = api.parser() base_series_parser.add_argument( 'begin', type=inputs.boolean, default=True, help='Show series begin episode' ) base_series_parser.add_argument( 'latest', type=inputs.boolean, default=True, help='Show series latest downloaded episode and release', ) sort_choices = ('show_name', 'last_download_date') series_list_parser = api.pagination_parser(base_series_parser, sort_choices=sort_choices) series_list_parser.add_argument( 'in_config', choices=('configured', 'unconfigured', 'all'), default='configured', help='Filter list if shows are currently in configuration.', ) series_list_parser.add_argument( 'premieres', type=inputs.boolean, default=False, help='Filter by downloaded premieres only.' ) series_list_parser.add_argument( 'lookup', choices=('tvdb', 'tvmaze'), action='append', help='Get lookup result for every show by sending another request to lookup API', ) series_list_parser.add_argument('query', help='Search by name based on the query') ep_identifier_doc = ( "'episode_identifier' should be one of SxxExx, integer or date formatted such as 2012-12-12" )
[docs] @series_api.route('/') class SeriesAPI(APIResource):
[docs] @etag @api.response(200, 'Series list retrieved successfully', series_list_schema) @api.response(NotFoundError) @api.doc(expect=[series_list_parser], description="Get a list of Flexget's shows in DB") def get(self, session=None): """List existing shows.""" args = series_list_parser.parse_args() # Filter params configured = args['in_config'] premieres = args['premieres'] # Pagination and sorting params page = args['page'] per_page = args['per_page'] sort_by = args['sort_by'] sort_order = args['order'] name = normalize_series_name(args['query']) if args['query'] else None # Handle max size limit per_page = min(per_page, 100) descending = sort_order == 'desc' # Data params lookup = args.get('lookup') begin = args.get('begin') latest = args.get('latest') start = per_page * (page - 1) stop = start + per_page kwargs = { 'configured': configured, 'premieres': premieres, 'start': start, 'stop': stop, 'sort_by': sort_by, 'descending': descending, 'session': session, 'name': name, } total_items = db.get_series_summary(count=True, **kwargs) if not total_items: return jsonify([]) series_list = [] for s in db.get_series_summary(**kwargs): series_object = series_details(s, begin, latest) series_list.append(series_object) # Total number of pages total_pages = ceil(total_items / float(per_page)) if total_pages < page and total_pages != 0: raise NotFoundError(f'page {page} does not exist') # Actual results in page actual_size = min(per_page, len(series_list)) # Do relevant lookups if lookup: api_client = APIClient() for endpoint in lookup: base_url = f'/{endpoint}/series/' for show in series_list: pos = series_list.index(show) series_list[pos].setdefault('lookup', {}) url = base_url + show['name'] + '/' result = api_client.get_endpoint(url) series_list[pos]['lookup'].update({endpoint: result}) # Get pagination headers pagination = pagination_headers(total_pages, total_items, actual_size, request) # Created response rsp = jsonify(series_list) # Add link header to response rsp.headers.extend(pagination) return rsp
[docs] @api.response(201, model=show_details_schema) @api.response(Conflict) @api.validate(series_input_schema, description=ep_identifier_doc) def post(self, session): """Create a new show and set its first accepted episode and/or alternate names.""" data = request.json series_name = data.get('name') normalized_name = normalize_series_name(series_name) matches = db.shows_by_exact_name(normalized_name, session=session) if matches: raise Conflict(f'Show `{series_name}` already exist in DB') show = db.Series() show.name = series_name session.add(show) ep_id = data.get('begin_episode') alt_names = data.get('alternate_names') if ep_id: db.set_series_begin(show, ep_id) if alt_names: try: db.set_alt_names(alt_names, show, session) except PluginError as e: # Alternate name already exist for a different show raise Conflict(e.value) session.commit() rsp = jsonify(series_details(show, begin=ep_id is not None)) rsp.status_code = 201 return rsp
[docs] @series_api.route('/search/<string:name>/') @api.doc( description='Searches for a show in the DB via its name. Returns a list of matching shows.' ) class SeriesGetShowsAPI(APIResource):
[docs] @etag @api.response(200, 'Show list retrieved successfully', series_list_schema) @api.doc(params={'name': 'Name of the show(s) to search'}, expect=[base_series_parser]) def get(self, name, session): """List of shows matching lookup name.""" name = normalize_series_name(name) matches = db.shows_by_name(name, session=session) args = series_list_parser.parse_args() begin = args.get('begin') latest = args.get('latest') shows = [series_details(match, begin, latest) for match in matches] return jsonify(shows)
delete_parser = api.parser() delete_parser.add_argument( 'forget', type=inputs.boolean, default=False, help="Enabling this will fire a 'forget' event that will delete the downloaded releases " 'from the entire DB, enabling to re-download them', )
[docs] @series_api.route('/<int:show_id>/') @api.doc(params={'show_id': 'ID of the show'}) @api.response(NotFoundError) class SeriesShowAPI(APIResource):
[docs] @etag @api.response(200, 'Show information retrieved successfully', show_details_schema) @api.doc(description='Get a specific show using its ID', expect=[base_series_parser]) def get(self, show_id, session): """Get show details by ID.""" try: show = db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'Show with ID {show_id} not found') args = series_list_parser.parse_args() begin = args.get('begin') latest = args.get('latest') return jsonify(series_details(show, begin, latest))
[docs] @api.response(200, 'Removed series from DB', model=base_message_schema) @api.doc(description='Delete a specific show using its ID', expect=[delete_parser]) def delete(self, show_id, session): """Remove series from DB.""" try: show = db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'Show with ID {show_id} not found') name = show.name args = delete_parser.parse_args() db.remove_series(name, forget=args.get('forget')) return success_response(f'successfully removed series {show_id} from DB')
[docs] @api.response( 200, 'Episodes for series will be accepted starting with ep_id', show_details_schema ) @api.response(Conflict) @api.validate(series_edit_schema, description=ep_identifier_doc) @api.doc( description='Set a begin episode or alternate names using a show ID. Note that alternate names override ' 'the existing names (if name does not belong to a different show).' ) def put(self, show_id, session): """Set the initial episode of an existing show.""" try: show = db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'Show with ID {show_id} not found') data = request.json ep_id = data.get('begin_episode') alt_names = data.get('alternate_names') if ep_id: db.set_series_begin(show, ep_id) if alt_names: try: db.set_alt_names(alt_names, show, session) except PluginError as e: # Alternate name already exist for a different show raise Conflict(e.value) return jsonify(series_details(show, begin=ep_id is not None))
entity_parser = api.pagination_parser(add_sort=True)
[docs] @api.response(NotFoundError) @series_api.route('/<int:show_id>/seasons/') @api.doc( params={'show_id': 'ID of the show'}, description="The 'Series-ID' header will be appended to the result headers", ) class SeriesSeasonsAPI(APIResource):
[docs] @etag @api.response(200, 'Seasons retrieved successfully for show', season_list_schema) @api.doc(description='Get all show seasons via its ID', expect=[entity_parser]) def get(self, show_id, session): """Get seasons by show ID.""" args = entity_parser.parse_args() # Pagination and sorting params page = args['page'] per_page = args['per_page'] sort_order = args['order'] # Handle max size limit per_page = min(per_page, 100) descending = sort_order == 'desc' start = per_page * (page - 1) stop = start + per_page kwargs = {'start': start, 'stop': stop, 'descending': descending, 'session': session} try: show = db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'show with ID {show_id} not found') total_items = db.show_seasons(show, count=True, session=session) if not total_items: return jsonify([]) seasons = [season.to_dict() for season in db.show_seasons(show, **kwargs)] total_pages = ceil(total_items / float(per_page)) if total_pages < page and total_pages != 0: raise NotFoundError(f'page {page} does not exist') # Actual results in page actual_size = min(per_page, len(seasons)) # Get pagination headers pagination = pagination_headers(total_pages, total_items, actual_size, request) # Created response rsp = jsonify(seasons) # Add link header to response rsp.headers.extend(pagination) # Add series ID header rsp.headers.extend({'Series-ID': show_id}) return rsp
[docs] @api.response(200, 'Successfully forgotten all seasons from show', model=base_message_schema) @api.doc( description='Delete all show seasons via its ID. Deleting a season will mark it as wanted again', expect=[delete_parser], ) def delete(self, show_id, session): """Delete all seasons of a show.""" try: show = db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'show with ID {show_id} not found') args = delete_parser.parse_args() forget = args.get('forget') for season in show.seasons: db.remove_series_entity(show.name, season.identifier, forget) return success_response(f'successfully removed all series {show_id} seasons from DB')
[docs] @api.response(NotFoundError) @api.response(BadRequest) @series_api.route('/<int:show_id>/seasons/<int:season_id>/') @api.doc(params={'show_id': 'ID of the show', 'season_id': 'Season ID'}) class SeriesSeasonAPI(APIResource):
[docs] @etag @api.response(200, 'Season retrieved successfully for show', season_schema) @api.doc(description='Get a specific season via its ID and show ID') def get(self, show_id, season_id, session): """Get season by show ID and season ID.""" try: db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'show with ID {show_id} not found') try: season = db.season_by_id(season_id, session) except NoResultFound: raise NotFoundError(f'season with ID {season_id} not found') if not db.season_in_show(show_id, season_id): raise BadRequest(f'season with id {season_id} does not belong to show {show_id}') rsp = jsonify(season.to_dict()) # Add Series-ID header rsp.headers.extend({'Series-ID': show_id}) return rsp
[docs] @api.response(200, 'Season successfully forgotten for show', model=base_message_schema) @api.doc( description='Delete a specific season via its ID and show ID. Deleting a season will mark it as ' 'wanted again', expect=[delete_parser], ) def delete(self, show_id, season_id, session): """Forgets season by show ID and season ID.""" try: show = db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'show with ID {show_id} not found') try: season = db.season_by_id(season_id, session) except NoResultFound: raise NotFoundError(f'season with ID {season_id} not found') if not db.season_in_show(show_id, season_id): raise BadRequest(f'season with id {season_id} does not belong to show {show_id}') args = delete_parser.parse_args() db.remove_series_entity(show.name, season.identifier, args.get('forget')) return success_response(f'successfully removed season {season_id} from show {show_id}')
[docs] @api.response(NotFoundError) @series_api.route('/<int:show_id>/episodes/') @api.doc( params={'show_id': 'ID of the show'}, description="The 'Series-ID' header will be appended to the result headers", ) class SeriesEpisodesAPI(APIResource):
[docs] @etag @api.response(200, 'Episodes retrieved successfully for show', episode_list_schema) @api.doc(description='Get all show episodes via its ID', expect=[entity_parser]) def get(self, show_id, session): """Get episodes by show ID.""" args = entity_parser.parse_args() # Pagination and sorting params page = args['page'] per_page = args['per_page'] sort_order = args['order'] # Handle max size limit per_page = min(per_page, 100) descending = sort_order == 'desc' start = per_page * (page - 1) stop = start + per_page kwargs = {'start': start, 'stop': stop, 'descending': descending, 'session': session} try: show = db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'show with ID {show_id} not found') total_items = db.show_episodes(show, count=True, session=session) if not total_items: return jsonify([]) episodes = [episode.to_dict() for episode in db.show_episodes(show, **kwargs)] total_pages = ceil(total_items / float(per_page)) if total_pages < page and total_pages != 0: raise NotFoundError(f'page {page} does not exist') # Actual results in page actual_size = min(per_page, len(episodes)) # Get pagination headers pagination = pagination_headers(total_pages, total_items, actual_size, request) # Created response rsp = jsonify(episodes) # Add link header to response rsp.headers.extend(pagination) # Add series ID header rsp.headers.extend({'Series-ID': show_id}) return rsp
[docs] @api.response(200, 'Successfully forgotten all episodes from show', model=base_message_schema) @api.doc( description='Delete all show episodes via its ID. Deleting an episode will mark it as wanted again', expect=[delete_parser], ) def delete(self, show_id, session): """Delete all episodes of a show.""" try: show = db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'show with ID {show_id} not found') args = delete_parser.parse_args() forget = args.get('forget') for episode in show.episodes: db.remove_series_entity(show.name, episode.identifier, forget) return success_response(f'successfully removed all series {show_id} episodes from DB')
[docs] @api.response(NotFoundError) @api.response(BadRequest) @series_api.route('/<int:show_id>/episodes/<int:ep_id>/') @api.doc(params={'show_id': 'ID of the show', 'ep_id': 'Episode ID'}) class SeriesEpisodeAPI(APIResource):
[docs] @etag @api.response(200, 'Episode retrieved successfully for show', episode_schema) @api.doc(description='Get a specific episode via its ID and show ID') def get(self, show_id, ep_id, session): """Get episode by show ID and episode ID.""" try: db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'show with ID {show_id} not found') try: episode = db.episode_by_id(ep_id, session) except NoResultFound: raise NotFoundError(f'episode with ID {ep_id} not found') if not db.episode_in_show(show_id, ep_id): raise BadRequest(f'episode with id {ep_id} does not belong to show {show_id}') rsp = jsonify(episode.to_dict()) # Add Series-ID header rsp.headers.extend({'Series-ID': show_id}) return rsp
[docs] @api.response(200, 'Episode successfully forgotten for show', model=base_message_schema) @api.doc( description='Delete a specific episode via its ID and show ID. Deleting an episode will mark it as ' 'wanted again', expect=[delete_parser], ) def delete(self, show_id, ep_id, session): """Forgets episode by show ID and episode ID.""" try: show = db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'show with ID {show_id} not found') try: episode = db.episode_by_id(ep_id, session) except NoResultFound: raise NotFoundError(f'episode with ID {ep_id} not found') if not db.episode_in_show(show_id, ep_id): raise BadRequest(f'episode with id {ep_id} does not belong to show {show_id}') args = delete_parser.parse_args() db.remove_series_entity(show.name, episode.identifier, args.get('forget')) return success_response(f'successfully removed episode {ep_id} from show {show_id}')
release_base_parser = api.parser() release_base_parser.add_argument( 'downloaded', type=inputs.boolean, help='Filter between release status' ) sort_choices = ('first_seen', 'downloaded', 'proper_count', 'title') release_list_parser = api.pagination_parser(release_base_parser, sort_choices) release_delete_parser = release_base_parser.copy() release_delete_parser.add_argument( 'forget', type=inputs.boolean, default=False, help="Enabling this will for 'forget' event that will delete the downloaded" ' releases from the entire DB, enabling to re-download them', )
[docs] @api.response(NotFoundError) @api.response(BadRequest) @series_api.route('/<int:show_id>/seasons/<int:season_id>/releases/') @api.doc( params={'show_id': 'ID of the show', 'season_id': 'Seasons ID'}, description='Releases are any seen entries that match the seasons. \n' "The 'Series-ID' header will be appended to the result headers.\n" "The 'Season-ID' header will be appended to the result headers.", ) class SeriesSeasonsReleasesAPI(APIResource):
[docs] @etag @api.response(200, 'Releases retrieved successfully for season', season_release_list_schema) @api.doc( description='Get all matching releases for a specific season of a specific show.', expect=[release_list_parser], ) def get(self, show_id, season_id, session): """Get all season releases by show ID and season ID.""" try: db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'show with ID {show_id} not found') try: season = db.season_by_id(season_id, session) except NoResultFound: raise NotFoundError(f'season with ID {season_id} not found') if not db.season_in_show(show_id, season_id): raise BadRequest(f'seasons with id {season_id} does not belong to show {show_id}') args = release_list_parser.parse_args() # Filter params downloaded = args.get('downloaded') # Pagination and sorting params page = args['page'] per_page = args['per_page'] sort_by = args['sort_by'] sort_order = args['order'] descending = sort_order == 'desc' # Handle max size limit per_page = min(per_page, 100) start = per_page * (page - 1) stop = start + per_page kwargs = { 'downloaded': downloaded, 'start': start, 'stop': stop, 'sort_by': sort_by, 'descending': descending, 'season': season, 'session': session, } # Total number of releases total_items = db.get_season_releases(count=True, **kwargs) # Release items release_items = [release.to_dict() for release in db.get_season_releases(**kwargs)] # Total number of pages total_pages = ceil(total_items / float(per_page)) if total_pages < page and total_pages != 0: raise NotFoundError(f'page {page} does not exist') # Actual results in page actual_size = min(per_page, len(release_items)) # Get pagination headers pagination = pagination_headers(total_pages, total_items, actual_size, request) # Created response rsp = jsonify(release_items) # Add link header to response rsp.headers.extend(pagination) # Add Series-ID and Episode-ID headers rsp.headers.extend({'Series-ID': show_id, 'Season-ID': season_id}) return rsp
[docs] @api.response(200, 'Successfully deleted all releases for season', model=base_message_schema) @api.doc( description='Delete all releases for a specific season of a specific show.', expect=[release_delete_parser], ) def delete(self, show_id, season_id, session): """Delete all season releases by show ID and season ID.""" try: db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'show with ID {show_id} not found') try: season = db.season_by_id(season_id, session) except NoResultFound: raise NotFoundError(f'seasons with ID {season_id} not found') if not db.season_in_show(show_id, season_id): raise BadRequest(f'season with id {season_id} does not belong to show {show_id}') args = release_delete_parser.parse_args() downloaded = args.get('downloaded') is True if args.get('downloaded') is not None else None release_items = [ release for release in season.releases if ( (downloaded and release.downloaded) or (downloaded is False and not release.downloaded) or not downloaded ) ] for release in release_items: if args.get('forget'): fire_event('forget', release.title) db.delete_season_release_by_id(release.id) return success_response( f'successfully deleted all releases for season {season_id} from show {show_id}' )
[docs] @api.response( 200, 'Successfully reset all downloaded releases for season', model=base_message_schema ) @api.doc( description='Resets all of the downloaded releases of an season, clearing the quality to be downloaded ' 'again,' ) def put(self, show_id, season_id, session): """Mark all downloaded season releases as not downloaded.""" try: db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'show with ID {show_id} not found') try: season = db.season_by_id(season_id, session) except NoResultFound: raise NotFoundError(f'season with ID {season_id} not found') if not db.season_in_show(show_id, season_id): raise BadRequest(f'season with id {season_id} does not belong to show {show_id}') for release in season.releases: if release.downloaded: release.downloaded = False return success_response( f'successfully reset download status for all releases for season {season_id} from show {show_id}' )
[docs] @api.response(NotFoundError) @api.response(BadRequest) @series_api.route('/<int:show_id>/seasons/<int:season_id>/releases/<int:rel_id>/') @api.doc( params={'show_id': 'ID of the show', 'season_id': 'Season ID', 'rel_id': 'Release ID'}, description='Releases are any seen entries that match the season. \n' "The 'Series-ID' header will be appended to the result headers.\n" "The 'Season-ID' header will be appended to the result headers.", ) class SeriesSeasonReleaseAPI(APIResource):
[docs] @etag @api.response(200, 'Release retrieved successfully for season', season_release_schema) @api.doc( description='Get a specific downloaded release for a specific season of a specific show' ) def get(self, show_id, season_id, rel_id, session): """Get season release by show ID, season ID and release ID.""" try: db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'show with ID {show_id} not found') try: db.season_by_id(season_id, session) except NoResultFound: raise NotFoundError(f'season with ID {season_id} not found') try: release = db.season_release_by_id(rel_id, session) except NoResultFound: raise NotFoundError(f'release with ID {rel_id} not found') if not db.season_in_show(show_id, season_id): raise BadRequest(f'season with id {season_id} does not belong to show {show_id}') if not db.release_in_season(season_id, rel_id): raise BadRequest(f'release id {rel_id} does not belong to season {season_id}') rsp = jsonify(release.to_dict()) rsp.headers.extend({'Series-ID': show_id, 'Season-ID': season_id}) return rsp
[docs] @api.response(200, 'Release successfully deleted', model=base_message_schema) @api.doc( description='Delete a specific releases for a specific season of a specific show.', expect=[delete_parser], ) def delete(self, show_id, season_id, rel_id, session): """Delete episode release by show ID, season ID and release ID.""" try: db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'show with ID {show_id} not found') try: db.season_by_id(season_id, session) except NoResultFound: raise NotFoundError(f'season with ID {season_id} not found') try: release = db.season_release_by_id(rel_id, session) except NoResultFound: raise NotFoundError(f'release with ID {rel_id} not found') if not db.season_in_show(show_id, season_id): raise BadRequest(f'season with id {season_id} does not belong to show {show_id}') if not db.release_in_season(season_id, rel_id): raise BadRequest(f'release id {rel_id} does not belong to season {season_id}') args = delete_parser.parse_args() if args.get('forget'): fire_event('forget', release.title) db.delete_season_release_by_id(rel_id) return success_response(f'successfully deleted release {rel_id} from season {season_id}')
[docs] @api.response(200, 'Successfully reset downloaded release status', model=season_release_schema) @api.doc( description='Resets the downloaded release status, clearing the quality to be downloaded again' ) def put(self, show_id, season_id, rel_id, session): """Reset a downloaded release status.""" try: db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'show with ID {show_id} not found') try: db.season_by_id(season_id, session) except NoResultFound: raise NotFoundError(f'season with ID {season_id} not found') try: release = db.season_release_by_id(rel_id, session) except NoResultFound: raise NotFoundError(f'release with ID {rel_id} not found') if not db.season_in_show(show_id, season_id): raise BadRequest(f'season with id {season_id} does not belong to show {show_id}') if not db.release_in_season(season_id, rel_id): raise BadRequest(f'release id {rel_id} does not belong to episode {season_id}') if not release.downloaded: raise BadRequest(f'release with id {rel_id} is not set as downloaded') release.downloaded = False rsp = jsonify(release.to_dict()) rsp.headers.extend({'Series-ID': show_id, 'Season-ID': season_id}) return rsp
[docs] @api.response(NotFoundError) @api.response(BadRequest) @series_api.route('/<int:show_id>/episodes/<int:ep_id>/releases/') @api.doc( params={'show_id': 'ID of the show', 'ep_id': 'Episode ID'}, description='Releases are any seen entries that match the episode. \n' "The 'Series-ID' header will be appended to the result headers.\n" "The 'Episode-ID' header will be appended to the result headers.", ) class SeriesEpisodeReleasesAPI(APIResource):
[docs] @etag @api.response(200, 'Releases retrieved successfully for episode', episode_release_list_schema) @api.doc( description='Get all matching releases for a specific episode of a specific show.', expect=[release_list_parser], ) def get(self, show_id, ep_id, session): """Get all episodes releases by show ID and episode ID.""" try: db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'show with ID {show_id} not found') try: episode = db.episode_by_id(ep_id, session) except NoResultFound: raise NotFoundError(f'episode with ID {ep_id} not found') if not db.episode_in_show(show_id, ep_id): raise BadRequest(f'episode with id {ep_id} does not belong to show {show_id}') args = release_list_parser.parse_args() # Filter params downloaded = args.get('downloaded') # Pagination and sorting params page = args['page'] per_page = args['per_page'] sort_by = args['sort_by'] sort_order = args['order'] descending = sort_order == 'desc' # Handle max size limit per_page = min(per_page, 100) start = per_page * (page - 1) stop = start + per_page kwargs = { 'downloaded': downloaded, 'start': start, 'stop': stop, 'sort_by': sort_by, 'descending': descending, 'episode': episode, 'session': session, } # Total number of releases total_items = db.get_episode_releases(count=True, **kwargs) # Release items release_items = [release.to_dict() for release in db.get_episode_releases(**kwargs)] # Total number of pages total_pages = ceil(total_items / float(per_page)) if total_pages < page and total_pages != 0: raise NotFoundError(f'page {page} does not exist') # Actual results in page actual_size = min(per_page, len(release_items)) # Get pagination headers pagination = pagination_headers(total_pages, total_items, actual_size, request) # Created response rsp = jsonify(release_items) # Add link header to response rsp.headers.extend(pagination) # Add Series-ID and Episode-ID headers rsp.headers.extend({'Series-ID': show_id, 'Episode-ID': ep_id}) return rsp
[docs] @api.response(200, 'Successfully deleted all releases for episode', model=base_message_schema) @api.doc( description='Delete all releases for a specific episode of a specific show.', expect=[release_delete_parser], ) def delete(self, show_id, ep_id, session): """Delete all episodes releases by show ID and episode ID.""" try: db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'show with ID {show_id} not found') try: episode = db.episode_by_id(ep_id, session) except NoResultFound: raise NotFoundError(f'episode with ID {ep_id} not found') if not db.episode_in_show(show_id, ep_id): raise BadRequest(f'episode with id {ep_id} does not belong to show {show_id}') args = release_delete_parser.parse_args() downloaded = args.get('downloaded') is True if args.get('downloaded') is not None else None release_items = [ release for release in episode.releases if ( (downloaded and release.downloaded) or (downloaded is False and not release.downloaded) or not downloaded ) ] for release in release_items: if args.get('forget'): fire_event('forget', release.title) db.delete_episode_release_by_id(release.id) return success_response( f'successfully deleted all releases for episode {ep_id} from show {show_id}' )
[docs] @api.response( 200, 'Successfully reset all downloaded releases for episode', model=base_message_schema ) @api.doc( description='Resets all of the downloaded releases of an episode, clearing the quality to be downloaded ' 'again,' ) def put(self, show_id, ep_id, session): """Mark all downloaded releases as not downloaded.""" try: db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'show with ID {show_id} not found') try: episode = db.episode_by_id(ep_id, session) except NoResultFound: raise NotFoundError(f'episode with ID {ep_id} not found') if not db.episode_in_show(show_id, ep_id): raise BadRequest(f'episode with id {ep_id} does not belong to show {show_id}') for release in episode.releases: if release.downloaded: release.downloaded = False return success_response( f'successfully reset download status for all releases for episode {ep_id} from show {show_id}' )
[docs] @api.response(NotFoundError) @api.response(BadRequest) @series_api.route('/<int:show_id>/episodes/<int:ep_id>/releases/<int:rel_id>/') @api.doc( params={'show_id': 'ID of the show', 'ep_id': 'Episode ID', 'rel_id': 'Release ID'}, description='Releases are any seen entries that match the episode. \n' "The 'Series-ID' header will be appended to the result headers.\n" "The 'Episode-ID' header will be appended to the result headers.", ) class SeriesEpisodeReleaseAPI(APIResource):
[docs] @etag @api.response(200, 'Release retrieved successfully for episode', episode_release_schema) @api.doc( description='Get a specific downloaded release for a specific episode of a specific show' ) def get(self, show_id, ep_id, rel_id, session): """Get episode release by show ID, episode ID and release ID.""" try: db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'show with ID {show_id} not found') try: db.episode_by_id(ep_id, session) except NoResultFound: raise NotFoundError(f'episode with ID {ep_id} not found') try: release = db.episode_release_by_id(rel_id, session) except NoResultFound: raise NotFoundError(f'release with ID {rel_id} not found') if not db.episode_in_show(show_id, ep_id): raise BadRequest(f'episode with id {ep_id} does not belong to show {show_id}') if not db.release_in_episode(ep_id, rel_id): raise BadRequest(f'release id {rel_id} does not belong to episode {ep_id}') rsp = jsonify(release.to_dict()) rsp.headers.extend({'Series-ID': show_id, 'Episode-ID': ep_id}) return rsp
[docs] @api.response(200, 'Release successfully deleted', model=base_message_schema) @api.doc( description='Delete a specific releases for a specific episode of a specific show.', expect=[delete_parser], ) def delete(self, show_id, ep_id, rel_id, session): """Delete episode release by show ID, episode ID and release ID.""" try: db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'show with ID {show_id} not found') try: db.episode_by_id(ep_id, session) except NoResultFound: raise NotFoundError(f'episode with ID {ep_id} not found') try: release = db.episode_release_by_id(rel_id, session) except NoResultFound: raise NotFoundError(f'release with ID {rel_id} not found') if not db.episode_in_show(show_id, ep_id): raise BadRequest(f'episode with id {ep_id} does not belong to show {show_id}') if not db.release_in_episode(ep_id, rel_id): raise BadRequest(f'release id {rel_id} does not belong to episode {ep_id}') args = delete_parser.parse_args() if args.get('forget'): fire_event('forget', release.title) db.delete_episode_release_by_id(rel_id) return success_response(f'successfully deleted release {rel_id} from episode {ep_id}')
[docs] @api.response( 200, 'Successfully reset downloaded release status', model=episode_release_schema ) @api.doc( description='Resets the downloaded release status, clearing the quality to be downloaded again' ) def put(self, show_id, ep_id, rel_id, session): """Reset a downloaded release status.""" try: db.show_by_id(show_id, session=session) except NoResultFound: raise NotFoundError(f'show with ID {show_id} not found') try: db.episode_by_id(ep_id, session) except NoResultFound: raise NotFoundError(f'episode with ID {ep_id} not found') try: release = db.episode_release_by_id(rel_id, session) except NoResultFound: raise NotFoundError(f'release with ID {rel_id} not found') if not db.episode_in_show(show_id, ep_id): raise BadRequest(f'episode with id {ep_id} does not belong to show {show_id}') if not db.release_in_episode(ep_id, rel_id): raise BadRequest(f'release id {rel_id} does not belong to episode {ep_id}') if not release.downloaded: raise BadRequest(f'release with id {rel_id} is not set as downloaded') release.downloaded = False rsp = jsonify(release.to_dict()) rsp.headers.extend({'Series-ID': show_id, 'Episode-ID': ep_id}) return rsp