diff --git a/src/disco_reaper/exporter.py b/src/disco_reaper/exporter.py index a3a3232..376f620 100644 --- a/src/disco_reaper/exporter.py +++ b/src/disco_reaper/exporter.py @@ -354,6 +354,7 @@ class DiscordExporter: # 1. Fetch new messages - Handle Forbidden gracefully try: async for msg in self.reader.fetch_message_history(channel_id, after_id=last_id): + await asyncio.sleep(0) # Yield control msg_data = await self._format_message(msg, asset_dir, base_filename, avatar_dir, avatar_rel_base) messages.append(msg_data) new_count += 1 @@ -393,6 +394,7 @@ class DiscordExporter: thread_count = len(all_threads) for t in all_threads: + await asyncio.sleep(0) # Yield for safety thread_msg_count += (t.message_count or 0) msg_type = "Text" @@ -428,6 +430,7 @@ class DiscordExporter: output_data[k] = v # Save channel messages + await asyncio.sleep(0) # Yield before writing large JSON with open(json_file, "w", encoding="utf-8") as f: json.dump(output_data, f, indent=4, ensure_ascii=False) @@ -646,6 +649,8 @@ class DiscordExporter: logger.info(f"Found {len(all_threads)} threads in {channel.name}. Starting backup...") for thread in all_threads: + await asyncio.sleep(0) # important yield between threads + # First backup the full thread — this creates {thread_id}.json with totalAttachmentSizeBytes accumulated_count = await self.export_channel_messages(thread.id, progress_callback=progress_callback, force=force, accumulated_count=accumulated_count) thread_count += 1 @@ -726,6 +731,7 @@ class DiscordExporter: # Keep chronological order forum_data["messages"].sort(key=lambda x: x["timestamp"]) + await asyncio.sleep(0) # Yield before writing with open(forum_json_file, "w", encoding="utf-8") as f: json.dump(forum_data, f, indent=4, ensure_ascii=False) logger.info(f"Appended starter message for {thread.name} to {forum_json_file.name}") diff --git a/src/ui/backup_ops.py b/src/ui/backup_ops.py index f6d3def..3f00d97 100644 --- a/src/ui/backup_ops.py +++ b/src/ui/backup_ops.py @@ -6,9 +6,13 @@ Embedded inside ModeScreen's "Backup" tab. import asyncio import json import re +import logging +import traceback from pathlib import Path from datetime import datetime +logger = logging.getLogger(__name__) + from textual.app import ComposeResult from textual.containers import Container, Vertical, VerticalScroll from textual.widgets import Button, Label, Rule @@ -139,11 +143,14 @@ class BackupPane(Container): modal.write("Exporting structure...") _, cat_count, chan_count = await self.exporter.export_channels_structure() + modal.write("Exporting roles...") + roles = await self.exporter.export_roles() + modal.write("Exporting assets...") e_count, s_count = await self.exporter.export_assets() modal.write(f"[bold green]Server Profile backed up to: {self.exporter.export_path}[/bold green]") - modal.write(f"- {e_count} emojis, {s_count} stickers.") + modal.write(f"- {len(roles)} roles, {e_count} emojis, {s_count} stickers.") modal.phase_report("Profile Backup") except self.engine.discord_reader.Forbidden as e: @@ -216,6 +223,7 @@ class BackupPane(Container): selected_channels = [c for c in eligible_channels if c.id in selected_ids] # Phase 2: Confirmation + modal_prog = ProgressScreen() # Re-instantiate to avoid Textual re-push UI freeze self.app.push_screen(modal_prog) await asyncio.sleep(0.1) @@ -242,11 +250,14 @@ class BackupPane(Container): modal_prog.write(f"[yellow]Starting backup for {total_chans} channels...[/yellow]") for chan in selected_channels: + await asyncio.sleep(0.01) # Yield to UI thread to keep it responsive + backup_exists = (self.exporter.export_path / "message_backup" / f"{chan.id}.json").exists() is_sync = backup_exists and not force_overwrite label = "Syncing Backup" if is_sync else "Backing up" modal_prog.write(f"[cyan]{label}: {chan.name}[/cyan]") + logger.info(f"{label} for channel: #{chan.name} ({chan.id})") async def update_msg_count(name, count): modal_prog.set_status(f"{name}: {count} messages") @@ -258,9 +269,11 @@ class BackupPane(Container): await self.exporter.export_metadata() modal_prog.write("[bold green]Message backup complete![/bold green]") + logger.info("Message backup operation completed successfully.") modal_prog.phase_report("Message Backup") except Exception as e: + logger.error(f"Message backup failed: {e}\n{traceback.format_exc()}") modal_prog.write(f"[bold red]Message backup failed: {e}[/bold red]") modal_prog.phase_report("Message Backup", "error") finally: @@ -304,7 +317,10 @@ class BackupPane(Container): else: modal_prog.write(f"[yellow]Syncing {len(selected_channels)} channels...[/yellow]") for chan in selected_channels: + await asyncio.sleep(0.01) # Yield to UI thread + modal_prog.write(f"[cyan]Syncing: {chan.name}[/cyan]") + logger.info(f"Syncing backup for channel: #{chan.name} ({chan.id})") async def update_msg_count(name, count): modal_prog.set_status(f"{name}: {count} messages") @@ -315,9 +331,11 @@ class BackupPane(Container): await self.exporter.export_metadata() modal_prog.write("[bold green]Sync operation complete![/bold green]") + logger.info("Sync operation completed successfully.") modal_prog.phase_report("Backup Sync") except Exception as e: + logger.error(f"Sync failed: {e}\n{traceback.format_exc()}") modal_prog.write(f"[bold red]Sync failed: {e}[/bold red]") modal_prog.phase_report("Backup Sync", "error") finally: