From 310ad64d1ad0b6e30a7dfaab0344dd78cabae463 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sat, 2 Aug 2025 15:08:33 +0300 Subject: also export reverse cards via apkg --- internal/anki/apkg_generator.go | 84 +++++++++++++++++++++++++++++++----- internal/processor/processor.go | 47 ++++++++++++++++---- internal/processor/processor_test.go | 27 ++++++++---- 3 files changed, 130 insertions(+), 28 deletions(-) (limited to 'internal') diff --git a/internal/anki/apkg_generator.go b/internal/anki/apkg_generator.go index 0f74ba1..17ff608 100644 --- a/internal/anki/apkg_generator.go +++ b/internal/anki/apkg_generator.go @@ -321,13 +321,13 @@ func (g *APKGGenerator) insertCollection(db *sql.DB) error { func (g *APKGGenerator) createNoteTypeConfig() map[string]interface{} { return map[string]interface{}{ "id": g.modelID, - "name": "Vocabulary from TotalRecall", + "name": "Vocabulary from TotalRecall (Basic + Reverse)", "type": 0, "mod": time.Now().Unix(), "usn": -1, "sortf": 0, "did": g.deckID, - "req": [][]interface{}{[]interface{}{0, "all", []int{0}}}, + "req": [][]interface{}{[]interface{}{0, "all", []int{0}}, []interface{}{1, "all", []int{1}}}, "vers": []int{}, "tags": []string{}, "latexPre": `\documentclass[12pt]{article} @@ -387,7 +387,7 @@ func (g *APKGGenerator) createNoteTypeConfig() map[string]interface{} { }, "tmpls": []map[string]interface{}{ { - "name": "Card 1", + "name": "Forward", "ord": 0, "qfmt": g.getFrontTemplate(), "afmt": g.getBackTemplate(), @@ -395,6 +395,15 @@ func (g *APKGGenerator) createNoteTypeConfig() map[string]interface{} { "bqfmt": "", "bafmt": "", }, + { + "name": "Reverse", + "ord": 1, + "qfmt": g.getReverseFrontTemplate(), + "afmt": g.getReverseBackTemplate(), + "did": nil, + "bqfmt": "", + "bafmt": "", + }, }, "css": g.getCSS(), } @@ -429,6 +438,35 @@ func (g *APKGGenerator) getBackTemplate() string { ` } +// getReverseFrontTemplate returns the question template for the reverse card +func (g *APKGGenerator) getReverseFrontTemplate() string { + return `
+
{{Bulgarian}}
+
` +} + +// getReverseBackTemplate returns the answer template for the reverse card +func (g *APKGGenerator) getReverseBackTemplate() string { + return `{{FrontSide}} + +
+ +
+
{{English}}
+{{#Image}} +
+{{Image}} +
+{{/Image}} +{{#Audio}} +
{{Audio}}
+{{/Audio}} +{{#Notes}} +
{{Notes}}
+{{/Notes}} +
` +} + // getCSS returns the card styling func (g *APKGGenerator) getCSS() string { return `.card { @@ -492,9 +530,10 @@ func (g *APKGGenerator) insertNotesAndCards(db *sql.DB) error { now := time.Now() for i, card := range g.cards { - // Generate unique IDs - noteID := now.UnixMilli() + int64(i*2) - cardID := noteID + 1 + // Generate unique IDs, leaving space for 2 cards per note + noteID := now.UnixMilli() + int64(i*3) + cardID1 := noteID + 1 + cardID2 := noteID + 2 // Prepare field values english := card.Translation @@ -561,13 +600,13 @@ func (g *APKGGenerator) insertNotesAndCards(db *sql.DB) error { return fmt.Errorf("failed to insert note: %w", err) } - // Insert card + // Insert card 1 (Forward) cardQuery := `INSERT INTO cards VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` _, err = db.Exec(cardQuery, - cardID, // id + cardID1, // id noteID, // nid g.deckID, // did - 0, // ord + 0, // ord (template 0) now.Unix(), // mod -1, // usn 0, // type (0=new) @@ -584,7 +623,32 @@ func (g *APKGGenerator) insertNotesAndCards(db *sql.DB) error { "", // data ) if err != nil { - return fmt.Errorf("failed to insert card: %w", err) + return fmt.Errorf("failed to insert forward card: %w", err) + } + + // Insert card 2 (Reverse) + _, err = db.Exec(cardQuery, + cardID2, // id + noteID, // nid + g.deckID, // did + 1, // ord (template 1) + now.Unix(), // mod + -1, // usn + 0, // type (0=new) + 0, // queue (0=new) + noteID+1, // due (for new cards, this is position, should be unique) + 0, // ivl + 0, // factor + 0, // reps + 0, // lapses + 0, // left + 0, // odue + 0, // odid + 0, // flags + "", // data + ) + if err != nil { + return fmt.Errorf("failed to insert reverse card: %w", err) } } diff --git a/internal/processor/processor.go b/internal/processor/processor.go index 1e1c20f..4bb6b0a 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -415,16 +415,45 @@ func (p *Processor) GenerateAnkiFile() (string, error) { AudioFormat: p.flags.AudioFormat, }) - // Generate cards from output directory - if err := gen.GenerateFromDirectory(p.flags.OutputDir); err != nil { - return "", fmt.Errorf("failed to generate cards: %w", err) - } - - // Add translations to cards + // Use the translation cache as the source of truth for cards translations := p.translationCache.GetAll() - for i := range gen.GetCards() { - if translation, ok := translations[gen.GetCards()[i].Bulgarian]; ok { - gen.GetCards()[i].Translation = translation + if len(translations) == 0 { + fmt.Println(" No translations found in cache, generating cards from directory...") + // Fallback to old method if cache is empty but files might exist + if err := gen.GenerateFromDirectory(p.flags.OutputDir); err != nil { + return "", fmt.Errorf("failed to generate cards from directory: %w", err) + } + } else { + fmt.Printf(" Generating cards from %d translations in cache...\n", len(translations)) + for bulgarian, english := range translations { + card := anki.Card{ + Bulgarian: bulgarian, + Translation: english, + } + + // Find associated media files in the output directory + wordDir := p.findCardDirectory(bulgarian) + if wordDir != "" { + // Look for audio file + audioFile := filepath.Join(wordDir, fmt.Sprintf("audio.%s", p.flags.AudioFormat)) + if _, err := os.Stat(audioFile); err == nil { + card.AudioFile = audioFile + } + + // Look for image file + imageFile := filepath.Join(wordDir, "image.jpg") // Assuming jpg, adjust if needed + if _, err := os.Stat(imageFile); err == nil { + card.ImageFile = imageFile + } + + // Load phonetic information as notes + phoneticFile := filepath.Join(wordDir, "phonetic.txt") + if data, err := os.ReadFile(phoneticFile); err == nil { + notes := strings.TrimSpace(string(data)) + card.Notes = strings.ReplaceAll(notes, "\n", "
") + } + } + gen.AddCard(card) } } diff --git a/internal/processor/processor_test.go b/internal/processor/processor_test.go index 26ab71b..40f5cbe 100644 --- a/internal/processor/processor_test.go +++ b/internal/processor/processor_test.go @@ -208,16 +208,22 @@ func TestGenerateAnkiFile(t *testing.T) { p.translationCache.Add("ябълка", "apple") p.translationCache.Add("котка", "cat") + // Create dummy word directories and files + p.findOrCreateWordDirectory("ябълка") + p.findOrCreateWordDirectory("котка") + _, err := p.GenerateAnkiFile() if err != nil { t.Errorf("GenerateAnkiFile failed: %v", err) } - // Check CSV file was created - csvFile := filepath.Join(flags.OutputDir, "anki_import.csv") + // Check CSV file was created in home directory + homeDir, _ := os.UserHomeDir() + csvFile := filepath.Join(homeDir, "anki_import.csv") if _, err := os.Stat(csvFile); os.IsNotExist(err) { - t.Error("CSV file was not created") + t.Error("CSV file was not created in home directory") } + os.Remove(csvFile) // Clean up } func TestGenerateAnkiFile_APKG(t *testing.T) { @@ -236,18 +242,21 @@ func TestGenerateAnkiFile_APKG(t *testing.T) { p.translationCache.Add("ябълка", "apple") p.translationCache.Add("котка", "cat") - // Create dummy audio files - os.WriteFile(filepath.Join(word1Dir, "ябълка.mp3"), []byte("audio1"), 0644) - os.WriteFile(filepath.Join(word2Dir, "котка.mp3"), []byte("audio2"), 0644) + // Create dummy audio and image files + os.WriteFile(filepath.Join(word1Dir, "audio.mp3"), []byte("audio1"), 0644) + os.WriteFile(filepath.Join(word2Dir, "audio.mp3"), []byte("audio2"), 0644) + os.WriteFile(filepath.Join(word1Dir, "image.jpg"), []byte("image1"), 0644) _, err := p.GenerateAnkiFile() if err != nil { t.Errorf("GenerateAnkiFile (APKG) failed: %v", err) } - // Check APKG file was created - apkgFile := filepath.Join(flags.OutputDir, "Test_Deck.apkg") + // Check APKG file was created in home directory + homeDir, _ := os.UserHomeDir() + apkgFile := filepath.Join(homeDir, "Test_Deck.apkg") if _, err := os.Stat(apkgFile); os.IsNotExist(err) { - t.Error("APKG file was not created") + t.Error("APKG file was not created in home directory") } + os.Remove(apkgFile) // Clean up } -- cgit v1.2.3