{"record":{"id":"d6620a4c5c1eca47","repo":"harry0703/MoneyPrinterTurbo","slug":"uploaded-file-must-contain-a-decodable-audio-strea","errorCode":null,"errorMessage":"uploaded file must contain a decodable audio stream","messagePattern":"uploaded file must contain a decodable audio stream","errorType":"validation","errorClass":"BgmUploadError","httpStatus":400,"severity":"error","filePath":"app/services/bgm.py","lineNumber":154,"sourceCode":"                \"-xerror\",\n                \"-i\",\n                file_path,\n                \"-map\",\n                \"0:a:0\",\n                \"-f\",\n                \"null\",\n                \"-\",\n            ],\n            capture_output=True,\n            timeout=timeout_seconds,\n            check=False,\n        )\n    except subprocess.TimeoutExpired as exc:\n        raise BgmServiceError(\"FFmpeg background music validation timed out\") from exc\n    except OSError as exc:\n        raise BgmServiceError(\"failed to run FFmpeg for background music validation\") from exc\n    if decoded.returncode != 0:\n        raise BgmUploadError(\"uploaded file must contain a decodable audio stream\")\n\n\ndef validate_audio_file(file_path: str, timeout_seconds: int = 120) -> None:\n    \"\"\"\n    校验磁盘上的音频文件可由项目 FFmpeg 完整解码。\n\n    上传预检通常只需 30 秒；Sonilo 生成的配乐最长可达 6 分钟，因此对外提供\n    可调整超时的复用入口。服务只依赖 FFmpeg，不要求系统额外安装 FFprobe。\n    \"\"\"\n    if not os.path.isfile(file_path) or os.path.getsize(file_path) <= 0:\n        raise BgmUploadError(\"background music file is empty or missing\")\n    _validate_audio(file_path, timeout_seconds=timeout_seconds)\n\n\ndef _stage_bgm_upload(filename: str, source: BinaryIO) -> tuple[str, str, int]:\n    \"\"\"\n    将上传流写入同目录临时文件，并返回安全文件名、临时路径和字节数。\n","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/harry0703/MoneyPrinterTurbo/blob/1f9f19c2021a68d04df228f33e9099a0c947f6f8/app/services/bgm.py#L136-L172","documentation":"Raised by _validate_audio in app/services/bgm.py when the FFmpeg full-decode probe exits non-zero. The probe runs FFmpeg with -map 0:a:0 (fails when there is no audio stream) and -xerror (promotes decode errors to failure), decoding to null output; full decoding also catches encrypted or random data that merely carries an audio extension. A file that passes the extension check but fails this probe is rejected as not containing a decodable audio stream (BgmUploadError → HTTP 400).","triggerScenarios":"Uploading an image/document renamed to .mp3; DRM-protected audio; a truncated/corrupt audio file where the container header exists but frames are broken; an MP4 with video only (no audio stream).","commonSituations":"Corrupted downloads (interrupted transfer); music services exporting DRM'd files; users renaming files to force acceptance; files with cover-art-only streams and no audio track.","solutions":["Verify locally first with the same test: ffmpeg -v error -i file -map 0:a:0 -xerror -f null - ; a non-zero exit means re-encode or re-download the file.","Re-encode with ffmpeg -i input -vn -c:a libmp3lame output.mp3 to strip anything non-audio and rebuild a clean stream.","If the file is DRM-protected, obtain a DRM-free copy; no server setting will accept it."],"exampleFix":"# before: upload raw, hope for the best\nfiles = {\"file\": (\"maybe-broken.mp3\", open(\"maybe-broken.mp3\", \"rb\"))}\n\n# after: validate locally with the server's own probe, re-encode if needed\nimport subprocess, sys\nrc = subprocess.run([\"ffmpeg\", \"-v\", \"error\", \"-i\", \"maybe-broken.mp3\", \"-map\", \"0:a:0\", \"-xerror\", \"-f\", \"null\", \"-\"]).returncode\nif rc != 0:\n    subprocess.run([\"ffmpeg\", \"-y\", \"-i\", \"maybe-broken.mp3\", \"-vn\", \"-c:a\", \"libmp3lane\" if False else \"libmp3lame\", \"fixed.mp3\"], check=True)\nfiles = {\"file\": (\"fixed.mp3\", open(\"fixed.mp3\", \"rb\"))}","handlingStrategy":"validation","validationCode":"import subprocess\ndef is_decodable_audio(path: str) -> bool:\n    return subprocess.run(\n        [\"ffmpeg\", \"-v\", \"error\", \"-i\", path, \"-map\", \"0:a:0\", \"-xerror\", \"-f\", \"null\", \"-\"],\n        capture_output=True,\n    ).returncode == 0","typeGuard":null,"tryCatchPattern":"try:\n    validate_upload(name, fh)\nexcept BgmUploadError as e:\n    if \"decodable audio\" in str(e):\n        reencode_to_mp3(local_path)  # ffmpeg -i in -vn -c:a libmp3lame out.mp3\n        retry_with(reencoded_file)\n    else:\n        raise","preventionTips":["Run the same ffmpeg -xerror probe locally before uploading.","Re-encode uploads once at ingest instead of retrying raw corrupt files.","Treat extension checks as cheap pre-filters, not proof of audio content."],"tags":["ffmpeg","audio-decode","upload","bgm","validation"],"backgroundTag":null,"analyzedSha":"1f9f19c2021a68d04df228f33e9099a0c947f6f8","analyzedAt":"2026-08-14T19:41:05.568Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}