#!/usr/bin/env bun /** * Item Asset Migration Script * * Downloads images from existing Discord CDN URLs and saves them locally. * Updates database records to use local asset paths. * * Usage: * bun run scripts/migrate-item-assets.ts # Dry run (no changes) * bun run scripts/migrate-item-assets.ts --execute # Actually perform migration */ import { resolve, join } from "path"; import { mkdir } from "node:fs/promises"; // Initialize database connection const { DrizzleClient } = await import("../shared/db/DrizzleClient"); const { items } = await import("../shared/db/schema"); const ASSETS_DIR = resolve(import.meta.dir, "../bot/assets/graphics/items"); const DRY_RUN = !process.argv.includes("--execute"); interface MigrationResult { itemId: number; itemName: string; originalUrl: string; newPath: string; status: "success" | "skipped" | "failed"; error?: string; } /** * Check if a URL is an external URL (not a local asset path) */ function isExternalUrl(url: string | null): boolean { if (!url) return false; return url.startsWith("http://") || url.startsWith("https://"); } /** * Check if a URL is likely a Discord CDN URL */ function isDiscordCdnUrl(url: string): boolean { return url.includes("cdn.discordapp.com") || url.includes("media.discordapp.net") || url.includes("discord.gg"); } /** * Download an image from a URL and save it locally */ async function downloadImage(url: string, destPath: string): Promise { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const contentType = response.headers.get("content-type") || ""; if (!contentType.startsWith("image/")) { throw new Error(`Invalid content type: ${contentType}`); } const buffer = await response.arrayBuffer(); await Bun.write(destPath, buffer); } /** * Migrate a single item's images */ async function migrateItem(item: { id: number; name: string; iconUrl: string | null; imageUrl: string | null; }): Promise { const result: MigrationResult = { itemId: item.id, itemName: item.name, originalUrl: item.iconUrl || item.imageUrl || "", newPath: `/assets/items/${item.id}.png`, status: "skipped" }; // Check if either URL needs migration const hasExternalIcon = isExternalUrl(item.iconUrl); const hasExternalImage = isExternalUrl(item.imageUrl); if (!hasExternalIcon && !hasExternalImage) { result.status = "skipped"; return result; } // Prefer iconUrl, fall back to imageUrl const urlToDownload = item.iconUrl || item.imageUrl; if (!urlToDownload || !isExternalUrl(urlToDownload)) { result.status = "skipped"; return result; } result.originalUrl = urlToDownload; const destPath = join(ASSETS_DIR, `${item.id}.png`); if (DRY_RUN) { console.log(` [DRY RUN] Would download: ${urlToDownload}`); console.log(` -> ${destPath}`); result.status = "success"; return result; } try { // Download the image await downloadImage(urlToDownload, destPath); // Update database record const { eq } = await import("drizzle-orm"); await DrizzleClient .update(items) .set({ iconUrl: `/assets/items/${item.id}.png`, imageUrl: `/assets/items/${item.id}.png`, }) .where(eq(items.id, item.id)); result.status = "success"; console.log(` ✅ Migrated: ${item.name} (ID: ${item.id})`); } catch (error) { result.status = "failed"; result.error = error instanceof Error ? error.message : String(error); console.log(` ❌ Failed: ${item.name} (ID: ${item.id}) - ${result.error}`); } return result; } /** * Main migration function */ async function main() { console.log("═══════════════════════════════════════════════════════════════"); console.log(" Item Asset Migration Script"); console.log("═══════════════════════════════════════════════════════════════"); console.log(); if (DRY_RUN) { console.log(" ⚠️ DRY RUN MODE - No changes will be made"); console.log(" Run with --execute to perform actual migration"); console.log(); } // Ensure assets directory exists await mkdir(ASSETS_DIR, { recursive: true }); console.log(` 📁 Assets directory: ${ASSETS_DIR}`); console.log(); // Fetch all items const allItems = await DrizzleClient.select({ id: items.id, name: items.name, iconUrl: items.iconUrl, imageUrl: items.imageUrl, }).from(items); console.log(` 📦 Found ${allItems.length} total items`); // Filter items that need migration const itemsToMigrate = allItems.filter(item => isExternalUrl(item.iconUrl) || isExternalUrl(item.imageUrl) ); console.log(` 🔄 ${itemsToMigrate.length} items have external URLs`); console.log(); if (itemsToMigrate.length === 0) { console.log(" ✨ No items need migration!"); return; } // Categorize by URL type const discordCdnItems = itemsToMigrate.filter(item => isDiscordCdnUrl(item.iconUrl || "") || isDiscordCdnUrl(item.imageUrl || "") ); const otherExternalItems = itemsToMigrate.filter(item => !isDiscordCdnUrl(item.iconUrl || "") && !isDiscordCdnUrl(item.imageUrl || "") ); console.log(` 📊 Breakdown:`); console.log(` - Discord CDN URLs: ${discordCdnItems.length}`); console.log(` - Other external URLs: ${otherExternalItems.length}`); console.log(); // Process migrations console.log(" Starting migration..."); console.log(); const results: MigrationResult[] = []; for (const item of itemsToMigrate) { const result = await migrateItem(item); results.push(result); } // Summary console.log(); console.log("═══════════════════════════════════════════════════════════════"); console.log(" Migration Summary"); console.log("═══════════════════════════════════════════════════════════════"); const successful = results.filter(r => r.status === "success").length; const skipped = results.filter(r => r.status === "skipped").length; const failed = results.filter(r => r.status === "failed").length; console.log(` ✅ Successful: ${successful}`); console.log(` ⏭️ Skipped: ${skipped}`); console.log(` ❌ Failed: ${failed}`); console.log(); if (failed > 0) { console.log(" Failed items:"); for (const result of results.filter(r => r.status === "failed")) { console.log(` - ${result.itemName}: ${result.error}`); } } if (DRY_RUN) { console.log(); console.log(" ⚠️ This was a dry run. Run with --execute to apply changes."); } // Exit with error code if any failures process.exit(failed > 0 ? 1 : 0); } // Run main().catch(error => { console.error("Migration failed:", error); process.exit(1); });