127 lines
4.4 KiB
Python
127 lines
4.4 KiB
Python
|
|
import logging
|
||
|
|
|
||
|
|
import httpx
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
UPLOAD_URL = "https://api.weixin.qq.com/cgi-bin/media/upload"
|
||
|
|
DOWNLOAD_URL = "https://api.weixin.qq.com/cgi-bin/media/get"
|
||
|
|
|
||
|
|
ALLOWED_MEDIA_TYPE_MAP = {
|
||
|
|
"image": {"image/jpeg", "image/jpg", "image/png"},
|
||
|
|
"voice": {"audio/amr", "audio/mp3", "audio/speex"},
|
||
|
|
"video": {"video/mp4"},
|
||
|
|
"thumb": {"image/jpeg", "image/jpg"},
|
||
|
|
}
|
||
|
|
|
||
|
|
MAX_BYTES_MAP = {
|
||
|
|
"image": 10 * 1024 * 1024,
|
||
|
|
"voice": 2 * 1024 * 1024,
|
||
|
|
"video": 10 * 1024 * 1024,
|
||
|
|
"thumb": 64 * 1024,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
class MiniProgramMedia:
|
||
|
|
|
||
|
|
def __init__(self, access_token_provider, app_id: str = "", app_secret: str = ""):
|
||
|
|
self._token_provider = access_token_provider
|
||
|
|
self._http: httpx.AsyncClient | None = None
|
||
|
|
self._app_id = app_id
|
||
|
|
self._app_secret = app_secret
|
||
|
|
|
||
|
|
async def _client(self) -> httpx.AsyncClient:
|
||
|
|
if self._http is None:
|
||
|
|
self._http = httpx.AsyncClient(timeout=30.0)
|
||
|
|
return self._http
|
||
|
|
|
||
|
|
async def upload(
|
||
|
|
self,
|
||
|
|
file_data: bytes,
|
||
|
|
filename: str,
|
||
|
|
media_type: str,
|
||
|
|
content_type: str | None = None,
|
||
|
|
) -> dict:
|
||
|
|
if media_type not in ALLOWED_MEDIA_TYPE_MAP:
|
||
|
|
return {"success": False, "error": f"unknown media_type: {media_type}"}
|
||
|
|
|
||
|
|
max_bytes = MAX_BYTES_MAP.get(media_type, 0)
|
||
|
|
if len(file_data) > max_bytes:
|
||
|
|
return {"success": False, "error": f"file too large: {len(file_data)} bytes (max {max_bytes})"}
|
||
|
|
|
||
|
|
token = self._token_provider()
|
||
|
|
if not token:
|
||
|
|
return {"success": False, "error": "no access_token"}
|
||
|
|
|
||
|
|
url = f"{UPLOAD_URL}?access_token={token}&type={media_type}"
|
||
|
|
files = {"media": (filename, file_data, content_type or "application/octet-stream")}
|
||
|
|
|
||
|
|
try:
|
||
|
|
client = await self._client()
|
||
|
|
resp = await client.post(url, files=files)
|
||
|
|
data = resp.json()
|
||
|
|
|
||
|
|
if "media_id" in data:
|
||
|
|
return {"success": True, "media_id": data["media_id"], "type": data.get("type", media_type)}
|
||
|
|
if "errcode" in data:
|
||
|
|
logger.warning(
|
||
|
|
"MiniProgram media upload failed: errcode=%s errmsg=%s",
|
||
|
|
data.get("errcode"),
|
||
|
|
data.get("errmsg"),
|
||
|
|
)
|
||
|
|
return {"success": False, "error": data.get("errmsg", "upload failed"), "errcode": data["errcode"]}
|
||
|
|
return {"success": False, "error": "unknown response"}
|
||
|
|
except Exception as e:
|
||
|
|
logger.exception("MiniProgram media upload exception")
|
||
|
|
return {"success": False, "error": str(e)}
|
||
|
|
|
||
|
|
async def download(self, media_id: str) -> dict:
|
||
|
|
token = self._token_provider()
|
||
|
|
if not token:
|
||
|
|
return {"success": False, "error": "no access_token"}
|
||
|
|
|
||
|
|
url = f"{DOWNLOAD_URL}?access_token={token}&media_id={media_id}"
|
||
|
|
|
||
|
|
try:
|
||
|
|
client = await self._client()
|
||
|
|
resp = await client.get(url)
|
||
|
|
|
||
|
|
content_type = resp.headers.get("content-type", "")
|
||
|
|
if "application/json" in content_type or resp.text.startswith("{"):
|
||
|
|
data = resp.json()
|
||
|
|
if "errcode" in data and data["errcode"] != 0:
|
||
|
|
logger.warning(
|
||
|
|
"MiniProgram media download failed: errcode=%s errmsg=%s",
|
||
|
|
data.get("errcode"),
|
||
|
|
data.get("errmsg"),
|
||
|
|
)
|
||
|
|
return {
|
||
|
|
"success": False,
|
||
|
|
"error": data.get("errmsg", "download failed"),
|
||
|
|
"errcode": data["errcode"],
|
||
|
|
}
|
||
|
|
return {"success": False, "error": "unexpected json response"}
|
||
|
|
|
||
|
|
return {
|
||
|
|
"success": True,
|
||
|
|
"data": resp.content,
|
||
|
|
"content_type": content_type,
|
||
|
|
}
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.exception("MiniProgram media download exception")
|
||
|
|
return {"success": False, "error": str(e)}
|
||
|
|
|
||
|
|
async def download_image_base64(self, media_id: str) -> str:
|
||
|
|
result = await self.download(media_id)
|
||
|
|
if result.get("success") and result.get("data"):
|
||
|
|
import base64
|
||
|
|
|
||
|
|
return f"data:image/jpeg;base64,{base64.b64encode(result['data']).decode()}"
|
||
|
|
return ""
|
||
|
|
|
||
|
|
async def close(self):
|
||
|
|
if self._http:
|
||
|
|
await self._http.aclose()
|
||
|
|
self._http = None
|