harry0703/MoneyPrinterTurbo · error · BgmUploadError

background music upload is not seekable

Error message

background music upload is not seekable

What it means

Raised in _stage_bgm_upload when source.seek(0) raises AttributeError (the stream object has no seek) or OSError (seek exists but fails, e.g. a non-regular file descriptor). The seek is mandatory: both the WebUI's upload preflight and the final persistence must read from byte 0 with identical chunking and size limits, otherwise the UI could show a file as valid while the server later rejects it — the state-split the staging function is designed to prevent.

Source

Thrown at app/services/bgm.py:189

    将上传流写入同目录临时文件,并返回安全文件名、临时路径和字节数。

    WebUI 的上传预检和最终持久化必须使用完全相同的分块读取、大小限制与文件名
    规则,否则可能出现界面显示可用、点击生成后却被服务端拒绝的状态分裂。
    临时文件由调用方在完成音频探测后删除或原子替换。
    """
    safe_name = sanitize_upload_filename(filename)
    try:
        target_dir = uploaded_bgm_dir(create=True)
    except OSError as exc:
        raise BgmServiceError("failed to prepare background music storage") from exc
    temp_path = ""
    total_bytes = 0

    try:
        try:
            source.seek(0)
        except (AttributeError, OSError) as exc:
            raise BgmUploadError("background music upload is not seekable") from exc

        # 保留原始扩展名便于 FFmpeg 针对无容器头的 AAC 等格式选择正确的
        # demuxer;临时文件仍放在目标目录,以保证最终 os.replace 是原子操作。
        descriptor, temp_path = tempfile.mkstemp(
            prefix=_INTERNAL_UPLOAD_PREFIX,
            suffix=Path(safe_name).suffix.lower(),
            dir=target_dir,
        )
        with os.fdopen(descriptor, "wb") as output:
            while True:
                chunk = source.read(_COPY_CHUNK_BYTES)
                if not chunk:
                    break
                if not isinstance(chunk, (bytes, bytearray, memoryview)):
                    raise BgmUploadError("background music upload must be binary")
                total_bytes += len(chunk)
                if total_bytes > MAX_BGM_UPLOAD_BYTES:
                    raise BgmUploadError("background music file exceeds the 30 MB limit")

View on GitHub (pinned to 1f9f19c202)

Solutions

  1. Buffer the bytes first: data = source.read() then pass io.BytesIO(data) to the upload.
  2. If wrapping streams, ensure the wrapper delegates seek/tell and that the underlying object supports random access.
  3. For framework code, use the framework's spooled upload file object directly rather than an iterator adapter.

Example fix

# before
def upload(source):  # source is a forward-only stream
    safe = save_bgm_upload(name, source)

# after
import io
def upload(source):
    source.seek(0) if hasattr(source, "seek") else None
    data = source.read()
    safe = save_bgm_upload(name, io.BytesIO(data))
Defensive patterns

Strategy: validation

Validate before calling

import io
def ensure_seekable(source):
    if not (hasattr(source, "seek") and source.seekable()):
        return io.BytesIO(source.read())
    source.seek(0)
    return source

Type guard

import io
def is_seekable_stream(source) -> bool:
    return isinstance(source, (io.BytesIO, io.BufferedIOBase)) and source.seekable()

Try / catch

try:
    safe_name = save_bgm_upload(name, source)
except BgmUploadError as e:
    if "not seekable" in str(e):
        source = io.BytesIO(original_bytes)  # buffer, then retry once
        safe_name = save_bgm_upload(name, source)
    else:
        raise

Prevention

When it happens

Trigger: Passing a non-seekable stream to the upload API: an HTTP request body iterator, a pipe (e.g. subprocess stdout), an open('file') on a stream-only pseudo-file, or a wrapper class without a seek method; also a SpooledTemporaryFile already rolled to disk and closed by a prior error.

Common situations: Clients streaming uploads directly from network objects instead of buffering; middleware wrapping UploadFile.file in a forward-only reader; testing with io.RawIOBase subclasses that don't implement seek.

Related errors


AI-assisted analysis of harry0703/MoneyPrinterTurbo@1f9f19c202 (2026-08-14). Data as JSON: /api/errors/d9c0cdba9e60b65e. Report an issue: GitHub.