1from math import ceil 2from urllib.parse import unquote 3 4from flask import jsonify, request 5from flask_restx import inputs 6from sqlalchemy.orm.exc import NoResultFound 7 8from flexget.api import APIResource, api 9from flexget.api.app import ( 10 NotFoundError, 11 base_message_schema, 12 etag, 13 pagination_headers, 14 success_response, 15) 16 17from . import db 18 19seen_api = api.namespace('seen', description='Managed Flexget seen entries and fields') 20 21 22class ObjectsContainer: 23 seen_field_object = { 24 'type': 'object', 25 'properties': { 26 'id': {'type': 'integer'}, 27 'field': {'type': 'string'}, 28 'value': {'type': 'string'}, 29 'added': {'type': 'string', 'format': 'date-time'}, 30 'seen_entry_id': {'type': 'integer'}, 31 }, 32 } 33 34 seen_object = { 35 'type': 'object', 36 'properties': { 37 'id': {'type': 'integer'}, 38 'title': {'type': 'string'}, 39 'reason': {'type': 'string'}, 40 'task': {'type': 'string'}, 41 'added': {'type': 'string', 'format': 'date-time'}, 42 'local': {'type': 'boolean'}, 43 'fields': {'type': 'array', 'items': seen_field_object}, 44 }, 45 } 46 47 seen_search_object = {'type': 'array', 'items': seen_object} 48 49 50seen_object_schema = api.schema_model('seen_object_schema', ObjectsContainer.seen_object) 51seen_search_schema = api.schema_model('seen_search_schema', ObjectsContainer.seen_search_object) 52 53seen_base_parser = api.parser() 54seen_base_parser.add_argument( 55 'value', help='Filter by any field value or leave empty to get all entries' 56) 57seen_base_parser.add_argument( 58 'local', type=inputs.boolean, default=None, help='Filter results by seen locality.' 59) 60 61sort_choices = ('title', 'task', 'added', 'local', 'reason', 'id') 62seen_search_parser = api.pagination_parser(seen_base_parser, sort_choices) 63 64 65@seen_api.route('/') 66class SeenSearchAPI(APIResource): 67 @etag 68 @api.response(NotFoundError) 69 @api.response(200, 'Successfully retrieved seen objects', seen_search_schema) 70 @api.doc(parser=seen_search_parser, description='Get seen entries') 71 def get(self, session): 72 """Search for seen entries""" 73 args = seen_search_parser.parse_args() 74 75 # Filter params 76 value = args['value'] 77 local = args['local'] 78 79 # Pagination and sorting params 80 page = args['page'] 81 per_page = args['per_page'] 82 sort_by = args['sort_by'] 83 sort_order = args['order'] 84 85 # Handle max size limit 86 if per_page > 100: 87 per_page = 100 88 89 descending = sort_order == 'desc' 90 91 # Unquotes and prepares value for DB lookup 92 if value: 93 value = unquote(value) 94 value = '%{0}%'.format(value) 95 96 start = per_page * (page - 1) 97 stop = start + per_page 98 99 kwargs = { 100 'value': value, 101 'status': local, 102 'stop': stop, 103 'start': start, 104 'order_by': sort_by, 105 'descending': descending, 106 'session': session, 107 } 108 109 total_items = db.search(count=True, **kwargs) 110 111 if not total_items: 112 return jsonify([]) 113 114 raw_seen_entries_list = db.search(**kwargs).all() 115 116 converted_seen_entry_list = [entry.to_dict() for entry in raw_seen_entries_list] 117 118 # Total number of pages 119 total_pages = int(ceil(total_items / float(per_page))) 120 121 # Actual results in page 122 actual_size = min(len(converted_seen_entry_list), per_page) 123 124 # Invalid page request 125 if page > total_pages and total_pages != 0: 126 raise NotFoundError('page %s does not exist' % page) 127 128 # Get pagination headers 129 pagination = pagination_headers(total_pages, total_items, actual_size, request) 130 131 # Create response 132 rsp = jsonify(converted_seen_entry_list) 133 134 # Add link header to response 135 rsp.headers.extend(pagination) 136 return rsp 137 138 @api.response(200, 'Successfully delete all entries', model=base_message_schema) 139 @api.doc(parser=seen_base_parser, description='Delete seen entries') 140 def delete(self, session): 141 """Delete seen entries""" 142 args = seen_base_parser.parse_args() 143 value = args['value'] 144 local = args['local'] 145 146 if value: 147 value = unquote(value) 148 value = '%' + value + '%' 149 seen_entries_list = db.search(value=value, status=local, session=session) 150 151 deleted = 0 152 for se in seen_entries_list: 153 db.forget_by_id(se.id, session=session) 154 deleted += 1 155 return success_response('successfully deleted %i entries' % deleted) 156 157 158@seen_api.route('/<int:seen_entry_id>/') 159@api.doc(params={'seen_entry_id': 'ID of seen entry'}) 160@api.response(NotFoundError) 161class SeenSearchIDAPI(APIResource): 162 @etag 163 @api.response(200, model=seen_object_schema) 164 def get(self, seen_entry_id, session): 165 """Get seen entry by ID""" 166 try: 167 seen_entry = db.get_entry_by_id(seen_entry_id, session=session) 168 except NoResultFound: 169 raise NotFoundError('Could not find entry ID {0}'.format(seen_entry_id)) 170 return jsonify(seen_entry.to_dict()) 171 172 @api.response(200, 'Successfully deleted entry', model=base_message_schema) 173 def delete(self, seen_entry_id, session): 174 """Delete seen entry by ID""" 175 try: 176 entry = db.get_entry_by_id(seen_entry_id, session=session) 177 except NoResultFound: 178 raise NotFoundError('Could not delete entry ID {0}'.format(seen_entry_id)) 179 db.forget_by_id(entry.id, session=session) 180 return success_response('successfully deleted seen entry {}'.format(seen_entry_id)) 181