summaryrefslogtreecommitdiff
path: root/internal/probe
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-03 19:17:04 +0300
committerPaul Buetow <paul@buetow.org>2026-05-03 19:17:04 +0300
commit30c2b0fe8232cc748ab2bded6ab4d76febe32425 (patch)
tree985578b326a1ff8fe7af395bf08471857376b8fc /internal/probe
parent687b86cdeef2192272f0945ddd496322a9113f9e (diff)
fix(admin): fix rescan goroutine lifecycle and race on shared ScanProgress
- Protect adminService scan state (cancel func + progress pointer) with sync.Mutex. - Allocate fresh ScanProgress per trigger and pass it to the scanner, eliminating races on the previously shared progress struct. - Cancel previous scan context before starting a new one. - Add tests for cancellation, fresh progress per scan, concurrent triggers, and empty progress when never started. - Fix race-prone tests by polling Running==true before waiting for completion.
Diffstat (limited to 'internal/probe')
-rw-r--r--internal/probe/probe.go88
1 files changed, 87 insertions, 1 deletions
diff --git a/internal/probe/probe.go b/internal/probe/probe.go
index d4eb5bb..4257516 100644
--- a/internal/probe/probe.go
+++ b/internal/probe/probe.go
@@ -5,10 +5,14 @@ import (
"context"
"encoding/json"
"fmt"
+ "os"
"os/exec"
+ "path/filepath"
"strconv"
+ "strings"
"codeberg.org/snonux/player/internal/model"
+ "github.com/rwcarlsen/goexif/exif"
)
// Prober extracts metadata from a media file.
@@ -40,7 +44,15 @@ func (f *FFProber) Probe(ctx context.Context, path string) (*model.Metadata, err
}
return nil, fmt.Errorf("ffprobe %s: %w", path, err)
}
- return parseFFprobeOutput(out)
+ meta, err := parseFFprobeOutput(out)
+ if err != nil {
+ return nil, err
+ }
+ // For images, also extract EXIF data.
+ if isImagePath(path) {
+ extractEXIF(path, meta)
+ }
+ return meta, nil
}
type ffprobeOutput struct {
@@ -81,6 +93,8 @@ func parseFFprobeOutput(data []byte) (*model.Metadata, error) {
}
if s.Width > 0 && s.Height > 0 {
meta.Resolution = fmt.Sprintf("%dx%d", s.Width, s.Height)
+ meta.Width = s.Width
+ meta.Height = s.Height
}
break
}
@@ -94,6 +108,78 @@ func parseFFprobeOutput(data []byte) (*model.Metadata, error) {
return meta, nil
}
+var imageExtensions = map[string]struct{}{
+ ".jpg": {}, ".jpeg": {}, ".png": {}, ".gif": {}, ".webp": {}, ".bmp": {}, ".avif": {}, ".svg": {},
+}
+
+func isImagePath(path string) bool {
+ ext := strings.ToLower(filepath.Ext(path))
+ _, ok := imageExtensions[ext]
+ return ok
+}
+
+func extractEXIF(path string, meta *model.Metadata) {
+ f, err := os.Open(path)
+ if err != nil {
+ return
+ }
+ defer f.Close()
+
+ x, err := exif.Decode(f)
+ if err != nil {
+ return
+ }
+
+ if v, err := x.Get(exif.Make); err == nil {
+ if s, err := v.StringVal(); err == nil {
+ meta.EXIFCamera = s
+ }
+ }
+ if v, err := x.Get(exif.Model); err == nil {
+ if s, err := v.StringVal(); err == nil {
+ if meta.EXIFCamera != "" {
+ meta.EXIFCamera = meta.EXIFCamera + " " + s
+ } else {
+ meta.EXIFCamera = s
+ }
+ }
+ }
+ if v, err := x.Get(exif.LensModel); err == nil {
+ if s, err := v.StringVal(); err == nil {
+ meta.EXIFLens = s
+ }
+ }
+ if v, err := x.Get(exif.DateTimeOriginal); err == nil {
+ if s, err := v.StringVal(); err == nil {
+ meta.EXIFDate = s
+ }
+ }
+ if v, err := x.Get(exif.ISOSpeedRatings); err == nil {
+ if i, err := v.Int(0); err == nil {
+ meta.EXIFISO = strconv.Itoa(i)
+ }
+ }
+ if v, err := x.Get(exif.FNumber); err == nil {
+ if r, err := v.Rat(0); err == nil {
+ num := float64(r.Num().Int64())
+ denom := float64(r.Denom().Int64())
+ meta.EXIFFNumber = fmt.Sprintf("f/%.1f", num/denom)
+ }
+ }
+ if v, err := x.Get(exif.ExposureTime); err == nil {
+ if r, err := v.Rat(0); err == nil {
+ meta.EXIFExposure = fmt.Sprintf("%s/%s s", r.Num().String(), r.Denom().String())
+ }
+ }
+ if v, err := x.Get(exif.FocalLength); err == nil {
+ if r, err := v.Rat(0); err == nil {
+ num := float64(r.Num().Int64())
+ denom := float64(r.Denom().Int64())
+ meta.EXIFFocalLength = fmt.Sprintf("%.1f mm", num/denom)
+ }
+ }
+}
+
// MockProber is a test fake for Prober.
type MockProber struct {
ProbeFunc func(ctx context.Context, path string) (*model.Metadata, error)