diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-03 19:17:04 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-03 19:17:04 +0300 |
| commit | 30c2b0fe8232cc748ab2bded6ab4d76febe32425 (patch) | |
| tree | 985578b326a1ff8fe7af395bf08471857376b8fc /internal/probe | |
| parent | 687b86cdeef2192272f0945ddd496322a9113f9e (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.go | 88 |
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) |
