Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat:_ add settings to enable thumbnails on dashboard #227

Merged
merged 1 commit into from
Jun 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions assets/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,29 @@
box-shadow: 0 0 0 2px rgba(164, 202, 254, 0.45);
}

.thumbnail-container {
position: relative;
display: inline-block;
}

.thumbnail-tooltip {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 10;
}

.thumbnail-container:hover .thumbnail-tooltip {
display: block;
}

/* Define your custom background colors here if the default Tailwind classes aren't working */
.bg-confidence-high { background-color: #10b981; } /* Green for high confidence */
.bg-confidence-medium { background-color: #f97316; } /* Orange for average confidence */
Expand Down
9 changes: 7 additions & 2 deletions internal/analysis/realtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,13 @@ func RealtimeAnalysis(settings *conf.Settings) error {
log.Fatalf("Error initializing metrics: %v", err)
}

// Intialize bird image cache
birdImageCache := initBirdImageCache(dataStore, metrics)
var birdImageCache *imageprovider.BirdImageCache
if settings.Realtime.Dashboard.Thumbnails.Summary || settings.Realtime.Dashboard.Thumbnails.Recent {
// Initialize the bird image cache
birdImageCache = initBirdImageCache(dataStore, metrics)
} else {
birdImageCache = nil
}
Comment on lines +113 to +119
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure robust initialization logic for birdImageCache.

The conditional initialization of birdImageCache based on dashboard settings is implemented correctly. This is a good use of configuration to control resource utilization. However, consider adding error handling within initBirdImageCache to ensure that issues during initialization are caught and logged properly.

+ if err != nil {
+     log.Printf("Failed to initialize bird image cache: %v", err)
+     return nil
+ }

Committable suggestion was skipped due to low confidence.


// Start worker pool for processing detections
processor.New(settings, dataStore, bn, metrics, birdImageCache)
Expand Down
14 changes: 12 additions & 2 deletions internal/conf/config.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// conf/config.go
// config.go: This file contains the configuration for the BirdNET-Go application. It defines the settings struct and functions to load and save the settings.
package conf

import (
Expand All @@ -17,11 +17,18 @@ import (
//go:embed config.yaml
var configFiles embed.FS

type Dashboard struct {
Thumbnails struct {
Summary bool // show thumbnails on summary table
Recent bool // show thumbnails on recent table
}
}

type Settings struct {
Debug bool // true to enable debug mode

Main struct {
Name string // name of go-birdnet node, can be used to identify source of notes
Name string // name of BirdNET-Go node, can be used to identify source of notes
TimeAs24h bool // true 24-hour time format, false 12-hour time format
Log LogConfig
}
Expand Down Expand Up @@ -66,6 +73,9 @@ type Settings struct {
}
}

// Apply dashboard settings
Dashboard Dashboard

DynamicThreshold struct {
Enabled bool // true to enable dynamic threshold
Debug bool // true to enable debug mode
Expand Down
5 changes: 5 additions & 0 deletions internal/conf/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ realtime:
maxusage: 80% # usage policy: percentage of disk usage to trigger eviction
minclips: 10 # minumum number of clips per species to keep before starting evictions

dashboard:
thumbnails:
summary: false
recent: true

dynamicthreshold:
enabled: true # true to enable dynamic confidence threshold
trigger: 0.90 # dynamic threshold is activated on detections at this confidence level
Expand Down
4 changes: 4 additions & 0 deletions internal/conf/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ func setDefaultConfig() {
viper.SetDefault("realtime.audio.export.path", "clips/")
viper.SetDefault("realtime.audio.export.type", "wav")

// Dashboard thumbnails configuration
viper.SetDefault("realtime.dashboard.thumbnails.summary", false)
viper.SetDefault("realtime.dashboard.thumbnails.recent", true)
Comment on lines +48 to +49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

Codebase Verification

Ensure the new settings are integrated and used within the application.

The search results indicate that the new settings realtime.dashboard.thumbnails.summary and realtime.dashboard.thumbnails.recent are only defined in internal/conf/defaults.go and are not referenced or used elsewhere in the codebase. This suggests that these settings might not be integrated into the application logic.

  • Integration Required: Ensure that the new settings are referenced and utilized appropriately in the relevant parts of the application where dashboard thumbnails are handled.
Analysis chain

Ensure consistency in default settings across the application.

The new dashboard thumbnail settings (summary and recent) are added correctly. Verify that these settings are consistently handled across different parts of the application, especially in areas where these settings are checked or modified.

Scripts executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Description: Search for usage of the new thumbnail settings across the application.

# Test: Search for the settings usage. Expect: Consistent handling across the application.
rg --type go "realtime.dashboard.thumbnails.(summary|recent)"

Length of output: 245



Script:

#!/bin/bash
# Description: Search for usage of the new thumbnail settings across the entire codebase.

# Test: Search for the settings usage in all files. Expect: Consistent handling across the application.
rg 'realtime.dashboard.thumbnails.(summary|recent)'

Length of output: 235


// Retention policy configuration
viper.SetDefault("realtime.audio.export.retention.enabled", true)
viper.SetDefault("realtime.audio.export.retention.debug", false)
Expand Down
28 changes: 20 additions & 8 deletions internal/httpcontroller/handlers.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// httpcontroller/handlers.go
// handlers.go: This file contains the request handlers for the web server.
package httpcontroller

import (
Expand Down Expand Up @@ -152,13 +152,15 @@ func (s *Server) topBirdsHandler(c echo.Context) error {

// Preparing data for rendering in the template
data := struct {
NotesWithIndex []NoteWithIndex
Hours []int
SelectedDate string
NotesWithIndex []NoteWithIndex
Hours []int
SelectedDate string
DashboardSettings *conf.Dashboard
}{
NotesWithIndex: notesWithIndex,
Hours: hours,
SelectedDate: selectedDate,
NotesWithIndex: notesWithIndex,
Hours: hours,
SelectedDate: selectedDate,
DashboardSettings: s.DashboardSettings,
}

// Render the birdsTableHTML template with the data
Expand Down Expand Up @@ -360,13 +362,23 @@ func (s *Server) searchHandler(c echo.Context) error {
func (s *Server) getLastDetections(c echo.Context) error {
numDetections := parseNumDetections(c.QueryParam("numDetections"), 10) // Default value is 10

// Retrieve the last detections from the database
notes, err := s.ds.GetLastDetections(numDetections)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Error fetching detections"})
}

// Preparing data for rendering in the template
data := struct {
Notes []datastore.Note
DashboardSettings conf.Dashboard
}{
Notes: notes,
DashboardSettings: *s.DashboardSettings,
}

// render the recentDetections template with the data
return c.Render(http.StatusOK, "recentDetections", notes)
return c.Render(http.StatusOK, "recentDetections", data)
}

// serveSpectrogramHandler serves or generates a spectrogram for a given clip.
Expand Down
20 changes: 11 additions & 9 deletions internal/httpcontroller/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@ func (t *TemplateRenderer) Render(w io.Writer, name string, data interface{}, c

// Server encapsulates Echo server and related configurations.
type Server struct {
Echo *echo.Echo // Echo framework instance
ds datastore.Interface // Datastore interface
Settings *conf.Settings // Application settings
Logger *logger.Logger // Custom logger
BirdImageCache *imageprovider.BirdImageCache
Echo *echo.Echo // Echo framework instance
ds datastore.Interface // Datastore interface
Settings *conf.Settings // Application settings
DashboardSettings *conf.Dashboard // Dashboard settings
Logger *logger.Logger // Custom logger
BirdImageCache *imageprovider.BirdImageCache
}

// New initializes a new HTTP server with given context and datastore.
Expand All @@ -41,10 +42,11 @@ func New(settings *conf.Settings, dataStore datastore.Interface, birdImageCache
configureDefaultSettings(settings)

s := &Server{
Echo: echo.New(),
ds: dataStore,
Settings: settings,
BirdImageCache: birdImageCache,
Echo: echo.New(),
ds: dataStore,
Settings: settings,
BirdImageCache: birdImageCache,
DashboardSettings: &settings.Realtime.Dashboard,
}

// Server initialization
Expand Down
97 changes: 32 additions & 65 deletions internal/httpcontroller/utils.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// utils.go
// utils.go: This file contains utility functions for the HTTP controller package.
package httpcontroller

import (
"fmt"
"html"
"html/template"
"log"
"os"
Expand Down Expand Up @@ -142,61 +143,6 @@ func (s *Server) getSpectrogramPath(wavFileName string, width int) (string, erro
return webFriendlyPath, nil
}

/*
// wrapNotesWithSpectrogram wraps notes with their corresponding spectrogram paths.
func (s *Server) wrapNotesWithSpectrogram(notes []datastore.Note) ([]NoteWithSpectrogram, error) {
notesWithSpectrogram := make([]NoteWithSpectrogram, len(notes))

// Create a channel to communicate between goroutines for results
type result struct {
index int
path string
err error
}
results := make(chan result, len(notes))

// Create a channel to limit the number of concurrent goroutines
semaphore := make(chan struct{}, 4) // Limit to 4

// Set the width of the spectrogram in pixels
const width = 400 // pixels

for i, note := range notes {
// Acquire a slot in the semaphore before starting a goroutine
semaphore <- struct{}{}

// Launch a goroutine for each spectrogram generation
go func(i int, note datastore.Note) {
defer func() { <-semaphore }() // Release the slot when done

spectrogramPath, err := s.getSpectrogramPath(note.ClipName, width)
results <- result{i, spectrogramPath, err}
}(i, note)
}

// Wait for all goroutines to finish
for i := 0; i < len(notes); i++ {
res := <-results
if res.err != nil {
log.Printf("Error generating spectrogram for %s: %v", notes[res.index].ClipName, res.err)
continue
}
notesWithSpectrogram[res.index] = NoteWithSpectrogram{
Note: notes[res.index],
Spectrogram: res.path,
}
}

// Wait for all slots to be released ensuring all goroutines have completed
for i := 0; i < cap(semaphore); i++ {
semaphore <- struct{}{}
}
close(results)
close(semaphore)

return notesWithSpectrogram, nil
}*/

// sumHourlyCounts calculates the total counts from hourly counts.
func sumHourlyCounts(hourlyCounts [24]int) int {
total := 0
Expand Down Expand Up @@ -239,35 +185,56 @@ func parseOffset(offsetStr string, defaultOffset int) int {
return offset
}

// thumbnail returns the url of a given bird's thumbnail
// Thumbnail returns the URL of a given bird's thumbnail image.
// It takes the bird's scientific name as input and returns the image URL as a string.
// If the image is not found or an error occurs, it returns an empty string.
func (s *Server) thumbnail(scientificName string) string {
if s.BirdImageCache == nil {
// Return empty string if the cache is not initialized
return ""
}

birdImage, err := s.BirdImageCache.Get(scientificName)
if err != nil {
// Return empty string if an error occurs
return ""
}

return birdImage.Url
return birdImage.URL
}
Comment on lines +188 to +203
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improve error handling and logging in thumbnail retrieval function.

The function correctly checks for an uninitialized cache and handles errors by returning an empty string, which is a safe failover. Consider adding a log statement for the error scenario to aid in debugging issues related to thumbnail retrieval.

+ log.Printf("Error retrieving thumbnail for %s: %v", scientificName, err)

Committable suggestion was skipped due to low confidence.


// thumbnailAttribution returns the thumbnail credits of a given bird.
// ThumbnailAttribution returns the HTML-formatted attribution for a bird's thumbnail image.
// It takes the bird's scientific name as input and returns a template.HTML string.
// If the attribution information is incomplete or an error occurs, it returns an empty template.HTML.
func (s *Server) thumbnailAttribution(scientificName string) template.HTML {
if s.BirdImageCache == nil {
// Return empty string if the cache is not initialized
return template.HTML("")
}

birdImage, err := s.BirdImageCache.Get(scientificName)
if err != nil {
log.Printf("Error getting thumbnail info for %s: %v", scientificName, err)
return template.HTML("")
}

// Skip if no author or license information
if birdImage.AuthorName == "" || birdImage.LicenseName == "" {
return template.HTML("")
}

var toReturn string
if birdImage.AuthorUrl == "" {
toReturn = fmt.Sprintf("© %s / <a href=%s>%s</a>", birdImage.AuthorName, birdImage.LicenseUrl, birdImage.LicenseName)
var attribution string
if birdImage.AuthorURL == "" {
attribution = fmt.Sprintf("© %s / <a href=\"%s\">%s</a>",
html.EscapeString(birdImage.AuthorName),
html.EscapeString(birdImage.LicenseURL),
html.EscapeString(birdImage.LicenseName))
} else {
toReturn = fmt.Sprintf("© <a href=%s>%s</a> / <a href=%s>%s</a>", birdImage.AuthorUrl, birdImage.AuthorName, birdImage.LicenseUrl, birdImage.LicenseName)
attribution = fmt.Sprintf("© <a href=\"%s\">%s</a> / <a href=\"%s\">%s</a>",
html.EscapeString(birdImage.AuthorURL),
html.EscapeString(birdImage.AuthorName),
html.EscapeString(birdImage.LicenseURL),
html.EscapeString(birdImage.LicenseName))
}

return template.HTML(toReturn)
return template.HTML(attribution)
}
Comment on lines +206 to +239
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refine HTML generation in the ThumbnailAttribution function.

The function handles various error conditions well. However, the HTML generation could be vulnerable to XSS if not properly sanitized. Ensure that html.EscapeString is used consistently to prevent XSS vulnerabilities, especially in dynamically generated links.

- attribution = fmt.Sprintf("© %s / <a href=\"%s\">%s</a>", birdImage.AuthorName, birdImage.LicenseURL, birdImage.LicenseName)
+ attribution = fmt.Sprintf("© %s / <a href=\"%s\">%s</a>", html.EscapeString(birdImage.AuthorName), html.EscapeString(birdImage.LicenseURL), html.EscapeString(birdImage.LicenseName))
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ThumbnailAttribution returns the HTML-formatted attribution for a bird's thumbnail image.
// It takes the bird's scientific name as input and returns a template.HTML string.
// If the attribution information is incomplete or an error occurs, it returns an empty template.HTML.
func (s *Server) thumbnailAttribution(scientificName string) template.HTML {
if s.BirdImageCache == nil {
// Return empty string if the cache is not initialized
return template.HTML("")
}
birdImage, err := s.BirdImageCache.Get(scientificName)
if err != nil {
log.Printf("Error getting thumbnail info for %s: %v", scientificName, err)
return template.HTML("")
}
// Skip if no author or license information
if birdImage.AuthorName == "" || birdImage.LicenseName == "" {
return template.HTML("")
}
var toReturn string
if birdImage.AuthorUrl == "" {
toReturn = fmt.Sprintf("© %s / <a href=%s>%s</a>", birdImage.AuthorName, birdImage.LicenseUrl, birdImage.LicenseName)
var attribution string
if birdImage.AuthorURL == "" {
attribution = fmt.Sprintf("© %s / <a href=\"%s\">%s</a>",
html.EscapeString(birdImage.AuthorName),
html.EscapeString(birdImage.LicenseURL),
html.EscapeString(birdImage.LicenseName))
} else {
toReturn = fmt.Sprintf("© <a href=%s>%s</a> / <a href=%s>%s</a>", birdImage.AuthorUrl, birdImage.AuthorName, birdImage.LicenseUrl, birdImage.LicenseName)
attribution = fmt.Sprintf("© <a href=\"%s\">%s</a> / <a href=\"%s\">%s</a>",
html.EscapeString(birdImage.AuthorURL),
html.EscapeString(birdImage.AuthorName),
html.EscapeString(birdImage.LicenseURL),
html.EscapeString(birdImage.LicenseName))
}
return template.HTML(toReturn)
return template.HTML(attribution)
var attribution string
if birdImage.AuthorURL == "" {
attribution = fmt.Sprintf("© %s / <a href=\"%s\">%s</a>",
html.EscapeString(birdImage.AuthorName),
html.EscapeString(birdImage.LicenseURL),
html.EscapeString(birdImage.LicenseName))
} else {
attribution = fmt.Sprintf("© <a href=\"%s\">%s</a> / <a href=\"%s\">%s</a>",
html.EscapeString(birdImage.AuthorURL),
html.EscapeString(birdImage.AuthorName),
html.EscapeString(birdImage.LicenseURL),
html.EscapeString(birdImage.LicenseName))
}

12 changes: 6 additions & 6 deletions internal/imageprovider/imageprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ type ImageProvider interface {
}

type BirdImage struct {
Url string
URL string
LicenseName string
LicenseUrl string
LicenseURL string
AuthorName string
AuthorUrl string
AuthorURL string
}

// BirdImageCache represents a cache for bird images.
Expand Down Expand Up @@ -112,9 +112,9 @@ func (c *BirdImageCache) fetch(scientificName string) (BirdImage, error) {
// EstimateSize estimates the memory size of a BirdImage instance in bytes
func (img *BirdImage) EstimateSize() int {
return int(unsafe.Sizeof(*img)) +
len(img.Url) + len(img.LicenseName) +
len(img.LicenseUrl) + len(img.AuthorName) +
len(img.AuthorUrl)
len(img.URL) + len(img.LicenseName) +
len(img.LicenseURL) + len(img.AuthorName) +
len(img.AuthorURL)
}

// MemoryUsage returns the approximate memory usage of the image cache in bytes
Expand Down
Loading
Loading