diff --git a/README.md b/README.md index b78fbc5..3589794 100644 --- a/README.md +++ b/README.md @@ -136,10 +136,7 @@ where `5d7b841764c598157d53ef4a` is project's `_id` you want to make a duplicate curl -X PUT \ http://0.0.0.0:5050/projects/5d7a35a04be797ba845e7871 \ -d '{ - "trim": { - "start": 2, - "end": 5 - } + "trim": "2,5" }' ``` where `2` and `5` are seconds. @@ -169,12 +166,7 @@ where `480` is width you want to scale video to. curl -X PUT \ http://0.0.0.0:5050/projects/5d7a35a04be797ba845e7871 \ -d '{ - "crop": { - "height": 180, - "width": 320, - "x": 0, - "y": 0 - } + "crop": "0,0,180,320" }' ``` where `width` and `height` are respectively width and height of capturing area, @@ -193,7 +185,7 @@ curl -X GET 'http://0.0.0.0:5050/projects/5d7b98f52fac91d2e1ad7512/thumbnails?ty where `position` is a position in the video (seconds) used to capture a thumbnail. You can also specify optional `crop` param if you want to crop a preview thumbnail, just add -`crop={ "height": 180, "width": 320, "x": 0, "y": 0 }`. +`crop="0,0,180,320"`. Example: ```bash curl -X GET \ diff --git a/setup.cfg b/setup.cfg index 617afa2..75e08fc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [flake8] max-line-length=120 -exclude=env,bin,lib,include,src,docs +exclude=.tox,__pycache__,env,bin,include,docs,*.pyc ignore=F811,D200,D202,D205,D400,D401,D100,D101,D102,D103,D104,D105,D107,W503,W504,W605,F401,E261,F841,D413 # W504, W605, F401, E261 and F841 are temporarly ignored, due to recent changes in flake8 \ No newline at end of file diff --git a/src/videoserver/apps/projects/routes.py b/src/videoserver/apps/projects/routes.py index f178a96..a7aea7d 100644 --- a/src/videoserver/apps/projects/routes.py +++ b/src/videoserver/apps/projects/routes.py @@ -2,7 +2,6 @@ import logging import os import re -from ast import literal_eval from datetime import datetime import bson @@ -15,8 +14,8 @@ from videoserver.lib.video_editor import get_video_editor from videoserver.lib.views import MethodView from videoserver.lib.utils import ( - add_urls, create_file_name, get_request_address, json_response, - paginate, save_activity_log, storage2response, validate_document + add_urls, create_file_name, get_request_address, json_response, paginate, save_activity_log, storage2response, + validate_document, coerce_crop_str_to_dict, coerce_trim_str_to_dict ) from . import bp @@ -167,7 +166,7 @@ def post(self): }, 'thumbnails': { 'timeline': [], - 'preview': None + 'preview': {}, } } @@ -338,20 +337,11 @@ class RetrieveEditDestroyProject(MethodView): def schema_edit(self): return { 'trim': { - 'type': 'dict', 'required': False, - 'schema': { - 'start': { - 'type': 'float', - 'min': 0, - 'required': True - }, - 'end': { - 'type': 'float', - 'min': 1, - 'required': True - }, - } + 'regex': r'^\d+\.?\d*,\d+\.?\d*$', + 'coerce': coerce_trim_str_to_dict, + 'min_trim_start': 0, + 'min_trim_end': 1 }, 'rotate': { 'type': 'integer', @@ -365,33 +355,11 @@ def schema_edit(self): 'required': False }, 'crop': { - 'type': 'dict', 'required': False, - 'empty': True, - 'schema': { - 'width': { - 'type': 'integer', - 'min': app.config.get('MIN_VIDEO_WIDTH'), - 'max': app.config.get('MAX_VIDEO_WIDTH'), - 'required': True - }, - 'height': { - 'type': 'integer', - 'min': app.config.get('MIN_VIDEO_HEIGHT'), - 'max': app.config.get('MAX_VIDEO_HEIGHT'), - 'required': True - }, - 'x': { - 'type': 'integer', - 'required': True, - 'min': 0 - }, - 'y': { - 'type': 'integer', - 'required': True, - 'min': 0 - } - } + 'regex': r'^\d+,\d+,\d+,\d+$', + 'coerce': coerce_crop_str_to_dict, + 'allow_crop_width': [app.config.get('MIN_VIDEO_WIDTH'), app.config.get('MAX_VIDEO_WIDTH')], + 'allow_crop_height': [app.config.get('MIN_VIDEO_HEIGHT'), app.config.get('MAX_VIDEO_HEIGHT')] } } @@ -520,29 +488,11 @@ def put(self, project_id): type: object properties: trim: - type: object - properties: - start: - type: integer - example: 5 - end: - type: integer - example: 10 + type: string + example: 5.1,10.5 crop: - type: object - properties: - width: - type: integer - example: 480 - height: - type: integer - example: 360 - x: - type: integer - example: 10 - y: - type: integer - example: 10 + type: string + example: 480,360,10,10 rotate: type: integer enum: [-270, -180, -90, 90, 180, 270] @@ -600,9 +550,10 @@ def put(self, project_id): {"start": [f"trimmed video must be at least {app.config.get('MIN_TRIM_DURATION')} seconds"]} ]}) elif document['trim']['end'] > metadata['duration']: - raise BadRequest({"trim": [ - {"end": [f"outside of initial video's length"]} - ]}) + document['trim']['end'] = metadata['duration'] + logger.info( + f"Trimmed video endtime greater than video duration, update it to equal duration, " + f"ID: {self.project['_id']}") elif document['trim']['start'] == 0 and document['trim']['end'] == metadata['duration']: raise BadRequest({"trim": [ {"end": ["trim is duplicating an entire video"]} @@ -804,7 +755,7 @@ def post(self, project_id): child_project['version'] += 1 child_project['thumbnails'] = { 'timeline': [], - 'preview': None + 'preview': {} } app.mongo.db.projects.insert_one(child_project) @@ -934,32 +885,11 @@ def schema_thumbnails(self): 'coerce': float, }, 'crop': { - 'type': 'dict', - 'coerce': literal_eval, # crop args are a string represent of a dict 'required': False, - 'empty': True, - 'schema': { - 'width': { - 'type': 'integer', - 'required': True, - 'min': app.config.get('MIN_VIDEO_WIDTH'), - }, - 'height': { - 'type': 'integer', - 'required': True, - 'min': app.config.get('MIN_VIDEO_HEIGHT'), - }, - 'x': { - 'type': 'integer', - 'required': True, - 'min': 0 - }, - 'y': { - 'type': 'integer', - 'required': True, - 'min': 0 - } - } + 'regex': r'^\d+,\d+,\d+,\d+$', + 'coerce': coerce_crop_str_to_dict, + 'allow_crop_width': [app.config.get('MIN_VIDEO_WIDTH'), app.config.get('MAX_VIDEO_WIDTH')], + 'allow_crop_height': [app.config.get('MIN_VIDEO_HEIGHT'), app.config.get('MAX_VIDEO_HEIGHT')] }, 'rotate': { 'type': 'integer', @@ -998,7 +928,7 @@ def get(self, project_id): in: query type: json description: Crop rules apply to preview thumbnail. Used only when `type` is `preview`. - default: "{'width': 720, 'height': 360, 'x': 0, 'y':0}" + default: "0,0,720,360" - name: rotate in: query type: integer @@ -1226,15 +1156,13 @@ def _get_preview_thumbnail(self, position, crop, rotate): raise BadRequest({"crop": [{"width": ["crop's frame is outside a video's frame"]}]}) elif crop['y'] + crop['height'] > self.project['metadata']['height']: raise BadRequest({"crop": [{"height": ["crop's frame is outside a video's frame"]}]}) - + # validate position param + if self.project['metadata']['duration'] < position: + position = self.project['metadata']['duration'] + logger.info(f"Postition greater than video duration, Update it equal duration, ID: {self.project['_id']}") # resource is busy if self.project['processing']['thumbnail_preview']: raise Conflict({"processing": ["Task get preview thumbnails video is still processing"]}) - elif self.project['metadata']['duration'] < position: - raise BadRequest({ - 'position': [f"Requested position: '{position}' is more than video's duration: " - f"'{self.project['metadata']['duration']}'."] - }) else: # set processing flag self.project = app.mongo.db.projects.find_one_and_update( diff --git a/src/videoserver/lib/storage/file_system_storage.py b/src/videoserver/lib/storage/file_system_storage.py index d00c8b9..972077b 100644 --- a/src/videoserver/lib/storage/file_system_storage.py +++ b/src/videoserver/lib/storage/file_system_storage.py @@ -67,7 +67,8 @@ def get_range(self, storage_id, start, length): return media_file - def put(self, content, filename, project_id=None, asset_type='project', storage_id=None, content_type=None): + def put(self, content, filename, project_id=None, asset_type='project', storage_id=None, content_type=None, + override=True): """ Save file into a fs storage. @@ -110,7 +111,9 @@ def put(self, content, filename, project_id=None, asset_type='project', storage_ file_path = self._get_file_path(storage_id) # check if file exists if os.path.exists(file_path): - raise Exception(f'File {file_path} already exists, use "replace" method instead.') + if not override: + raise Exception(f'File {file_path} already exists, use "replace" method instead.') + self.replace(content, storage_id) # check if dir exists, if not create it file_dir = os.path.dirname(file_path) @@ -181,4 +184,4 @@ def delete_dir(self, storage_id): shutil.rmtree(dir_path) logger.info(f"Removed '{dir_path}' from fs storage") else: - logger.warning(f"Directory '{dir_path}' was not found in fs storage.") \ No newline at end of file + logger.warning(f"Directory '{dir_path}' was not found in fs storage.") diff --git a/src/videoserver/lib/utils.py b/src/videoserver/lib/utils.py index 057351a..c05909e 100644 --- a/src/videoserver/lib/utils.py +++ b/src/videoserver/lib/utils.py @@ -92,7 +92,7 @@ def _handle_doc(doc): _external=True ) - if doc['thumbnails']['preview']: + if doc['thumbnails']['preview'] or doc['processing']['thumbnail_preview']: doc['thumbnails']['preview']['url'] = url_for( 'projects.get_raw_preview_thumbnail', project_id=doc['_id'], @@ -125,6 +125,22 @@ def save_activity_log(action, project_id, payload=None): }) +def coerce_crop_str_to_dict(value): + """ + Use for coerce crop value from str (x,y,w,h) to dict + """ + x, y, width, height = [int(item) for item in value.split(',')] + return {"x": x, "y": y, "width": width, "height": height} + + +def coerce_trim_str_to_dict(value): + """ + Use for coerce trim value from str (start,end) to dict + """ + start, end = [float(item) for item in value.split(',')] + return {"start": start, "end": end} + + def validate_document(document, schema, **kwargs): """ Validate `document` against provided `schema` @@ -138,12 +154,54 @@ def validate_document(document, schema, **kwargs): :raise: `BadRequest` if `document` is not valid """ - validator = Validator(schema, **kwargs) + validator = VideoValidator(schema, **kwargs) if not validator.validate(document): raise BadRequest(validator.errors) return validator.document +class VideoValidator(Validator): + def _validate_allow_crop_width(self, limit, field, value): + """Test allowed crop width range + The rule's arguments are validated against this schema: + {'min': 'limit[0]', 'max': 'limit[1]'} + """ + if limit and len(limit) == 2: + wmin, wmax = limit + if value['width'] < wmin: + self._error(field, "width is lesser than minimum allowed crop width") + if value['width'] > wmax: + self._error(field, "width is greater than maximum allowed crop width") + + def _validate_allow_crop_height(self, limit, field, value): + """Test allowed crop height range + The rule's arguments are validated against this schema: + {'min': 'limit[0]', 'max': 'limit[1]'} + """ + if limit and len(limit) == 2: + hmin, hmax = limit + if value['height'] < hmin: + self._error(field, "height is lesser than minimum allowed crop height") + if value['height'] > hmax: + self._error(field, "height is greater than maximum allowed crop height") + + def _validate_min_trim_start(self, min_trim, field, value): + """Test minimum allowed trim start value + The rule's arguments are validated against this schema: + {'min': 'min_trim'} + """ + if min_trim is not None and value['start'] < min_trim: + self._error(field, "start time must be greater than %s" % min_trim) + + def _validate_min_trim_end(self, min_trim, field, value): + """Test minium allowed trim end value + The rule's arguments are validated against this schema: + {'min': 'min_trim'} + """ + if min_trim is not None and value['end'] < min_trim: + self._error(field, "end time must be greater than %s" % min_trim) + + def get_request_address(request_headers): return request_headers.get('HTTP_X_FORWARDED_FOR') or request_headers.get('REMOTE_ADDR') @@ -170,10 +228,10 @@ def create_temp_file(file_stream, suffix=None): def storage2response(storage_id, headers=None, status=200, start=None, length=None): """ Fetch binary using `storage_id` and return http response. - + :param storage_id: Unique storage id :type storage_id: str - :param headers: header for response + :param headers: header for response :type headers: dict :param status: http status code :type status: int diff --git a/src/videoserver/lib/validator.py b/src/videoserver/lib/validator.py index bc938ec..e273284 100644 --- a/src/videoserver/lib/validator.py +++ b/src/videoserver/lib/validator.py @@ -2,10 +2,10 @@ from werkzeug.datastructures import FileStorage as WerkzeugFileStorage - class Validator(DefaultValidator): """ Custom validator with additional types """ + types_mapping = DefaultValidator.types_mapping.copy() types_mapping['filestorage'] = TypeDefinition('filestorage', (WerkzeugFileStorage,), ()) diff --git a/src/videoserver/lib/video_editor/ffmpeg.py b/src/videoserver/lib/video_editor/ffmpeg.py index 5de178e..7ea1344 100644 --- a/src/videoserver/lib/video_editor/ffmpeg.py +++ b/src/videoserver/lib/video_editor/ffmpeg.py @@ -161,7 +161,7 @@ def capture_thumbnail(self, stream_file, filename, duration, position, crop=None if rotate: vfilter += ',' if vfilter else '-vf ' transpose = f'transpose=1' if rotate > 0 else f'transpose=2' - vfilter += ','.join([transpose] * (rotate // 90)) + vfilter += ','.join([transpose] * abs(rotate // 90)) try: # run ffmpeg command diff --git a/tests/api/test_duplicate_project.py b/tests/api/test_duplicate_project.py index 1cc7c15..1a03fda 100644 --- a/tests/api/test_duplicate_project.py +++ b/tests/api/test_duplicate_project.py @@ -27,7 +27,7 @@ def test_duplicate_project_success(test_app, client, projects): assert resp_data['version'] == 2 assert resp_data['parent'] == project['_id'] assert resp_data['processing'] == {'video': False, 'thumbnail_preview': False, 'thumbnails_timeline': False} - assert resp_data['thumbnails'] == {'timeline': [], 'preview': None} + assert resp_data['thumbnails'] == {'timeline': [], 'preview': {}} assert resp_data['url'] == url_for('projects.get_raw_video', project_id=resp_data["_id"], _external=True) assert resp_data['metadata']['codec_name'] == 'h264' assert resp_data['metadata']['codec_long_name'] == 'H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10' diff --git a/tests/api/test_list_upload.py b/tests/api/test_list_upload.py index f65d410..0dbcd64 100644 --- a/tests/api/test_list_upload.py +++ b/tests/api/test_list_upload.py @@ -34,7 +34,7 @@ def test_upload_project_success(test_app, client, filestreams): assert resp_data['version'] == 1 assert resp_data['parent'] is None assert resp_data['processing'] == {'video': False, 'thumbnail_preview': False, 'thumbnails_timeline': False} - assert resp_data['thumbnails'] == {'timeline': [], 'preview': None} + assert resp_data['thumbnails'] == {'timeline': [], 'preview': {}} assert resp_data['url'] == url_for('projects.get_raw_video', project_id=resp_data["_id"], _external=True) @@ -168,7 +168,7 @@ def test_list_projects(test_app, client, filestreams): 'thumbnail_preview': False, 'thumbnails_timeline': False } - assert resp_data['_items'][0]['thumbnails'] == {'timeline': [], 'preview': None} + assert resp_data['_items'][0]['thumbnails'] == {'timeline': [], 'preview': {}} assert resp_data['_items'][0]['url'] == url_for('projects.get_raw_video', project_id=resp_data["_items"][0]["_id"], _external=True) # list 1nd page explicitly diff --git a/tests/api/test_retrieve_create_thumbnails.py b/tests/api/test_retrieve_create_thumbnails.py index a2c4379..bdec044 100644 --- a/tests/api/test_retrieve_create_thumbnails.py +++ b/tests/api/test_retrieve_create_thumbnails.py @@ -104,25 +104,30 @@ def test_capture_preview_thumbnail_success(test_app, client, projects): assert resp.status == '200 OK' assert test_app.fs.get(resp_data['thumbnails']['preview']['storage_id']).__class__ is bytes - -@pytest.mark.parametrize('projects', [({'file': 'sample_0.mp4', 'duplicate': False},)], indirect=True) -def test_capture_preview_thumbnail_bad_position(test_app, client, projects): - project = projects[0] - position = 700 - + # postion greater than duration + position = 20 with test_app.test_request_context(): url = url_for( 'projects.retrieve_or_create_thumbnails', project_id=project['_id'] ) + f'?type=preview&position={position}' resp = client.get(url) - assert resp.status == '400 BAD REQUEST' + resp_data = json.loads(resp.data) + assert resp.status == '202 ACCEPTED' + assert resp_data == {'processing': True} + + resp = client.get( + url_for('projects.retrieve_edit_destroy_project', project_id=project['_id']) + ) + resp_data = json.loads(resp.data) + assert resp.status == '200 OK' + assert test_app.fs.get(resp_data['thumbnails']['preview']['storage_id']).__class__ is bytes @pytest.mark.parametrize('projects', [({'file': 'sample_0.mp4', 'duplicate': False},)], indirect=True) def test_capture_preview_thumbnail_crop_success(test_app, client, projects): project = projects[0] position = 4 - crop = {'width': 640, 'height': 480, 'x': 0, 'y': 0} + crop = "0,0,640,480" with test_app.test_request_context(): url = url_for( @@ -146,7 +151,7 @@ def test_capture_preview_thumbnail_crop_fail(test_app, client, projects): position = 4 with test_app.test_request_context(): - crop = {'width': 640, 'height': 480, 'x': 1000, 'y': 0} + crop = "1000,0,640,480" url = url_for( 'projects.retrieve_or_create_thumbnails', project_id=project['_id'] ) + f'?type=preview&position={position}&crop={crop}' @@ -155,7 +160,7 @@ def test_capture_preview_thumbnail_crop_fail(test_app, client, projects): assert resp.status == '400 BAD REQUEST' assert resp_data == {'crop': [{'x': ['less than minimum allowed crop width']}]} - crop = {'width': 640, 'height': 480, 'x': 0, 'y': 1000} + crop = "0,1000,640,480" url = url_for( 'projects.retrieve_or_create_thumbnails', project_id=project['_id'] ) + f'?type=preview&position={position}&crop={crop}' @@ -164,7 +169,25 @@ def test_capture_preview_thumbnail_crop_fail(test_app, client, projects): assert resp.status == '400 BAD REQUEST' assert resp_data == {'crop': [{'y': ['less than minimum allowed crop height']}]} - crop = {'width': 10000, 'height': 480, 'x': 0, 'y': 0} + crop = "0,0,10000,480" + url = url_for( + 'projects.retrieve_or_create_thumbnails', project_id=project['_id'] + ) + f'?type=preview&position={position}&crop={crop}' + resp = client.get(url) + resp_data = json.loads(resp.data) + assert resp.status == '400 BAD REQUEST' + assert resp_data == {'crop': ['width is greater than maximum allowed crop width']} + + crop = "0,0,640,10000" + url = url_for( + 'projects.retrieve_or_create_thumbnails', project_id=project['_id'] + ) + f'?type=preview&position={position}&crop={crop}' + resp = client.get(url) + resp_data = json.loads(resp.data) + assert resp.status == '400 BAD REQUEST' + assert resp_data == {'crop': ['height is greater than maximum allowed crop height']} + + crop = "0,0,1640,480" url = url_for( 'projects.retrieve_or_create_thumbnails', project_id=project['_id'] ) + f'?type=preview&position={position}&crop={crop}' @@ -173,7 +196,7 @@ def test_capture_preview_thumbnail_crop_fail(test_app, client, projects): assert resp.status == '400 BAD REQUEST' assert resp_data == {'crop': [{'width': ["crop's frame is outside a video's frame"]}]} - crop = {'width': 640, 'height': 10000, 'x': 0, 'y': 0} + crop = "0,0,640,1480" url = url_for( 'projects.retrieve_or_create_thumbnails', project_id=project['_id'] ) + f'?type=preview&position={position}&crop={crop}' diff --git a/tests/api/test_retrieve_edit_destroy.py b/tests/api/test_retrieve_edit_destroy.py index 5038cd2..bafe57c 100644 --- a/tests/api/test_retrieve_edit_destroy.py +++ b/tests/api/test_retrieve_edit_destroy.py @@ -26,7 +26,7 @@ def test_retrieve_project_success(test_app, client, projects): assert resp_data['version'] == 1 assert resp_data['parent'] is None assert resp_data['processing'] == {'video': False, 'thumbnail_preview': False, 'thumbnails_timeline': False} - assert resp_data['thumbnails'] == {'timeline': [], 'preview': None} + assert resp_data['thumbnails'] == {'timeline': [], 'preview': {}} assert resp_data['url'] == url_for('projects.get_raw_video', project_id=resp_data["_id"], _external=True) assert resp_data['metadata']['codec_name'] == 'h264' assert resp_data['metadata']['codec_long_name'] == 'H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10' @@ -92,15 +92,11 @@ def test_edit_project_409_response(test_app, client, projects): # edit request url = url_for('projects.retrieve_edit_destroy_project', project_id=project['_id']) - start = 2.0 - end = 6.0 + trim = '2.0,6.0' resp = client.put( url, data=json.dumps({ - "trim": { - "start": start, - "end": end - } + "trim": trim }), content_type='application/json' ) @@ -129,15 +125,11 @@ def test_edit_project_version_1(test_app, client, projects): with test_app.test_request_context(): # edit request url = url_for('projects.retrieve_edit_destroy_project', project_id=project['_id']) - start = 2.0 - end = 6.0 + trim = '2.0,6.0' resp = client.put( url, data=json.dumps({ - "trim": { - "start": start, - "end": end - } + "trim": trim }), content_type='application/json' ) @@ -156,10 +148,7 @@ def test_edit_project_trim_success(test_app, client, projects): resp = client.put( url, data=json.dumps({ - "trim": { - "start": start, - "end": end - } + "trim": "%s,%s" % (start, end) }), content_type='application/json' ) @@ -171,6 +160,26 @@ def test_edit_project_trim_success(test_app, client, projects): resp_data = json.loads(resp.data) assert not resp_data['processing']['video'] assert resp_data['metadata']['duration'] == end - start + # edit request have trim end greater than duration + old_duration = resp_data['metadata']['duration'] + url = url_for('projects.retrieve_edit_destroy_project', project_id=project['_id']) + start = 2.0 + end = 6.0 + resp = client.put( + url, + data=json.dumps({ + "trim": "%s,%s" % (start, end) + }), + content_type='application/json' + ) + resp_data = json.loads(resp.data) + assert resp.status == '202 ACCEPTED' + assert resp_data == {'processing': True} + # get details + resp = client.get(url) + resp_data = json.loads(resp.data) + assert not resp_data['processing']['video'] + assert resp_data['metadata']['duration'] == old_duration - start @pytest.mark.parametrize('projects', [({'file': 'sample_0.mp4', 'duplicate': True},)], indirect=True) @@ -184,10 +193,7 @@ def test_edit_project_trim_fail(test_app, client, projects): resp = client.put( url, data=json.dumps({ - "trim": { - "start": 6.0, - "end": 2.0 - } + "trim": "6.0,2.0" }), content_type='application/json' ) @@ -199,10 +205,7 @@ def test_edit_project_trim_fail(test_app, client, projects): resp = client.put( url, data=json.dumps({ - "trim": { - "start": 0, - "end": 1 - } + "trim": "0,1" }), content_type='application/json' ) @@ -214,31 +217,37 @@ def test_edit_project_trim_fail(test_app, client, projects): resp = client.put( url, data=json.dumps({ - "trim": { - "start": 10, - "end": 20 - } + "trim": "0,15" }), content_type='application/json' ) resp_data = json.loads(resp.data) assert resp.status == '400 BAD REQUEST' - assert resp_data == {'trim': [{'end': ["outside of initial video's length"]}]} + assert resp_data == {'trim': [{'end': ['trim is duplicating an entire video']}]} # edit request resp = client.put( url, data=json.dumps({ - "trim": { - "start": 0, - "end": 15 - } + "trim": "-1,3" }), content_type='application/json' ) resp_data = json.loads(resp.data) assert resp.status == '400 BAD REQUEST' - assert resp_data == {'trim': [{'end': ['trim is duplicating an entire video']}]} + assert resp_data == {'trim': ['start time must be greater than 0']} + + # edit request + resp = client.put( + url, + data=json.dumps({ + "trim": "0,0" + }), + content_type='application/json' + ) + resp_data = json.loads(resp.data) + assert resp.status == '400 BAD REQUEST' + assert resp_data == {'trim': ['end time must be greater than 1']} @pytest.mark.parametrize('projects', [({'file': 'sample_0.mp4', 'duplicate': True},)], indirect=True) @@ -292,12 +301,7 @@ def test_edit_project_crop_success(test_app, client, projects): resp = client.put( url, data=json.dumps({ - "crop": { - "x": 0, - "y": 0, - "width": 640, - "height": 480 - } + "crop": "0,0,640,480" }), content_type='application/json' ) @@ -322,12 +326,7 @@ def test_edit_project_crop_fail(test_app, client, projects): resp = client.put( url, data=json.dumps({ - "crop": { - "x": 2000, - "y": 0, - "width": 640, - "height": 480 - } + "crop": "2000,0,640,480" }), content_type='application/json' ) @@ -339,12 +338,7 @@ def test_edit_project_crop_fail(test_app, client, projects): resp = client.put( url, data=json.dumps({ - "crop": { - "x": 0, - "y": 1000, - "width": 640, - "height": 480 - } + "crop": "0,1000,640,480" }), content_type='application/json' ) @@ -356,12 +350,7 @@ def test_edit_project_crop_fail(test_app, client, projects): resp = client.put( url, data=json.dumps({ - "crop": { - "x": 300, - "y": 0, - "width": 1000, - "height": 480 - } + "crop": "300,0,1000,480" }), content_type='application/json' ) @@ -373,12 +362,7 @@ def test_edit_project_crop_fail(test_app, client, projects): resp = client.put( url, data=json.dumps({ - "crop": { - "x": 0, - "y": 200, - "width": 640, - "height": 600 - } + "crop": "0,200,640,600" }), content_type='application/json' ) @@ -386,6 +370,42 @@ def test_edit_project_crop_fail(test_app, client, projects): assert resp.status == '400 BAD REQUEST' assert resp_data == {'crop': [{'height': ["crop's frame is outside a video's frame"]}]} + # edit request + resp = client.put( + url, + data=json.dumps({ + "crop": "0,200,10000,600" + }), + content_type='application/json' + ) + resp_data = json.loads(resp.data) + assert resp.status == '400 BAD REQUEST' + assert resp_data == {'crop': ['width is greater than maximum allowed crop width']} + + # edit request + resp = client.put( + url, + data=json.dumps({ + "crop": "0,0,300,600" + }), + content_type='application/json' + ) + resp_data = json.loads(resp.data) + assert resp.status == '400 BAD REQUEST' + assert resp_data == {'crop': ['width is lesser than minimum allowed crop width']} + + # edit request + resp = client.put( + url, + data=json.dumps({ + "crop": "0,0,640,100" + }), + content_type='application/json' + ) + resp_data = json.loads(resp.data) + assert resp.status == '400 BAD REQUEST' + assert resp_data == {'crop': ['height is lesser than minimum allowed crop height']} + @pytest.mark.parametrize('projects', [({'file': 'sample_0.mp4', 'duplicate': True},)], indirect=True) def test_edit_project_scale_success(test_app, client, projects): @@ -495,12 +515,7 @@ def test_edit_project_scale_and_crop_success(test_app, client, projects): url, data=json.dumps({ "scale": 640, - "crop": { - "x": 0, - "y": 0, - "width": 400, - "height": 400 - } + "crop": "0,0,400,400" }), content_type='application/json' ) diff --git a/tests/storage/test_file_system_storage.py b/tests/storage/test_file_system_storage.py index 1c55ea2..82947c5 100644 --- a/tests/storage/test_file_system_storage.py +++ b/tests/storage/test_file_system_storage.py @@ -47,6 +47,18 @@ def test_fs_storage_put_already_exist(test_app, filestreams): project_id = 'project_one' with test_app.app_context(): + with pytest.raises(ValueError, match="Argument 'project_id' is required when 'asset_type' is 'project'"): + storage_id = storage.put( + content=mp4_stream, + filename='sample_video.mp4', + asset_type='project' + ) + with pytest.raises(ValueError, match="Argument 'storage_id' is required when 'asset_type' is not 'project'"): + storage.put( + content=jpg_stream_0, + filename='sample_image.jpg', + asset_type='thumbnail' + ) storage_id = storage.put( content=mp4_stream, filename='sample_video.mp4', @@ -64,8 +76,15 @@ def test_fs_storage_put_already_exist(test_app, filestreams): content=jpg_stream_0, filename='sample_image.jpg', storage_id=storage_id, - asset_type='thumbnail' + asset_type='thumbnail', + override=False ) + storage.put( + content=jpg_stream_0, + filename='sample_image.jpg', + storage_id=storage_id, + asset_type='thumbnail', + ) @pytest.mark.parametrize('filestreams', [('sample_0.mp4',)], indirect=True)