support for lottie json stickers

This commit is contained in:
rambros 2026-03-10 19:43:15 +05:30
parent c60ad506df
commit cae47b697c
5 changed files with 123 additions and 15 deletions

View file

@ -18,6 +18,8 @@ def setup_logging():
level=level,
handlers=handlers
)
# Suppress PIL debug logs which are very noisy during Lottie conversion
logging.getLogger('PIL').setLevel(logging.INFO)
def relaunch_in_terminal():
"""Detects if running without a terminal on Linux and relaunches in one."""

View file

@ -5,3 +5,6 @@ rich # Terminal formatting and rich text
textual # Terminal UI framework
PyYAML # YAML parsing and serialization
pydantic # Data validation using Python type hints
lottie # Lottie file manipulation and conversion
Pillow # Image processing (required for GIF rendering)
cairosvg # SVG rendering (required for Lottie conversion)

View file

@ -1,6 +1,6 @@
import discord
import logging
from typing import AsyncGenerator, Dict, Any
from typing import AsyncGenerator, Dict, Any, Union
logger = logging.getLogger(__name__)
@ -227,9 +227,44 @@ class DiscordReader:
"""Downloads a Discord emoji into memory."""
return await emoji.read()
async def download_sticker(self, sticker: discord.GuildSticker) -> bytes:
async def download_sticker(self, sticker: Union[discord.GuildSticker, discord.StickerItem]) -> bytes:
"""Downloads a Discord sticker into memory."""
return await sticker.read()
logger.debug(f"Attempting to download sticker: {getattr(sticker, 'name', 'unknown')} (type: {type(sticker)})")
# 1. Try directly reading
if hasattr(sticker, 'read'):
try:
return await sticker.read()
except Exception as e:
logger.debug(f"Direct read failed for sticker: {e}")
# 2. Try converting to full sticker (only for StickerItem)
if hasattr(sticker, 'to_sticker'):
try:
logger.debug(f"Attempting to_sticker() for {getattr(sticker, 'name', 'unknown')}")
full_sticker = await sticker.to_sticker()
if hasattr(full_sticker, 'read'):
return await full_sticker.read()
except Exception as e:
logger.debug(f"to_sticker fallback failed: {e}")
# 3. Try downloading from URL as last resort
url = getattr(sticker, 'url', None)
if url:
try:
import aiohttp
logger.debug(f"Attempting URL download for sticker from {url}")
async with aiohttp.ClientSession() as session:
async with session.get(str(url)) as resp:
if resp.status == 200:
return await resp.read()
else:
logger.debug(f"URL download failed with status {resp.status}")
except Exception as e:
logger.debug(f"URL download failed for sticker: {e}")
logger.warning(f"Failed to download sticker {getattr(sticker, 'name', 'unknown')} after all attempts")
return b""
async def download_attachment(self, attachment: discord.Attachment) -> bytes:
"""Downloads a Discord attachment into memory."""

View file

@ -1,8 +1,17 @@
import asyncio
import logging
import re
import json
import io
from typing import Callable, Awaitable, Dict, Any
try:
from lottie.objects import Animation
from lottie.exporters.gif import export_gif
HAS_LOTTIE = True
except ImportError:
HAS_LOTTIE = False
from src.core.base import MigrationContext
from src.core.utils import resolve_discord_links
@ -242,18 +251,43 @@ async def migrate_messages(
sticker_data = await context.discord_reader.download_sticker(s)
if sticker_data:
# Use format to determine extension
ext = getattr(s, 'format', 'png')
if hasattr(ext, 'name'): # discord.py StickerFormat enum
ext = ext.name
format_val = getattr(s, 'format', 'png')
logger.debug(f"Sticker {getattr(s, 'name', 'unknown')} format_val type: {type(format_val)}, value: {format_val}")
# Handle Lottie (json)
if hasattr(format_val, 'name'): # discord.py StickerFormat enum
ext = format_val.name.lower()
elif isinstance(format_val, int):
# Map common StickerFormat values if it's an int
format_map = {1: 'png', 2: 'apng', 3: 'lottie', 4: 'gif'}
ext = format_map.get(format_val, 'png')
else:
ext = str(format_val).lower()
logger.debug(f"Determined sticker extension: {ext}")
# Handle Lottie (json) -> Convert to GIF
if ext == 'lottie':
ext = 'json'
if HAS_LOTTIE:
try:
logger.debug(f"Converting Lottie sticker {s.name} to GIF...")
lottie_data = json.loads(sticker_data)
animation = Animation.load(lottie_data)
output = io.BytesIO()
export_gif(animation, output)
sticker_data = output.getvalue()
ext = 'gif'
logger.debug(f"Successfully converted Lottie sticker {s.name} to GIF")
except Exception as conv_err:
logger.error(f"Failed to convert Lottie sticker {s.name} to GIF: {conv_err}")
ext = 'json' # Fallback to json if conversion fails
else:
logger.warning(f"Lottie library not available, sending sticker {s.name} as raw JSON")
ext = 'json'
filename = f"sticker_{s.name}_{s.id}.{ext}"
files.append({"filename": filename, "data": sticker_data})
stats["attachments"] += 1
logger.debug(f"Added sticker {s.name} as attachment")
logger.debug(f"Added sticker {s.name} as attachment (extension: {ext})")
except Exception as e:
logger.error(f"Failed to download sticker {getattr(s, 'name', 'unknown')}: {e}")

View file

@ -1,8 +1,17 @@
import asyncio
import logging
import re
import json
import io
from typing import Callable, Awaitable, Dict, Any
try:
from lottie.objects import Animation
from lottie.exporters.gif import export_gif
HAS_LOTTIE = True
except ImportError:
HAS_LOTTIE = False
from src.core.base import MigrationContext
from src.core.utils import resolve_discord_links
@ -245,18 +254,43 @@ async def migrate_messages(
sticker_data = await context.discord_reader.download_sticker(s)
if sticker_data:
# Use format to determine extension
ext = getattr(s, 'format', 'png')
if hasattr(ext, 'name'): # discord.py StickerFormat enum
ext = ext.name
format_val = getattr(s, 'format', 'png')
logger.debug(f"Sticker {getattr(s, 'name', 'unknown')} format_val type: {type(format_val)}, value: {format_val}")
# Handle Lottie (json)
if hasattr(format_val, 'name'): # discord.py StickerFormat enum
ext = format_val.name.lower()
elif isinstance(format_val, int):
# Map common StickerFormat values if it's an int
format_map = {1: 'png', 2: 'apng', 3: 'lottie', 4: 'gif'}
ext = format_map.get(format_val, 'png')
else:
ext = str(format_val).lower()
logger.debug(f"Determined sticker extension: {ext}")
# Handle Lottie (json) -> Convert to GIF
if ext == 'lottie':
ext = 'json'
if HAS_LOTTIE:
try:
logger.debug(f"Converting Lottie sticker {s.name} to GIF...")
lottie_data = json.loads(sticker_data)
animation = Animation.load(lottie_data)
output = io.BytesIO()
export_gif(animation, output)
sticker_data = output.getvalue()
ext = 'gif'
logger.debug(f"Successfully converted Lottie sticker {s.name} to GIF")
except Exception as conv_err:
logger.error(f"Failed to convert Lottie sticker {s.name} to GIF: {conv_err}")
ext = 'json' # Fallback to json if conversion fails
else:
logger.warning(f"Lottie library not available, sending sticker {s.name} as raw JSON")
ext = 'json'
filename = f"sticker_{s.name}_{s.id}.{ext}"
files.append({"filename": filename, "data": sticker_data})
stats["attachments"] += 1
logger.debug(f"Added sticker {s.name} as attachment")
logger.debug(f"Added sticker {s.name} as attachment (extension: {ext})")
except Exception as e:
logger.error(f"Failed to download sticker {getattr(s, 'name', 'unknown')}: {e}")