{"record":{"id":"ec0f3a8242f58457","repo":"calesthio/OpenMontage","slug":"ffmpeg-lavfi-movie-paths-containing-single-quotes","errorCode":null,"errorMessage":"FFmpeg lavfi movie paths containing single quotes are unsupported","messagePattern":"FFmpeg lavfi movie paths containing single quotes are unsupported","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"tools/analysis/scene_detect.py","lineNumber":172,"sourceCode":"        scenes = []\n        for i, (scene_start, scene_end) in enumerate(scene_list):\n            scenes.append({\n                \"index\": i,\n                \"start_seconds\": round(scene_start.get_seconds(), 3),\n                \"end_seconds\": round(scene_end.get_seconds(), 3),\n                \"duration_seconds\": round(\n                    scene_end.get_seconds() - scene_start.get_seconds(), 3\n                ),\n            })\n\n        return scenes\n\n    @staticmethod\n    def _escape_lavfi_movie_path(path: str) -> str:\n        \"\"\"Escape a path for FFmpeg lavfi movie=... without allowing filter injection.\"\"\"\n        normalized = path.replace(\"\\\\\", \"/\")\n        if \"'\" in normalized:\n            raise ValueError(\"FFmpeg lavfi movie paths containing single quotes are unsupported\")\n        escaped = []\n        for char in normalized:\n            if char in \"\\\\:,[];\":\n                escaped.append(\"\\\\\" + char)\n            else:\n                escaped.append(char)\n        return \"\".join(escaped)\n\n    def _detect_ffmpeg(self, inputs: dict[str, Any]) -> list[dict]:\n        \"\"\"Fallback: use FFmpeg scene change filter.\"\"\"\n        input_path = str(inputs[\"input_path\"])\n        threshold = inputs.get(\"threshold\", 0.3)\n        min_scene_len = inputs.get(\"min_scene_length_seconds\", 1.0)\n        escaped_input = self._escape_lavfi_movie_path(input_path)\n\n        cmd = [\n            \"ffprobe\",\n            \"-v\", \"quiet\",","sourceCodeStart":154,"sourceCodeEnd":190,"githubUrl":"https://github.com/calesthio/OpenMontage/blob/95e1c3d0ab93482159818560f6a8c8e866b9139f/tools/analysis/scene_detect.py#L154-L190","documentation":"Raised by _escape_lavfi_movie_path when the input path contains a single quote, which cannot be safely escaped inside an FFmpeg lavfi movie='...' filtergraph without risking filter injection. The code treats this as an unsupported input rather than attempting partial escaping. It is a deliberate security guard, so the fix is on the caller's side: rename or copy the file.","triggerScenarios":"Calling the FFmpeg scene-detect fallback with an input_path like /tmp/user's clip.mp4 or any path produced from user-titled content (episode names, song titles) interpolated into a temp filename; on Windows, paths normalized to forward slashes still carry the quote.","commonSituations":"Batch pipelines processing files named from spreadsheet rows or media-library titles containing apostrophes; macOS/Unix directories like /home/o'brien/videos; user uploads retaining original filenames.","solutions":["Rename the file (or copy it) to a path without single quotes before calling scene detection","Sanitize filenames at ingest time: strip/replace ' and other shell/filter metacharacters when a file enters the pipeline","If you control the code path, pass the file via a symlink or hardlink with a safe generated name (e.g. mkdtemp + sanitized basename)"],"exampleFix":"// before\nresult = detect._detect_ffmpeg({\"input_path\": \"/tmp/Bob's clip.mp4\"})\n\n// after\nimport shutil, tempfile, os\nsafe_dir = tempfile.mkdtemp()\nsafe_path = os.path.join(safe_dir, \"input.mp4\")\nshutil.copy(\"/tmp/Bob's clip.mp4\", safe_path)\nresult = detect._detect_ffmpeg({\"input_path\": safe_path})","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef safe_movie_path(path: str) -> str:\n    if \"'\" in path:\n        raise ValueError(f\"path contains a single quote; rename the file: {path}\")\n    return path","typeGuard":"def is_lavfi_safe(path: str) -> bool:\n    return \"'\" not in path.replace(\"\\\\\", \"/\")","tryCatchPattern":"try:\n    escaped = _escape_lavfi_movie_path(path)\nexcept ValueError:\n    safe = Path(tempfile.mkdtemp()) / re.sub(r\"[^A-Za-z0-9._-]\", \"_\", Path(path).name)\n    shutil.copy(path, safe)\n    escaped = _escape_lavfi_movie_path(str(safe))","preventionTips":["Sanitize filenames (strip quotes and filter metacharacters) at ingest time","Generate internal temp names from UUIDs or indices instead of user titles","Prefer passing inputs by safe symlink when filenames are user-controlled"],"tags":["ffmpeg","lavfi","sanitization","filesystem","security"],"backgroundTag":null,"analyzedSha":"95e1c3d0ab93482159818560f6a8c8e866b9139f","analyzedAt":"2026-08-15T06:31:20.014Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}