From 6efe2bf5addd1c2db2a72c0d15547f873e1199eb Mon Sep 17 00:00:00 2001 From: nsfisis Date: Mon, 15 Dec 2025 22:37:22 +0900 Subject: feat(anki): add import script for Anki packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add CLI script to import .apkg files into Kioku. The script parses Anki packages using the existing parser, maps notes/cards to Kioku format, and bulk inserts them into the database for a specified user. Usage: pnpm anki:import [file.apkg] 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/server/scripts/import-anki.ts | 117 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 src/server/scripts/import-anki.ts (limited to 'src/server/scripts') diff --git a/src/server/scripts/import-anki.ts b/src/server/scripts/import-anki.ts new file mode 100644 index 0000000..739b485 --- /dev/null +++ b/src/server/scripts/import-anki.ts @@ -0,0 +1,117 @@ +import * as readline from "node:readline/promises"; +import { mapAnkiToKioku, parseAnkiPackage } from "../anki/index.js"; +import { db } from "../db/index.js"; +import { cards, decks } from "../db/schema.js"; +import { userRepository } from "../repositories/index.js"; + +async function main() { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + // Get file path from command line argument or prompt + let filePath = process.argv[2]; + if (!filePath) { + filePath = await rl.question("Anki package path (.apkg): "); + } + + if (!filePath) { + console.error("Error: File path is required"); + process.exit(1); + } + + // Get username + const username = await rl.question("Username: "); + rl.close(); + + if (!username) { + console.error("Error: Username is required"); + process.exit(1); + } + + // Find user + const user = await userRepository.findByUsername(username); + if (!user) { + console.error(`Error: User "${username}" not found`); + process.exit(1); + } + + console.log(`\nParsing Anki package: ${filePath}`); + + // Parse the Anki package + const ankiPackage = await parseAnkiPackage(filePath); + + console.log(`Found ${ankiPackage.decks.length} deck(s)`); + console.log(`Found ${ankiPackage.notes.length} note(s)`); + console.log(`Found ${ankiPackage.cards.length} card(s)`); + + // Convert to Kioku format + const importData = mapAnkiToKioku(ankiPackage); + + if (importData.length === 0) { + console.log("\nNo decks to import (all decks may be empty or skipped)"); + process.exit(0); + } + + console.log(`\nImporting ${importData.length} deck(s):`); + for (const { deck, cards: deckCards } of importData) { + console.log(` - ${deck.name}: ${deckCards.length} card(s)`); + } + + // Import decks and cards + let totalCards = 0; + for (const { deck, cards: kiokuCards } of importData) { + // Create deck + const [newDeck] = await db + .insert(decks) + .values({ + userId: user.id, + name: deck.name, + description: deck.description, + newCardsPerDay: 20, + }) + .returning(); + + if (!newDeck) { + console.error(`Error: Failed to create deck "${deck.name}"`); + continue; + } + + // Create cards in batches + if (kiokuCards.length > 0) { + const cardValues = kiokuCards.map((card) => ({ + deckId: newDeck.id, + front: card.front, + back: card.back, + state: card.state, + due: card.due, + stability: card.stability, + difficulty: card.difficulty, + elapsedDays: card.elapsedDays, + scheduledDays: card.scheduledDays, + reps: card.reps, + lapses: card.lapses, + lastReview: card.lastReview, + })); + + await db.insert(cards).values(cardValues); + totalCards += kiokuCards.length; + } + + console.log( + ` Created deck "${deck.name}" with ${kiokuCards.length} cards`, + ); + } + + console.log(`\nImport complete!`); + console.log(` Decks: ${importData.length}`); + console.log(` Cards: ${totalCards}`); + + process.exit(0); +} + +main().catch((error) => { + console.error("Error:", error.message); + process.exit(1); +}); -- cgit v1.2.3-70-g09d2