# Snapshots

Session persistence and state management in Sonora v1.2.8.

# Table of Contents

  • Overview
  • Creating Snapshots
  • Loading Snapshots
  • Snapshot Management
  • Auto-Save
  • Backup & Recovery
  • Security
  • Examples

# Overview

Snapshots capture the complete state of your music bot at any point in time, allowing you to:

  • Save sessions for later restoration
  • Backup configurations before major changes
  • Recover from crashes or unexpected shutdowns
  • Share setups between different instances
  • Version control your bot's state

# What Gets Saved

Snapshots include:

  • Current track and playback position
  • Queue contents (upcoming and history)
  • Player settings (volume, filters, loop mode)
  • Autoplay configuration and state
  • Plugin states and configurations
  • Guild-specific settings

# Creating Snapshots

# Manual Snapshots

from sonora import SonoraClient

client = SonoraClient(...)
player = await client.get_player(guild_id)

# Create snapshot
snapshot = await player.create_snapshot()

# Save to file
await snapshot.save_to_file("my_session.json")

# Or get as dictionary
snapshot_data = snapshot.to_dict()

# CLI Snapshots

# Save current session
sonoractl snapshot save --name "party_session"

# Save with custom path
sonoractl snapshot save --output "/backups/session_$(date +%Y%m%d).json"

# Automatic Snapshots

# Enable auto-save
player.snapshots.auto_save = True
player.snapshots.auto_save_interval = 300  # Every 5 minutes

# Custom save location
player.snapshots.save_path = "./snapshots/"

# Loading Snapshots

# From File

# Load from file
snapshot = await Snapshot.load_from_file("my_session.json")

# Restore to player
await player.load_snapshot(snapshot)

# Or restore specific parts
await player.load_queue_snapshot(snapshot)
await player.load_filters_snapshot(snapshot)

# CLI Restore

# Restore session
sonoractl snapshot restore "party_session"

# Restore to specific guild
sonoractl snapshot restore "party_session" --guild-id 123456789

# Preview before restoring
sonoractl snapshot info "party_session"

# Selective Restore

# Load only queue state
await player.load_snapshot(snapshot, components=["queue"])

# Load multiple components
await player.load_snapshot(snapshot, components=[
    "queue",
    "filters",
    "autoplay"
])

# Available components:
# - "queue": Upcoming tracks and history
# - "current": Current track and position
# - "filters": Audio filters and settings
# - "autoplay": Autoplay configuration
# - "settings": Player settings (volume, loop mode)

# Snapshot Management

# Listing Snapshots

# Get all snapshots
snapshots = await player.snapshots.list_snapshots()

# Filter by criteria
recent_snapshots = await player.snapshots.list_snapshots(
    since=datetime.now() - timedelta(hours=24)
)

# Sort by different criteria
snapshots_by_size = sorted(snapshots, key=lambda s: s.size)
snapshots_by_date = sorted(snapshots, key=lambda s: s.created_at, reverse=True)

# CLI Management

# List all snapshots
sonoractl snapshot list

# Show snapshot details
sonoractl snapshot info "party_session"

# Delete old snapshots
sonoractl snapshot delete "old_session"

# Clean up snapshots older than 7 days
sonoractl snapshot cleanup --older-than 7d

# Snapshot Metadata

# Access snapshot information
print(f"Created: {snapshot.created_at}")
print(f"Size: {snapshot.size} bytes")
print(f"Version: {snapshot.version}")
print(f"Guild ID: {snapshot.guild_id}")

# Custom metadata
snapshot.metadata = {
    "description": "End of year party",
    "tags": ["party", "christmas", "2024"],
    "quality": "high"
}

# Auto-Save

# Configuration

# Enable auto-save
player.snapshots.auto_save_enabled = True

# Save interval (seconds)
player.snapshots.auto_save_interval = 600  # 10 minutes

# Maximum snapshots to keep
player.snapshots.max_snapshots = 50

# Save path
player.snapshots.save_directory = "./auto_snapshots/"

# Auto-Save Events

from sonora import event_manager, EventType

@event_manager.on(EventType.SNAPSHOT_AUTO_SAVED)
async def on_auto_save(event):
    snapshot = event.data['snapshot']
    print(f"Auto-saved snapshot: {snapshot.name}")

@event_manager.on(EventType.SNAPSHOT_AUTO_CLEANUP)
async def on_cleanup(event):
    deleted_count = event.data['deleted_count']
    print(f"Cleaned up {deleted_count} old snapshots")

# Conditional Auto-Save

# Only save when queue has tracks
player.snapshots.auto_save_condition = lambda: len(player.queue.upcoming) > 0

# Save only during active playback
player.snapshots.auto_save_condition = lambda: player.current is not None

# Custom condition
def should_save():
    return (len(player.queue.upcoming) > 5 and
            player.current is not None and
            not player.is_paused)

player.snapshots.auto_save_condition = should_save

# Backup & Recovery

# Full System Backup

# Backup entire bot state
backup = await client.create_full_backup()

# Save backup
await backup.save_to_file("full_backup.json")

# Restore from backup
restored_backup = await Backup.load_from_file("full_backup.json")
await client.restore_from_backup(restored_backup)

# Incremental Backups

# Create incremental backup
incremental = await player.create_incremental_backup(
    since_last_backup=True
)

# Differential backup
differential = await player.create_differential_backup(
    base_snapshot=previous_snapshot
)

# Disaster Recovery

# Emergency restore from any available snapshot
async def emergency_restore(player, preferred_snapshot=None):
    try:
        if preferred_snapshot:
            await player.load_snapshot(preferred_snapshot)
            return True
    except Exception:
        pass

    # Try to find any recent snapshot
    snapshots = await player.snapshots.list_snapshots(
        limit=5,
        sort_by="created_at",
        reverse=True
    )

    for snapshot in snapshots:
        try:
            await player.load_snapshot(snapshot)
            print(f"Restored from backup: {snapshot.name}")
            return True
        except Exception as e:
            print(f"Failed to restore {snapshot.name}: {e}")
            continue

    # Last resort: reset to clean state
    await player.reset_to_defaults()
    print("Reset to clean state")
    return False

# Security

# Encrypted Snapshots

# Create encrypted snapshot
encrypted_snapshot = await player.create_encrypted_snapshot(
    encryption_key="your-secure-key"
)

# Save encrypted
await encrypted_snapshot.save_encrypted("secure_snapshot.enc")

# Load and decrypt
decrypted_snapshot = await Snapshot.load_encrypted(
    "secure_snapshot.enc",
    decryption_key="your-secure-key"
)

# Access Control

# Restrict snapshot access
player.snapshots.require_permission = True
player.snapshots.allowed_users = ["admin_user_id"]

# Audit snapshot operations
@event_manager.on(EventType.SNAPSHOT_CREATED)
async def audit_snapshot(event):
    snapshot = event.data['snapshot']
    user = event.data['user']

    await log_audit_event(
        action="snapshot_created",
        resource=snapshot.name,
        user=user,
        details={"size": snapshot.size}
    )

# Integrity Verification

# Enable integrity checking
player.snapshots.enable_integrity_check = True

# Verify snapshot integrity
is_valid = await snapshot.verify_integrity()
if not is_valid:
    raise SecurityError("Snapshot integrity check failed")

# Automatic integrity monitoring
@event_manager.on(EventType.SNAPSHOT_CORRUPTION_DETECTED)
async def on_corruption(event):
    snapshot = event.data['snapshot']
    await send_alert(f"Snapshot corruption detected: {snapshot.name}")
    await snapshot.repair()  # Attempt automatic repair

# Examples

# Session Management Bot

import discord
from discord.ext import commands
from sonora import SonoraClient

class SessionManager(commands.Cog):
    def __init__(self, bot, sonora):
        self.bot = bot
        self.sonora = sonora

    @commands.command()
    async def save_session(self, ctx, name: str):
        """Save current session"""
        player = await self.sonora.get_player(ctx.guild.id)

        snapshot = await player.create_snapshot()
        snapshot.name = name
        snapshot.description = f"Saved by {ctx.author.name}"

        await snapshot.save_to_file(f"sessions/{name}.json")

        embed = discord.Embed(
            title="šŸ’¾ Session Saved",
            description=f"Session '{name}' has been saved",
            color=0x00ff00
        )
        embed.add_field(name="Tracks in Queue", value=len(player.queue.upcoming))
        embed.add_field(name="Current Track", value=player.current.title if player.current else "None")

        await ctx.send(embed=embed)

    @commands.command()
    async def load_session(self, ctx, name: str):
        """Load a saved session"""
        player = await self.sonora.get_player(ctx.guild.id)

        try:
            snapshot = await Snapshot.load_from_file(f"sessions/{name}.json")
            await player.load_snapshot(snapshot)

            embed = discord.Embed(
                title="šŸ”„ Session Loaded",
                description=f"Session '{name}' has been restored",
                color=0x00ff00
            )
            embed.add_field(name="Tracks Restored", value=len(player.queue.upcoming))
            embed.add_field(name="Current Track", value=player.current.title if player.current else "None")

            await ctx.send(embed=embed)

        except FileNotFoundError:
            await ctx.send(f"āŒ Session '{name}' not found")
        except Exception as e:
            await ctx.send(f"āŒ Failed to load session: {e}")

    @commands.command()
    async def list_sessions(self, ctx):
        """List saved sessions"""
        import os

        sessions_dir = "sessions"
        if not os.path.exists(sessions_dir):
            await ctx.send("No saved sessions")
            return

        sessions = [f for f in os.listdir(sessions_dir) if f.endswith('.json')]

        if not sessions:
            await ctx.send("No saved sessions")
            return

        embed = discord.Embed(
            title="šŸ“ Saved Sessions",
            description="\n".join(f"• {s[:-5]}" for s in sessions),  # Remove .json
            color=0x0099ff
        )

        await ctx.send(embed=embed)

# Add cog to bot
bot.add_cog(SessionManager(bot, sonora))

# Auto-Backup System

class AutoBackupManager:
    def __init__(self, client):
        self.client = client
        self.backup_interval = 1800  # 30 minutes
        self.max_backups = 24  # Keep 12 hours worth
        self.backup_task = None

    async def start_auto_backup(self):
        """Start automatic backup system"""
        if self.backup_task:
            return

        self.backup_task = asyncio.create_task(self._backup_loop())

    async def stop_auto_backup(self):
        """Stop automatic backup system"""
        if self.backup_task:
            self.backup_task.cancel()
            try:
                await self.backup_task
            except asyncio.CancelledError:
                pass
            self.backup_task = None

    async def _backup_loop(self):
        """Main backup loop"""
        while True:
            try:
                await asyncio.sleep(self.backup_interval)
                await self._perform_backup()
                await self._cleanup_old_backups()
            except Exception as e:
                print(f"Backup failed: {e}")

    async def _perform_backup(self):
        """Perform a backup of all active players"""
        timestamp = int(time.time())

        for guild_id, player in self.client.players.items():
            try:
                # Only backup if there's activity
                if player.current or player.queue.upcoming:
                    snapshot = await player.create_snapshot()
                    filename = f"backups/guild_{guild_id}_{timestamp}.json"
                    await snapshot.save_to_file(filename)
                    print(f"Backed up guild {guild_id}")
            except Exception as e:
                print(f"Failed to backup guild {guild_id}: {e}")

    async def _cleanup_old_backups(self):
        """Clean up old backup files"""
        import os
        from pathlib import Path

        backup_dir = Path("backups")
        if not backup_dir.exists():
            return

        # Get all backup files
        backup_files = list(backup_dir.glob("*.json"))
        backup_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)

        # Remove old backups
        if len(backup_files) > self.max_backups:
            for old_file in backup_files[self.max_backups:]:
                old_file.unlink()
                print(f"Removed old backup: {old_file.name}")

    async def restore_latest_backup(self, guild_id):
        """Restore the latest backup for a guild"""
        import os
        from pathlib import Path

        backup_dir = Path("backups")
        if not backup_dir.exists():
            return False

        # Find latest backup for this guild
        pattern = f"guild_{guild_id}_*.json"
        backups = list(backup_dir.glob(pattern))

        if not backups:
            return False

        # Get most recent
        latest_backup = max(backups, key=lambda x: x.stat().st_mtime)

        try:
            snapshot = await Snapshot.load_from_file(str(latest_backup))
            player = await self.client.get_player(guild_id)
            await player.load_snapshot(snapshot)
            print(f"Restored backup for guild {guild_id}")
            return True
        except Exception as e:
            print(f"Failed to restore backup: {e}")
            return False

# Usage
backup_manager = AutoBackupManager(client)
await backup_manager.start_auto_backup()

# Later, to restore
success = await backup_manager.restore_latest_backup(guild_id)

# Snapshot Analytics

class SnapshotAnalytics:
    def __init__(self, player):
        self.player = player
        self.analytics = {}

    async def analyze_snapshots(self):
        """Analyze snapshot patterns and usage"""
        snapshots = await self.player.snapshots.list_snapshots()

        # Analyze creation patterns
        creation_times = [s.created_at for s in snapshots]
        hourly_pattern = self.analyze_hourly_pattern(creation_times)

        # Analyze content changes
        content_changes = []
        for i in range(1, len(snapshots)):
            changes = self.compare_snapshots(snapshots[i-1], snapshots[i])
            content_changes.append(changes)

        # Generate insights
        insights = {
            "total_snapshots": len(snapshots),
            "average_interval": self.calculate_average_interval(creation_times),
            "peak_hours": hourly_pattern.most_common(3),
            "most_changed_component": max(content_changes, key=lambda x: x['total_changes']) if content_changes else None,
            "storage_used": sum(s.size for s in snapshots)
        }

        self.analytics = insights
        return insights

    def analyze_hourly_pattern(self, timestamps):
        """Analyze when snapshots are typically created"""
        from collections import Counter
        hours = [ts.hour for ts in timestamps]
        return Counter(hours)

    def calculate_average_interval(self, timestamps):
        """Calculate average time between snapshots"""
        if len(timestamps) < 2:
            return 0

        intervals = []
        for i in range(1, len(timestamps)):
            interval = (timestamps[i] - timestamps[i-1]).total_seconds()
            intervals.append(interval)

        return sum(intervals) / len(intervals)

    def compare_snapshots(self, snap1, snap2):
        """Compare two snapshots for changes"""
        changes = {
            "queue_changes": len(snap2.queue) - len(snap1.queue),
            "filter_changes": len(snap2.filters) - len(snap1.filters),
            "setting_changes": self.count_setting_changes(snap1.settings, snap2.settings)
        }
        changes["total_changes"] = sum(abs(v) for v in changes.values())
        return changes

    def count_setting_changes(self, settings1, settings2):
        """Count changes in settings"""
        changes = 0
        all_keys = set(settings1.keys()) | set(settings2.keys())

        for key in all_keys:
            val1 = settings1.get(key)
            val2 = settings2.get(key)
            if val1 != val2:
                changes += 1

        return changes

    async def generate_report(self):
        """Generate a comprehensive analytics report"""
        insights = await self.analyze_snapshots()

        report = f"""
šŸ“Š Snapshot Analytics Report
===============================

Total Snapshots: {insights['total_snapshots']}
Storage Used: {insights['storage_used'] / 1024:.1f} KB
Average Interval: {insights['average_interval'] / 3600:.1f} hours

šŸ“ˆ Creation Patterns:
"""

        for hour, count in insights['peak_hours']:
            report += f"  {hour:02d}:00 - {count} snapshots\n"

        if insights['most_changed_component']:
            report += f"\nšŸ”„ Most Volatile Component:\n"
            changes = insights['most_changed_component']
            report += f"  Queue: {changes['queue_changes']} changes\n"
            report += f"  Filters: {changes['filter_changes']} changes\n"
            report += f"  Settings: {changes['setting_changes']} changes\n"

        return report

# Usage
analytics = SnapshotAnalytics(player)
report = await analytics.generate_report()
print(report)

This comprehensive snapshot system provides robust state management and recovery capabilities for Sonora v1.2.8.