diff --git a/disco-reaper.py b/disco-reaper.py index 2de0517..ccee593 100644 --- a/disco-reaper.py +++ b/disco-reaper.py @@ -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.""" diff --git a/requirements.txt b/requirements.txt index 53a3669..8036581 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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) diff --git a/src/core/discord_reader.py b/src/core/discord_reader.py index 3789f52..ec259b7 100644 --- a/src/core/discord_reader.py +++ b/src/core/discord_reader.py @@ -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.""" diff --git a/src/fluxer/migrate_message.py b/src/fluxer/migrate_message.py index e1d440e..4a3ce59 100644 --- a/src/fluxer/migrate_message.py +++ b/src/fluxer/migrate_message.py @@ -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}") diff --git a/src/stoat/migrate_message.py b/src/stoat/migrate_message.py index 448200c..b07af7b 100644 --- a/src/stoat/migrate_message.py +++ b/src/stoat/migrate_message.py @@ -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}")