Files
OnePieceDle/src/routes/(admin)/admin/character-changes/+page.server.ts

248 lines
6.2 KiB
TypeScript

import { db } from '$lib/server/db';
import { character, characterScrapeValidation } from '$lib/server/db/schema';
import { eq } from 'drizzle-orm';
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
const execAsync = promisify(exec);
let isScrapeImportRunning = false;
const EXEC_OPTIONS = {
cwd: process.cwd(),
maxBuffer: 50 * 1024 * 1024
};
async function upsertCharacterFromScrapeValidation(characterId: string): Promise<boolean> {
const [scraped] = await db
.select()
.from(characterScrapeValidation)
.where(eq(characterScrapeValidation.id, characterId));
if (!scraped) {
return false;
}
await db
.insert(character)
.values({
id: scraped.id,
name: scraped.name,
frName: scraped.frName,
gender: scraped.gender,
age: scraped.age,
affiliations: scraped.affiliations,
devilFruitId: scraped.devilFruitId,
hakiObservation: scraped.hakiObservation,
hakiArmament: scraped.hakiArmament,
hakiConqueror: scraped.hakiConqueror,
bounty: scraped.bounty,
height: scraped.height,
origin: scraped.origin,
frOrigin: scraped.frOrigin,
firstAppearance: scraped.firstAppearance,
pictureUrl: scraped.pictureUrl,
epithets: scraped.epithets,
frEpithets: scraped.frEpithets,
status: scraped.status,
arcId: scraped.arcId,
url: scraped.url,
frUrl: scraped.frUrl,
})
.onConflictDoUpdate({
target: character.id,
set: {
name: scraped.name,
frName: scraped.frName,
gender: scraped.gender,
age: scraped.age,
affiliations: scraped.affiliations,
devilFruitId: scraped.devilFruitId,
hakiObservation: scraped.hakiObservation,
hakiArmament: scraped.hakiArmament,
hakiConqueror: scraped.hakiConqueror,
bounty: scraped.bounty,
height: scraped.height,
origin: scraped.origin,
frOrigin: scraped.frOrigin,
firstAppearance: scraped.firstAppearance,
pictureUrl: scraped.pictureUrl,
epithets: scraped.epithets,
frEpithets: scraped.frEpithets,
status: scraped.status,
arcId: scraped.arcId,
url: scraped.url,
frUrl: scraped.frUrl
}
});
return true;
}
export async function load() {
// Get all characters from both tables
const currentCharacters = await db.select().from(character);
const scrapedCharacters = await db.select().from(characterScrapeValidation);
// Create a map for quick lookup
const currentCharMap = new Map(currentCharacters.map(c => [c.id, c]));
// Compare and categorize changes
const changes: {
type: 'new' | 'modified';
id: string;
scraped: (typeof scrapedCharacters)[0];
current?: (typeof currentCharacters)[0];
differences?: Record<string, { current: any; scraped: any }>;
}[] = [];
for (const scraped of scrapedCharacters) {
const current = currentCharMap.get(scraped.id);
if (!current) {
// New character
changes.push({
type: 'new',
id: scraped.id,
scraped
});
} else {
// Check if different
const differences: Record<string, { current: any; scraped: any }> = {};
const fieldsToCompare = [
'name',
'frName',
'gender',
'age',
'affiliations',
'devilFruitId',
'hakiObservation',
'hakiArmament',
'hakiConqueror',
'bounty',
'height',
'origin',
'frOrigin',
'firstAppearance',
'pictureUrl',
'epithets',
'frEpithets',
'status',
'arcId',
'url',
'frUrl'
];
for (const field of fieldsToCompare) {
const currentValue = current[field as keyof typeof current];
const scrapedValue = scraped[field as keyof typeof scraped];
// Deep comparison for JSON fields
if (JSON.stringify(currentValue) !== JSON.stringify(scrapedValue)) {
differences[field] = {
current: currentValue,
scraped: scrapedValue
};
}
}
if (Object.keys(differences).length > 0) {
changes.push({
type: 'modified',
id: scraped.id,
scraped,
current,
differences
});
}
}
}
return {
changes: changes.sort((a, b) => {
// Show 'new' first, then 'modified'
if (a.type !== b.type) {
return a.type === 'new' ? -1 : 1;
}
return a.id.localeCompare(b.id);
})
};
}
export const actions = {
runScrapeImport: async () => {
if (isScrapeImportRunning) {
return {
success: false,
message: 'A scrape is already running. Please wait for it to finish.',
logs: ''
};
}
isScrapeImportRunning = true;
try {
const scrapeResult = await execAsync('npm run scrape', EXEC_OPTIONS);
const importResult = await execAsync('npm run db:import', EXEC_OPTIONS);
const logs = [
'=== npm run scrape ===',
scrapeResult.stdout || '',
scrapeResult.stderr ? `\n[stderr]\n${scrapeResult.stderr}` : '',
'\n=== npm run db:import ===',
importResult.stdout || '',
importResult.stderr ? `\n[stderr]\n${importResult.stderr}` : ''
]
.filter(Boolean)
.join('\n');
return {
success: true,
message: 'Scrape and import completed successfully',
logs
};
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to run scripts';
const stdout = typeof error === 'object' && error && 'stdout' in error ? String((error as any).stdout || '') : '';
const stderr = typeof error === 'object' && error && 'stderr' in error ? String((error as any).stderr || '') : '';
const logs = [stdout, stderr ? `\n[stderr]\n${stderr}` : ''].filter(Boolean).join('\n');
return {
success: false,
message,
logs
};
} finally {
isScrapeImportRunning = false;
}
},
acceptOne: async ({ request }) => {
const formData = await request.formData();
const characterId = formData.get('characterId');
if (!characterId || typeof characterId !== 'string') {
return { success: false, message: 'characterId is required' };
}
const applied = await upsertCharacterFromScrapeValidation(characterId);
return {
success: applied,
message: applied ? 'Character applied successfully' : 'Character not found in scrape validation table'
};
},
acceptAll: async () => {
const scrapedCharacters = await db.select().from(characterScrapeValidation);
let appliedCount = 0;
for (const scraped of scrapedCharacters) {
const applied = await upsertCharacterFromScrapeValidation(scraped.id);
if (applied) {
appliedCount++;
}
}
return {
success: true,
appliedCount
};
}
};