feat: add translated names for Devil Fruits and update related schemas
Build Docker Image / build (push) Successful in 1m33s
Build Docker Image / build (push) Successful in 1m33s
- Added support for translated names and French translated names in the DevilFruitRecord and Character interfaces. - Updated JSON import/export scripts to handle new translated name fields. - Enhanced scraping logic to extract translated names from the source. - Modified database schema to include translated names for Devil Fruits. - Updated daily fruit selection to include translated names for characters.
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE `devil_fruit` ADD `translated_name` text;--> statement-breakpoint
|
||||||
|
ALTER TABLE `devil_fruit` ADD `fr_translated_name` text;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,13 @@
|
|||||||
"when": 1782152097346,
|
"when": 1782152097346,
|
||||||
"tag": "0004_magenta_screwball",
|
"tag": "0004_magenta_screwball",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 5,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1783545248600,
|
||||||
|
"tag": "0005_uneven_talkback",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,8 @@ type ArcRecord = {
|
|||||||
type DevilFruitRecord = {
|
type DevilFruitRecord = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
translatedName?: string | null;
|
||||||
|
frTranslatedName?: string | null;
|
||||||
type?: DevilFruitType | string | null;
|
type?: DevilFruitType | string | null;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
};
|
};
|
||||||
@@ -94,7 +96,7 @@ function toJsonArray(value: string[] | string | null | undefined): string[] | nu
|
|||||||
|
|
||||||
function toDevilFruitType(value: DevilFruitType | string | null | undefined): DevilFruitType | null {
|
function toDevilFruitType(value: DevilFruitType | string | null | undefined): DevilFruitType | null {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
if (value === 'Paramecia' || value === 'Zoan' || value === 'Logia' || value === 'Smile' || value === 'Unknown') {
|
if (value === 'Paramecia' || value === 'Zoan' || value === 'Logia' || value === 'Unknown') {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
return 'Unknown';
|
return 'Unknown';
|
||||||
@@ -229,6 +231,8 @@ async function importFromJson(): Promise<void> {
|
|||||||
.values({
|
.values({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
|
translatedName: toNullable(item.translatedName),
|
||||||
|
frTranslatedName: toNullable(item.frTranslatedName),
|
||||||
type: toDevilFruitType(item.type),
|
type: toDevilFruitType(item.type),
|
||||||
url: toNullable(item.url)
|
url: toNullable(item.url)
|
||||||
})
|
})
|
||||||
@@ -236,6 +240,8 @@ async function importFromJson(): Promise<void> {
|
|||||||
target: devilFruit.id,
|
target: devilFruit.id,
|
||||||
set: {
|
set: {
|
||||||
name: item.name,
|
name: item.name,
|
||||||
|
translatedName: toNullable(item.translatedName),
|
||||||
|
frTranslatedName: toNullable(item.frTranslatedName),
|
||||||
type: toDevilFruitType(item.type),
|
type: toDevilFruitType(item.type),
|
||||||
url: toNullable(item.url)
|
url: toNullable(item.url)
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-16
@@ -23,6 +23,8 @@ interface Character {
|
|||||||
frOrigin: string | null;
|
frOrigin: string | null;
|
||||||
devilFruitId: string | null;
|
devilFruitId: string | null;
|
||||||
devilFruitName: string | null;
|
devilFruitName: string | null;
|
||||||
|
devilFruitTranslatedName: string | null;
|
||||||
|
frDevilFruitTranslatedName: string | null;
|
||||||
devilFruitUrl: string | null;
|
devilFruitUrl: string | null;
|
||||||
affiliation: string | null;
|
affiliation: string | null;
|
||||||
frAffiliation: string | null;
|
frAffiliation: string | null;
|
||||||
@@ -49,12 +51,16 @@ interface CharacterListItem {
|
|||||||
interface DevilFruitData {
|
interface DevilFruitData {
|
||||||
devilFruitId: string;
|
devilFruitId: string;
|
||||||
devilFruitTitle: string;
|
devilFruitTitle: string;
|
||||||
|
devilFruitTranslatedName?: string | null;
|
||||||
|
frDevilFruitTranslatedName?: string | null;
|
||||||
devilFruitUrl: string;
|
devilFruitUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DevilFruit {
|
interface DevilFruit {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
translatedName?: string | null;
|
||||||
|
frTranslatedName?: string | null;
|
||||||
type: string | null;
|
type: string | null;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
@@ -173,17 +179,6 @@ function removeParentheticalNotes(text: string): string {
|
|||||||
return text.replace(/\([^)]*\)/g, '').trim();
|
return text.replace(/\([^)]*\)/g, '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the French link from the API response links array
|
|
||||||
*/
|
|
||||||
|
|
||||||
function getFrLink(links: { lang: string; ['*']: string; url: string }[]): { url: string } | null {
|
|
||||||
// Get french url by getting parse.langlinks where lang is "fr" and extract the name from there
|
|
||||||
const frLink = links.find(
|
|
||||||
(link: { lang: string; ['*']: string; url: string }) => link.lang === 'fr'
|
|
||||||
);
|
|
||||||
return frLink ? { url: frLink['url'] } : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize string by decoding URI components, punctuation, and replacing spaces with underscores
|
* Normalize string by decoding URI components, punctuation, and replacing spaces with underscores
|
||||||
@@ -485,9 +480,10 @@ async function fetchCharacter(
|
|||||||
const epithets = extractEpithets($);
|
const epithets = extractEpithets($);
|
||||||
|
|
||||||
// Extract devil fruit
|
// Extract devil fruit
|
||||||
const devilFruitData = await extractDevilFruit($);
|
const devilFruitData = await extractDevilFruit($, 'en');
|
||||||
const devilFruitId = devilFruitData?.devilFruitId || null;
|
const devilFruitId = devilFruitData?.devilFruitId || null;
|
||||||
const devilFruitName = devilFruitData?.devilFruitTitle || null;
|
const devilFruitName = devilFruitData?.devilFruitTitle || null;
|
||||||
|
const devilFruitTranslatedName = devilFruitData?.devilFruitTranslatedName || null;
|
||||||
const devilFruitUrl = devilFruitData?.devilFruitUrl || null;
|
const devilFruitUrl = devilFruitData?.devilFruitUrl || null;
|
||||||
|
|
||||||
// Extract haki from JSON categories
|
// Extract haki from JSON categories
|
||||||
@@ -546,6 +542,8 @@ async function fetchCharacter(
|
|||||||
|
|
||||||
const frOrigin = frPage ? extractOrigin(frPage) : null;
|
const frOrigin = frPage ? extractOrigin(frPage) : null;
|
||||||
|
|
||||||
|
const frDevilFruitTranslatedName = frPage ? (await extractDevilFruit(frPage, 'fr'))?.devilFruitTranslatedName || null : null;
|
||||||
|
|
||||||
if (name !== jsonData.parse?.title) {
|
if (name !== jsonData.parse?.title) {
|
||||||
frName = name;
|
frName = name;
|
||||||
}
|
}
|
||||||
@@ -563,6 +561,8 @@ async function fetchCharacter(
|
|||||||
frOrigin,
|
frOrigin,
|
||||||
devilFruitId,
|
devilFruitId,
|
||||||
devilFruitName,
|
devilFruitName,
|
||||||
|
devilFruitTranslatedName,
|
||||||
|
frDevilFruitTranslatedName,
|
||||||
devilFruitUrl,
|
devilFruitUrl,
|
||||||
affiliation,
|
affiliation,
|
||||||
frAffiliation,
|
frAffiliation,
|
||||||
@@ -703,7 +703,7 @@ function extractEpithets($: cheerio.CheerioAPI): string[] {
|
|||||||
* Extract devil fruit from infobox
|
* Extract devil fruit from infobox
|
||||||
* Returns both normalized ID and URL
|
* Returns both normalized ID and URL
|
||||||
*/
|
*/
|
||||||
async function extractDevilFruit($: cheerio.CheerioAPI): Promise<DevilFruitData | null> {
|
async function extractDevilFruit($: cheerio.CheerioAPI, lang: 'en' | 'fr'): Promise<DevilFruitData | null> {
|
||||||
// dfname or dfename are used as data-source for devil fruit in the infobox, we check both to be safe
|
// dfname or dfename are used as data-source for devil fruit in the infobox, we check both to be safe
|
||||||
const fruit = $('[data-source="dfname"] .pi-data-value, [data-source="dfename"] .pi-data-value').first();
|
const fruit = $('[data-source="dfname"] .pi-data-value, [data-source="dfename"] .pi-data-value').first();
|
||||||
const link = fruit.find('a').first();
|
const link = fruit.find('a').first();
|
||||||
@@ -712,6 +712,20 @@ async function extractDevilFruit($: cheerio.CheerioAPI): Promise<DevilFruitData
|
|||||||
let fruitTitle = fruit.text().trim().toLowerCase().includes('smile') ? fruit.text().trim() : link.text().trim();
|
let fruitTitle = fruit.text().trim().toLowerCase().includes('smile') ? fruit.text().trim() : link.text().trim();
|
||||||
if (!fruitTitle) return null;
|
if (!fruitTitle) return null;
|
||||||
|
|
||||||
|
let translatedName = null;
|
||||||
|
|
||||||
|
if (lang === 'en') {
|
||||||
|
const translatedNameBox = $('[data-source="dfename"] .pi-data-value').first();
|
||||||
|
if (translatedNameBox.length > 0) {
|
||||||
|
translatedName = translatedNameBox.text().trim();
|
||||||
|
}
|
||||||
|
} else if (lang === 'fr') {
|
||||||
|
const translatedNameBox = $('[data-source="dfnomf"] .pi-data-value').first();
|
||||||
|
if (translatedNameBox.length > 0) {
|
||||||
|
translatedName = translatedNameBox.text().trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const href = link.attr('href');
|
const href = link.attr('href');
|
||||||
if (!href || !href.startsWith('/wiki/')) return null;
|
if (!href || !href.startsWith('/wiki/')) return null;
|
||||||
|
|
||||||
@@ -727,6 +741,7 @@ async function extractDevilFruit($: cheerio.CheerioAPI): Promise<DevilFruitData
|
|||||||
return {
|
return {
|
||||||
devilFruitId: normalizeId(fruitTitle),
|
devilFruitId: normalizeId(fruitTitle),
|
||||||
devilFruitTitle: fruitTitle,
|
devilFruitTitle: fruitTitle,
|
||||||
|
devilFruitTranslatedName: translatedName || null,
|
||||||
devilFruitUrl: cleanUrl
|
devilFruitUrl: cleanUrl
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -918,6 +933,8 @@ async function saveToCSV(characters: Character[]): Promise<void> {
|
|||||||
async function fetchDevilFruit(
|
async function fetchDevilFruit(
|
||||||
devilFruitUrl: string,
|
devilFruitUrl: string,
|
||||||
devilFruitTitle: string,
|
devilFruitTitle: string,
|
||||||
|
devilFruitTranslatedName: string | null,
|
||||||
|
frdevilFruitTranslatedName: string | null,
|
||||||
devilFruitId: string
|
devilFruitId: string
|
||||||
): Promise<DevilFruit | null> {
|
): Promise<DevilFruit | null> {
|
||||||
try {
|
try {
|
||||||
@@ -956,6 +973,8 @@ async function fetchDevilFruit(
|
|||||||
return {
|
return {
|
||||||
id: devilFruitId,
|
id: devilFruitId,
|
||||||
name: devilFruitTitle.toLowerCase().includes('smile') ? devilFruitTitle : name,
|
name: devilFruitTitle.toLowerCase().includes('smile') ? devilFruitTitle : name,
|
||||||
|
translatedName: devilFruitTranslatedName || null,
|
||||||
|
frTranslatedName: frdevilFruitTranslatedName || null,
|
||||||
type,
|
type,
|
||||||
url: devilFruitUrl
|
url: devilFruitUrl
|
||||||
};
|
};
|
||||||
@@ -984,6 +1003,8 @@ async function saveDevilFruitsToCSV(devilFruits: DevilFruit[]): Promise<void> {
|
|||||||
header: [
|
header: [
|
||||||
{ id: 'id', title: 'ID' },
|
{ id: 'id', title: 'ID' },
|
||||||
{ id: 'name', title: 'Name' },
|
{ id: 'name', title: 'Name' },
|
||||||
|
{ id: 'translatedName', title: 'Translated Name' },
|
||||||
|
{ id: 'frTranslatedName', title: 'Translated Name (FR)' },
|
||||||
{ id: 'type', title: 'Type' },
|
{ id: 'type', title: 'Type' },
|
||||||
{ id: 'url', title: 'URL' }
|
{ id: 'url', title: 'URL' }
|
||||||
]
|
]
|
||||||
@@ -994,6 +1015,8 @@ async function saveDevilFruitsToCSV(devilFruits: DevilFruit[]): Promise<void> {
|
|||||||
.map((df) => ({
|
.map((df) => ({
|
||||||
id: df.id || '',
|
id: df.id || '',
|
||||||
name: df.name || '',
|
name: df.name || '',
|
||||||
|
translatedName: df.translatedName || '',
|
||||||
|
frTranslatedName: df.frTranslatedName || '',
|
||||||
type: df.type || '',
|
type: df.type || '',
|
||||||
url: df.url || ''
|
url: df.url || ''
|
||||||
}));
|
}));
|
||||||
@@ -1056,7 +1079,9 @@ async function main(): Promise<void> {
|
|||||||
const name = c.devilFruitName!;
|
const name = c.devilFruitName!;
|
||||||
const url = c.devilFruitUrl!;
|
const url = c.devilFruitUrl!;
|
||||||
const id = normalizeId(name);
|
const id = normalizeId(name);
|
||||||
return [`${normalizeId(url)}::${id}`, { url, name, id }] as const;
|
const translatedName = c.devilFruitTranslatedName || null;
|
||||||
|
const frTranslatedName = c.frDevilFruitTranslatedName || null;
|
||||||
|
return [`${normalizeId(url)}::${id}`, { url, name, id, translatedName, frTranslatedName }] as const;
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.values()
|
.values()
|
||||||
@@ -1064,7 +1089,7 @@ async function main(): Promise<void> {
|
|||||||
console.log(`✓ Found ${devilFruitEntries.length} unique devil fruits\n`);
|
console.log(`✓ Found ${devilFruitEntries.length} unique devil fruits\n`);
|
||||||
|
|
||||||
// Step 3: Scraping Devil Fruits
|
// Step 3: Scraping Devil Fruits
|
||||||
console.log('=== Step 2: Scraping Devil Fruits ===\n');
|
console.log('=== Step 3: Scraping Devil Fruits ===\n');
|
||||||
|
|
||||||
if (devilFruitEntries.length === 0) {
|
if (devilFruitEntries.length === 0) {
|
||||||
console.warn('No devil fruits found from characters, skipping...\n');
|
console.warn('No devil fruits found from characters, skipping...\n');
|
||||||
@@ -1075,7 +1100,7 @@ async function main(): Promise<void> {
|
|||||||
const batch = devilFruitEntries.slice(i, i + FETCH_CONCURRENCY);
|
const batch = devilFruitEntries.slice(i, i + FETCH_CONCURRENCY);
|
||||||
const batchResults = await Promise.all(
|
const batchResults = await Promise.all(
|
||||||
batch.map(async (entry) => {
|
batch.map(async (entry) => {
|
||||||
const data = await fetchDevilFruit(entry.url, entry.name, entry.id);
|
const data = await fetchDevilFruit(entry.url, entry.name, entry.translatedName, entry.frTranslatedName, entry.id);
|
||||||
return { entry, data };
|
return { entry, data };
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import { eq, and } from 'drizzle-orm';
|
|||||||
const characterWithFruitSelect = {
|
const characterWithFruitSelect = {
|
||||||
id: character.id,
|
id: character.id,
|
||||||
name: character.name,
|
name: character.name,
|
||||||
|
frName: character.frName,
|
||||||
|
translatedName: character.translatedName,
|
||||||
|
frTranslatedName: character.frTranslatedName,
|
||||||
devilFruitId: character.devilFruitId,
|
devilFruitId: character.devilFruitId,
|
||||||
devilFruitName: devilFruit.name,
|
devilFruitName: devilFruit.name,
|
||||||
devilFruitType: devilFruit.type,
|
devilFruitType: devilFruit.type,
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ export type Arc = InferSelectModel<typeof arc>;
|
|||||||
export const devilFruit = sqliteTable('devil_fruit', {
|
export const devilFruit = sqliteTable('devil_fruit', {
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
name: text('name').notNull().unique(),
|
name: text('name').notNull().unique(),
|
||||||
|
translatedName: text('translated_name'),
|
||||||
|
frTranslatedName: text('fr_translated_name'),
|
||||||
type: text('type').$type<DevilFruitType>(),
|
type: text('type').$type<DevilFruitType>(),
|
||||||
url: text('url')
|
url: text('url')
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user