This commit is contained in:
Dreamgineer
2025-10-18 09:02:35 -04:00 Unverified
parent 0448e74755
commit 92bf46cf60
11 changed files with 190 additions and 58 deletions
+5 -1
View File
@@ -55,9 +55,13 @@ const columns: ColumnDef<{
enableHiding: false,
},
{
accessorFn: (row) => row.name || `[${row.id}]`,
accessorKey: "name",
header: "Name",
},
{
accessorKey: "id",
header: "ID",
},
{
accessorFn: (row) => b2s(Number(row.size)),
header: "Size",
+1 -1
View File
@@ -238,7 +238,7 @@ export async function cdnReferences(
.select({ id: table.id })
.from(table)
.where(
and(
or(
...cols.map((c) =>
eq(table[c as keyof typeof table._.columns], id),
),
+9 -9
View File
@@ -52,7 +52,7 @@ export const characters = pgTable("characters", {
vision: element().notNull(),
image: text()
.notNull()
.references(() => cdn.id, { onDelete: "cascade", onUpdate: "cascade" }),
.references(() => cdn.id, { onUpdate: "cascade" }),
weapon: text().notNull(),
amber: text().notNull(), // Amber character ID
order: integer().notNull(),
@@ -62,7 +62,7 @@ export const versions = pgTable("versions", {
id: text().primaryKey(), // LI
name: text().notNull(), // LunaI
from: text().references((): AnyPgColumn => versions.id, {
onDelete: "cascade",
onDelete: "set null",
onUpdate: "cascade",
}),
// deprecate: date().notNull() // 21/03/2077
@@ -108,7 +108,7 @@ export const tierlistTypes = tierlist.table("types", {
id: text().primaryKey().$defaultFn(uuidv7),
name: text().notNull(),
image: text().references(() => cdn.id, {
onDelete: "cascade",
onDelete: "set null",
onUpdate: "cascade",
}),
order: integer().notNull(),
@@ -119,12 +119,12 @@ export const tierlistTiers = tierlist.table("tiers", {
name: text().notNull(),
badges: text()
.references(() => tierlistBadges.id, {
onDelete: "cascade",
onDelete: "set null",
onUpdate: "cascade",
})
.array(),
image: text().references(() => cdn.id, {
onDelete: "cascade",
onDelete: "set null",
onUpdate: "cascade",
}),
order: integer().notNull(),
@@ -134,7 +134,7 @@ export const tierlistColumns = tierlist.table("columns", {
id: text().primaryKey().$defaultFn(uuidv7),
name: text().notNull(),
image: text().references(() => cdn.id, {
onDelete: "cascade",
onDelete: "set null",
onUpdate: "cascade",
}),
order: integer().notNull(),
@@ -144,7 +144,7 @@ export const tierlistBadges = tierlist.table("badges", {
id: text().primaryKey().$defaultFn(uuidv7),
name: text().notNull(),
image: text().references(() => cdn.id, {
onDelete: "cascade",
onDelete: "set null",
onUpdate: "cascade",
}),
order: integer().notNull(),
@@ -161,11 +161,11 @@ export const tierlistVersions = tierlist.table("versions", {
onUpdate: "cascade",
}),
image: text().references(() => cdn.id, {
onDelete: "cascade",
onDelete: "set null",
onUpdate: "cascade",
}),
disclaimer: text().references(() => cdn.id, {
onDelete: "cascade",
onDelete: "set null",
onUpdate: "cascade",
}),
deprecates: text().notNull(),
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+175 -47
View File
@@ -3,6 +3,95 @@ import { cdnify, cdnReferences } from "@/lib/api";
import { AmberElementMap } from "@/lib/const";
import { db } from "@/lib/db";
import { cdn, characters, versions } from "@/lib/db/schema";
import { readFile } from "node:fs/promises";
const exclude = ["Traveler"];
const extra: Avatar[] = [
{
id: "10000005-pyro",
rank: 5,
name: "Traveler Pyro",
element: "Fire",
weaponType: "WEAPON_SWORD_ONE_HAND",
region: "MAINACTOR",
specialProp: "FIGHT_PROP_ATTACK_PERCENT",
bodyType: "BOY",
icon: "custom/traveler_pyro.webp:image/webp",
birthday: [0, 0],
route: "Pyro Traveler Boy",
release: 1735758000,
},
{
id: "10000005-hydro",
rank: 5,
name: "Traveler Hydro",
element: "Water",
weaponType: "WEAPON_SWORD_ONE_HAND",
region: "MAINACTOR",
specialProp: "FIGHT_PROP_ATTACK_PERCENT",
bodyType: "BOY",
icon: "custom/traveler_hydro.webp:image/webp",
birthday: [0, 0],
route: "Hydro Traveler Boy",
release: 1690146000,
},
{
id: "10000005-anemo",
rank: 5,
name: "Traveler Anemo",
element: "Wind",
weaponType: "WEAPON_SWORD_ONE_HAND",
region: "MAINACTOR",
specialProp: "FIGHT_PROP_ATTACK_PERCENT",
bodyType: "BOY",
icon: "custom/traveler_anemo.webp:image/webp",
birthday: [0, 0],
route: "Anemo Traveler Boy",
release: 1601244000,
},
{
id: "10000005-geo",
rank: 5,
name: "Traveler Geo",
element: "Rock",
weaponType: "WEAPON_SWORD_ONE_HAND",
region: "MAINACTOR",
specialProp: "FIGHT_PROP_ATTACK_PERCENT",
bodyType: "BOY",
icon: "custom/traveler_geo.webp:image/webp",
birthday: [0, 0],
route: "Geo Traveler Boy",
release: 1601244000,
},
{
id: "10000005-electro",
rank: 5,
name: "Traveler Electro",
element: "Electric",
weaponType: "WEAPON_SWORD_ONE_HAND",
region: "MAINACTOR",
specialProp: "FIGHT_PROP_ATTACK_PERCENT",
bodyType: "BOY",
icon: "custom/traveler_electro.webp:image/webp",
birthday: [0, 0],
route: "Electro Traveler Boy",
release: 1626814800,
},
{
id: "10000005-dendro",
rank: 5,
name: "Traveler Dendro",
element: "Grass",
weaponType: "WEAPON_SWORD_ONE_HAND",
region: "MAINACTOR",
specialProp: "FIGHT_PROP_ATTACK_PERCENT",
bodyType: "BOY",
icon: "custom/traveler_dendro.webp:image/webp",
birthday: [0, 0],
route: "Dendro Traveler Boy",
release: 1657659600,
},
];
async function main() {
console.log("Fetching Amber data...");
@@ -16,71 +105,94 @@ async function main() {
),
]);
await db.transaction(async (tx) => {
console.log("Deleting existing versions...");
await tx.delete(versions);
console.log("Listing dangling CDN images...");
const files = await tx.select({ id: cdn.id }).from(cdn);
const pendingDelete: string[] = [];
for (const file of files) {
if ((await cdnReferences(file.id, tx)).length) continue;
console.log("-", file.id);
pendingDelete.push(file.id);
}
console.log("Deleting dangling CDN images...");
await tx.delete(cdn).where(inArray(cdn.id, pendingDelete));
console.log("Creating versions...");
// console.log("Deleting existing versions...");
// await tx.delete(versions);
console.log("Creating new versions...");
const amberVersions = Object.entries(changelog.data).filter(
(v) => "avatar" in v[1].items,
);
const createdVersions = await tx
.insert(versions)
.values([
{
id: "base",
name: `Base 1.0`,
},
...amberVersions.map(([id, ver], i) => ({
id,
name: `Amber ${ver.version}`,
from: amberVersions[i - 1]?.[0] || "base",
})),
])
.returning({ id: versions.id })
.then((v) => v.map((v) => v.id));
console.log(createdVersions.map((v) => `- ${v}`).join("\n"));
process.stdout.write("Syncing characters...");
const total = Object.keys(avatars.data.items).length * 3;
const existingVersions = (await tx.select().from(versions)).map(
(v) => v.id,
);
const newVersions = [
{
id: "base",
name: `Base 1.0`,
},
...amberVersions.map(([id, ver], i) => ({
id,
name: `Amber ${ver.version}`,
from: amberVersions[i - 1]?.[0] || "base",
})),
].filter((v) => !existingVersions.includes(v.id));
const createdVersions = newVersions.length
? await tx
.insert(versions)
.values(newVersions)
.returning({ id: versions.id })
.then((v) => v.map((v) => v.id))
: [];
console.log(
createdVersions.map((v) => `- ${v}`).join("\n") || "- None created.",
);
process.stdout.write("Calculating characters... ");
const existingChars = await tx
.select({ name: characters.name })
.from(characters)
.then((l) => l.map((c) => c.name));
existingChars.push(...exclude);
const newChars = [...Object.values(avatars.data.items), ...extra].filter(
(c) => !existingChars.includes(c.name),
);
process.stdout.write(
`${newChars.length} new characters to be added.\nSyncing characters...`,
);
const total = Object.keys(newChars).length;
var done = 0;
const pros = [];
function progress<K>(p: K): K {
done++;
done += 1 / 3;
process.stdout.write(
`\rSyncing characters... ${done}/${total} (${Math.floor(
`\rSyncing characters... ${Math.round(done)}/${total} (${Math.floor(
(done / total) * 100,
)}%)`,
);
return p;
}
async function handle(char: Avatar, order: number) {
const customFile = char.icon.match(customImageRegex);
await tx
.insert(characters)
.values({
amber: char.id.toString(),
image: await cdnify(
await fetch(
`https://gi.yatta.moe/assets/UI/${
char.icon || "UI_AvatarIcon_PlayerBoy"
}.png`,
)
.then((r) =>
r.ok
? progress(r)
: Promise.reject(
new Error(`Failed to fetch image for ${char.name}`),
customFile
? new Blob(
[
new Uint8Array(
await readFile(`${__dirname}/img/${customFile[1]}`).then(
progress,
),
),
)
.then(toFile),
{ tx },
],
{
type: customFile[2] || "image/png",
},
)
: await fetch(
`https://gi.yatta.moe/assets/UI/${
char.icon || "UI_AvatarIcon_PlayerBoy"
}.png`,
)
.then((r) =>
r.ok
? progress(r)
: Promise.reject(
new Error(`Failed to fetch image for ${char.name}`),
),
)
.then(toFile),
{ tx, name: customFile?.[1] },
).then(progress),
name: char.name,
vision: AmberElementMap[char.element],
@@ -93,11 +205,12 @@ async function main() {
)?.[0] || "base",
weapon: char.weaponType,
order,
id: char.name.toLowerCase().replace(/ /g, "_"),
})
.onConflictDoNothing();
}
let i = 0;
for (const char of Object.values(avatars.data.items)) {
for (const char of newChars) {
i += 10;
process.stdout.write(
`\rSyncing characters... ${char.name} `,
@@ -105,6 +218,19 @@ async function main() {
pros.push(handle(char, i).then(progress));
}
await Promise.all(pros);
console.log("\nListing dangling CDN images...");
const files = await tx.select({ id: cdn.id }).from(cdn);
const pendingDelete: string[] = [];
for (const file of files) {
if ((await cdnReferences(file.id, tx)).length) continue;
console.log("-", file.id);
pendingDelete.push(file.id);
}
if (!pendingDelete.length) console.log("- Found none.");
console.log("Deleting dangling CDN images...");
await tx.delete(cdn).where(inArray(cdn.id, pendingDelete));
console.log("\nDone!");
});
}
@@ -120,6 +246,8 @@ async function toFile(res: Response) {
);
}
const customImageRegex = /^custom\/([a-z0-9_.-]+)(?::([a-z]+\/[a-z]+))/im;
main();
export interface AvatarApiResponse {