Convert Pixiv ugoira zips to animated GIFs, add filetype to info panel
- Detect .zip files (Pixiv ugoira) and convert frames to animated GIF - Cache the converted GIF so subsequent loads are instant - Add filetype field to the info panel - Add ZIP to valid media magic bytes
This commit is contained in:
parent
495eb4c64d
commit
526606c7c5
@ -3,9 +3,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
from .config import cache_dir, thumbnails_dir, USER_AGENT
|
from .config import cache_dir, thumbnails_dir, USER_AGENT
|
||||||
|
|
||||||
@ -21,6 +23,7 @@ _IMAGE_MAGIC = {
|
|||||||
b'RIFF': True, # WebP
|
b'RIFF': True, # WebP
|
||||||
b'\x00\x00\x00': True, # MP4/MOV
|
b'\x00\x00\x00': True, # MP4/MOV
|
||||||
b'\x1aE\xdf\xa3': True, # WebM/MKV
|
b'\x1aE\xdf\xa3': True, # WebM/MKV
|
||||||
|
b'PK\x03\x04': True, # ZIP (ugoira)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -48,6 +51,27 @@ def _ext_from_url(url: str) -> str:
|
|||||||
return ".jpg"
|
return ".jpg"
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_ugoira_to_gif(zip_path: Path) -> Path:
|
||||||
|
"""Convert a Pixiv ugoira zip (numbered JPEG/PNG frames) to an animated GIF."""
|
||||||
|
gif_path = zip_path.with_suffix(".gif")
|
||||||
|
if gif_path.exists():
|
||||||
|
return gif_path
|
||||||
|
with zipfile.ZipFile(zip_path, "r") as zf:
|
||||||
|
names = sorted(zf.namelist())
|
||||||
|
frames = []
|
||||||
|
for name in names:
|
||||||
|
data = zf.read(name)
|
||||||
|
frames.append(Image.open(__import__("io").BytesIO(data)).convert("RGBA"))
|
||||||
|
if not frames:
|
||||||
|
raise ValueError("Zip contains no image frames")
|
||||||
|
frames[0].save(
|
||||||
|
gif_path, save_all=True, append_images=frames[1:],
|
||||||
|
duration=80, loop=0, disposal=2,
|
||||||
|
)
|
||||||
|
zip_path.unlink()
|
||||||
|
return gif_path
|
||||||
|
|
||||||
|
|
||||||
async def download_image(
|
async def download_image(
|
||||||
url: str,
|
url: str,
|
||||||
client: httpx.AsyncClient | None = None,
|
client: httpx.AsyncClient | None = None,
|
||||||
@ -62,6 +86,12 @@ async def download_image(
|
|||||||
filename = _url_hash(url) + _ext_from_url(url)
|
filename = _url_hash(url) + _ext_from_url(url)
|
||||||
local = dest_dir / filename
|
local = dest_dir / filename
|
||||||
|
|
||||||
|
# Check if a ugoira zip was already converted to gif
|
||||||
|
if local.suffix.lower() == ".zip":
|
||||||
|
gif_path = local.with_suffix(".gif")
|
||||||
|
if gif_path.exists():
|
||||||
|
return gif_path
|
||||||
|
|
||||||
# Validate cached file isn't corrupt (e.g. HTML error page saved as image)
|
# Validate cached file isn't corrupt (e.g. HTML error page saved as image)
|
||||||
if local.exists():
|
if local.exists():
|
||||||
if _is_valid_media(local):
|
if _is_valid_media(local):
|
||||||
@ -119,6 +149,10 @@ async def download_image(
|
|||||||
if not _is_valid_media(local):
|
if not _is_valid_media(local):
|
||||||
local.unlink()
|
local.unlink()
|
||||||
raise ValueError("Downloaded file is not valid media")
|
raise ValueError("Downloaded file is not valid media")
|
||||||
|
|
||||||
|
# Convert ugoira zip to animated GIF
|
||||||
|
if local.suffix.lower() == ".zip" and zipfile.is_zipfile(local):
|
||||||
|
local = _convert_ugoira_to_gif(local)
|
||||||
finally:
|
finally:
|
||||||
if own_client:
|
if own_client:
|
||||||
await client.aclose()
|
await client.aclose()
|
||||||
|
|||||||
@ -123,10 +123,12 @@ class InfoPanel(QWidget):
|
|||||||
|
|
||||||
def set_post(self, post: Post) -> None:
|
def set_post(self, post: Post) -> None:
|
||||||
self._title.setText(f"Post #{post.id}")
|
self._title.setText(f"Post #{post.id}")
|
||||||
|
filetype = Path(post.file_url.split("?")[0]).suffix.lstrip(".").upper() if post.file_url else "unknown"
|
||||||
self._details.setText(
|
self._details.setText(
|
||||||
f"Size: {post.width}x{post.height}\n"
|
f"Size: {post.width}x{post.height}\n"
|
||||||
f"Score: {post.score}\n"
|
f"Score: {post.score}\n"
|
||||||
f"Rating: {post.rating or 'unknown'}\n"
|
f"Rating: {post.rating or 'unknown'}\n"
|
||||||
|
f"Filetype: {filetype}\n"
|
||||||
f"Source: {post.source or 'none'}"
|
f"Source: {post.source or 'none'}"
|
||||||
)
|
)
|
||||||
# Clear old tags
|
# Clear old tags
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user