"""媒体本地落盘:把图文帖的图片、视频帖的整段 mp4 存到 data////。 解耦设计:仅当 settings.data_dir 非空时调用;视频落盘在 extract_video 内(复用其内存 字节),这里只管图片下载 + 卡片 url 改写为本地 /data 路径。 """ from __future__ import annotations import datetime import logging from dataclasses import dataclass from pathlib import Path from typing import Callable from creation_knowledge.integrations.video_extract import _default_download from core.models import Post logger = logging.getLogger(__name__) Downloader = Callable[[str, str], bytes] def run_id() -> str: """运行批次 id,默认时间戳到秒。""" return datetime.datetime.now().strftime("%Y%m%d-%H%M%S") @dataclass class MediaTarget: dir: Path # 本地目录 data//// public_base: str # 浏览器前缀 /data/// def media_target(data_dir: str, batch: str, platform: str, post_id: str) -> MediaTarget: rel = f"{batch}/{platform}/{post_id}" return MediaTarget(dir=Path(data_dir) / rel, public_base=f"/data/{rel}") def _ext_from_bytes(b: bytes) -> str: """按文件头嗅探扩展名(不依赖 Content-Type,便于注入式测试)。""" if b[:4] == b"\x89PNG": return ".png" if b[:3] == b"GIF": return ".gif" if b[:4] == b"RIFF" and b[8:12] == b"WEBP": return ".webp" return ".jpg" def save_images( post: Post, media_dir: Path, public_base: str, *, downloader: Downloader = _default_download, ) -> None: """下载图文帖每张远程图片 → media_dir/image_.,并把 card.url 改写为本地路径。 单张失败只告警跳过(不影响其余卡片与整条流水线)。 """ media_dir = Path(media_dir) for card in post.cards: if card.kind != "image" or not card.url or not card.url.startswith("http"): continue try: data = downloader(card.url, post.platform) except Exception as exc: # 单张图失败不致命 logger.warning("图片下载失败 idx=%s %s: %s", card.index, card.url, exc) continue if not data: continue media_dir.mkdir(parents=True, exist_ok=True) fname = f"image_{card.index}{_ext_from_bytes(data)}" (media_dir / fname).write_bytes(data) card.url = f"{public_base}/{fname}"