jud pai
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 34 KiB |
@@ -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 {
|
||||
|
||||