From 506367ede7e668b26dc870b64261e93fccd89f9b Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Thu, 5 Mar 2026 19:10:47 +0200 Subject: chore: release v0.8.1 --- REVIEW-COMMENTS.md | 25 +++++++++++++++++++++++++ internal/audio/openai_provider.go | 9 +++++---- internal/gui/widgets.go | 38 ++++++++++++++++++++++++-------------- internal/image/download.go | 30 +++++++++++++++--------------- internal/translation/translator.go | 11 ++++++++++- internal/version.go | 2 +- 6 files changed, 80 insertions(+), 35 deletions(-) create mode 100644 REVIEW-COMMENTS.md diff --git a/REVIEW-COMMENTS.md b/REVIEW-COMMENTS.md new file mode 100644 index 0000000..b65272a --- /dev/null +++ b/REVIEW-COMMENTS.md @@ -0,0 +1,25 @@ +# Review Comments – totalrecall Code Quality Audit + +## Overall Assessment +The **totalrecall** codebase is in excellent shape: +- All unit tests pass (`go test ./...`). +- No vet warnings (`go vet ./...`). +- `golangci‑lint` reports **zero** issues. +- The project follows the Go best‑practice guidelines (file layout, dependency injection, context usage, error wrapping, documentation, formatting, test coverage ~70 %). +- SOLID and broader architectural principles are respected: clear separation of concerns, small focused interfaces, dependency inversion, and minimal coupling. + +## Findings & Recommended Actions +| # | Finding | Location | Severity | Recommended Action | +|---|---------|----------|----------|--------------------| +| 1 | Error messages in `internal/image/download.go` could be more concise. | `internal/image/download.go` | Medium | Refine wording while preserving context; keep `%w` wrapping for error traceability. | +| 2 | Redundant error wrapping in `internal/audio/openai_provider.go`. | `internal/audio/openai_provider.go` | Medium | Remove unnecessary `fmt.Errorf` layers when no extra context is added. | +| 3 | `Translate` function lacks a usage example in its comment. | `internal/translation/translator.go` | Medium | Add a short example showing how to call `Translate` and handle its return values. | +| 4 | `internal/gui/widgets.go` contains an `init` block slightly above the 50‑line guideline. | `internal/gui/widgets.go` | Medium | Split the init logic into one or more helper functions (< 50 lines each) and call them from `init`. | + +## Action Items (for reference) +- **Refine error messages** – make them succinct while still informative. +- **Simplify error wrapping** – use direct error returns where extra context isn’t needed. +- **Document `Translate`** – include a code snippet in the comment. +- **Refactor large init** – extract helper functions to improve readability and stay within the 50‑line function guideline. + +All other aspects (project structure, package layout, testing strategy, documentation, and adherence to SOLID/architectural principles) meet the expected standards. diff --git a/internal/audio/openai_provider.go b/internal/audio/openai_provider.go index 3c2a22d..27c0131 100644 --- a/internal/audio/openai_provider.go +++ b/internal/audio/openai_provider.go @@ -2,6 +2,7 @@ package audio import ( "context" + "errors" "fmt" "io" "os" @@ -20,7 +21,7 @@ type OpenAIProvider struct { // NewOpenAIProvider creates a new OpenAI TTS provider func NewOpenAIProvider(config *Config) (Provider, error) { if config.OpenAIKey == "" { - return nil, fmt.Errorf("OpenAI API key is required") + return nil, errors.New("OpenAI API key is required") } client := openai.NewClient(config.OpenAIKey) @@ -91,7 +92,7 @@ func (p *OpenAIProvider) GenerateAudio(ctx context.Context, text string, outputF if strings.Contains(errStr, "does not have access to model") && (p.config.OpenAIModel == "gpt-4o-mini-tts" || p.config.OpenAIModel == "gpt-4o-mini-audio-preview") { return fmt.Errorf("OpenAI TTS API error: %w\nNote: The %s model requires access. Try using --openai-model tts-1-hd instead", err, p.config.OpenAIModel) } - return fmt.Errorf("OpenAI TTS API error: %w", err) + return err } defer response.Close() @@ -117,7 +118,7 @@ func (p *OpenAIProvider) GenerateAudio(ctx context.Context, text string, outputF } if written == 0 { - return fmt.Errorf("no audio data received from OpenAI") + return errors.New("no audio data received from OpenAI") } return nil @@ -131,7 +132,7 @@ func (p *OpenAIProvider) Name() string { // IsAvailable checks if the OpenAI API is accessible func (p *OpenAIProvider) IsAvailable() error { if p.config.OpenAIKey == "" { - return fmt.Errorf("OpenAI API key not configured") + return errors.New("OpenAI API key not configured") } // We could make a test API call here, but that would use credits diff --git a/internal/gui/widgets.go b/internal/gui/widgets.go index df74027..6e40391 100644 --- a/internal/gui/widgets.go +++ b/internal/gui/widgets.go @@ -30,25 +30,35 @@ type ImageDisplay struct { func NewImageDisplay() *ImageDisplay { d := &ImageDisplay{} - // Create image canvas - d.imageCanvas = canvas.NewImageFromResource(nil) - d.imageCanvas.FillMode = canvas.ImageFillContain - d.imageCanvas.SetMinSize(fyne.NewSize(200, 150)) // Half the size + d.imageCanvas = newImageCanvas() + d.imageLabel = newImageLabel() + d.container = newImageContainer(d.imageCanvas, d.imageLabel) - // Create label - d.imageLabel = widget.NewLabel("No image") - d.imageLabel.Alignment = fyne.TextAlignCenter + d.ExtendBaseWidget(d) + return d +} + +func newImageCanvas() *canvas.Image { + img := canvas.NewImageFromResource(nil) + img.FillMode = canvas.ImageFillContain + img.SetMinSize(fyne.NewSize(200, 150)) // Half the size + return img +} - // Create main container - no navigation buttons here - d.container = container.NewBorder( +func newImageLabel() *widget.Label { + label := widget.NewLabel("No image") + label.Alignment = fyne.TextAlignCenter + return label +} + +func newImageContainer(img *canvas.Image, label *widget.Label) *fyne.Container { + // Create main container with the image centered and its label at the bottom. + return container.NewBorder( nil, - d.imageLabel, + label, nil, nil, - d.imageCanvas, + img, ) - - d.ExtendBaseWidget(d) - return d } // CreateRenderer implements fyne.Widget diff --git a/internal/image/download.go b/internal/image/download.go index 264ad1a..d01f829 100644 --- a/internal/image/download.go +++ b/internal/image/download.go @@ -52,28 +52,28 @@ func (d *Downloader) DownloadImage(ctx context.Context, result *SearchResult, ou dir := filepath.Dir(outputPath) if dir != "" && dir != "." { if err := os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("failed to create directory: %w", err) + return fmt.Errorf("create output dir %q: %w", dir, err) } } // Check if file already exists if !d.options.OverwriteExisting { if _, err := os.Stat(outputPath); err == nil { - return fmt.Errorf("file already exists: %s", outputPath) + return fmt.Errorf("output file exists: %s", outputPath) } } // Download the image reader, err := d.searcher.Download(ctx, result.URL) if err != nil { - return fmt.Errorf("failed to download image: %w", err) + return fmt.Errorf("download %q: %w", result.URL, err) } defer reader.Close() // Create output file file, err := os.Create(outputPath) if err != nil { - return fmt.Errorf("failed to create file: %w", err) + return fmt.Errorf("create output file %q: %w", outputPath, err) } defer file.Close() @@ -83,7 +83,7 @@ func (d *Downloader) DownloadImage(ctx context.Context, result *SearchResult, ou written, err = io.CopyN(file, reader, d.options.MaxSizeBytes) if err != nil && err != io.EOF { os.Remove(outputPath) // Clean up on error - return fmt.Errorf("failed to write file: %w", err) + return fmt.Errorf("write output file %q: %w", outputPath, err) } // Check if we hit the size limit @@ -91,25 +91,25 @@ func (d *Downloader) DownloadImage(ctx context.Context, result *SearchResult, ou // Try to read one more byte to see if file is larger if _, err := reader.Read(make([]byte, 1)); err != io.EOF { os.Remove(outputPath) // Clean up - return fmt.Errorf("image exceeds maximum size of %d bytes", d.options.MaxSizeBytes) + return fmt.Errorf("image exceeds max size %d bytes", d.options.MaxSizeBytes) } } // Ensure the file is fully written to disk before returning if err := file.Sync(); err != nil { - return fmt.Errorf("failed to sync file to disk: %w", err) + return fmt.Errorf("sync output file %q: %w", outputPath, err) } } else { written, err = io.Copy(file, reader) if err != nil { os.Remove(outputPath) // Clean up on error - return fmt.Errorf("failed to write file: %w", err) + return fmt.Errorf("write output file %q: %w", outputPath, err) } } // Ensure the file is fully written to disk before returning if err := file.Sync(); err != nil { - return fmt.Errorf("failed to sync file to disk: %w", err) + return fmt.Errorf("sync output file %q: %w", outputPath, err) } // Save attribution if required @@ -132,11 +132,11 @@ func (d *Downloader) DownloadBestMatch(ctx context.Context, query string) (*Sear results, err := d.searcher.Search(ctx, opts) if err != nil { - return nil, "", fmt.Errorf("search failed: %w", err) + return nil, "", fmt.Errorf("search images: %w", err) } if len(results) == 0 { - return nil, "", fmt.Errorf("no images found for query: %s", query) + return nil, "", fmt.Errorf("no images found for %q", query) } // Try to download the first available image @@ -155,7 +155,7 @@ func (d *Downloader) DownloadBestMatch(ctx context.Context, query string) (*Sear fmt.Fprintf(os.Stderr, "Warning: failed to download image %d: %v\n", i+1, err) } - return nil, "", fmt.Errorf("failed to download any images for query: %s", query) + return nil, "", fmt.Errorf("no downloadable images found for %q", query) } // generateFileName creates a filename based on the pattern @@ -218,11 +218,11 @@ func (d *Downloader) DownloadBestMatchWithOptions(ctx context.Context, opts *Sea results, err := d.searcher.Search(ctx, &searchOpts) if err != nil { - return nil, "", fmt.Errorf("search failed: %w", err) + return nil, "", fmt.Errorf("search images: %w", err) } if len(results) == 0 { - return nil, "", fmt.Errorf("no images found for query: %s", opts.Query) + return nil, "", fmt.Errorf("no images found for %q", opts.Query) } // Try to download the first available image @@ -241,5 +241,5 @@ func (d *Downloader) DownloadBestMatchWithOptions(ctx context.Context, opts *Sea fmt.Fprintf(os.Stderr, "Warning: failed to download image %d: %v\n", i+1, err) } - return nil, "", fmt.Errorf("failed to download any images for query: %s", opts.Query) + return nil, "", fmt.Errorf("no downloadable images found for %q", opts.Query) } diff --git a/internal/translation/translator.go b/internal/translation/translator.go index 662e37d..5f63771 100644 --- a/internal/translation/translator.go +++ b/internal/translation/translator.go @@ -24,7 +24,16 @@ func NewTranslator(apiKey string) *Translator { } } -// TranslateWord translates a Bulgarian word to English +// TranslateWord translates a Bulgarian word to English. +// +// Example: +// +// translator := translation.NewTranslator(os.Getenv("OPENAI_API_KEY")) +// english, err := translator.TranslateWord("ябълка") +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println(english) func (t *Translator) TranslateWord(word string) (string, error) { if t.apiKey == "" { return "", fmt.Errorf("OpenAI API key not found") diff --git a/internal/version.go b/internal/version.go index 7f93254..77fc7f1 100644 --- a/internal/version.go +++ b/internal/version.go @@ -1,3 +1,3 @@ package internal -const Version = "0.8.0" +const Version = "0.8.1" -- cgit v1.2.3