feat(media): هر رسانهٔ آپلودی به WebP نرمال می‌شود — Pillow مسیر اصلی، FFmpeg برای ویدیو و فرمت‌های خاص؛ ویدیو تا ۸ثانیه/۱۲fps وب متحرک، غیرقابل‌رمزگشایی ۴۱۵

This commit is contained in:
aramesh-site deploy
2026-09-30 09:27:21 +00:00
parent 79ebe69221
commit c0eaac1daa
6 changed files with 258 additions and 71 deletions
+94 -2
View File
@@ -404,12 +404,103 @@ def test_upload_with_readable_name(auth_client, monkeypatch):
)
assert res.status_code == 200, res.text
key = res.json()["key"]
# نام خوانا (فارسی/لاتین، بدون نویسه ناامن) + پسوند یکتا + پسوند تصویر
# نام خوانا (فارسی/لاتین، بدون نویسه ناامن) + پسوند یکتا + نرمال‌شدن به WebP
assert "برج-الهیه-tower" in key
assert key.endswith(".png")
assert key.endswith(".webp")
assert res.json()["content_type"] == "image/webp"
assert "!" not in key
def _animated_gif_bytes() -> bytes:
from PIL import Image
import io
buf = io.BytesIO()
first = Image.new("RGB", (12, 8), (200, 120, 60))
second = Image.new("RGB", (12, 8), (60, 120, 200))
first.save(buf, "GIF", save_all=True, append_images=[second], duration=100, loop=0)
return buf.getvalue()
def test_upload_jpeg_converted_to_webp(auth_client, monkeypatch):
_mock_storage(monkeypatch)
from PIL import Image
import io
buf = io.BytesIO()
Image.new("RGB", (3000, 1500), (10, 10, 10)).save(buf, "JPEG", quality=90)
res = auth_client.post(
"/api/admin/upload",
files={"file": ("tower.jpg", buf.getvalue(), "image/jpeg")},
)
assert res.status_code == 200, res.text
body = res.json()
assert body["content_type"] == "image/webp"
assert body["key"].endswith(".webp")
# سقف ضلع ۲۰۰۰ پیکسل روی خروجی اعمال شده
assert body["width"] == 2000 and body["height"] == 1000
def test_upload_animated_gif_becomes_animated_webp(auth_client, monkeypatch):
fake = _mock_storage(monkeypatch)
res = auth_client.post(
"/api/admin/upload",
files={"file": ("anim.gif", _animated_gif_bytes(), "image/gif")},
)
assert res.status_code == 200, res.text
assert res.json()["content_type"] == "image/webp"
from PIL import Image
import io
stored = next(iter(fake.objects.values()))
out = Image.open(io.BytesIO(stored))
assert out.format == "WEBP"
assert getattr(out, "n_frames", 1) == 2 # حرکت از بین نرفته
def test_upload_undecodable_file_rejected(auth_client, monkeypatch):
_mock_storage(monkeypatch)
res = auth_client.post(
"/api/admin/upload",
files={"file": ("notes.txt", b"this is not a media file at all", "text/plain")},
)
assert res.status_code == 415
def test_upload_video_becomes_animated_webp(auth_client, monkeypatch):
# ویدیو تنها با FFmpeg قابل پذیرش است — بدون آن ۴۱۵، با آن WebP متحرک
import shutil as _shutil
import subprocess as _subprocess
import tempfile as _tempfile
import os as _os
_mock_storage(monkeypatch)
if _shutil.which("ffmpeg") is None:
res = auth_client.post(
"/api/admin/upload",
files={"file": ("clip.mp4", b"\x00\x00\x00\x18ftypmp42", "video/mp4")},
)
assert res.status_code == 415
return
with _tempfile.TemporaryDirectory() as tmp:
clip = _os.path.join(tmp, "clip.mp4")
_subprocess.run(
["ffmpeg", "-y", "-v", "error", "-f", "lavfi",
"-i", "testsrc=duration=0.5:size=128x96:rate=10", clip],
check=True, capture_output=True, timeout=30,
)
with open(clip, "rb") as fh:
clip_bytes = fh.read()
res = auth_client.post(
"/api/admin/upload",
files={"file": ("clip.mp4", clip_bytes, "video/mp4")},
)
assert res.status_code == 200, res.text
assert res.json()["content_type"] == "image/webp"
def test_upload_without_name_uses_filename(auth_client, monkeypatch):
_mock_storage(monkeypatch)
res = auth_client.post(
@@ -475,6 +566,7 @@ def test_import_image_happy_path(auth_client, monkeypatch):
res = auth_client.post("/api/admin/import-image", json={"url": "https://wiki.example/Elahieh pic.png?token=1"})
assert res.status_code == 200, res.text
assert res.json()["url"].startswith("/media/")
assert res.json()["key"].endswith(".webp")
assert "Elahieh-pic" in res.json()["key"]