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, level=level,
handlers=handlers handlers=handlers
) )
# Suppress PIL debug logs which are very noisy during Lottie conversion
logging.getLogger('PIL').setLevel(logging.INFO)
def relaunch_in_terminal(): def relaunch_in_terminal():
"""Detects if running without a terminal on Linux and relaunches in one.""" """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 textual # Terminal UI framework
PyYAML # YAML parsing and serialization PyYAML # YAML parsing and serialization
pydantic # Data validation using Python type hints 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 discord
import logging import logging
from typing import AsyncGenerator, Dict, Any from typing import AsyncGenerator, Dict, Any, Union
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -227,9 +227,44 @@ class DiscordReader:
"""Downloads a Discord emoji into memory.""" """Downloads a Discord emoji into memory."""
return await emoji.read() 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.""" """Downloads a Discord sticker into memory."""
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() 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: async def download_attachment(self, attachment: discord.Attachment) -> bytes:
"""Downloads a Discord attachment into memory.""" """Downloads a Discord attachment into memory."""

View file

@ -1,8 +1,17 @@
import asyncio import asyncio
import logging import logging
import re import re
import json
import io
from typing import Callable, Awaitable, Dict, Any 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.base import MigrationContext
from src.core.utils import resolve_discord_links 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) sticker_data = await context.discord_reader.download_sticker(s)
if sticker_data: if sticker_data:
# Use format to determine extension # Use format to determine extension
ext = getattr(s, 'format', 'png') format_val = getattr(s, 'format', 'png')
if hasattr(ext, 'name'): # discord.py StickerFormat enum logger.debug(f"Sticker {getattr(s, 'name', 'unknown')} format_val type: {type(format_val)}, value: {format_val}")
ext = ext.name
# 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': if ext == 'lottie':
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' ext = 'json'
filename = f"sticker_{s.name}_{s.id}.{ext}" filename = f"sticker_{s.name}_{s.id}.{ext}"
files.append({"filename": filename, "data": sticker_data}) files.append({"filename": filename, "data": sticker_data})
stats["attachments"] += 1 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: except Exception as e:
logger.error(f"Failed to download sticker {getattr(s, 'name', 'unknown')}: {e}") logger.error(f"Failed to download sticker {getattr(s, 'name', 'unknown')}: {e}")

View file

@ -1,8 +1,17 @@
import asyncio import asyncio
import logging import logging
import re import re
import json
import io
from typing import Callable, Awaitable, Dict, Any 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.base import MigrationContext
from src.core.utils import resolve_discord_links 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) sticker_data = await context.discord_reader.download_sticker(s)
if sticker_data: if sticker_data:
# Use format to determine extension # Use format to determine extension
ext = getattr(s, 'format', 'png') format_val = getattr(s, 'format', 'png')
if hasattr(ext, 'name'): # discord.py StickerFormat enum logger.debug(f"Sticker {getattr(s, 'name', 'unknown')} format_val type: {type(format_val)}, value: {format_val}")
ext = ext.name
# 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': if ext == 'lottie':
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' ext = 'json'
filename = f"sticker_{s.name}_{s.id}.{ext}" filename = f"sticker_{s.name}_{s.id}.{ext}"
files.append({"filename": filename, "data": sticker_data}) files.append({"filename": filename, "data": sticker_data})
stats["attachments"] += 1 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: except Exception as e:
logger.error(f"Failed to download sticker {getattr(s, 'name', 'unknown')}: {e}") logger.error(f"Failed to download sticker {getattr(s, 'name', 'unknown')}: {e}")