forked from syntaxbullet/aurorabot
240 lines
7.6 KiB
TypeScript
240 lines
7.6 KiB
TypeScript
#!/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<void> {
|
|
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<MigrationResult> {
|
|
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);
|
|
});
|