jud pai
@@ -55,9 +55,13 @@ const columns: ColumnDef<{
|
|||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorFn: (row) => row.name || `[${row.id}]`,
|
accessorKey: "name",
|
||||||
header: "Name",
|
header: "Name",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "id",
|
||||||
|
header: "ID",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
accessorFn: (row) => b2s(Number(row.size)),
|
accessorFn: (row) => b2s(Number(row.size)),
|
||||||
header: "Size",
|
header: "Size",
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ export async function cdnReferences(
|
|||||||
.select({ id: table.id })
|
.select({ id: table.id })
|
||||||
.from(table)
|
.from(table)
|
||||||
.where(
|
.where(
|
||||||
and(
|
or(
|
||||||
...cols.map((c) =>
|
...cols.map((c) =>
|
||||||
eq(table[c as keyof typeof table._.columns], id),
|
eq(table[c as keyof typeof table._.columns], id),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const characters = pgTable("characters", {
|
|||||||
vision: element().notNull(),
|
vision: element().notNull(),
|
||||||
image: text()
|
image: text()
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => cdn.id, { onDelete: "cascade", onUpdate: "cascade" }),
|
.references(() => cdn.id, { onUpdate: "cascade" }),
|
||||||
weapon: text().notNull(),
|
weapon: text().notNull(),
|
||||||
amber: text().notNull(), // Amber character ID
|
amber: text().notNull(), // Amber character ID
|
||||||
order: integer().notNull(),
|
order: integer().notNull(),
|
||||||
@@ -62,7 +62,7 @@ export const versions = pgTable("versions", {
|
|||||||
id: text().primaryKey(), // LI
|
id: text().primaryKey(), // LI
|
||||||
name: text().notNull(), // LunaI
|
name: text().notNull(), // LunaI
|
||||||
from: text().references((): AnyPgColumn => versions.id, {
|
from: text().references((): AnyPgColumn => versions.id, {
|
||||||
onDelete: "cascade",
|
onDelete: "set null",
|
||||||
onUpdate: "cascade",
|
onUpdate: "cascade",
|
||||||
}),
|
}),
|
||||||
// deprecate: date().notNull() // 21/03/2077
|
// deprecate: date().notNull() // 21/03/2077
|
||||||
@@ -108,7 +108,7 @@ export const tierlistTypes = tierlist.table("types", {
|
|||||||
id: text().primaryKey().$defaultFn(uuidv7),
|
id: text().primaryKey().$defaultFn(uuidv7),
|
||||||
name: text().notNull(),
|
name: text().notNull(),
|
||||||
image: text().references(() => cdn.id, {
|
image: text().references(() => cdn.id, {
|
||||||
onDelete: "cascade",
|
onDelete: "set null",
|
||||||
onUpdate: "cascade",
|
onUpdate: "cascade",
|
||||||
}),
|
}),
|
||||||
order: integer().notNull(),
|
order: integer().notNull(),
|
||||||
@@ -119,12 +119,12 @@ export const tierlistTiers = tierlist.table("tiers", {
|
|||||||
name: text().notNull(),
|
name: text().notNull(),
|
||||||
badges: text()
|
badges: text()
|
||||||
.references(() => tierlistBadges.id, {
|
.references(() => tierlistBadges.id, {
|
||||||
onDelete: "cascade",
|
onDelete: "set null",
|
||||||
onUpdate: "cascade",
|
onUpdate: "cascade",
|
||||||
})
|
})
|
||||||
.array(),
|
.array(),
|
||||||
image: text().references(() => cdn.id, {
|
image: text().references(() => cdn.id, {
|
||||||
onDelete: "cascade",
|
onDelete: "set null",
|
||||||
onUpdate: "cascade",
|
onUpdate: "cascade",
|
||||||
}),
|
}),
|
||||||
order: integer().notNull(),
|
order: integer().notNull(),
|
||||||
@@ -134,7 +134,7 @@ export const tierlistColumns = tierlist.table("columns", {
|
|||||||
id: text().primaryKey().$defaultFn(uuidv7),
|
id: text().primaryKey().$defaultFn(uuidv7),
|
||||||
name: text().notNull(),
|
name: text().notNull(),
|
||||||
image: text().references(() => cdn.id, {
|
image: text().references(() => cdn.id, {
|
||||||
onDelete: "cascade",
|
onDelete: "set null",
|
||||||
onUpdate: "cascade",
|
onUpdate: "cascade",
|
||||||
}),
|
}),
|
||||||
order: integer().notNull(),
|
order: integer().notNull(),
|
||||||
@@ -144,7 +144,7 @@ export const tierlistBadges = tierlist.table("badges", {
|
|||||||
id: text().primaryKey().$defaultFn(uuidv7),
|
id: text().primaryKey().$defaultFn(uuidv7),
|
||||||
name: text().notNull(),
|
name: text().notNull(),
|
||||||
image: text().references(() => cdn.id, {
|
image: text().references(() => cdn.id, {
|
||||||
onDelete: "cascade",
|
onDelete: "set null",
|
||||||
onUpdate: "cascade",
|
onUpdate: "cascade",
|
||||||
}),
|
}),
|
||||||
order: integer().notNull(),
|
order: integer().notNull(),
|
||||||
@@ -161,11 +161,11 @@ export const tierlistVersions = tierlist.table("versions", {
|
|||||||
onUpdate: "cascade",
|
onUpdate: "cascade",
|
||||||
}),
|
}),
|
||||||
image: text().references(() => cdn.id, {
|
image: text().references(() => cdn.id, {
|
||||||
onDelete: "cascade",
|
onDelete: "set null",
|
||||||
onUpdate: "cascade",
|
onUpdate: "cascade",
|
||||||
}),
|
}),
|
||||||
disclaimer: text().references(() => cdn.id, {
|
disclaimer: text().references(() => cdn.id, {
|
||||||
onDelete: "cascade",
|
onDelete: "set null",
|
||||||
onUpdate: "cascade",
|
onUpdate: "cascade",
|
||||||
}),
|
}),
|
||||||
deprecates: text().notNull(),
|
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 { AmberElementMap } from "@/lib/const";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { cdn, characters, versions } from "@/lib/db/schema";
|
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() {
|
async function main() {
|
||||||
console.log("Fetching Amber data...");
|
console.log("Fetching Amber data...");
|
||||||
@@ -16,71 +105,94 @@ async function main() {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
console.log("Deleting existing versions...");
|
// console.log("Deleting existing versions...");
|
||||||
await tx.delete(versions);
|
// await tx.delete(versions);
|
||||||
console.log("Listing dangling CDN images...");
|
console.log("Creating new versions...");
|
||||||
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...");
|
|
||||||
const amberVersions = Object.entries(changelog.data).filter(
|
const amberVersions = Object.entries(changelog.data).filter(
|
||||||
(v) => "avatar" in v[1].items,
|
(v) => "avatar" in v[1].items,
|
||||||
);
|
);
|
||||||
const createdVersions = await tx
|
const existingVersions = (await tx.select().from(versions)).map(
|
||||||
.insert(versions)
|
(v) => v.id,
|
||||||
.values([
|
);
|
||||||
{
|
const newVersions = [
|
||||||
id: "base",
|
{
|
||||||
name: `Base 1.0`,
|
id: "base",
|
||||||
},
|
name: `Base 1.0`,
|
||||||
...amberVersions.map(([id, ver], i) => ({
|
},
|
||||||
id,
|
...amberVersions.map(([id, ver], i) => ({
|
||||||
name: `Amber ${ver.version}`,
|
id,
|
||||||
from: amberVersions[i - 1]?.[0] || "base",
|
name: `Amber ${ver.version}`,
|
||||||
})),
|
from: amberVersions[i - 1]?.[0] || "base",
|
||||||
])
|
})),
|
||||||
.returning({ id: versions.id })
|
].filter((v) => !existingVersions.includes(v.id));
|
||||||
.then((v) => v.map((v) => v.id));
|
const createdVersions = newVersions.length
|
||||||
console.log(createdVersions.map((v) => `- ${v}`).join("\n"));
|
? await tx
|
||||||
process.stdout.write("Syncing characters...");
|
.insert(versions)
|
||||||
const total = Object.keys(avatars.data.items).length * 3;
|
.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;
|
var done = 0;
|
||||||
const pros = [];
|
const pros = [];
|
||||||
function progress<K>(p: K): K {
|
function progress<K>(p: K): K {
|
||||||
done++;
|
done += 1 / 3;
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
`\rSyncing characters... ${done}/${total} (${Math.floor(
|
`\rSyncing characters... ${Math.round(done)}/${total} (${Math.floor(
|
||||||
(done / total) * 100,
|
(done / total) * 100,
|
||||||
)}%)`,
|
)}%)`,
|
||||||
);
|
);
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
async function handle(char: Avatar, order: number) {
|
async function handle(char: Avatar, order: number) {
|
||||||
|
const customFile = char.icon.match(customImageRegex);
|
||||||
await tx
|
await tx
|
||||||
.insert(characters)
|
.insert(characters)
|
||||||
.values({
|
.values({
|
||||||
amber: char.id.toString(),
|
amber: char.id.toString(),
|
||||||
image: await cdnify(
|
image: await cdnify(
|
||||||
await fetch(
|
customFile
|
||||||
`https://gi.yatta.moe/assets/UI/${
|
? new Blob(
|
||||||
char.icon || "UI_AvatarIcon_PlayerBoy"
|
[
|
||||||
}.png`,
|
new Uint8Array(
|
||||||
)
|
await readFile(`${__dirname}/img/${customFile[1]}`).then(
|
||||||
.then((r) =>
|
progress,
|
||||||
r.ok
|
),
|
||||||
? progress(r)
|
|
||||||
: Promise.reject(
|
|
||||||
new Error(`Failed to fetch image for ${char.name}`),
|
|
||||||
),
|
),
|
||||||
)
|
],
|
||||||
.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),
|
).then(progress),
|
||||||
name: char.name,
|
name: char.name,
|
||||||
vision: AmberElementMap[char.element],
|
vision: AmberElementMap[char.element],
|
||||||
@@ -93,11 +205,12 @@ async function main() {
|
|||||||
)?.[0] || "base",
|
)?.[0] || "base",
|
||||||
weapon: char.weaponType,
|
weapon: char.weaponType,
|
||||||
order,
|
order,
|
||||||
|
id: char.name.toLowerCase().replace(/ /g, "_"),
|
||||||
})
|
})
|
||||||
.onConflictDoNothing();
|
.onConflictDoNothing();
|
||||||
}
|
}
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (const char of Object.values(avatars.data.items)) {
|
for (const char of newChars) {
|
||||||
i += 10;
|
i += 10;
|
||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
`\rSyncing characters... ${char.name} `,
|
`\rSyncing characters... ${char.name} `,
|
||||||
@@ -105,6 +218,19 @@ async function main() {
|
|||||||
pros.push(handle(char, i).then(progress));
|
pros.push(handle(char, i).then(progress));
|
||||||
}
|
}
|
||||||
await Promise.all(pros);
|
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!");
|
console.log("\nDone!");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -120,6 +246,8 @@ async function toFile(res: Response) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const customImageRegex = /^custom\/([a-z0-9_.-]+)(?::([a-z]+\/[a-z]+))/im;
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|
||||||
export interface AvatarApiResponse {
|
export interface AvatarApiResponse {
|
||||||
|
|||||||