summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-08-02 15:08:33 +0300
committerPaul Buetow <paul@buetow.org>2025-08-02 15:08:33 +0300
commit310ad64d1ad0b6e30a7dfaab0344dd78cabae463 (patch)
tree41d51e7ab49f2bbbb0bb0a13faf9678cccaf34c3 /internal
parent18a475657cbc7b2ff8ee537b082eeef25e9bf619 (diff)
also export reverse cards via apkg
Diffstat (limited to 'internal')
-rw-r--r--internal/anki/apkg_generator.go84
-rw-r--r--internal/processor/processor.go47
-rw-r--r--internal/processor/processor_test.go27
3 files changed, 130 insertions, 28 deletions
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 {
</div>`
}
+// getReverseFrontTemplate returns the question template for the reverse card
+func (g *APKGGenerator) getReverseFrontTemplate() string {
+ return `<div class="front">
+<div class="bulgarian">{{Bulgarian}}</div>
+</div>`
+}
+
+// getReverseBackTemplate returns the answer template for the reverse card
+func (g *APKGGenerator) getReverseBackTemplate() string {
+ return `{{FrontSide}}
+
+<hr id="answer">
+
+<div class="back">
+<div class="english">{{English}}</div>
+{{#Image}}
+<div class="image-container">
+{{Image}}
+</div>
+{{/Image}}
+{{#Audio}}
+<div class="audio">{{Audio}}</div>
+{{/Audio}}
+{{#Notes}}
+<div class="notes">{{Notes}}</div>
+{{/Notes}}
+</div>`
+}
+
// 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", "<br>")
+ }
+ }
+ 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
}