|
|
|
|
@@ -7,7 +7,6 @@ export const syncWithWaniKani = async (user) => {
|
|
|
|
|
|
|
|
|
|
console.log(`Starting sync for user: ${user._id}`);
|
|
|
|
|
|
|
|
|
|
// 1. Fetch all assigned Kanji Subject IDs
|
|
|
|
|
let allSubjectIds = [];
|
|
|
|
|
let nextUrl = 'https://api.wanikani.com/v2/assignments?subject_types=kanji&started=true';
|
|
|
|
|
|
|
|
|
|
@@ -26,26 +25,22 @@ export const syncWithWaniKani = async (user) => {
|
|
|
|
|
|
|
|
|
|
if (allSubjectIds.length === 0) return { count: 0, message: "No unlocked kanji found." };
|
|
|
|
|
|
|
|
|
|
// 2. Filter out items we already have
|
|
|
|
|
const existingItems = await StudyItem.find({ userId: user._id }).select('wkSubjectId');
|
|
|
|
|
const existingIds = new Set(existingItems.map(i => i.wkSubjectId));
|
|
|
|
|
const newIds = allSubjectIds.filter(id => !existingIds.has(id));
|
|
|
|
|
|
|
|
|
|
console.log(`Found ${newIds.length} new items.`);
|
|
|
|
|
|
|
|
|
|
// 3. Process in chunks
|
|
|
|
|
const CHUNK_SIZE = 50; // Reduced chunk size to accommodate secondary radical fetches
|
|
|
|
|
const CHUNK_SIZE = 50;
|
|
|
|
|
for (let i = 0; i < newIds.length; i += CHUNK_SIZE) {
|
|
|
|
|
const chunk = newIds.slice(i, i + CHUNK_SIZE);
|
|
|
|
|
|
|
|
|
|
// A. Fetch Kanji Details
|
|
|
|
|
const subRes = await fetch(`https://api.wanikani.com/v2/subjects?ids=${chunk.join(',')}`, {
|
|
|
|
|
headers: { Authorization: `Bearer ${apiKey}` }
|
|
|
|
|
});
|
|
|
|
|
const subJson = await subRes.json();
|
|
|
|
|
const kanjiDataList = subJson.data;
|
|
|
|
|
|
|
|
|
|
// B. Identify all needed Radicals
|
|
|
|
|
const radicalIdsToFetch = new Set();
|
|
|
|
|
kanjiDataList.forEach(k => {
|
|
|
|
|
if (k.data.component_subject_ids) {
|
|
|
|
|
@@ -53,11 +48,9 @@ export const syncWithWaniKani = async (user) => {
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// C. Fetch Radical Details (if any)
|
|
|
|
|
const radicalMap = new Map();
|
|
|
|
|
if (radicalIdsToFetch.size > 0) {
|
|
|
|
|
const rIds = Array.from(radicalIdsToFetch);
|
|
|
|
|
// Fetch radicals in sub-chunks if necessary (max 100 per req)
|
|
|
|
|
for (let j = 0; j < rIds.length; j += 100) {
|
|
|
|
|
const rChunk = rIds.slice(j, j + 100);
|
|
|
|
|
const rRes = await fetch(`https://api.wanikani.com/v2/subjects?ids=${rChunk.join(',')}`, {
|
|
|
|
|
@@ -68,7 +61,6 @@ export const syncWithWaniKani = async (user) => {
|
|
|
|
|
rJson.data.forEach(r => {
|
|
|
|
|
const primaryMeaning = r.data.meanings.find(m => m.primary)?.meaning || 'Unknown';
|
|
|
|
|
const char = r.data.characters;
|
|
|
|
|
// Find SVG image if no character exists
|
|
|
|
|
const image = !char && r.data.character_images
|
|
|
|
|
? r.data.character_images.find(img => img.content_type === 'image/svg+xml' && !img.metadata.inline_styles)?.url
|
|
|
|
|
: null;
|
|
|
|
|
@@ -82,14 +74,12 @@ export const syncWithWaniKani = async (user) => {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// D. Build Database Objects
|
|
|
|
|
const operations = kanjiDataList.map(d => {
|
|
|
|
|
const readings = d.data.readings || [];
|
|
|
|
|
|
|
|
|
|
// Map the IDs to our fetched radical data
|
|
|
|
|
const itemRadicals = (d.data.component_subject_ids || [])
|
|
|
|
|
.map(rid => radicalMap.get(rid))
|
|
|
|
|
.filter(Boolean); // Remove any not found
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
userId: user._id,
|
|
|
|
|
@@ -97,12 +87,12 @@ export const syncWithWaniKani = async (user) => {
|
|
|
|
|
char: d.data.characters,
|
|
|
|
|
meaning: d.data.meanings.find(m => m.primary)?.meaning || 'Unknown',
|
|
|
|
|
level: d.data.level,
|
|
|
|
|
srsLevel: 1,
|
|
|
|
|
srsLevel: 0,
|
|
|
|
|
nextReview: Date.now(),
|
|
|
|
|
onyomi: readings.filter(r => r.type === 'onyomi').map(r => r.reading),
|
|
|
|
|
kunyomi: readings.filter(r => r.type === 'kunyomi').map(r => r.reading),
|
|
|
|
|
nanori: readings.filter(r => r.type === 'nanori').map(r => r.reading),
|
|
|
|
|
radicals: itemRadicals, // Save them
|
|
|
|
|
radicals: itemRadicals,
|
|
|
|
|
stats: { correct: 0, total: 0 }
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|