@@ -225,7 +225,8 @@ async def validate_output_path(
225225def validate_operations (operations : List [Dict [str , Any ]]) -> List [Dict [str , Any ]]:
226226 """Validate and normalize operations list with enhanced security checks."""
227227 if not operations :
228- raise ValueError ("Operations list cannot be empty" )
228+ # Empty operations list is valid - will use default transcoding
229+ return []
229230
230231 max_ops = settings .MAX_OPERATIONS_PER_JOB
231232 if len (operations ) > max_ops : # Prevent DOS through too many operations
@@ -256,10 +257,24 @@ def validate_operations(operations: List[Dict[str, Any]]) -> List[Dict[str, Any]
256257 validated_op = validate_watermark_operation (op )
257258 elif op_type == "filter" :
258259 validated_op = validate_filter_operation (op )
259- elif op_type == "stream" :
260+ elif op_type in ( "stream" , "streaming" ) :
260261 validated_op = validate_stream_operation (op )
261262 elif op_type == "transcode" :
262263 validated_op = validate_transcode_operation (op )
264+ elif op_type == "scale" :
265+ validated_op = validate_scale_operation (op )
266+ elif op_type == "crop" :
267+ validated_op = validate_crop_operation (op )
268+ elif op_type == "rotate" :
269+ validated_op = validate_rotate_operation (op )
270+ elif op_type == "flip" :
271+ validated_op = validate_flip_operation (op )
272+ elif op_type == "audio" :
273+ validated_op = validate_audio_operation (op )
274+ elif op_type == "subtitle" :
275+ validated_op = validate_subtitle_operation (op )
276+ elif op_type == "concat" :
277+ validated_op = validate_concat_operation (op )
263278 else :
264279 raise ValueError (f"Unknown operation type: { op_type } " )
265280
@@ -381,31 +396,54 @@ def validate_watermark_operation(op: Dict[str, Any]) -> Dict[str, Any]:
381396
382397def validate_filter_operation (op : Dict [str , Any ]) -> Dict [str , Any ]:
383398 """Validate filter operation."""
384- if "name" not in op :
385- raise ValueError ("Filter operation requires 'name' field" )
386-
387399 allowed_filters = {
388400 "denoise" , "deinterlace" , "stabilize" , "sharpen" , "blur" ,
389- "brightness" , "contrast" , "saturation" , "hue" , "eq"
390- }
391-
392- filter_name = op ["name" ]
393- if filter_name not in allowed_filters :
394- raise ValueError (f"Unknown filter: { filter_name } " )
395-
396- return {
397- "type" : "filter" ,
398- "name" : filter_name ,
399- "params" : op .get ("params" , {}),
401+ "brightness" , "contrast" , "saturation" , "hue" , "eq" , "gamma" ,
402+ "fade_in" , "fade_out" , "speed"
400403 }
401404
405+ validated = {"type" : "filter" }
406+
407+ # Support named filter or direct params
408+ if "name" in op :
409+ filter_name = op ["name" ]
410+ if filter_name not in allowed_filters :
411+ raise ValueError (f"Unknown filter: { filter_name } " )
412+ validated ["name" ] = filter_name
413+ validated ["params" ] = op .get ("params" , {})
414+ else :
415+ # Support direct filter params without name
416+ for key in op :
417+ if key != "type" and key in allowed_filters :
418+ validated [key ] = op [key ]
419+
420+ # Validate specific filter parameters
421+ if "brightness" in validated :
422+ b = validated ["brightness" ]
423+ if not isinstance (b , (int , float )) or b < - 1 or b > 1 :
424+ raise ValueError ("Brightness must be between -1 and 1" )
425+ if "contrast" in validated :
426+ c = validated ["contrast" ]
427+ if not isinstance (c , (int , float )) or c < 0 or c > 4 :
428+ raise ValueError ("Contrast must be between 0 and 4" )
429+ if "saturation" in validated :
430+ s = validated ["saturation" ]
431+ if not isinstance (s , (int , float )) or s < 0 or s > 3 :
432+ raise ValueError ("Saturation must be between 0 and 3" )
433+ if "speed" in validated :
434+ sp = validated ["speed" ]
435+ if not isinstance (sp , (int , float )) or sp < 0.25 or sp > 4 :
436+ raise ValueError ("Speed must be between 0.25 and 4" )
437+
438+ return validated
439+
402440
403441def validate_stream_operation (op : Dict [str , Any ]) -> Dict [str , Any ]:
404442 """Validate streaming operation."""
405443 stream_format = op .get ("format" , "hls" ).lower ()
406444 if stream_format not in ["hls" , "dash" ]:
407445 raise ValueError (f"Unknown streaming format: { stream_format } " )
408-
446+
409447 return {
410448 "type" : "stream" ,
411449 "format" : stream_format ,
@@ -414,6 +452,186 @@ def validate_stream_operation(op: Dict[str, Any]) -> Dict[str, Any]:
414452 }
415453
416454
455+ def validate_scale_operation (op : Dict [str , Any ]) -> Dict [str , Any ]:
456+ """Validate scale operation."""
457+ validated = {"type" : "scale" }
458+
459+ # Width and height
460+ if "width" in op :
461+ width = op ["width" ]
462+ if width != "auto" and width != - 1 :
463+ if not isinstance (width , (int , float )):
464+ raise ValueError ("Width must be a number or 'auto'" )
465+ width = int (width )
466+ if width < 32 or width > 7680 :
467+ raise ValueError ("Width out of valid range (32-7680)" )
468+ if width % 2 != 0 :
469+ raise ValueError ("Width must be even number" )
470+ validated ["width" ] = width
471+
472+ if "height" in op :
473+ height = op ["height" ]
474+ if height != "auto" and height != - 1 :
475+ if not isinstance (height , (int , float )):
476+ raise ValueError ("Height must be a number or 'auto'" )
477+ height = int (height )
478+ if height < 32 or height > 4320 :
479+ raise ValueError ("Height out of valid range (32-4320)" )
480+ if height % 2 != 0 :
481+ raise ValueError ("Height must be even number" )
482+ validated ["height" ] = height
483+
484+ # Scaling algorithm
485+ if "algorithm" in op :
486+ allowed_algorithms = {"lanczos" , "bicubic" , "bilinear" , "neighbor" , "area" , "fast_bilinear" }
487+ if op ["algorithm" ] not in allowed_algorithms :
488+ raise ValueError (f"Invalid scaling algorithm: { op ['algorithm' ]} " )
489+ validated ["algorithm" ] = op ["algorithm" ]
490+
491+ return validated
492+
493+
494+ def validate_crop_operation (op : Dict [str , Any ]) -> Dict [str , Any ]:
495+ """Validate crop operation."""
496+ validated = {"type" : "crop" }
497+
498+ for field in ["width" , "height" , "x" , "y" ]:
499+ if field in op :
500+ value = op [field ]
501+ if isinstance (value , str ):
502+ # Allow FFmpeg expressions like 'iw', 'ih', 'iw/2'
503+ if not re .match (r'^[a-zA-Z0-9\+\-\*\/\(\)\.]+$' , value ):
504+ raise ValueError (f"Invalid { field } expression: { value } " )
505+ validated [field ] = value
506+ elif isinstance (value , (int , float )):
507+ if value < 0 :
508+ raise ValueError (f"{ field } must be non-negative" )
509+ validated [field ] = int (value ) if field in ["x" , "y" ] else value
510+ else :
511+ raise ValueError (f"{ field } must be a number or expression" )
512+
513+ return validated
514+
515+
516+ def validate_rotate_operation (op : Dict [str , Any ]) -> Dict [str , Any ]:
517+ """Validate rotate operation."""
518+ validated = {"type" : "rotate" }
519+
520+ if "angle" in op :
521+ angle = op ["angle" ]
522+ if not isinstance (angle , (int , float )):
523+ raise ValueError ("Angle must be a number" )
524+ # Normalize to -360 to 360 range
525+ angle = angle % 360
526+ if angle > 180 :
527+ angle -= 360
528+ validated ["angle" ] = angle
529+
530+ return validated
531+
532+
533+ def validate_flip_operation (op : Dict [str , Any ]) -> Dict [str , Any ]:
534+ """Validate flip operation."""
535+ validated = {"type" : "flip" }
536+
537+ direction = op .get ("direction" , "horizontal" )
538+ if direction not in ["horizontal" , "vertical" , "both" ]:
539+ raise ValueError (f"Invalid flip direction: { direction } " )
540+ validated ["direction" ] = direction
541+
542+ return validated
543+
544+
545+ def validate_audio_operation (op : Dict [str , Any ]) -> Dict [str , Any ]:
546+ """Validate audio processing operation."""
547+ validated = {"type" : "audio" }
548+
549+ # Volume adjustment
550+ if "volume" in op :
551+ volume = op ["volume" ]
552+ if isinstance (volume , (int , float )):
553+ if volume < 0 or volume > 10 :
554+ raise ValueError ("Volume must be between 0 and 10" )
555+ validated ["volume" ] = volume
556+ elif isinstance (volume , str ):
557+ # Allow dB notation like "-3dB" or "2dB"
558+ if not re .match (r'^-?\d+(\.\d+)?dB$' , volume ):
559+ raise ValueError ("Volume string must be in dB format (e.g., '-3dB')" )
560+ validated ["volume" ] = volume
561+
562+ # Normalization
563+ if "normalize" in op :
564+ validated ["normalize" ] = bool (op ["normalize" ])
565+ if "normalize_type" in op :
566+ if op ["normalize_type" ] not in ["loudnorm" , "dynaudnorm" ]:
567+ raise ValueError ("Invalid normalize type" )
568+ validated ["normalize_type" ] = op ["normalize_type" ]
569+
570+ # Sample rate
571+ if "sample_rate" in op :
572+ sr = op ["sample_rate" ]
573+ allowed_sample_rates = [8000 , 11025 , 16000 , 22050 , 32000 , 44100 , 48000 , 96000 ]
574+ if sr not in allowed_sample_rates :
575+ raise ValueError (f"Invalid sample rate: { sr } " )
576+ validated ["sample_rate" ] = sr
577+
578+ # Channels
579+ if "channels" in op :
580+ channels = op ["channels" ]
581+ if channels not in [1 , 2 , 6 , 8 ]:
582+ raise ValueError ("Channels must be 1, 2, 6, or 8" )
583+ validated ["channels" ] = channels
584+
585+ return validated
586+
587+
588+ def validate_subtitle_operation (op : Dict [str , Any ]) -> Dict [str , Any ]:
589+ """Validate subtitle operation."""
590+ validated = {"type" : "subtitle" }
591+
592+ if "path" not in op :
593+ raise ValueError ("Subtitle operation requires 'path' field" )
594+
595+ path = op ["path" ]
596+ # Validate subtitle file extension
597+ allowed_ext = {".srt" , ".ass" , ".ssa" , ".vtt" , ".sub" }
598+ ext = Path (path ).suffix .lower ()
599+ if ext not in allowed_ext :
600+ raise ValueError (f"Invalid subtitle format: { ext } " )
601+
602+ validated ["path" ] = path
603+
604+ # Optional styling
605+ if "style" in op :
606+ validated ["style" ] = op ["style" ]
607+
608+ return validated
609+
610+
611+ def validate_concat_operation (op : Dict [str , Any ]) -> Dict [str , Any ]:
612+ """Validate concatenation operation."""
613+ validated = {"type" : "concat" }
614+
615+ if "inputs" not in op :
616+ raise ValueError ("Concat operation requires 'inputs' field with list of files" )
617+
618+ inputs = op ["inputs" ]
619+ if not isinstance (inputs , list ) or len (inputs ) < 2 :
620+ raise ValueError ("Concat requires at least 2 input files" )
621+
622+ if len (inputs ) > 100 :
623+ raise ValueError ("Too many inputs for concat (max 100)" )
624+
625+ validated ["inputs" ] = inputs
626+
627+ # Demuxer mode (safer) vs filter mode (more flexible)
628+ validated ["mode" ] = op .get ("mode" , "demuxer" )
629+ if validated ["mode" ] not in ["demuxer" , "filter" ]:
630+ raise ValueError ("Concat mode must be 'demuxer' or 'filter'" )
631+
632+ return validated
633+
634+
417635def validate_transcode_operation (op : Dict [str , Any ]) -> Dict [str , Any ]:
418636 """Validate transcode operation with enhanced security checks."""
419637 validated = {"type" : "transcode" }
0 commit comments