{"record":{"id":"8a635efcf4a62623","repo":"harry0703/MoneyPrinterTurbo","slug":"unsupported-local-material-type-extension-or-no","errorCode":null,"errorMessage":"unsupported local material type {extension or '<none>'}: {material.url}; allowed extensions: {allowed}","messagePattern":"unsupported local material type (.+?): (.+?); allowed extensions: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"cli.py","lineNumber":730,"sourceCode":"            raise ValueError(\"subtitle font must use the .ttf or .ttc extension\")\n        # 下游根据 resource/fonts 内的文件名拼接路径，因此仍保留纯文件名。\n        params.font_name = os.path.basename(font_path)\n\n    if params.video_source != \"local\" or stop_at not in {\"materials\", \"video\"}:\n        return\n\n    local_videos_dir = utils.storage_dir(\"local_videos\", create=True)\n    resolved_materials: list[tuple[MaterialInfo, str, str]] = []\n    for material in params.video_materials or []:\n        source_path = _resolve_cli_file(\n            material.url,\n            description=\"local material\",\n            fallback_dir=local_videos_dir,\n        )\n        extension = os.path.splitext(source_path)[1].lower()\n        if extension not in local_material_extensions:\n            allowed = \", \".join(sorted(local_material_extensions))\n            raise ValueError(\n                f\"unsupported local material type {extension or '<none>'}: \"\n                f\"{material.url}; allowed extensions: {allowed}\"\n            )\n        resolved_materials.append((material, source_path, extension))\n\n    # 所有输入检查通过后再复制，避免第二个文件无效时留下第一个文件的\n    # 孤儿副本。\n    prepared_paths: dict[str, str] = {}\n    for material, source_path, extension in resolved_materials:\n        prepared_path = prepared_paths.get(source_path)\n        if prepared_path is None:\n            if _path_is_within_directory(source_path, local_videos_dir):\n                prepared_path = source_path\n            else:\n                prepared_path = os.path.join(\n                    local_videos_dir,\n                    f\"cli-material-{uuid4().hex}{extension}\",\n                )","sourceCodeStart":712,"sourceCodeEnd":748,"githubUrl":"https://github.com/harry0703/MoneyPrinterTurbo/blob/1f9f19c2021a68d04df228f33e9099a0c947f6f8/cli.py#L712-L748","documentation":"Thrown while validating --video-materials for local video source tasks. Each material URL is first resolved via _resolve_cli_file (against the local_videos storage dir), then its lowercase extension is checked against local_material_extensions, which is const.FILE_TYPE_VIDEOS + const.FILE_TYPE_IMAGES plus .avi and .flv (cli.py:658-663). Any extension outside that set — including no extension at all ('<none>') — aborts the run. Validation happens for all materials before any copy, so no orphan files are left behind.","triggerScenarios":"CLI run with --video-source local, stop_at in {materials, video}, and a --video-material entry whose resolved path ends in an unsupported extension (e.g. .gifv, .webp if not in const lists, .mkv if excluded, .txt) or has no extension; also triggered by directories or misresolved relative paths.","commonSituations":"Mixing in an unsupported format like .mkv/.wmv/.bmp that const.FILE_TYPE_* does not include; a material filename missing its extension after download; passing a URL-style string that _resolve_cli_file resolves to something unexpected; Windows-hidden double extensions.","solutions":["Convert the offending material to a supported format (mp4/webm/jpg/png etc., or avi/flv) with ffmpeg before passing it","Fix the filename so it has the correct supported extension — check the exact allowed list printed in the error message","Add the extension to const.FILE_TYPE_VIDEOS or const.FILE_TYPE_IMAGES in app/constants if the format is genuinely supported downstream, then rerun (cli.py builds the allowed set from those constants)"],"exampleFix":"# before\n--video-source local --stop-at video \\\n  --video-material clip.mkv\n\n# after\nffmpeg -i clip.mkv -c:v libx264 -c:a aac clip.mp4\n--video-source local --stop-at video \\\n  --video-material clip.mp4","handlingStrategy":"validation","validationCode":"import os\nfrom app import const\n\ndef build_local_material_extensions() -> set[str]:\n    return {\n        *(f\".{e}\" for e in const.FILE_TYPE_VIDEOS),\n        *(f\".{e}\" for e in const.FILE_TYPE_IMAGES),\n        \".avi\",\n        \".flv\",\n    }\n\ndef materials_are_valid(paths: list[str]) -> bool:\n    allowed = build_local_material_extensions()\n    return all(os.path.splitext(p)[1].lower() in allowed for p in paths)","typeGuard":"def is_supported_material(path: str) -> bool:\n    \"\"\"Guard: material path has an extension in the local-material allowlist.\"\"\"\n    return isinstance(path, str) and os.path.splitext(path)[1].lower() in build_local_material_extensions()","tryCatchPattern":null,"preventionTips":["Derive the allowed extension set from const.FILE_TYPE_VIDEOS/IMAGES instead of maintaining a second list","Validate every material path before submitting the job — the CLI validates all-before-copy, so should the caller","Run a preflight loop printing each material with its resolved extension to catch '<none>' cases"],"tags":["cli","video-materials","validation","file-extension"],"backgroundTag":null,"analyzedSha":"1f9f19c2021a68d04df228f33e9099a0c947f6f8","analyzedAt":"2026-08-14T19:41:05.568Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}