diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-08 08:38:05 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-08 08:38:05 +0200 |
| commit | 3a255c0c64f858d5c05797aba9a6d159b0c7d82f (patch) | |
| tree | df517cfa866c3ba344c443f5c42a3c1e932ff0dd | |
| parent | ffa8e0bab35e430ed0e23ccc2383d56559924acc (diff) | |
fix(task-373): handle runtime cleanup errors in production paths
| -rw-r--r-- | internal/anki/generator.go | 117 | ||||
| -rw-r--r-- | internal/gui/audio_player.go | 25 | ||||
| -rw-r--r-- | internal/image/download.go | 10 | ||||
| -rw-r--r-- | internal/image/openai.go | 4 |
4 files changed, 91 insertions, 65 deletions
diff --git a/internal/anki/generator.go b/internal/anki/generator.go index 85a4155..07ea5df 100644 --- a/internal/anki/generator.go +++ b/internal/anki/generator.go @@ -12,22 +12,22 @@ import ( // Card represents a single Anki flashcard type Card struct { - Bulgarian string // The Bulgarian word/phrase - AudioFile string // Path to audio file (for en-bg: Bulgarian audio, for bg-bg: front audio) - AudioFileBack string // Path to back audio file (only for bg-bg cards) - ImageFile string // Path to image file - Translation string // Translation (English for en-bg, Bulgarian definition for bg-bg) - Notes string // Optional notes - CardType string // Card type: "en-bg" or "bg-bg" + Bulgarian string // The Bulgarian word/phrase + AudioFile string // Path to audio file (for en-bg: Bulgarian audio, for bg-bg: front audio) + AudioFileBack string // Path to back audio file (only for bg-bg cards) + ImageFile string // Path to image file + Translation string // Translation (English for en-bg, Bulgarian definition for bg-bg) + Notes string // Optional notes + CardType string // Card type: "en-bg" or "bg-bg" } // GeneratorOptions configures the Anki export type GeneratorOptions struct { - OutputPath string // Output CSV file path - MediaFolder string // Folder containing media files - IncludeHeaders bool // Include CSV headers - AudioFormat string // Audio file format (mp3, wav) - ImageFormat string // Image file format (jpg, png) + OutputPath string // Output CSV file path + MediaFolder string // Folder containing media files + IncludeHeaders bool // Include CSV headers + AudioFormat string // Audio file format (mp3, wav) + ImageFormat string // Image file format (jpg, png) } // DefaultGeneratorOptions returns sensible defaults @@ -68,19 +68,28 @@ func (g *Generator) GetCards() []Card { return g.cards } -// GenerateCSV creates a CSV file for Anki import -func (g *Generator) GenerateCSV() error { +// GenerateCSV creates a CSV file for Anki import. +func (g *Generator) GenerateCSV() (err error) { // Create output file file, err := os.Create(g.options.OutputPath) if err != nil { return fmt.Errorf("failed to create CSV file: %w", err) } - defer file.Close() - + defer func() { + if closeErr := file.Close(); err == nil && closeErr != nil { + err = fmt.Errorf("failed to close CSV file: %w", closeErr) + } + }() + // Create CSV writer writer := csv.NewWriter(file) - defer writer.Flush() - + defer func() { + writer.Flush() + if flushErr := writer.Error(); err == nil && flushErr != nil { + err = fmt.Errorf("failed to flush CSV file: %w", flushErr) + } + }() + // Write headers if requested if g.options.IncludeHeaders { headers := []string{"Bulgarian", "Audio", "Image", "Translation", "Notes"} @@ -88,7 +97,7 @@ func (g *Generator) GenerateCSV() error { return fmt.Errorf("failed to write headers: %w", err) } } - + // Write cards for _, card := range g.cards { record := []string{ @@ -98,12 +107,12 @@ func (g *Generator) GenerateCSV() error { card.Translation, card.Notes, } - + if err := writer.Write(record); err != nil { return fmt.Errorf("failed to write card: %w", err) } } - + return nil } @@ -112,14 +121,14 @@ func (g *Generator) formatAudioField(audioFile string) string { if audioFile == "" { return "" } - + // Get card ID from the source path (parent directory name) cardID := filepath.Base(filepath.Dir(audioFile)) originalFilename := filepath.Base(audioFile) - + // Create filename with card ID prefix for uniqueness in Anki filename := fmt.Sprintf("%s_%s", cardID, originalFilename) - + // Anki audio format: [sound:filename.mp3] return fmt.Sprintf("[sound:%s]", filename) } @@ -129,14 +138,14 @@ func (g *Generator) formatImageField(imageFile string) string { if imageFile == "" { return "" } - + // Get card ID from the source path (parent directory name) cardID := filepath.Base(filepath.Dir(imageFile)) originalFilename := filepath.Base(imageFile) - + // Create filename with card ID prefix for uniqueness in Anki filename := fmt.Sprintf("%s_%s", cardID, originalFilename) - + return fmt.Sprintf(`<img src="%s">`, filename) } @@ -254,13 +263,13 @@ func (g *Generator) GeneratePackage(outputDir string) error { if err := os.MkdirAll(outputDir, 0755); err != nil { return fmt.Errorf("failed to create output directory: %w", err) } - + // Create media directory mediaDir := filepath.Join(outputDir, "collection.media") if err := os.MkdirAll(mediaDir, 0755); err != nil { return fmt.Errorf("failed to create media directory: %w", err) } - + // Copy media files and update paths for i, card := range g.cards { // Copy audio file @@ -271,7 +280,7 @@ func (g *Generator) GeneratePackage(outputDir string) error { } g.cards[i].AudioFile = newPath } - + // Copy image file if card.ImageFile != "" { newPath, err := g.copyMediaFile(card.ImageFile, mediaDir) @@ -281,10 +290,10 @@ func (g *Generator) GeneratePackage(outputDir string) error { g.cards[i].ImageFile = newPath } } - + // Update output path to package directory g.options.OutputPath = filepath.Join(outputDir, "import.csv") - + // Generate CSV return g.GenerateCSV() } @@ -293,32 +302,32 @@ func (g *Generator) GeneratePackage(outputDir string) error { func (g *Generator) GenerateAPKG(outputPath, deckName string) error { // Create APKG generator apkgGen := NewAPKGGenerator(deckName) - + // Add all cards for _, card := range g.cards { apkgGen.AddCard(card) } - + // Generate the .apkg file return apkgGen.GenerateAPKG(outputPath) } -// copyMediaFile copies a media file to the destination directory -func (g *Generator) copyMediaFile(src, destDir string) (string, error) { +// copyMediaFile copies a media file to the destination directory. +func (g *Generator) copyMediaFile(src, destDir string) (filename string, err error) { // Get source file info srcInfo, err := os.Stat(src) if err != nil { return "", err } - + // Get the card ID from the source path (parent directory name) cardID := filepath.Base(filepath.Dir(src)) - + // Create destination filename with card ID prefix originalFilename := filepath.Base(src) - filename := fmt.Sprintf("%s_%s", cardID, originalFilename) + filename = fmt.Sprintf("%s_%s", cardID, originalFilename) destPath := filepath.Join(destDir, filename) - + // Check if file already exists if _, err := os.Stat(destPath); err == nil { // File exists, generate unique name @@ -332,38 +341,46 @@ func (g *Generator) copyMediaFile(src, destDir string) (string, error) { } } } - + // Open source file srcFile, err := os.Open(src) if err != nil { return "", err } - defer srcFile.Close() - + defer func() { + if closeErr := srcFile.Close(); err == nil && closeErr != nil { + err = fmt.Errorf("failed to close source media file: %w", closeErr) + } + }() + // Create destination file destFile, err := os.Create(destPath) if err != nil { return "", err } - defer destFile.Close() - + defer func() { + if closeErr := destFile.Close(); err == nil && closeErr != nil { + err = fmt.Errorf("failed to close destination media file: %w", closeErr) + } + }() + // Copy content if _, err := destFile.ReadFrom(srcFile); err != nil { return "", err } - + // Preserve file mode if err := os.Chmod(destPath, srcInfo.Mode()); err != nil { return "", err } - + return filename, nil } // Stats returns statistics about the card collection func (g *Generator) Stats() (totalCards, withAudio, withImages int) { totalCards = len(g.cards) - + for _, card := range g.cards { if card.AudioFile != "" { withAudio++ @@ -372,6 +389,6 @@ func (g *Generator) Stats() (totalCards, withAudio, withImages int) { withImages++ } } - + return -}
\ No newline at end of file +} diff --git a/internal/gui/audio_player.go b/internal/gui/audio_player.go index 8ab7f76..a214ee0 100644 --- a/internal/gui/audio_player.go +++ b/internal/gui/audio_player.go @@ -23,9 +23,9 @@ type AudioPlayer struct { container *fyne.Container playButton *ttwidget.Button - playButtonLabel *widget.Label // Label for front audio button - playBackButton *ttwidget.Button // Play back audio for bg-bg cards - playBackLabel *widget.Label // Label for back audio button + playButtonLabel *widget.Label // Label for front audio button + playBackButton *ttwidget.Button // Play back audio for bg-bg cards + playBackLabel *widget.Label // Label for back audio button stopButton *ttwidget.Button statusLabel *widget.Label phoneticLabel *widget.Label @@ -46,13 +46,13 @@ func NewAudioPlayer() *AudioPlayer { // Create controls (tooltips will be set later after tooltip layer is created) p.playButton = ttwidget.NewButton("", p.onPlay) p.playButton.Icon = theme.MediaPlayIcon() - + p.playButtonLabel = widget.NewLabel("") p.playButtonLabel.TextStyle = fyne.TextStyle{Bold: true} p.playBackButton = ttwidget.NewButton("", p.onPlayBack) p.playBackButton.Icon = theme.MediaPlayIcon() // Same icon as front button - + p.playBackLabel = widget.NewLabel("") p.playBackLabel.TextStyle = fyne.TextStyle{Bold: true} @@ -240,7 +240,7 @@ func (p *AudioPlayer) onPlay() { fmt.Printf(" - audioFileBack: %s\n", p.audioFileBack) fmt.Printf(" - isBgBg: %v\n", p.isBgBg) fmt.Printf(" - isPlaying: %v\n", p.isPlaying) - + if p.audioFile == "" { fmt.Printf("DEBUG (onPlay): No audioFile set, returning\n") return @@ -275,7 +275,7 @@ func (p *AudioPlayer) onPlayBack() { fmt.Printf(" - audioFileBack: %s\n", p.audioFileBack) fmt.Printf(" - isBgBg: %v\n", p.isBgBg) fmt.Printf(" - isPlaying: %v\n", p.isPlaying) - + if p.audioFileBack == "" { fmt.Printf("DEBUG (onPlayBack): No audioFileBack set, returning\n") return @@ -295,7 +295,7 @@ func (p *AudioPlayer) onPlayBack() { } p.isPlaying = true - p.playBackButton.SetIcon(theme.MediaPauseIcon()) // Back button, not front + p.playBackButton.SetIcon(theme.MediaPauseIcon()) // Back button, not front p.stopButton.Enable() p.statusLabel.SetText(fmt.Sprintf("Playing back audio: %s", filepath.Base(p.audioFileBack))) fmt.Printf("DEBUG (onPlayBack): Back audio playback started successfully\n") @@ -304,16 +304,19 @@ func (p *AudioPlayer) onPlayBack() { // onStop handles stop button click func (p *AudioPlayer) onStop() { if p.playCmd != nil && p.playCmd.Process != nil { - p.playCmd.Process.Kill() + if err := p.playCmd.Process.Kill(); err != nil { + fmt.Printf("DEBUG (onStop): Failed to kill playback process: %v\n", err) + p.statusLabel.SetText(fmt.Sprintf("failed to stop playback: %v", err)) + } p.playCmd = nil } p.isPlaying = false // Set correct button icon based on which audio was playing if p.isBgBg && p.audioFileBack != "" { - p.playBackButton.SetIcon(theme.MediaPlayIcon()) // Back button if it was playing + p.playBackButton.SetIcon(theme.MediaPlayIcon()) // Back button if it was playing } else { - p.playButton.SetIcon(theme.MediaPlayIcon()) // Front button otherwise + p.playButton.SetIcon(theme.MediaPlayIcon()) // Front button otherwise } p.stopButton.Disable() p.statusLabel.SetText(fmt.Sprintf("Stopped: %s%s", filepath.Base(p.audioFile), p.voiceInfo)) diff --git a/internal/image/download.go b/internal/image/download.go index 8ea897e..b2af843 100644 --- a/internal/image/download.go +++ b/internal/image/download.go @@ -46,8 +46,8 @@ func NewDownloader(searcher ImageSearcher, options *DownloadOptions) *Downloader } } -// DownloadImage downloads a single image to the specified path -func (d *Downloader) DownloadImage(ctx context.Context, result *SearchResult, outputPath string) error { +// DownloadImage downloads a single image to the specified path. +func (d *Downloader) DownloadImage(ctx context.Context, result *SearchResult, outputPath string) (err error) { // Ensure directory exists dir := filepath.Dir(outputPath) if dir != "" && dir != "." { @@ -77,7 +77,11 @@ func (d *Downloader) DownloadImage(ctx context.Context, result *SearchResult, ou if err != nil { return fmt.Errorf("create output file %q: %w", outputPath, err) } - defer file.Close() + defer func() { + if closeErr := file.Close(); err == nil && closeErr != nil { + err = fmt.Errorf("close output file %q: %w", outputPath, closeErr) + } + }() // Copy with size limit if specified var written int64 diff --git a/internal/image/openai.go b/internal/image/openai.go index 5bb7db4..f2db41d 100644 --- a/internal/image/openai.go +++ b/internal/image/openai.go @@ -197,7 +197,9 @@ func (c *OpenAIClient) Download(ctx context.Context, url string) (io.ReadCloser, } if resp.StatusCode != http.StatusOK { - resp.Body.Close() + if closeErr := resp.Body.Close(); closeErr != nil { + return nil, fmt.Errorf("HTTP %d: %s (failed to close response body: %v)", resp.StatusCode, resp.Status, closeErr) + } return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status) } |
