精简后的项目:仅保留 api/ui 源码、文档、构建依赖

This commit is contained in:
gallery
2026-09-11 16:11:09 +08:00
commit 490e85634e
457 changed files with 86862 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
package exif
import (
"fmt"
"sync"
"github.com/photoview/photoview/api/graphql/models"
"github.com/photoview/photoview/api/log"
"github.com/photoview/photoview/api/scanner/externaltools/exiftool"
)
var globalExifParser *exiftool.Exiftool
var globalInit sync.Once
func Initialize() (func(), error) {
var err error
globalInit.Do(func() {
globalExifParser, err = exiftool.New()
})
if err != nil {
return nil, err
}
log.Info(nil, "Found exiftool.", "binary_path", globalExifParser.BinaryPath(), "version", globalExifParser.Version())
return func() {
globalMu.Lock()
defer globalMu.Unlock()
if globalExifParser == nil {
return
}
if err := globalExifParser.Close(); err != nil {
log.Error(nil, "Cleanup exiftool error", "error", err)
return
}
globalExifParser = nil
}, nil
}
var globalMu sync.Mutex
func Parse(filepath string) (*models.MediaEXIF, error) {
globalMu.Lock()
defer globalMu.Unlock()
if globalExifParser == nil {
return nil, fmt.Errorf("no exif parser initialized")
}
var values struct {
exiftool.PhotoMeta
exiftool.TimeAll
exiftool.GPS
}
if err := globalExifParser.QueryJSONTagsByNumber(filepath, &values); err != nil {
return nil, err
}
values.PhotoMeta.SanitizeFloats()
ret := models.MediaEXIF{
Camera: values.Model,
Maker: values.Make,
Lens: values.LensModel,
Iso: values.ISO,
Flash: values.Flash,
Orientation: values.Orientation,
ExposureProgram: values.ExposureProgram,
Exposure: values.ExposureTime,
Aperture: values.Aperture,
FocalLength: values.FocalLength,
Description: values.ImageDescription,
}
dateShot := values.TimeAll.TimeInLocal()
if !dateShot.IsZero() {
ret.DateShot = new(dateShot)
}
offsetSec, ok := values.TimeAll.OffsetSecs(dateShot)
if ok {
ret.OffsetSecShot = &offsetSec
}
if values.GPS.IsValid() {
ret.GPSLatitude = values.GPS.GPSLatitude
ret.GPSLongitude = values.GPS.GPSLongitude
}
return &ret, nil
}
func MIMEType(filepath string) (string, error) {
globalMu.Lock()
defer globalMu.Unlock()
if globalExifParser == nil {
return "", fmt.Errorf("no exif parser initialized")
}
var mime exiftool.MIMEType
if err := globalExifParser.QueryJSONTagsByNumber(filepath, &mime); err != nil {
return "", err
}
if mime.MIMEType == nil {
return "", nil
}
return *mime.MIMEType, nil
}
+141
View File
@@ -0,0 +1,141 @@
package exif
import (
"os"
"sync"
"testing"
"time"
_ "github.com/photoview/photoview/api/test_utils/flags"
)
func resetForTest() {
globalMu.Lock()
defer globalMu.Unlock()
if globalExifParser != nil {
_ = globalExifParser.Close()
}
globalExifParser = nil
// Allow Initialize() to run again in subsequent tests
globalInit = sync.Once{}
}
func TestParseWithoutInit(t *testing.T) {
resetForTest()
if _, err := Parse("./test_data/bird.jpg"); err == nil {
t.Fatalf("Parse() without Init() doesn't return an error")
}
}
func TestParse(t *testing.T) {
resetForTest()
cleanup, err := Initialize()
if err != nil {
t.Fatalf("Initialize() error: %v", err)
}
defer cleanup()
filename := "./test_data/bird.jpg"
metadata, err := Parse(filename)
if err != nil {
t.Fatalf("Parse() returns an error: %v", err)
}
if metadata == nil {
t.Errorf("Parse(%q) should not return nil", filename)
}
}
func TestMIMEType(t *testing.T) {
resetForTest()
cleanup, err := Initialize()
if err != nil {
t.Fatalf("Initialize() error: %v", err)
}
defer cleanup()
filename := "./test_data/bird.jpg"
mime, err := MIMEType(filename)
if err != nil {
t.Fatalf("MIMEType() returns an error: %v", err)
}
if mime == "" {
t.Errorf("MIMEType(%q) should not return an empty string", filename)
}
}
func fileModifyDateLiteralInUTC(t *testing.T, file string) time.Time {
fstat, err := os.Stat(file)
if err != nil {
t.Fatalf("os.Stat(%q) error: %v", file, err)
}
ret := fstat.ModTime().Truncate(time.Second)
_, offset := ret.Zone()
ret = ret.Add(time.Duration(offset) * time.Second).UTC()
return ret
}
func mustParseNoTimeZone(t *testing.T, timeStr string) time.Time {
layout := "2006:01:02 15:04:05.999"
ret, err := time.ParseInLocation(layout, timeStr, time.UTC)
if err != nil {
t.Fatalf("time.Parse(%q) error: %v", timeStr, err)
}
return ret
}
func TestSamplesTime(t *testing.T) {
resetForTest()
cleanup, err := Initialize()
if err != nil {
t.Fatalf("Initialize() error: %v", err)
}
defer cleanup()
tests := []struct {
file string
wantTime time.Time
wantOffsetSec *int
}{
{"./test_data/sample1.heif", fileModifyDateLiteralInUTC(t, "./test_data/sample1.heif"), nil},
{"./test_data/sample1_nef.jpg", mustParseNoTimeZone(t, "2008:03:15 07:44:21.49"), new(-7 * 60 * 60)},
}
for _, tc := range tests {
t.Run(tc.file, func(t *testing.T) {
metadata, err := Parse(tc.file)
if err != nil {
t.Fatalf("Parse(%q) returns an error: %v", tc.file, err)
}
if metadata == nil {
t.Fatalf("metadata == nil")
}
if metadata.DateShot == nil {
t.Fatalf("metadata.DateShot == nil")
}
if got := *metadata.DateShot; !got.Equal(tc.wantTime) {
t.Errorf("metadata.DateShot = %v, want: %v", got, tc.wantTime)
}
if tc.wantOffsetSec == nil {
if got := metadata.OffsetSecShot; got != nil {
t.Errorf("metadata.OffsetSecShot = %+v, want: nil", got)
}
} else {
if got, want := *metadata.OffsetSecShot, *tc.wantOffsetSec; got != want {
t.Errorf("metadata.OffsetSecShot = &(%v), want: &(%v)", got, want)
}
}
})
}
}