fix message backup screen

This commit is contained in:
rambros 2026-03-04 13:51:14 +05:30
parent 3a6a138c89
commit ce11c85f49
2 changed files with 25 additions and 1 deletions

View file

@ -354,6 +354,7 @@ class DiscordExporter:
# 1. Fetch new messages - Handle Forbidden gracefully # 1. Fetch new messages - Handle Forbidden gracefully
try: try:
async for msg in self.reader.fetch_message_history(channel_id, after_id=last_id): 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) msg_data = await self._format_message(msg, asset_dir, base_filename, avatar_dir, avatar_rel_base)
messages.append(msg_data) messages.append(msg_data)
new_count += 1 new_count += 1
@ -393,6 +394,7 @@ class DiscordExporter:
thread_count = len(all_threads) thread_count = len(all_threads)
for t in all_threads: for t in all_threads:
await asyncio.sleep(0) # Yield for safety
thread_msg_count += (t.message_count or 0) thread_msg_count += (t.message_count or 0)
msg_type = "Text" msg_type = "Text"
@ -428,6 +430,7 @@ class DiscordExporter:
output_data[k] = v output_data[k] = v
# Save channel messages # Save channel messages
await asyncio.sleep(0) # Yield before writing large JSON
with open(json_file, "w", encoding="utf-8") as f: with open(json_file, "w", encoding="utf-8") as f:
json.dump(output_data, f, indent=4, ensure_ascii=False) 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...") logger.info(f"Found {len(all_threads)} threads in {channel.name}. Starting backup...")
for thread in all_threads: 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 # 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) accumulated_count = await self.export_channel_messages(thread.id, progress_callback=progress_callback, force=force, accumulated_count=accumulated_count)
thread_count += 1 thread_count += 1
@ -726,6 +731,7 @@ class DiscordExporter:
# Keep chronological order # Keep chronological order
forum_data["messages"].sort(key=lambda x: x["timestamp"]) 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: with open(forum_json_file, "w", encoding="utf-8") as f:
json.dump(forum_data, f, indent=4, ensure_ascii=False) json.dump(forum_data, f, indent=4, ensure_ascii=False)
logger.info(f"Appended starter message for {thread.name} to {forum_json_file.name}") logger.info(f"Appended starter message for {thread.name} to {forum_json_file.name}")

View file

@ -6,9 +6,13 @@ Embedded inside ModeScreen's "Backup" tab.
import asyncio import asyncio
import json import json
import re import re
import logging
import traceback
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
logger = logging.getLogger(__name__)
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.containers import Container, Vertical, VerticalScroll from textual.containers import Container, Vertical, VerticalScroll
from textual.widgets import Button, Label, Rule from textual.widgets import Button, Label, Rule
@ -139,11 +143,14 @@ class BackupPane(Container):
modal.write("Exporting structure...") modal.write("Exporting structure...")
_, cat_count, chan_count = await self.exporter.export_channels_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...") modal.write("Exporting assets...")
e_count, s_count = await self.exporter.export_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"[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") modal.phase_report("Profile Backup")
except self.engine.discord_reader.Forbidden as e: 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] selected_channels = [c for c in eligible_channels if c.id in selected_ids]
# Phase 2: Confirmation # Phase 2: Confirmation
modal_prog = ProgressScreen() # Re-instantiate to avoid Textual re-push UI freeze
self.app.push_screen(modal_prog) self.app.push_screen(modal_prog)
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
@ -242,11 +250,14 @@ class BackupPane(Container):
modal_prog.write(f"[yellow]Starting backup for {total_chans} channels...[/yellow]") modal_prog.write(f"[yellow]Starting backup for {total_chans} channels...[/yellow]")
for chan in selected_channels: 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() backup_exists = (self.exporter.export_path / "message_backup" / f"{chan.id}.json").exists()
is_sync = backup_exists and not force_overwrite is_sync = backup_exists and not force_overwrite
label = "Syncing Backup" if is_sync else "Backing up" label = "Syncing Backup" if is_sync else "Backing up"
modal_prog.write(f"[cyan]{label}: {chan.name}[/cyan]") 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): async def update_msg_count(name, count):
modal_prog.set_status(f"{name}: {count} messages") modal_prog.set_status(f"{name}: {count} messages")
@ -258,9 +269,11 @@ class BackupPane(Container):
await self.exporter.export_metadata() await self.exporter.export_metadata()
modal_prog.write("[bold green]Message backup complete![/bold green]") modal_prog.write("[bold green]Message backup complete![/bold green]")
logger.info("Message backup operation completed successfully.")
modal_prog.phase_report("Message Backup") modal_prog.phase_report("Message Backup")
except Exception as e: 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.write(f"[bold red]Message backup failed: {e}[/bold red]")
modal_prog.phase_report("Message Backup", "error") modal_prog.phase_report("Message Backup", "error")
finally: finally:
@ -304,7 +317,10 @@ class BackupPane(Container):
else: else:
modal_prog.write(f"[yellow]Syncing {len(selected_channels)} channels...[/yellow]") modal_prog.write(f"[yellow]Syncing {len(selected_channels)} channels...[/yellow]")
for chan in selected_channels: for chan in selected_channels:
await asyncio.sleep(0.01) # Yield to UI thread
modal_prog.write(f"[cyan]Syncing: {chan.name}[/cyan]") 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): async def update_msg_count(name, count):
modal_prog.set_status(f"{name}: {count} messages") modal_prog.set_status(f"{name}: {count} messages")
@ -315,9 +331,11 @@ class BackupPane(Container):
await self.exporter.export_metadata() await self.exporter.export_metadata()
modal_prog.write("[bold green]Sync operation complete![/bold green]") modal_prog.write("[bold green]Sync operation complete![/bold green]")
logger.info("Sync operation completed successfully.")
modal_prog.phase_report("Backup Sync") modal_prog.phase_report("Backup Sync")
except Exception as e: 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.write(f"[bold red]Sync failed: {e}[/bold red]")
modal_prog.phase_report("Backup Sync", "error") modal_prog.phase_report("Backup Sync", "error")
finally: finally: