from __future__ import annotations import logging import os import tempfile from pathlib import Path import httpx from yuxi.channel.extensions.imessage.errors import IMessageMediaError logger = logging.getLogger(__name__) DEFAULT_MEDIA_MAX_MB = 16 async def download_media( media_url: str, max_mb: int = DEFAULT_MEDIA_MAX_MB, temp_dir: str | None = None, ) -> tuple[bytes, str, str]: async with httpx.AsyncClient(timeout=httpx.Timeout(60.0)) as client: resp = await client.get(media_url) resp.raise_for_status() data = resp.content size_mb = len(data) / (1024 * 1024) if size_mb > max_mb: raise IMessageMediaError( f"Media size {size_mb:.1f}MB exceeds limit of {max_mb}MB" ) content_type = resp.headers.get("content-type", "application/octet-stream") ext = _guess_extension(content_type) filename = f"imessage_media_{os.urandom(8).hex()}{ext}" write_dir = temp_dir or tempfile.gettempdir() filepath = os.path.join(write_dir, filename) Path(filepath).write_bytes(data) return data, content_type, filepath def cleanup_temp_file(filepath: str) -> None: try: os.unlink(filepath) except OSError: pass async def download_attachment_via_api( attachment_guid: str, server_url: str, password: str, max_mb: int = DEFAULT_MEDIA_MAX_MB, ) -> tuple[bytes, str]: async with httpx.AsyncClient(timeout=httpx.Timeout(60.0)) as client: resp = await client.get( f"{server_url}/api/v1/attachment/{attachment_guid}/download", headers={"Password": password}, ) resp.raise_for_status() data = resp.content size_mb = len(data) / (1024 * 1024) if size_mb > max_mb: raise IMessageMediaError( f"Attachment size {size_mb:.1f}MB exceeds limit of {max_mb}MB" ) content_type = resp.headers.get("content-type", "application/octet-stream") return data, content_type async def download_attachment_chunked( attachment_guid: str, server_url: str, password: str, chunk_size: int = 1024 * 1024, ) -> bytes: async with httpx.AsyncClient(timeout=httpx.Timeout(300.0)) as client: resp = await client.get( f"{server_url}/api/v1/attachment/{attachment_guid}/chunk", headers={"Password": password}, ) resp.raise_for_status() return resp.content def _guess_extension(content_type: str) -> str: ext_map = { "image/jpeg": ".jpg", "image/png": ".png", "image/gif": ".gif", "image/heic": ".heic", "image/heif": ".heif", "audio/mpeg": ".mp3", "audio/aac": ".aac", "audio/wav": ".wav", "video/mp4": ".mp4", "video/quicktime": ".mov", "application/pdf": ".pdf", } return ext_map.get(content_type, ".bin")