XMP: Group files based on DocumentID and Instance ID #335
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
301e510b2d
commit
f510ac994c
|
@ -6,6 +6,7 @@ import Util from "common/util";
|
|||
export class File extends RestModel {
|
||||
getDefaults() {
|
||||
return {
|
||||
InstanceID: "",
|
||||
UID: "",
|
||||
PhotoUID: "",
|
||||
Root: "",
|
||||
|
|
|
@ -4,8 +4,8 @@ import {DateTime} from "luxon";
|
|||
import File from "model/file";
|
||||
import Util from "../common/util";
|
||||
|
||||
export const RootOriginals = "originals";
|
||||
export const RootImport = "import";
|
||||
export const RootOriginals = "originals";
|
||||
|
||||
export class Folder extends RestModel {
|
||||
getDefaults() {
|
||||
|
|
|
@ -14,6 +14,7 @@ export const MonthUnknown = -1;
|
|||
export class Photo extends RestModel {
|
||||
getDefaults() {
|
||||
return {
|
||||
DocumentID: "",
|
||||
UID: "",
|
||||
Type: TypeImage,
|
||||
Favorite: false,
|
||||
|
|
|
@ -23,7 +23,7 @@ type FoldersResponse struct {
|
|||
}
|
||||
|
||||
// GetFolders is a reusable request handler for directory listings (GET /api/v1/folders/*).
|
||||
func GetFolders(router *gin.RouterGroup, conf *config.Config, root, rootPath string) {
|
||||
func GetFolders(router *gin.RouterGroup, conf *config.Config, urlPath, rootName, rootPath string) {
|
||||
handler := func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
|
@ -35,7 +35,7 @@ func GetFolders(router *gin.RouterGroup, conf *config.Config, root, rootPath str
|
|||
recursive := c.Query("recursive") != ""
|
||||
listFiles := c.Query("files") != ""
|
||||
cached := !listFiles && c.Query("uncached") == ""
|
||||
resp := FoldersResponse{Root: root, Recursive: recursive, Cached: cached}
|
||||
resp := FoldersResponse{Root: rootName, Recursive: recursive, Cached: cached}
|
||||
path := c.Param("path")
|
||||
|
||||
cacheKey := fmt.Sprintf("folders:%s:%t:%t", filepath.Join(rootPath, path), recursive, listFiles)
|
||||
|
@ -48,7 +48,7 @@ func GetFolders(router *gin.RouterGroup, conf *config.Config, root, rootPath str
|
|||
}
|
||||
}
|
||||
|
||||
if folders, err := query.FoldersByPath(root, rootPath, path, recursive); err != nil {
|
||||
if folders, err := query.FoldersByPath(rootName, rootPath, path, recursive); err != nil {
|
||||
log.Errorf("folders: %s", err)
|
||||
c.JSON(http.StatusOK, resp)
|
||||
return
|
||||
|
@ -57,7 +57,7 @@ func GetFolders(router *gin.RouterGroup, conf *config.Config, root, rootPath str
|
|||
}
|
||||
|
||||
if listFiles {
|
||||
if files, err := query.FilesByPath(root, path); err != nil {
|
||||
if files, err := query.FilesByPath(rootName, path); err != nil {
|
||||
log.Errorf("folders: %s", err)
|
||||
} else {
|
||||
resp.Files = files
|
||||
|
@ -75,16 +75,16 @@ func GetFolders(router *gin.RouterGroup, conf *config.Config, root, rootPath str
|
|||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
router.GET("/folders/"+root, handler)
|
||||
router.GET("/folders/"+root+"/*path", handler)
|
||||
router.GET("/folders/"+urlPath, handler)
|
||||
router.GET("/folders/"+urlPath+"/*path", handler)
|
||||
}
|
||||
|
||||
// GET /api/v1/folders/originals
|
||||
func GetFoldersOriginals(router *gin.RouterGroup, conf *config.Config) {
|
||||
GetFolders(router, conf, entity.RootOriginals, conf.OriginalsPath())
|
||||
GetFolders(router, conf, "originals", entity.RootDefault, conf.OriginalsPath())
|
||||
}
|
||||
|
||||
// GET /api/v1/folders/import
|
||||
func GetFoldersImport(router *gin.RouterGroup, conf *config.Config) {
|
||||
GetFolders(router, conf, entity.RootImport, conf.ImportPath())
|
||||
GetFolders(router, conf, "import", entity.RootImport, conf.ImportPath())
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ func TestGetFoldersOriginals(t *testing.T) {
|
|||
assert.Equal(t, "", folder.FolderDescription)
|
||||
assert.Equal(t, entity.TypeDefault, folder.FolderType)
|
||||
assert.Equal(t, entity.SortOrderName, folder.FolderOrder)
|
||||
assert.Equal(t, entity.RootOriginals, folder.Root)
|
||||
assert.Equal(t, entity.RootDefault, folder.Root)
|
||||
assert.IsType(t, "", folder.FolderUID)
|
||||
assert.Equal(t, false, folder.FolderFavorite)
|
||||
assert.Equal(t, false, folder.FolderIgnore)
|
||||
|
@ -85,7 +85,7 @@ func TestGetFoldersOriginals(t *testing.T) {
|
|||
assert.Equal(t, "", folder.FolderDescription)
|
||||
assert.Equal(t, entity.TypeDefault, folder.FolderType)
|
||||
assert.Equal(t, entity.SortOrderName, folder.FolderOrder)
|
||||
assert.Equal(t, entity.RootOriginals, folder.Root)
|
||||
assert.Equal(t, entity.RootDefault, folder.Root)
|
||||
assert.IsType(t, "", folder.FolderUID)
|
||||
assert.Equal(t, false, folder.FolderFavorite)
|
||||
assert.Equal(t, false, folder.FolderIgnore)
|
||||
|
|
|
@ -41,7 +41,7 @@ type Album struct {
|
|||
|
||||
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
||||
func (m *Album) BeforeCreate(scope *gorm.Scope) error {
|
||||
if rnd.IsPPID(m.AlbumUID, 'a') {
|
||||
if rnd.IsUID(m.AlbumUID, 'a') {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ const (
|
|||
TypeRaw = "raw"
|
||||
TypeText = "text"
|
||||
|
||||
RootOriginals = "originals"
|
||||
RootImport = "import"
|
||||
RootPath = "/"
|
||||
RootDefault = ""
|
||||
RootImport = "import"
|
||||
RootPath = "/"
|
||||
)
|
||||
|
|
|
@ -12,15 +12,16 @@ import (
|
|||
"github.com/ulule/deepcopier"
|
||||
)
|
||||
|
||||
// File represents an image or sidecar file that belongs to a photo
|
||||
// File represents an image or sidecar file that belongs to a photo.
|
||||
type File struct {
|
||||
ID uint `gorm:"primary_key" json:"-" yaml:"-"`
|
||||
InstanceID string `gorm:"type:varbinary(36);index;" json:"InstanceID,omitempty" yaml:"InstanceID,omitempty"`
|
||||
Photo *Photo `json:"-" yaml:"-"`
|
||||
PhotoID uint `gorm:"index;" json:"-" yaml:"-"`
|
||||
PhotoUID string `gorm:"type:varbinary(36);index;" json:"PhotoUID" yaml:"PhotoUID"`
|
||||
FileUID string `gorm:"type:varbinary(36);unique_index;" json:"UID" yaml:"UID"`
|
||||
FileName string `gorm:"type:varbinary(768);unique_index:idx_files_name_root;" json:"Name" yaml:"Name"`
|
||||
FileRoot string `gorm:"type:varbinary(16);default:'originals';unique_index:idx_files_name_root;" json:"Root" yaml:"Root"`
|
||||
FileRoot string `gorm:"type:varbinary(16);default:'';unique_index:idx_files_name_root;" json:"Root" yaml:"Root,omitempty"`
|
||||
OriginalName string `gorm:"type:varbinary(768);" json:"OriginalName" yaml:"OriginalName,omitempty"`
|
||||
FileHash string `gorm:"type:varbinary(128);index" json:"Hash" yaml:"Hash,omitempty"`
|
||||
FileModified time.Time `json:"Modified" yaml:"Modified,omitempty"`
|
||||
|
@ -79,7 +80,7 @@ func FirstFileByHash(fileHash string) (File, error) {
|
|||
|
||||
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
||||
func (m *File) BeforeCreate(scope *gorm.Scope) error {
|
||||
if rnd.IsPPID(m.FileUID, 'f') {
|
||||
if rnd.IsUID(m.FileUID, 'f') {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ import (
|
|||
// Folder represents a file system directory.
|
||||
type Folder struct {
|
||||
Path string `gorm:"type:varbinary(255);unique_index:idx_folders_path_root;" json:"Path" yaml:"Path"`
|
||||
Root string `gorm:"type:varbinary(16);default:'originals';unique_index:idx_folders_path_root;" json:"Root" yaml:"Root"`
|
||||
Root string `gorm:"type:varbinary(16);default:'';unique_index:idx_folders_path_root;" json:"Root" yaml:"Root,omitempty"`
|
||||
FolderUID string `gorm:"type:varbinary(36);primary_key;" json:"UID,omitempty" yaml:"UID,omitempty"`
|
||||
FolderType string `gorm:"type:varbinary(16);" json:"Type" yaml:"Type,omitempty"`
|
||||
FolderTitle string `gorm:"type:varchar(255);" json:"Title" yaml:"Title,omitempty"`
|
||||
|
@ -41,7 +41,7 @@ type Folder struct {
|
|||
|
||||
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
||||
func (m *Folder) BeforeCreate(scope *gorm.Scope) error {
|
||||
if rnd.IsPPID(m.FolderUID, 'd') {
|
||||
if rnd.IsUID(m.FolderUID, 'd') {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -80,7 +80,12 @@ func (m *Folder) SetTitleFromPath() {
|
|||
s = strings.TrimSpace(s)
|
||||
|
||||
if s == "" || s == RootPath {
|
||||
s = m.Root
|
||||
if m.Root == RootDefault {
|
||||
m.FolderTitle = "Originals"
|
||||
return
|
||||
} else {
|
||||
s = m.Root
|
||||
}
|
||||
} else {
|
||||
s = path.Base(s)
|
||||
}
|
||||
|
|
|
@ -8,8 +8,8 @@ import (
|
|||
|
||||
func TestNewFolder(t *testing.T) {
|
||||
t.Run("2020/05", func(t *testing.T) {
|
||||
folder := NewFolder(RootOriginals, "2020/05", nil)
|
||||
assert.Equal(t, RootOriginals, folder.Root)
|
||||
folder := NewFolder(RootDefault, "2020/05", nil)
|
||||
assert.Equal(t, RootDefault, folder.Root)
|
||||
assert.Equal(t, "2020/05", folder.Path)
|
||||
assert.Equal(t, "May 2020", folder.FolderTitle)
|
||||
assert.Equal(t, "", folder.FolderDescription)
|
||||
|
@ -22,7 +22,7 @@ func TestNewFolder(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("/2020/05/01/", func(t *testing.T) {
|
||||
folder := NewFolder(RootOriginals, "/2020/05/01/", nil)
|
||||
folder := NewFolder(RootDefault, "/2020/05/01/", nil)
|
||||
assert.Equal(t, "2020/05/01", folder.Path)
|
||||
assert.Equal(t, "May 2020", folder.FolderTitle)
|
||||
})
|
||||
|
@ -34,19 +34,19 @@ func TestNewFolder(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("/2020/05/23 Birthday", func(t *testing.T) {
|
||||
folder := NewFolder(RootOriginals, "/2020/05/23 Birthday", nil)
|
||||
folder := NewFolder(RootDefault, "/2020/05/23 Birthday", nil)
|
||||
assert.Equal(t, "2020/05/23 Birthday", folder.Path)
|
||||
assert.Equal(t, "23 Birthday", folder.FolderTitle)
|
||||
})
|
||||
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
folder := NewFolder(RootOriginals, "", nil)
|
||||
folder := NewFolder(RootDefault, "", nil)
|
||||
assert.Equal(t, "", folder.Path)
|
||||
assert.Equal(t, "Originals", folder.FolderTitle)
|
||||
})
|
||||
|
||||
t.Run("root", func(t *testing.T) {
|
||||
folder := NewFolder(RootOriginals, RootPath, nil)
|
||||
folder := NewFolder(RootDefault, RootPath, nil)
|
||||
assert.Equal(t, "", folder.Path)
|
||||
assert.Equal(t, "Originals", folder.FolderTitle)
|
||||
})
|
||||
|
|
|
@ -33,7 +33,7 @@ type Label struct {
|
|||
|
||||
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
||||
func (m *Label) BeforeCreate(scope *gorm.Scope) error {
|
||||
if rnd.IsPPID(m.LabelUID, 'l') {
|
||||
if rnd.IsUID(m.LabelUID, 'l') {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
// Photo represents a photo, all its properties, and link to all its images and sidecar files.
|
||||
type Photo struct {
|
||||
ID uint `gorm:"primary_key" yaml:"-"`
|
||||
DocumentID string `gorm:"type:varbinary(36);index;" json:"DocumentID,omitempty" yaml:"DocumentID,omitempty"`
|
||||
TakenAt time.Time `gorm:"type:datetime;index:idx_photos_taken_uid;" json:"TakenAt" yaml:"TakenAt"`
|
||||
TakenAtLocal time.Time `gorm:"type:datetime;" yaml:"-"`
|
||||
TakenSrc string `gorm:"type:varbinary(8);" json:"TakenSrc" yaml:"TakenSrc,omitempty"`
|
||||
|
@ -192,7 +193,7 @@ func (m *Photo) BeforeCreate(scope *gorm.Scope) error {
|
|||
}
|
||||
}
|
||||
|
||||
if rnd.IsPPID(m.PhotoUID, 'p') {
|
||||
if rnd.IsUID(m.PhotoUID, 'p') {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -391,8 +392,9 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
|
|||
knownLocation = true
|
||||
loc := m.Location
|
||||
|
||||
if title := labels.Title(loc.Name()); title != "" { // TODO: User defined title format
|
||||
log.Infof("photo: using label %s to create photo title", txt.Quote(title))
|
||||
// TODO: User defined title format
|
||||
if title := labels.Title(loc.Name()); title != "" {
|
||||
log.Infof("photo: using label %s to create title for %s", txt.Quote(title), m.PhotoUID)
|
||||
if loc.NoCity() || loc.LongCity() || loc.CityContains(title) {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s / %s", txt.Title(title), loc.CountryName(), m.TakenAt.Format("2006")), SrcAuto)
|
||||
} else {
|
||||
|
@ -417,7 +419,7 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
|
|||
knownLocation = true
|
||||
|
||||
if title := labels.Title(""); title != "" {
|
||||
log.Infof("photo: using label %s to create photo title", txt.Quote(title))
|
||||
log.Infof("photo: using label %s to create title for %s", txt.Quote(title), m.PhotoUID)
|
||||
if m.Place.NoCity() || m.Place.LongCity() || m.Place.CityContains(title) {
|
||||
m.SetTitle(fmt.Sprintf("%s / %s / %s", txt.Title(title), m.Place.CountryName(), m.TakenAt.Format("2006")), SrcAuto)
|
||||
} else {
|
||||
|
@ -445,9 +447,9 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
|
|||
m.SetTitle(TitleUnknown, SrcAuto)
|
||||
}
|
||||
|
||||
log.Infof("photo: changed photo title to %s", txt.Quote(m.PhotoTitle))
|
||||
log.Infof("photo: changed title of %s to %s", m.PhotoUID, txt.Quote(m.PhotoTitle))
|
||||
} else {
|
||||
log.Infof("photo: new title is %s", txt.Quote(m.PhotoTitle))
|
||||
log.Infof("photo: new title of %s is %s", m.PhotoUID, txt.Quote(m.PhotoTitle))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -4,7 +4,7 @@ import "github.com/jinzhu/gorm"
|
|||
|
||||
// UpdatePhotoCounts updates photos count in related tables as needed.
|
||||
func UpdatePhotoCounts() error {
|
||||
log.Info("index: updating photo counts")
|
||||
// log.Info("index: updating photo counts")
|
||||
|
||||
if err := Db().Table("places").
|
||||
UpdateColumn("photo_count", gorm.Expr("(SELECT COUNT(*) FROM photos ph "+
|
||||
|
|
|
@ -3,6 +3,8 @@ package meta
|
|||
const CodecUnknown = ""
|
||||
const CodecJpeg = "jpeg"
|
||||
const CodecAvc1 = "avc1"
|
||||
const CodecHeic = "heic"
|
||||
const CodecXMP = "xmp"
|
||||
|
||||
// CodecAvc1 returns true if the video is encoded with H.264/AVC
|
||||
func (data Data) CodecAvc1() bool {
|
||||
|
|
|
@ -7,12 +7,13 @@ import (
|
|||
|
||||
// Data represents image meta data.
|
||||
type Data struct {
|
||||
UniqueID string `meta:"ImageUniqueID"`
|
||||
DocumentID string `meta:"ImageUniqueID,OriginalDocumentID,DocumentID"`
|
||||
InstanceID string `meta:"InstanceID,DocumentID"`
|
||||
TakenAt time.Time `meta:"DateTimeOriginal,CreateDate,MediaCreateDate,DateTimeDigitized,DateTime"`
|
||||
TakenAtLocal time.Time `meta:"DateTimeOriginal,CreateDate,MediaCreateDate,DateTimeDigitized,DateTime"`
|
||||
TimeZone string `meta:"-"`
|
||||
Duration time.Duration `meta:"Duration,MediaDuration,TrackDuration"`
|
||||
Codec string `meta:"CompressorID,Compression"`
|
||||
Codec string `meta:"CompressorID,Compression,FileType"`
|
||||
Title string `meta:"Title"`
|
||||
Subject string `meta:"Subject,PersonInImage"`
|
||||
Keywords string `meta:"Keywords"`
|
||||
|
@ -42,6 +43,7 @@ type Data struct {
|
|||
Height int `meta:"PixelYDimension,ImageHeight,ImageLength,ExifImageHeight,SourceImageHeight"`
|
||||
Orientation int `meta:"-"`
|
||||
Rotation int `meta:"Rotation"`
|
||||
Error error `meta:"-"`
|
||||
All map[string]string
|
||||
}
|
||||
|
||||
|
@ -68,3 +70,18 @@ func (data Data) Portrait() bool {
|
|||
func (data Data) Megapixels() int {
|
||||
return int(math.Round(float64(data.Width*data.Height) / 1000000))
|
||||
}
|
||||
|
||||
// HasDocumentID returns true if a DocumentID exists.
|
||||
func (data Data) HasDocumentID() bool {
|
||||
return len(data.DocumentID) >= 15
|
||||
}
|
||||
|
||||
// HasInstanceID returns true if an InstanceID exists.
|
||||
func (data Data) HasInstanceID() bool {
|
||||
return len(data.InstanceID) >= 15
|
||||
}
|
||||
|
||||
// HasTimeAndPlace if data contains a time and gps position.
|
||||
func (data Data) HasTimeAndPlace() bool {
|
||||
return !data.TakenAt.IsZero() && data.Lat != 0 && data.Lng != 0
|
||||
}
|
||||
|
|
|
@ -25,12 +25,6 @@ func ValidDateTime(s string) bool {
|
|||
return len(s) == len(DateTimeZero) && s != DateTimeZero
|
||||
}
|
||||
|
||||
// SanitizeString removes unwanted character from an exif value string.
|
||||
func SanitizeString(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
return strings.Replace(value, "\"", "", -1)
|
||||
}
|
||||
|
||||
// Exif parses an image file for Exif meta data and returns as Data struct.
|
||||
func Exif(fileName string) (data Data, err error) {
|
||||
err = data.Exif(fileName)
|
||||
|
@ -254,7 +248,7 @@ func (data *Data) Exif(fileName string) (err error) {
|
|||
}
|
||||
|
||||
if value, ok := tags["ImageUniqueID"]; ok {
|
||||
data.UniqueID = value
|
||||
data.DocumentID = SanitizeUID(value)
|
||||
}
|
||||
|
||||
if value, ok := tags["PixelXDimension"]; ok {
|
||||
|
|
|
@ -144,5 +144,15 @@ func (data *Data) JSON(fileName string) (err error) {
|
|||
data.Codec = CodecJpeg
|
||||
}
|
||||
|
||||
// Validate and normalize optional DocumentID.
|
||||
if len(data.DocumentID) > 0 {
|
||||
data.DocumentID = SanitizeUID(data.DocumentID)
|
||||
}
|
||||
|
||||
// Validate and normalize optional InstanceID.
|
||||
if len(data.InstanceID) > 0 {
|
||||
data.InstanceID = SanitizeUID(data.InstanceID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -128,7 +128,7 @@ func TestJSON(t *testing.T) {
|
|||
|
||||
// t.Logf("DATA: %+v", data)
|
||||
|
||||
assert.Equal(t, CodecUnknown, data.Codec)
|
||||
assert.Equal(t, CodecXMP, data.Codec)
|
||||
assert.Equal(t, "0s", data.Duration.String())
|
||||
assert.Equal(t, float32(52.45969), data.Lat)
|
||||
assert.Equal(t, float32(13.321831), data.Lng)
|
||||
|
@ -212,7 +212,7 @@ func TestJSON(t *testing.T) {
|
|||
|
||||
// t.Logf("DATA: %+v", data)
|
||||
|
||||
assert.Equal(t, CodecUnknown, data.Codec)
|
||||
assert.Equal(t, CodecHeic, data.Codec)
|
||||
assert.Equal(t, "", data.Title)
|
||||
assert.Equal(t, "", data.Artist)
|
||||
assert.Equal(t, "", data.Description)
|
||||
|
@ -223,4 +223,78 @@ func TestJSON(t *testing.T) {
|
|||
assert.Equal(t, "iPhone 7 back camera 3.99mm f/1.8", data.LensModel)
|
||||
})
|
||||
|
||||
t.Run("uuid-original.json", func(t *testing.T) {
|
||||
data, err := JSON("testdata/uuid-original.json")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// t.Logf("DATA: %+v", data)
|
||||
|
||||
assert.Equal(t, "9bafc58c-6c82-4e66-a45f-c13f915f99c5", data.DocumentID)
|
||||
assert.Equal(t, "", data.InstanceID)
|
||||
assert.Equal(t, CodecJpeg, data.Codec)
|
||||
assert.Equal(t, "0s", data.Duration.String())
|
||||
assert.Equal(t, "2018-12-06 12:32:26 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2018-12-06 11:32:26 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, "Europe/Berlin", data.TimeZone)
|
||||
assert.Equal(t, 3024, data.Width)
|
||||
assert.Equal(t, 4032, data.Height)
|
||||
assert.Equal(t, float32(48.300003), data.Lat)
|
||||
assert.Equal(t, float32(8.929067), data.Lng)
|
||||
assert.Equal(t, "Apple", data.CameraMake)
|
||||
assert.Equal(t, "iPhone SE", data.CameraModel)
|
||||
assert.Equal(t, "iPhone SE back camera 4.15mm f/2.2", data.LensModel)
|
||||
})
|
||||
|
||||
t.Run("uuid-copy.json", func(t *testing.T) {
|
||||
data, err := JSON("testdata/uuid-copy.json")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// t.Logf("DATA: %+v", data)
|
||||
|
||||
assert.Equal(t, "4b1fef2d1cf4a5be38b263e0637edead", data.DocumentID)
|
||||
assert.Equal(t, "dafbfeb8-a129-4e7c-9cf0-e7996a701cdb", data.InstanceID)
|
||||
assert.Equal(t, CodecJpeg, data.Codec)
|
||||
assert.Equal(t, "0s", data.Duration.String())
|
||||
assert.Equal(t, "2018-12-06 12:32:26 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2018-12-06 11:32:26 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, "Europe/Berlin", data.TimeZone)
|
||||
assert.Equal(t, 1024, data.Width)
|
||||
assert.Equal(t, 1365, data.Height)
|
||||
assert.Equal(t, float32(48.300003), data.Lat)
|
||||
assert.Equal(t, float32(8.929067), data.Lng)
|
||||
assert.Equal(t, "Apple", data.CameraMake)
|
||||
assert.Equal(t, "iPhone SE", data.CameraModel)
|
||||
assert.Equal(t, "iPhone SE back camera 4.15mm f/2.2", data.LensModel)
|
||||
})
|
||||
|
||||
t.Run("uuid-imagemagick.json", func(t *testing.T) {
|
||||
data, err := JSON("testdata/uuid-imagemagick.json")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// t.Logf("DATA: %+v", data)
|
||||
|
||||
assert.Equal(t, "9bafc58c-6c82-4e66-a45f-c13f915f99c5", data.DocumentID)
|
||||
assert.Equal(t, "", data.InstanceID)
|
||||
assert.Equal(t, CodecJpeg, data.Codec)
|
||||
assert.Equal(t, "0s", data.Duration.String())
|
||||
assert.Equal(t, "2018-12-06 12:32:26 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2018-12-06 11:32:26 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, "Europe/Berlin", data.TimeZone)
|
||||
assert.Equal(t, 1125, data.Width)
|
||||
assert.Equal(t, 1500, data.Height)
|
||||
assert.Equal(t, float32(48.300003), data.Lat)
|
||||
assert.Equal(t, float32(8.929067), data.Lng)
|
||||
assert.Equal(t, "Apple", data.CameraMake)
|
||||
assert.Equal(t, "iPhone SE", data.CameraModel)
|
||||
assert.Equal(t, "iPhone SE back camera 4.15mm f/2.2", data.LensModel)
|
||||
})
|
||||
}
|
||||
|
|
27
internal/meta/sanitize.go
Normal file
27
internal/meta/sanitize.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package meta
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SanitizeString removes unwanted character from an exif value string.
|
||||
func SanitizeString(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
return strings.Replace(value, "\"", "", -1)
|
||||
}
|
||||
|
||||
// SanitizeUID normalizes unique IDs found in XMP or Exif metadata.
|
||||
func SanitizeUID(value string) string {
|
||||
value = SanitizeString(value)
|
||||
|
||||
if start := strings.LastIndex(value, ":"); start != -1 {
|
||||
value = value[start+1:]
|
||||
}
|
||||
|
||||
// Not a unique ID?
|
||||
if len(value) < 15 || len(value) > 36 {
|
||||
value = ""
|
||||
}
|
||||
|
||||
return strings.ToLower(value)
|
||||
}
|
179
internal/meta/testdata/uuid-copy.json
vendored
Normal file
179
internal/meta/testdata/uuid-copy.json
vendored
Normal file
|
@ -0,0 +1,179 @@
|
|||
[{
|
||||
"SourceFile": "20181206-123226-Toy-Bisingen-2018-copy.jpg",
|
||||
"ExifToolVersion": 11.85,
|
||||
"FileName": "20181206-123226-Toy-Bisingen-2018-copy.jpg",
|
||||
"Directory": ".",
|
||||
"FileSize": "242 kB",
|
||||
"FileModifyDate": "2020:05:27 10:32:16+02:00",
|
||||
"FileAccessDate": "2020:05:27 10:32:18+02:00",
|
||||
"FileInodeChangeDate": "2020:05:27 10:32:16+02:00",
|
||||
"FilePermissions": "rw-r--r--",
|
||||
"FileType": "JPEG",
|
||||
"FileTypeExtension": "jpg",
|
||||
"MIMEType": "image/jpeg",
|
||||
"ExifByteOrder": "Big-endian (Motorola, MM)",
|
||||
"PhotometricInterpretation": "RGB",
|
||||
"Make": "Apple",
|
||||
"Model": "iPhone SE",
|
||||
"Orientation": "Horizontal (normal)",
|
||||
"SamplesPerPixel": 3,
|
||||
"XResolution": 72,
|
||||
"YResolution": 72,
|
||||
"ResolutionUnit": "inches",
|
||||
"Software": "Adobe Photoshop 21.1 (Macintosh)",
|
||||
"ModifyDate": "2020:05:27 10:32:12",
|
||||
"ExposureTime": "1/185",
|
||||
"FNumber": 2.2,
|
||||
"ExposureProgram": "Program AE",
|
||||
"ISO": 25,
|
||||
"ExifVersion": "0221",
|
||||
"DateTimeOriginal": "2018:12:06 12:32:26",
|
||||
"CreateDate": "2018:12:06 12:32:26",
|
||||
"ComponentsConfiguration": "Y, Cb, Cr, -",
|
||||
"ShutterSpeedValue": "1/185",
|
||||
"ApertureValue": 2.2,
|
||||
"BrightnessValue": 6.784544005,
|
||||
"ExposureCompensation": 0,
|
||||
"MeteringMode": "Multi-segment",
|
||||
"Flash": "Off, Did not fire",
|
||||
"FocalLength": "4.2 mm",
|
||||
"SubjectArea": "2015 1511 2217 1330",
|
||||
"SubSecTimeOriginal": 600,
|
||||
"SubSecTimeDigitized": 600,
|
||||
"FlashpixVersion": "0100",
|
||||
"ColorSpace": "sRGB",
|
||||
"ExifImageWidth": 1024,
|
||||
"ExifImageHeight": 1365,
|
||||
"SensingMethod": "One-chip color area",
|
||||
"SceneType": "Directly photographed",
|
||||
"CustomRendered": "HDR (original saved)",
|
||||
"ExposureMode": "Auto",
|
||||
"WhiteBalance": "Auto",
|
||||
"FocalLengthIn35mmFormat": "29 mm",
|
||||
"SceneCaptureType": "Standard",
|
||||
"LensInfo": "4.150000095mm f/2.2",
|
||||
"LensMake": "Apple",
|
||||
"LensModel": "iPhone SE back camera 4.15mm f/2.2",
|
||||
"GPSLatitudeRef": "North",
|
||||
"GPSLongitudeRef": "East",
|
||||
"GPSAltitudeRef": "Above Sea Level",
|
||||
"GPSTimeStamp": "11:32:25",
|
||||
"GPSSpeedRef": "km/h",
|
||||
"GPSSpeed": 0,
|
||||
"GPSImgDirectionRef": "True North",
|
||||
"GPSImgDirection": 105.7627258,
|
||||
"GPSDestBearingRef": "True North",
|
||||
"GPSDestBearing": 105.7627258,
|
||||
"GPSDateStamp": "2018:12:06",
|
||||
"GPSHPositioningError": "5 m",
|
||||
"Compression": "JPEG (old-style)",
|
||||
"ThumbnailOffset": 1274,
|
||||
"ThumbnailLength": 7386,
|
||||
"CurrentIPTCDigest": "c166ecde819b2c0da5a3082cbe444b5c",
|
||||
"ApplicationRecordVersion": 0,
|
||||
"TimeCreated": "12:32:26",
|
||||
"IPTCDigest": "c166ecde819b2c0da5a3082cbe444b5c",
|
||||
"DisplayedUnitsX": "inches",
|
||||
"DisplayedUnitsY": "inches",
|
||||
"PrintStyle": "Centered",
|
||||
"PrintPosition": "0 0",
|
||||
"PrintScale": 1,
|
||||
"GlobalAngle": 30,
|
||||
"GlobalAltitude": 30,
|
||||
"URL_List": [],
|
||||
"SlicesGroupName": "20181206-123226-Toy-Bisingen-2018-1du",
|
||||
"NumSlices": 1,
|
||||
"PixelAspectRatio": 1,
|
||||
"PhotoshopThumbnail": "(Binary data 7386 bytes, use -b option to extract)",
|
||||
"HasRealMergedData": "Yes",
|
||||
"WriterName": "Adobe Photoshop",
|
||||
"ReaderName": "Adobe Photoshop 2020",
|
||||
"PhotoshopQuality": 5,
|
||||
"PhotoshopFormat": "Progressive",
|
||||
"ProgressiveScans": "3 Scans",
|
||||
"XMPToolkit": "Adobe XMP Core 6.0-c002 79.164352, 2020/01/30-15:50:38 ",
|
||||
"CreatorTool": 12.1,
|
||||
"MetadataDate": "2020:05:27 10:32:12+02:00",
|
||||
"DateCreated": "2018:12:06 12:32:26",
|
||||
"LegacyIPTCDigest": "D41D8CD98F00B204E9800998ECF8427E",
|
||||
"ColorMode": "RGB",
|
||||
"ICCProfileName": "sRGB IEC61966-2.1",
|
||||
"Lens": "iPhone SE back camera 4.15mm f/2.2",
|
||||
"DocumentID": "adobe:docid:photoshop:59836323-aab2-2049-9a3d-bb9c2455a265",
|
||||
"InstanceID": "xmp.iid:dafbfeb8-a129-4e7c-9cf0-e7996a701cdb",
|
||||
"OriginalDocumentID": "4B1FEF2D1CF4A5BE38B263E0637EDEAD",
|
||||
"Format": "image/jpeg",
|
||||
"HistoryAction": ["saved","saved"],
|
||||
"HistoryInstanceID": ["xmp.iid:74b46793-af66-441d-8c03-985a5792dbeb","xmp.iid:dafbfeb8-a129-4e7c-9cf0-e7996a701cdb"],
|
||||
"HistoryWhen": ["2020:05:27 10:32:12+02:00","2020:05:27 10:32:12+02:00"],
|
||||
"HistorySoftwareAgent": ["Adobe Photoshop 21.1 (Macintosh)","Adobe Photoshop 21.1 (Macintosh)"],
|
||||
"HistoryChanged": ["/","/"],
|
||||
"ProfileCMMType": "Linotronic",
|
||||
"ProfileVersion": "2.1.0",
|
||||
"ProfileClass": "Display Device Profile",
|
||||
"ColorSpaceData": "RGB ",
|
||||
"ProfileConnectionSpace": "XYZ ",
|
||||
"ProfileDateTime": "1998:02:09 06:49:00",
|
||||
"ProfileFileSignature": "acsp",
|
||||
"PrimaryPlatform": "Microsoft Corporation",
|
||||
"CMMFlags": "Not Embedded, Independent",
|
||||
"DeviceManufacturer": "Hewlett-Packard",
|
||||
"DeviceModel": "sRGB",
|
||||
"DeviceAttributes": "Reflective, Glossy, Positive, Color",
|
||||
"RenderingIntent": "Perceptual",
|
||||
"ConnectionSpaceIlluminant": "0.9642 1 0.82491",
|
||||
"ProfileCreator": "Hewlett-Packard",
|
||||
"ProfileID": 0,
|
||||
"ProfileCopyright": "Copyright (c) 1998 Hewlett-Packard Company",
|
||||
"ProfileDescription": "sRGB IEC61966-2.1",
|
||||
"MediaWhitePoint": "0.95045 1 1.08905",
|
||||
"MediaBlackPoint": "0 0 0",
|
||||
"RedMatrixColumn": "0.43607 0.22249 0.01392",
|
||||
"GreenMatrixColumn": "0.38515 0.71687 0.09708",
|
||||
"BlueMatrixColumn": "0.14307 0.06061 0.7141",
|
||||
"DeviceMfgDesc": "IEC http://www.iec.ch",
|
||||
"DeviceModelDesc": "IEC 61966-2.1 Default RGB colour space - sRGB",
|
||||
"ViewingCondDesc": "Reference Viewing Condition in IEC61966-2.1",
|
||||
"ViewingCondIlluminant": "19.6445 20.3718 16.8089",
|
||||
"ViewingCondSurround": "3.92889 4.07439 3.36179",
|
||||
"ViewingCondIlluminantType": "D50",
|
||||
"Luminance": "76.03647 80 87.12462",
|
||||
"MeasurementObserver": "CIE 1931",
|
||||
"MeasurementBacking": "0 0 0",
|
||||
"MeasurementGeometry": "Unknown",
|
||||
"MeasurementFlare": "0.999%",
|
||||
"MeasurementIlluminant": "D65",
|
||||
"Technology": "Cathode Ray Tube Display",
|
||||
"RedTRC": "(Binary data 2060 bytes, use -b option to extract)",
|
||||
"GreenTRC": "(Binary data 2060 bytes, use -b option to extract)",
|
||||
"BlueTRC": "(Binary data 2060 bytes, use -b option to extract)",
|
||||
"DCTEncodeVersion": 100,
|
||||
"APP14Flags0": "Encoded with Blend=1 downsampling",
|
||||
"APP14Flags1": "(none)",
|
||||
"ColorTransform": "YCbCr",
|
||||
"ImageWidth": 1024,
|
||||
"ImageHeight": 1365,
|
||||
"EncodingProcess": "Progressive DCT, Huffman coding",
|
||||
"BitsPerSample": 8,
|
||||
"ColorComponents": 3,
|
||||
"YCbCrSubSampling": "YCbCr4:2:0 (2 2)",
|
||||
"Aperture": 2.2,
|
||||
"ImageSize": "1024x1365",
|
||||
"Megapixels": 1.4,
|
||||
"ScaleFactor35efl": 7.0,
|
||||
"ShutterSpeed": "1/185",
|
||||
"SubSecCreateDate": "2018:12:06 12:32:26.600",
|
||||
"SubSecDateTimeOriginal": "2018:12:06 12:32:26.600",
|
||||
"ThumbnailImage": "(Binary data 7386 bytes, use -b option to extract)",
|
||||
"GPSAltitude": "591.3 m Above Sea Level",
|
||||
"GPSDateTime": "2018:12:06 11:32:25Z",
|
||||
"GPSLatitude": "48 deg 18' 0.01\" N",
|
||||
"GPSLongitude": "8 deg 55' 44.64\" E",
|
||||
"DateTimeCreated": "2018:12:06 12:32:26",
|
||||
"CircleOfConfusion": "0.004 mm",
|
||||
"FOV": "63.7 deg",
|
||||
"FocalLength35efl": "4.2 mm (35 mm equivalent: 29.0 mm)",
|
||||
"GPSPosition": "48 deg 18' 0.01\" N, 8 deg 55' 44.64\" E",
|
||||
"HyperfocalDistance": "1.82 m",
|
||||
"LightValue": 11.8
|
||||
}]
|
103
internal/meta/testdata/uuid-imagemagick.json
vendored
Normal file
103
internal/meta/testdata/uuid-imagemagick.json
vendored
Normal file
|
@ -0,0 +1,103 @@
|
|||
[{
|
||||
"SourceFile": "20181206-123226-Toy-Bisingen-2018-imagemagick.jpg",
|
||||
"ExifToolVersion": 11.85,
|
||||
"FileName": "20181206-123226-Toy-Bisingen-2018-imagemagick.jpg",
|
||||
"Directory": ".",
|
||||
"FileSize": "545 kB",
|
||||
"FileModifyDate": "2020:05:27 11:02:01+02:00",
|
||||
"FileAccessDate": "2020:05:27 11:02:01+02:00",
|
||||
"FileInodeChangeDate": "2020:05:27 11:02:01+02:00",
|
||||
"FilePermissions": "rw-r--r--",
|
||||
"FileType": "JPEG",
|
||||
"FileTypeExtension": "jpg",
|
||||
"MIMEType": "image/jpeg",
|
||||
"JFIFVersion": 1.01,
|
||||
"CurrentIPTCDigest": "d41d8cd98f00b204e9800998ecf8427e",
|
||||
"IPTCDigest": "d41d8cd98f00b204e9800998ecf8427e",
|
||||
"ExifByteOrder": "Big-endian (Motorola, MM)",
|
||||
"Make": "Apple",
|
||||
"Model": "iPhone SE",
|
||||
"XResolution": 72,
|
||||
"YResolution": 72,
|
||||
"ResolutionUnit": "inches",
|
||||
"Software": 12.1,
|
||||
"ModifyDate": "2018:12:06 12:32:26",
|
||||
"ExposureTime": "1/185",
|
||||
"FNumber": 2.2,
|
||||
"ExposureProgram": "Program AE",
|
||||
"ISO": 25,
|
||||
"ExifVersion": "0221",
|
||||
"DateTimeOriginal": "2018:12:06 12:32:26",
|
||||
"CreateDate": "2018:12:06 12:32:26",
|
||||
"ComponentsConfiguration": "Y, Cb, Cr, -",
|
||||
"ShutterSpeedValue": "1/185",
|
||||
"ApertureValue": 2.2,
|
||||
"BrightnessValue": 6.784544005,
|
||||
"ExposureCompensation": 0,
|
||||
"MeteringMode": "Multi-segment",
|
||||
"Flash": "Off, Did not fire",
|
||||
"FocalLength": "4.2 mm",
|
||||
"SubjectArea": "2015 1511 2217 1330",
|
||||
"RunTimeFlags": "Valid",
|
||||
"RunTimeValue": 540640445748916,
|
||||
"RunTimeScale": 1000000000,
|
||||
"RunTimeEpoch": 0,
|
||||
"AccelerationVector": "0.04416524988 0.9474721547 -0.2289867999",
|
||||
"HDRImageType": "HDR Image",
|
||||
"ImageUniqueID": "9BAFC58C-6C82-4E66-A45F-C13F915F99C5",
|
||||
"SubSecTimeOriginal": 600,
|
||||
"SubSecTimeDigitized": 600,
|
||||
"FlashpixVersion": "0100",
|
||||
"ColorSpace": "sRGB",
|
||||
"ExifImageWidth": 3024,
|
||||
"ExifImageHeight": 4032,
|
||||
"SensingMethod": "One-chip color area",
|
||||
"SceneType": "Directly photographed",
|
||||
"CustomRendered": "HDR (original saved)",
|
||||
"ExposureMode": "Auto",
|
||||
"WhiteBalance": "Auto",
|
||||
"FocalLengthIn35mmFormat": "29 mm",
|
||||
"SceneCaptureType": "Standard",
|
||||
"LensInfo": "4.150000095mm f/2.2",
|
||||
"LensMake": "Apple",
|
||||
"LensModel": "iPhone SE back camera 4.15mm f/2.2",
|
||||
"GPSLatitudeRef": "North",
|
||||
"GPSLongitudeRef": "East",
|
||||
"GPSAltitudeRef": "Above Sea Level",
|
||||
"GPSTimeStamp": "11:32:25",
|
||||
"GPSSpeedRef": "km/h",
|
||||
"GPSSpeed": 0,
|
||||
"GPSImgDirectionRef": "True North",
|
||||
"GPSImgDirection": 105.7627258,
|
||||
"GPSDestBearingRef": "True North",
|
||||
"GPSDestBearing": 105.7627258,
|
||||
"GPSDateStamp": "2018:12:06",
|
||||
"GPSHPositioningError": "5 m",
|
||||
"XMPToolkit": "XMP Core 5.4.0",
|
||||
"CreatorTool": 12.1,
|
||||
"DateCreated": "2018:12:06 12:32:26",
|
||||
"ImageWidth": 1125,
|
||||
"ImageHeight": 1500,
|
||||
"EncodingProcess": "Baseline DCT, Huffman coding",
|
||||
"BitsPerSample": 8,
|
||||
"ColorComponents": 3,
|
||||
"YCbCrSubSampling": "YCbCr4:2:0 (2 2)",
|
||||
"RunTimeSincePowerUp": "6 days 6:10:40",
|
||||
"Aperture": 2.2,
|
||||
"ImageSize": "1125x1500",
|
||||
"Megapixels": 1.7,
|
||||
"ScaleFactor35efl": 7.0,
|
||||
"ShutterSpeed": "1/185",
|
||||
"SubSecCreateDate": "2018:12:06 12:32:26.600",
|
||||
"SubSecDateTimeOriginal": "2018:12:06 12:32:26.600",
|
||||
"GPSAltitude": "591.3 m Above Sea Level",
|
||||
"GPSDateTime": "2018:12:06 11:32:25Z",
|
||||
"GPSLatitude": "48 deg 18' 0.01\" N",
|
||||
"GPSLongitude": "8 deg 55' 44.64\" E",
|
||||
"CircleOfConfusion": "0.004 mm",
|
||||
"FOV": "63.7 deg",
|
||||
"FocalLength35efl": "4.2 mm (35 mm equivalent: 29.0 mm)",
|
||||
"GPSPosition": "48 deg 18' 0.01\" N, 8 deg 55' 44.64\" E",
|
||||
"HyperfocalDistance": "1.82 m",
|
||||
"LightValue": 11.8
|
||||
}]
|
103
internal/meta/testdata/uuid-original.json
vendored
Normal file
103
internal/meta/testdata/uuid-original.json
vendored
Normal file
|
@ -0,0 +1,103 @@
|
|||
[{
|
||||
"SourceFile": "20181206-123226-Toy-Bisingen-2018-1du.jpg",
|
||||
"ExifToolVersion": 11.85,
|
||||
"FileName": "20181206-123226-Toy-Bisingen-2018-1du.jpg",
|
||||
"Directory": ".",
|
||||
"FileSize": "4.1 MB",
|
||||
"FileModifyDate": "2020:05:27 10:29:23+02:00",
|
||||
"FileAccessDate": "2020:05:27 10:31:20+02:00",
|
||||
"FileInodeChangeDate": "2020:05:27 10:31:17+02:00",
|
||||
"FilePermissions": "rw-r--r--",
|
||||
"FileType": "JPEG",
|
||||
"FileTypeExtension": "jpg",
|
||||
"MIMEType": "image/jpeg",
|
||||
"JFIFVersion": 1.01,
|
||||
"ExifByteOrder": "Big-endian (Motorola, MM)",
|
||||
"Make": "Apple",
|
||||
"Model": "iPhone SE",
|
||||
"XResolution": 72,
|
||||
"YResolution": 72,
|
||||
"ResolutionUnit": "inches",
|
||||
"Software": 12.1,
|
||||
"ModifyDate": "2018:12:06 12:32:26",
|
||||
"ExposureTime": "1/185",
|
||||
"FNumber": 2.2,
|
||||
"ExposureProgram": "Program AE",
|
||||
"ISO": 25,
|
||||
"ExifVersion": "0221",
|
||||
"DateTimeOriginal": "2018:12:06 12:32:26",
|
||||
"CreateDate": "2018:12:06 12:32:26",
|
||||
"ComponentsConfiguration": "Y, Cb, Cr, -",
|
||||
"ShutterSpeedValue": "1/185",
|
||||
"ApertureValue": 2.2,
|
||||
"BrightnessValue": 6.784544005,
|
||||
"ExposureCompensation": 0,
|
||||
"MeteringMode": "Multi-segment",
|
||||
"Flash": "Off, Did not fire",
|
||||
"FocalLength": "4.2 mm",
|
||||
"SubjectArea": "2015 1511 2217 1330",
|
||||
"RunTimeFlags": "Valid",
|
||||
"RunTimeValue": 540640445748916,
|
||||
"RunTimeScale": 1000000000,
|
||||
"RunTimeEpoch": 0,
|
||||
"AccelerationVector": "0.04416524988 0.9474721547 -0.2289867999",
|
||||
"HDRImageType": "HDR Image",
|
||||
"ImageUniqueID": "9BAFC58C-6C82-4E66-A45F-C13F915F99C5",
|
||||
"SubSecTimeOriginal": 600,
|
||||
"SubSecTimeDigitized": 600,
|
||||
"FlashpixVersion": "0100",
|
||||
"ColorSpace": "sRGB",
|
||||
"ExifImageWidth": 3024,
|
||||
"ExifImageHeight": 4032,
|
||||
"SensingMethod": "One-chip color area",
|
||||
"SceneType": "Directly photographed",
|
||||
"CustomRendered": "HDR (original saved)",
|
||||
"ExposureMode": "Auto",
|
||||
"WhiteBalance": "Auto",
|
||||
"FocalLengthIn35mmFormat": "29 mm",
|
||||
"SceneCaptureType": "Standard",
|
||||
"LensInfo": "4.150000095mm f/2.2",
|
||||
"LensMake": "Apple",
|
||||
"LensModel": "iPhone SE back camera 4.15mm f/2.2",
|
||||
"GPSLatitudeRef": "North",
|
||||
"GPSLongitudeRef": "East",
|
||||
"GPSAltitudeRef": "Above Sea Level",
|
||||
"GPSTimeStamp": "11:32:25",
|
||||
"GPSSpeedRef": "km/h",
|
||||
"GPSSpeed": 0,
|
||||
"GPSImgDirectionRef": "True North",
|
||||
"GPSImgDirection": 105.7627258,
|
||||
"GPSDestBearingRef": "True North",
|
||||
"GPSDestBearing": 105.7627258,
|
||||
"GPSDateStamp": "2018:12:06",
|
||||
"GPSHPositioningError": "5 m",
|
||||
"XMPToolkit": "XMP Core 5.4.0",
|
||||
"CreatorTool": 12.1,
|
||||
"DateCreated": "2018:12:06 12:32:26",
|
||||
"CurrentIPTCDigest": "d41d8cd98f00b204e9800998ecf8427e",
|
||||
"IPTCDigest": "d41d8cd98f00b204e9800998ecf8427e",
|
||||
"ImageWidth": 3024,
|
||||
"ImageHeight": 4032,
|
||||
"EncodingProcess": "Baseline DCT, Huffman coding",
|
||||
"BitsPerSample": 8,
|
||||
"ColorComponents": 3,
|
||||
"YCbCrSubSampling": "YCbCr4:2:0 (2 2)",
|
||||
"RunTimeSincePowerUp": "6 days 6:10:40",
|
||||
"Aperture": 2.2,
|
||||
"ImageSize": "3024x4032",
|
||||
"Megapixels": 12.2,
|
||||
"ScaleFactor35efl": 7.0,
|
||||
"ShutterSpeed": "1/185",
|
||||
"SubSecCreateDate": "2018:12:06 12:32:26.600",
|
||||
"SubSecDateTimeOriginal": "2018:12:06 12:32:26.600",
|
||||
"GPSAltitude": "591.3 m Above Sea Level",
|
||||
"GPSDateTime": "2018:12:06 11:32:25Z",
|
||||
"GPSLatitude": "48 deg 18' 0.01\" N",
|
||||
"GPSLongitude": "8 deg 55' 44.64\" E",
|
||||
"CircleOfConfusion": "0.004 mm",
|
||||
"FOV": "63.7 deg",
|
||||
"FocalLength35efl": "4.2 mm (35 mm equivalent: 29.0 mm)",
|
||||
"GPSPosition": "48 deg 18' 0.01\" N, 8 deg 55' 44.64\" E",
|
||||
"HyperfocalDistance": "1.82 m",
|
||||
"LightValue": 11.8
|
||||
}]
|
|
@ -49,13 +49,7 @@ func TestConvert_ToJpeg(t *testing.T) {
|
|||
assert.Equal(t, jpegFile.FileName(), outputName)
|
||||
assert.Truef(t, fs.FileExists(jpegFile.FileName()), "output file does not exist: %s", jpegFile.FileName())
|
||||
|
||||
metaData, err := jpegFile.MetaData()
|
||||
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
} else {
|
||||
t.Logf("video metadata: %+v", metaData)
|
||||
}
|
||||
t.Logf("video metadata: %+v", jpegFile.MetaData())
|
||||
|
||||
_ = os.Remove(outputName)
|
||||
})
|
||||
|
@ -79,11 +73,7 @@ func TestConvert_ToJpeg(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
infoJpeg, err := imageJpeg.MetaData()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("%s for %s", err.Error(), imageJpeg.FileName())
|
||||
}
|
||||
infoJpeg := imageJpeg.MetaData()
|
||||
|
||||
assert.Equal(t, jpegFilename, imageJpeg.fileName)
|
||||
|
||||
|
@ -113,7 +103,7 @@ func TestConvert_ToJpeg(t *testing.T) {
|
|||
|
||||
assert.NotEqual(t, rawFilename, imageRaw.fileName)
|
||||
|
||||
infoRaw, err := imageRaw.MetaData()
|
||||
infoRaw := imageRaw.MetaData()
|
||||
|
||||
assert.Equal(t, "Canon EOS 6D", infoRaw.CameraModel)
|
||||
})
|
||||
|
@ -246,7 +236,7 @@ func TestConvert_Start(t *testing.T) {
|
|||
|
||||
assert.Equal(t, jpegFilename, image.fileName, "FileName must be the same")
|
||||
|
||||
infoRaw, err := image.MetaData()
|
||||
infoRaw := image.MetaData()
|
||||
|
||||
assert.Equal(t, "Canon EOS 6D", infoRaw.CameraModel, "UpdateCamera model should be Canon EOS M10")
|
||||
|
||||
|
@ -254,9 +244,11 @@ func TestConvert_Start(t *testing.T) {
|
|||
|
||||
oldHash := fs.Hash(existingJpegFilename)
|
||||
|
||||
os.Remove(existingJpegFilename)
|
||||
_ = os.Remove(existingJpegFilename)
|
||||
|
||||
convert.Start(conf.ImportPath())
|
||||
if err := convert.Start(conf.ImportPath()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
newHash := fs.Hash(existingJpegFilename)
|
||||
|
||||
|
|
|
@ -121,7 +121,7 @@ func (ind *Index) Start(opt IndexOptions) map[string]bool {
|
|||
|
||||
if skip, result := fs.SkipWalk(fileName, isDir, isSymlink, done, ignore); skip {
|
||||
if isDir && result != filepath.SkipDir {
|
||||
folder := entity.NewFolder(entity.RootOriginals, fs.RelativeName(fileName, originalsPath), nil)
|
||||
folder := entity.NewFolder(entity.RootDefault, fs.RelativeName(fileName, originalsPath), nil)
|
||||
|
||||
if err := folder.Create(); err == nil {
|
||||
log.Infof("index: added folder /%s", folder.Path)
|
||||
|
|
|
@ -70,8 +70,9 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
|
||||
fileBase := m.Base(ind.conf.Settings().Index.Group)
|
||||
filePath := m.RelativePath(ind.originalsPath())
|
||||
fileRoot := entity.RootOriginals
|
||||
fileRoot := entity.RootDefault
|
||||
fileName := m.RelativeName(ind.originalsPath())
|
||||
quotedName := txt.Quote(m.RelativeName(ind.originalsPath()))
|
||||
fileHash := ""
|
||||
fileSize, fileModified := m.Stat()
|
||||
fileChanged := true
|
||||
|
@ -82,6 +83,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
"fileHash": fileHash,
|
||||
"fileSize": fileSize,
|
||||
"fileName": fileName,
|
||||
"fileRoot": fileRoot,
|
||||
"baseName": filepath.Base(fileName),
|
||||
})
|
||||
|
||||
|
@ -97,15 +99,24 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
result.Status = IndexDuplicate
|
||||
return result
|
||||
}
|
||||
|
||||
if !fileExists && m.MetaData().HasInstanceID(){
|
||||
fileQuery = ind.db.Unscoped().First(&file, "instance_id = ?", m.MetaData().InstanceID)
|
||||
fileExists = fileQuery.Error == nil
|
||||
}
|
||||
}
|
||||
|
||||
if !fileExists {
|
||||
photoQuery = ind.db.Unscoped().First(&photo, "photo_path = ? AND photo_name = ?", filePath, fileBase)
|
||||
|
||||
if photoQuery.Error != nil && m.HasTimeAndPlace() {
|
||||
metaData, _ = m.MetaData()
|
||||
if photoQuery.Error != nil && m.MetaData().HasTimeAndPlace() {
|
||||
metaData = m.MetaData()
|
||||
photoQuery = ind.db.Unscoped().First(&photo, "photo_lat = ? AND photo_lng = ? AND taken_at = ?", metaData.Lat, metaData.Lng, metaData.TakenAt)
|
||||
}
|
||||
|
||||
if photoQuery.Error != nil && m.MetaData().HasDocumentID() {
|
||||
photoQuery = ind.db.Unscoped().First(&photo, "document_id = ?", m.MetaData().DocumentID)
|
||||
}
|
||||
} else {
|
||||
photoQuery = ind.db.Unscoped().First(&photo, "id = ?", file.PhotoID)
|
||||
|
||||
|
@ -160,7 +171,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
case m.IsJpeg():
|
||||
// Color information
|
||||
if p, err := m.Colors(ind.thumbPath()); err != nil {
|
||||
log.Errorf("index: %s for %s", err.Error(), txt.Quote(m.RelativeName(ind.originalsPath())))
|
||||
log.Errorf("index: %s for %s", err.Error(), quotedName)
|
||||
} else {
|
||||
file.FileMainColor = p.MainColor.Name()
|
||||
file.FileColors = p.Colors.Hex()
|
||||
|
@ -204,7 +215,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
photo.PhotoType = entity.TypeRaw
|
||||
}
|
||||
case m.IsVideo():
|
||||
metaData, _ = m.MetaData()
|
||||
metaData = m.MetaData()
|
||||
|
||||
file.FileCodec = metaData.Codec
|
||||
file.FileWidth = metaData.Width
|
||||
|
@ -258,7 +269,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
}
|
||||
|
||||
// read metadata from embedded Exif and JSON sidecar file (if exists)
|
||||
if metaData, err := m.MetaData(); err == nil {
|
||||
if metaData := m.MetaData(); metaData.Error == nil {
|
||||
photo.SetTitle(metaData.Title, entity.SrcMeta)
|
||||
photo.SetDescription(metaData.Description, entity.SrcMeta)
|
||||
photo.SetTakenAt(metaData.TakenAt, metaData.TakenAtLocal, metaData.TimeZone, entity.SrcMeta)
|
||||
|
@ -288,10 +299,16 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
photo.CameraSerial = metaData.CameraSerial
|
||||
}
|
||||
|
||||
if len(metaData.UniqueID) > 15 {
|
||||
log.Debugf("index: found file uid %s for %s", txt.Quote(metaData.UniqueID), txt.Quote(m.RelativeName(ind.originalsPath())))
|
||||
if metaData.HasDocumentID() && photo.DocumentID == "" {
|
||||
log.Debugf("index: %s has document id %s", quotedName, txt.Quote(metaData.DocumentID))
|
||||
|
||||
file.FileUID = metaData.UniqueID
|
||||
photo.DocumentID = metaData.DocumentID
|
||||
}
|
||||
|
||||
if metaData.HasInstanceID() && file.InstanceID == "" {
|
||||
log.Debugf("index: %s has instance id %s", quotedName, txt.Quote(metaData.InstanceID))
|
||||
|
||||
file.InstanceID = metaData.InstanceID
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -329,7 +346,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
locKeywords, locLabels = photo.UpdateLocation(ind.conf.GeoCodingApi())
|
||||
labels = append(labels, locLabels...)
|
||||
} else {
|
||||
log.Debugf("index: no coordinates in metadata for %s", txt.Quote(m.RelativeName(ind.originalsPath())))
|
||||
log.Debugf("index: no coordinates in metadata for %s", quotedName)
|
||||
|
||||
photo.Location = &entity.UnknownLocation
|
||||
photo.LocUID = entity.UnknownLocation.LocUID
|
||||
|
@ -368,7 +385,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
|
||||
if photoExists {
|
||||
if err := ind.db.Unscoped().Save(&photo).Error; err != nil {
|
||||
log.Errorf("index: %s for %s", err.Error(), txt.Quote(m.RelativeName(ind.originalsPath())))
|
||||
log.Errorf("index: %s for %s", err.Error(), quotedName)
|
||||
result.Status = IndexFailed
|
||||
result.Error = err
|
||||
return result
|
||||
|
@ -376,7 +393,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
} else {
|
||||
if yamlName := fs.TypeYaml.FindSub(m.FileName(), fs.HiddenPath, ind.conf.Settings().Index.Group); yamlName != "" {
|
||||
if err := photo.LoadFromYaml(yamlName); err != nil {
|
||||
log.Errorf("index: %s (restore from yaml) for %s", err.Error(), txt.Quote(m.RelativeName(ind.originalsPath())))
|
||||
log.Errorf("index: %s (restore from yaml) for %s", err.Error(), quotedName)
|
||||
} else {
|
||||
log.Infof("index: restored from %s", txt.Quote(fs.RelativeName(yamlName, ind.originalsPath())))
|
||||
}
|
||||
|
@ -423,7 +440,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
labels := photo.ClassifyLabels()
|
||||
|
||||
if err := photo.UpdateTitle(labels); err != nil {
|
||||
log.Warnf("%s for %s", err.Error(), txt.Quote(m.RelativeName(ind.originalsPath())))
|
||||
log.Warnf("%s for %s", err.Error(), quotedName)
|
||||
}
|
||||
|
||||
w := txt.Keywords(photo.Details.Keywords)
|
||||
|
@ -441,22 +458,22 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
photo.Details.Keywords = strings.Join(txt.UniqueWords(w), ", ")
|
||||
|
||||
if photo.Details.Keywords != "" {
|
||||
log.Debugf("index: set keywords %s for %s", photo.Details.Keywords, txt.Quote(m.RelativeName(ind.originalsPath())))
|
||||
log.Debugf("index: set keywords %s for %s", photo.Details.Keywords, quotedName)
|
||||
} else {
|
||||
log.Debugf("index: no keywords for %s", txt.Quote(m.RelativeName(ind.originalsPath())))
|
||||
log.Debugf("index: no keywords for %s", quotedName)
|
||||
}
|
||||
|
||||
photo.PhotoQuality = photo.QualityScore()
|
||||
|
||||
if err := ind.db.Unscoped().Save(&photo).Error; err != nil {
|
||||
log.Errorf("index: %s for %s", err, txt.Quote(m.RelativeName(ind.originalsPath())))
|
||||
log.Errorf("index: %s for %s", err, quotedName)
|
||||
result.Status = IndexFailed
|
||||
result.Error = err
|
||||
return result
|
||||
}
|
||||
|
||||
if err := photo.IndexKeywords(); err != nil {
|
||||
log.Errorf("%s for %s", err, txt.Quote(m.RelativeName(ind.originalsPath())))
|
||||
log.Errorf("%s for %s", err, quotedName)
|
||||
}
|
||||
} else {
|
||||
if photo.PhotoQuality >= 0 {
|
||||
|
@ -464,7 +481,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
}
|
||||
|
||||
if err := ind.db.Unscoped().Save(&photo).Error; err != nil {
|
||||
log.Errorf("index: %s for %s", err, txt.Quote(m.RelativeName(ind.originalsPath())))
|
||||
log.Errorf("index: %s for %s", err, quotedName)
|
||||
result.Status = IndexFailed
|
||||
result.Error = err
|
||||
return result
|
||||
|
@ -477,7 +494,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
file.UpdatedIn = int64(time.Since(start))
|
||||
|
||||
if err := ind.db.Unscoped().Save(&file).Error; err != nil {
|
||||
log.Errorf("index: %s for %s", err, txt.Quote(m.RelativeName(ind.originalsPath())))
|
||||
log.Errorf("index: %s for %s", err, quotedName)
|
||||
result.Status = IndexFailed
|
||||
result.Error = err
|
||||
return result
|
||||
|
@ -486,7 +503,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
file.CreatedIn = int64(time.Since(start))
|
||||
|
||||
if err := ind.db.Create(&file).Error; err != nil {
|
||||
log.Errorf("index: %s for %s", err, txt.Quote(m.RelativeName(ind.originalsPath())))
|
||||
log.Errorf("index: %s for %s", err, quotedName)
|
||||
result.Status = IndexFailed
|
||||
result.Error = err
|
||||
return result
|
||||
|
@ -501,7 +518,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
|
||||
if (photo.PhotoType == entity.TypeVideo || photo.PhotoType == entity.TypeLive) && file.FilePrimary {
|
||||
if err := file.UpdateVideoInfos(); err != nil {
|
||||
log.Errorf("index: %s for %s", err, txt.Quote(m.RelativeName(ind.originalsPath())))
|
||||
log.Errorf("index: %s for %s", err, quotedName)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -515,7 +532,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
}
|
||||
|
||||
if err := query.SetDownloadFileID(downloadedAs, file.ID); err != nil {
|
||||
log.Errorf("index: %s for %s", err, txt.Quote(m.RelativeName(ind.originalsPath())))
|
||||
log.Errorf("index: %s for %s", err, quotedName)
|
||||
}
|
||||
|
||||
// Write YAML sidecar file (optional).
|
||||
|
@ -523,7 +540,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
yamlFile := photo.YamlFileName(ind.originalsPath(), ind.conf.SidecarHidden())
|
||||
|
||||
if err := photo.SaveAsYaml(yamlFile); err != nil {
|
||||
log.Errorf("index: %s (update yaml) for %s", err.Error(), txt.Quote(m.RelativeName(ind.originalsPath())))
|
||||
log.Errorf("index: %s (update yaml) for %s", err.Error(), quotedName)
|
||||
} else {
|
||||
log.Infof("index: updated yaml file %s", txt.Quote(fs.RelativeName(yamlFile, ind.originalsPath())))
|
||||
}
|
||||
|
|
|
@ -12,11 +12,7 @@ func (m *MediaFile) Location() (*entity.Location, error) {
|
|||
return m.location, nil
|
||||
}
|
||||
|
||||
data, err := m.MetaData()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := m.MetaData()
|
||||
|
||||
if data.Lat == 0 && data.Lng == 0 {
|
||||
return nil, errors.New("mediafile: no latitude and longitude in metadata")
|
||||
|
|
|
@ -92,10 +92,10 @@ func (m *MediaFile) TakenAt() (time.Time, string) {
|
|||
|
||||
m.takenAt = time.Now().UTC()
|
||||
|
||||
info, err := m.MetaData()
|
||||
data := m.MetaData()
|
||||
|
||||
if err == nil && !info.TakenAt.IsZero() && info.TakenAt.Year() > 1000 {
|
||||
m.takenAt = info.TakenAt.UTC()
|
||||
if data.Error == nil && !data.TakenAt.IsZero() && data.TakenAt.Year() > 1000 {
|
||||
m.takenAt = data.TakenAt.UTC()
|
||||
m.takenAtSrc = entity.SrcMeta
|
||||
|
||||
log.Infof("mediafile: %s was taken at %s (%s)", filepath.Base(m.fileName), m.takenAt.String(), m.takenAtSrc)
|
||||
|
@ -135,119 +135,67 @@ func (m *MediaFile) TakenAt() (time.Time, string) {
|
|||
}
|
||||
|
||||
func (m *MediaFile) HasTimeAndPlace() bool {
|
||||
exifData, err := m.MetaData()
|
||||
data := m.MetaData()
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
result := !exifData.TakenAt.IsZero() && exifData.Lat != 0 && exifData.Lng != 0
|
||||
result := !data.TakenAt.IsZero() && data.Lat != 0 && data.Lng != 0
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// CameraModel returns the camera model with which the media file was created.
|
||||
func (m *MediaFile) CameraModel() string {
|
||||
info, err := m.MetaData()
|
||||
data := m.MetaData()
|
||||
|
||||
var result string
|
||||
|
||||
if err == nil {
|
||||
result = info.CameraModel
|
||||
}
|
||||
|
||||
return result
|
||||
return data.CameraModel
|
||||
}
|
||||
|
||||
// CameraMake returns the make of the camera with which the file was created.
|
||||
func (m *MediaFile) CameraMake() string {
|
||||
info, err := m.MetaData()
|
||||
data := m.MetaData()
|
||||
|
||||
var result string
|
||||
|
||||
if err == nil {
|
||||
result = info.CameraMake
|
||||
}
|
||||
|
||||
return result
|
||||
return data.CameraMake
|
||||
}
|
||||
|
||||
// LensModel returns the lens model of a media file.
|
||||
func (m *MediaFile) LensModel() string {
|
||||
info, err := m.MetaData()
|
||||
data := m.MetaData()
|
||||
|
||||
var result string
|
||||
|
||||
if err == nil {
|
||||
result = info.LensModel
|
||||
}
|
||||
|
||||
return result
|
||||
return data.LensModel
|
||||
}
|
||||
|
||||
// LensMake returns the make of the Lens.
|
||||
func (m *MediaFile) LensMake() string {
|
||||
info, err := m.MetaData()
|
||||
data := m.MetaData()
|
||||
|
||||
var result string
|
||||
|
||||
if err == nil {
|
||||
result = info.LensMake
|
||||
}
|
||||
|
||||
return result
|
||||
return data.LensMake
|
||||
}
|
||||
|
||||
// FocalLength return the length of the focal for a file.
|
||||
func (m *MediaFile) FocalLength() int {
|
||||
info, err := m.MetaData()
|
||||
data := m.MetaData()
|
||||
|
||||
var result int
|
||||
|
||||
if err == nil {
|
||||
result = info.FocalLength
|
||||
}
|
||||
|
||||
return result
|
||||
return data.FocalLength
|
||||
}
|
||||
|
||||
// FNumber returns the F number with which the media file was created.
|
||||
func (m *MediaFile) FNumber() float32 {
|
||||
info, err := m.MetaData()
|
||||
data := m.MetaData()
|
||||
|
||||
var result float32
|
||||
|
||||
if err == nil {
|
||||
result = info.FNumber
|
||||
}
|
||||
|
||||
return result
|
||||
return data.FNumber
|
||||
}
|
||||
|
||||
// Iso returns the iso rating as int.
|
||||
func (m *MediaFile) Iso() int {
|
||||
info, err := m.MetaData()
|
||||
data := m.MetaData()
|
||||
|
||||
var result int
|
||||
|
||||
if err == nil {
|
||||
result = info.Iso
|
||||
}
|
||||
|
||||
return result
|
||||
return data.Iso
|
||||
}
|
||||
|
||||
// Exposure returns the exposure time as string.
|
||||
func (m *MediaFile) Exposure() string {
|
||||
info, err := m.MetaData()
|
||||
data := m.MetaData()
|
||||
|
||||
var result string
|
||||
|
||||
if err == nil {
|
||||
result = info.Exposure
|
||||
}
|
||||
|
||||
return result
|
||||
return data.Exposure
|
||||
}
|
||||
|
||||
// CanonicalName returns the canonical name of a media file.
|
||||
|
@ -678,9 +626,9 @@ func (m *MediaFile) decodeDimensions() error {
|
|||
|
||||
var width, height int
|
||||
|
||||
data, err := m.MetaData()
|
||||
data := m.MetaData()
|
||||
|
||||
if err == nil {
|
||||
if data.Error == nil {
|
||||
width = data.Width
|
||||
height = data.Height
|
||||
}
|
||||
|
@ -757,7 +705,7 @@ func (m *MediaFile) AspectRatio() float32 {
|
|||
|
||||
// Orientation returns the orientation of a MediaFile.
|
||||
func (m *MediaFile) Orientation() int {
|
||||
if data, err := m.MetaData(); err == nil {
|
||||
if data := m.MetaData(); data.Error == nil {
|
||||
return data.Orientation
|
||||
}
|
||||
|
||||
|
|
|
@ -9,9 +9,9 @@ import (
|
|||
)
|
||||
|
||||
// MetaData returns exif meta data of a media file.
|
||||
func (m *MediaFile) MetaData() (result meta.Data, err error) {
|
||||
func (m *MediaFile) MetaData() (result meta.Data) {
|
||||
m.metaDataOnce.Do(func() {
|
||||
err = m.metaData.Exif(m.FileName())
|
||||
err := m.metaData.Exif(m.FileName())
|
||||
|
||||
if jsonFile := fs.TypeJson.FindSub(m.FileName(), fs.HiddenPath, false); jsonFile == "" {
|
||||
log.Debugf("mediafile: no json sidecar file found for %s", txt.Quote(filepath.Base(m.FileName())))
|
||||
|
@ -22,9 +22,10 @@ func (m *MediaFile) MetaData() (result meta.Data, err error) {
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
m.metaData.Error = err
|
||||
log.Debugf("mediafile: %s", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
return m.metaData, err
|
||||
return m.metaData
|
||||
}
|
||||
|
|
|
@ -17,36 +17,36 @@ func TestMediaFile_Exif_JPEG(t *testing.T) {
|
|||
|
||||
assert.Nil(t, err)
|
||||
|
||||
info, err := img.MetaData()
|
||||
data := img.MetaData()
|
||||
|
||||
assert.Empty(t, err)
|
||||
|
||||
assert.IsType(t, meta.Data{}, info)
|
||||
assert.IsType(t, meta.Data{}, data)
|
||||
|
||||
assert.Equal(t, "", info.UniqueID)
|
||||
assert.Equal(t, "2013-11-26 13:53:55 +0000 UTC", info.TakenAt.String())
|
||||
assert.Equal(t, "2013-11-26 15:53:55 +0000 UTC", info.TakenAtLocal.String())
|
||||
assert.Equal(t, 1, info.Orientation)
|
||||
assert.Equal(t, "Canon EOS 6D", info.CameraModel)
|
||||
assert.Equal(t, "Canon", info.CameraMake)
|
||||
assert.Equal(t, "EF70-200mm f/4L IS USM", info.LensModel)
|
||||
assert.Equal(t, "", info.LensMake)
|
||||
assert.Equal(t, "Africa/Johannesburg", info.TimeZone)
|
||||
assert.Equal(t, "", info.Artist)
|
||||
assert.Equal(t, 111, info.FocalLength)
|
||||
assert.Equal(t, "1/640", info.Exposure)
|
||||
assert.Equal(t, float32(6.644), info.Aperture)
|
||||
assert.Equal(t, float32(10), info.FNumber)
|
||||
assert.Equal(t, 200, info.Iso)
|
||||
assert.Equal(t, float32(-33.45347), info.Lat)
|
||||
assert.Equal(t, float32(25.764645), info.Lng)
|
||||
assert.Equal(t, 190, info.Altitude)
|
||||
assert.Equal(t, 497, info.Width)
|
||||
assert.Equal(t, 331, info.Height)
|
||||
assert.Equal(t, false, info.Flash)
|
||||
assert.Equal(t, "", info.Description)
|
||||
t.Logf("UTC: %s", info.TakenAt.String())
|
||||
t.Logf("Local: %s", info.TakenAtLocal.String())
|
||||
assert.Equal(t, "", data.DocumentID)
|
||||
assert.Equal(t, "2013-11-26 13:53:55 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, "2013-11-26 15:53:55 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, 1, data.Orientation)
|
||||
assert.Equal(t, "Canon EOS 6D", data.CameraModel)
|
||||
assert.Equal(t, "Canon", data.CameraMake)
|
||||
assert.Equal(t, "EF70-200mm f/4L IS USM", data.LensModel)
|
||||
assert.Equal(t, "", data.LensMake)
|
||||
assert.Equal(t, "Africa/Johannesburg", data.TimeZone)
|
||||
assert.Equal(t, "", data.Artist)
|
||||
assert.Equal(t, 111, data.FocalLength)
|
||||
assert.Equal(t, "1/640", data.Exposure)
|
||||
assert.Equal(t, float32(6.644), data.Aperture)
|
||||
assert.Equal(t, float32(10), data.FNumber)
|
||||
assert.Equal(t, 200, data.Iso)
|
||||
assert.Equal(t, float32(-33.45347), data.Lat)
|
||||
assert.Equal(t, float32(25.764645), data.Lng)
|
||||
assert.Equal(t, 190, data.Altitude)
|
||||
assert.Equal(t, 497, data.Width)
|
||||
assert.Equal(t, 331, data.Height)
|
||||
assert.Equal(t, false, data.Flash)
|
||||
assert.Equal(t, "", data.Description)
|
||||
t.Logf("UTC: %s", data.TakenAt.String())
|
||||
t.Logf("Local: %s", data.TakenAtLocal.String())
|
||||
})
|
||||
|
||||
t.Run("fern_green.jpg", func(t *testing.T) {
|
||||
|
@ -54,13 +54,13 @@ func TestMediaFile_Exif_JPEG(t *testing.T) {
|
|||
|
||||
assert.Nil(t, err)
|
||||
|
||||
info, err := img.MetaData()
|
||||
info := img.MetaData()
|
||||
|
||||
assert.Empty(t, err)
|
||||
|
||||
assert.IsType(t, meta.Data{}, info)
|
||||
|
||||
assert.Equal(t, "", info.UniqueID)
|
||||
assert.Equal(t, "", info.DocumentID)
|
||||
assert.Equal(t, 1, info.Orientation)
|
||||
assert.Equal(t, "Canon EOS 7D", info.CameraModel)
|
||||
assert.Equal(t, "Canon", info.CameraMake)
|
||||
|
@ -94,13 +94,13 @@ func TestMediaFile_Exif_DNG(t *testing.T) {
|
|||
|
||||
assert.Nil(t, err)
|
||||
|
||||
info, err := img.MetaData()
|
||||
info := img.MetaData()
|
||||
|
||||
assert.Empty(t, err)
|
||||
|
||||
assert.IsType(t, meta.Data{}, info)
|
||||
|
||||
assert.Equal(t, "", info.UniqueID)
|
||||
assert.Equal(t, "", info.DocumentID)
|
||||
assert.Equal(t, "2019-06-06 07:29:51 +0000 UTC", info.TakenAt.String())
|
||||
assert.Equal(t, "2019-06-06 07:29:51 +0000 UTC", info.TakenAtLocal.String())
|
||||
assert.Equal(t, 1, info.Orientation)
|
||||
|
@ -132,7 +132,7 @@ func TestMediaFile_Exif_HEIF(t *testing.T) {
|
|||
|
||||
assert.Nil(t, err)
|
||||
|
||||
info, err := img.MetaData()
|
||||
info := img.MetaData()
|
||||
|
||||
assert.IsType(t, meta.Data{}, info)
|
||||
|
||||
|
@ -144,13 +144,13 @@ func TestMediaFile_Exif_HEIF(t *testing.T) {
|
|||
|
||||
assert.Nil(t, err)
|
||||
|
||||
jpegInfo, err := jpeg.MetaData()
|
||||
jpegInfo := jpeg.MetaData()
|
||||
|
||||
assert.IsType(t, meta.Data{}, jpegInfo)
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, "", jpegInfo.UniqueID)
|
||||
assert.Equal(t, "", jpegInfo.DocumentID)
|
||||
assert.Equal(t, "2018-09-10 03:16:13 +0000 UTC", jpegInfo.TakenAt.String())
|
||||
assert.Equal(t, "2018-09-10 12:16:13 +0000 UTC", jpegInfo.TakenAtLocal.String())
|
||||
assert.Equal(t, 6, jpegInfo.Orientation)
|
||||
|
|
|
@ -1,16 +1,8 @@
|
|||
package photoprism
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// TimeZone returns the time zone where the photo was taken.
|
||||
func (m *MediaFile) TimeZone() (string, error) {
|
||||
meta, err := m.MetaData()
|
||||
func (m *MediaFile) TimeZone() string {
|
||||
data := m.MetaData()
|
||||
|
||||
if err != nil {
|
||||
return "UTC", errors.New("mediafile: unknown time zone, using UTC")
|
||||
}
|
||||
|
||||
return meta.TimeZone, nil
|
||||
return data.TimeZone
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ func TestMediaFile_TimeZone(t *testing.T) {
|
|||
|
||||
assert.Nil(t, err)
|
||||
|
||||
zone, err := img.TimeZone()
|
||||
zone := img.TimeZone()
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "Africa/Johannesburg", zone)
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
type Files []entity.File
|
||||
|
||||
// FilesByPath returns a slice of files in a given originals folder.
|
||||
func FilesByPath(root, pathName string) (files Files, err error) {
|
||||
func FilesByPath(rootName, pathName string) (files Files, err error) {
|
||||
if strings.HasPrefix(pathName, "/") {
|
||||
pathName = pathName[1:]
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ func FilesByPath(root, pathName string) (files Files, err error) {
|
|||
Table("files").Select("files.*").
|
||||
Joins("JOIN photos ON photos.id = files.photo_id AND photos.deleted_at IS NULL").
|
||||
Where("files.file_missing = 0").
|
||||
Where("files.file_root = ? AND photos.photo_path = ?", root, pathName).
|
||||
Where("files.file_root = ? AND photos.photo_path = ?", rootName, pathName).
|
||||
Order("files.file_name").
|
||||
Find(&files).Error
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
|
||||
func TestFilesByPath(t *testing.T) {
|
||||
t.Run("files found", func(t *testing.T) {
|
||||
files, err := FilesByPath(entity.RootOriginals, "2016/11")
|
||||
files, err := FilesByPath(entity.RootDefault, "2016/11")
|
||||
|
||||
t.Logf("files: %+v", files)
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
type Folders []entity.Folder
|
||||
|
||||
// FoldersByPath returns a slice of folders in a given directory incl sub directories in recursive mode.
|
||||
func FoldersByPath(root, rootPath, path string, recursive bool) (folders Folders, err error) {
|
||||
func FoldersByPath(rootName, rootPath, path string, recursive bool) (folders Folders, err error) {
|
||||
dirs, err := fs.Dirs(filepath.Join(rootPath, path), recursive)
|
||||
|
||||
if err != nil {
|
||||
|
@ -20,7 +20,7 @@ func FoldersByPath(root, rootPath, path string, recursive bool) (folders Folders
|
|||
folders = make(Folders, len(dirs))
|
||||
|
||||
for i, dir := range dirs {
|
||||
folder := entity.NewFolder(root, filepath.Join(path, dir), nil)
|
||||
folder := entity.NewFolder(rootName, filepath.Join(path, dir), nil)
|
||||
|
||||
if f := entity.FirstOrCreateFolder(&folder); f != nil {
|
||||
folders[i] = *f
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
|
||||
func TestFoldersByPath(t *testing.T) {
|
||||
t.Run("root", func(t *testing.T) {
|
||||
folders, err := FoldersByPath(entity.RootOriginals, "testdata", "", false)
|
||||
folders, err := FoldersByPath(entity.RootDefault, "testdata", "", false)
|
||||
|
||||
t.Logf("folders: %+v", folders)
|
||||
|
||||
|
@ -21,7 +21,7 @@ func TestFoldersByPath(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("subdirectory", func(t *testing.T) {
|
||||
folders, err := FoldersByPath(entity.RootOriginals, "testdata", "directory", false)
|
||||
folders, err := FoldersByPath(entity.RootDefault, "testdata", "directory", false)
|
||||
|
||||
t.Logf("folders: %+v", folders)
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ func PhotoByID(photoID uint64) (photo entity.Photo, err error) {
|
|||
Preload("Camera").
|
||||
Preload("Lens").
|
||||
Preload("Details").
|
||||
Preload("Place").
|
||||
Preload("Location").
|
||||
Preload("Location.Place").
|
||||
First(&photo).Error; err != nil {
|
||||
|
@ -38,6 +39,7 @@ func PhotoByUID(photoUID string) (photo entity.Photo, err error) {
|
|||
Preload("Camera").
|
||||
Preload("Lens").
|
||||
Preload("Details").
|
||||
Preload("Place").
|
||||
Preload("Location").
|
||||
Preload("Location.Place").
|
||||
First(&photo).Error; err != nil {
|
||||
|
@ -58,6 +60,7 @@ func PhotoPreloadByUID(photoUID string) (photo entity.Photo, err error) {
|
|||
Preload("Camera").
|
||||
Preload("Lens").
|
||||
Preload("Details").
|
||||
Preload("Place").
|
||||
Preload("Location").
|
||||
Preload("Location.Place").
|
||||
First(&photo).Error; err != nil {
|
||||
|
@ -101,6 +104,7 @@ func PhotosMaintenance(limit int, offset int) (entities Photos, err error) {
|
|||
Preload("Camera").
|
||||
Preload("Lens").
|
||||
Preload("Details").
|
||||
Preload("Place").
|
||||
Preload("Location").
|
||||
Preload("Location.Place").
|
||||
Where("maintained_at IS NULL OR maintained_at < ?", time.Now().Add(-1*time.Hour*24*7)).
|
||||
|
|
|
@ -16,7 +16,8 @@ type Photos []entity.Photo
|
|||
|
||||
// PhotoResult contains found photos and their main file plus other meta data.
|
||||
type PhotoResult struct {
|
||||
ID uint `json:"ID"`
|
||||
ID uint `json:"-"`
|
||||
DocumentID string `json:"DocumentID,omitempty"`
|
||||
PhotoUID string `json:"UID"`
|
||||
PhotoType string `json:"Type"`
|
||||
TakenAt time.Time `json:"TakenAt"`
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
package rnd
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PPID returns a unique id with prefix as string.
|
||||
func PPID(prefix byte) string {
|
||||
result := make([]byte, 0, 16)
|
||||
result = append(result, prefix)
|
||||
result = append(result, strconv.FormatInt(time.Now().UTC().Unix(), 36)[0:6]...)
|
||||
result = append(result, Token(9)...)
|
||||
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// IsPPID returns true if the id seems to be a PhotoPrism unique id.
|
||||
func IsPPID(id string, prefix byte) bool {
|
||||
if len(id) != 16 {
|
||||
return false
|
||||
}
|
||||
|
||||
return id[0] == prefix
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package rnd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPPID(t *testing.T) {
|
||||
for n := 0; n < 5; n++ {
|
||||
uid := PPID('x')
|
||||
t.Logf("id: %s", uid)
|
||||
assert.Equal(t, len(uid), 16)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPPID(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
PPID('x')
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPPID(t *testing.T) {
|
||||
prefix := byte('x')
|
||||
|
||||
for n := 0; n < 10; n++ {
|
||||
id := PPID(prefix)
|
||||
assert.True(t, IsPPID(id, prefix))
|
||||
}
|
||||
|
||||
assert.True(t, IsPPID("lt9k3pw1wowuy3c2", 'l'))
|
||||
}
|
40
pkg/rnd/uid.go
Normal file
40
pkg/rnd/uid.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package rnd
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PPID returns a unique id with prefix as string.
|
||||
func PPID(prefix byte) string {
|
||||
result := make([]byte, 0, 16)
|
||||
result = append(result, prefix)
|
||||
result = append(result, strconv.FormatInt(time.Now().UTC().Unix(), 36)[0:6]...)
|
||||
result = append(result, Token(9)...)
|
||||
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// IsPPID returns true if string is a unique id as generated by PhotoPrism.
|
||||
func IsPPID(s string, prefix byte) bool {
|
||||
if len(s) != 16 {
|
||||
return false
|
||||
}
|
||||
|
||||
return s[0] == prefix
|
||||
}
|
||||
|
||||
// IsUID returns true if string is a seemingly unique id.
|
||||
func IsUID(s string, prefix byte) bool {
|
||||
// Regular UUID.
|
||||
if len(s) == 36 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Not a known UID format.
|
||||
if len(s) != 16 {
|
||||
return false
|
||||
}
|
||||
|
||||
return IsPPID(s, prefix)
|
||||
}
|
46
pkg/rnd/uid_test.go
Normal file
46
pkg/rnd/uid_test.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
package rnd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPPID(t *testing.T) {
|
||||
for n := 0; n < 5; n++ {
|
||||
uid := PPID('x')
|
||||
t.Logf("id: %s", uid)
|
||||
assert.Equal(t, len(uid), 16)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPPID(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
PPID('x')
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPPID(t *testing.T) {
|
||||
prefix := byte('x')
|
||||
|
||||
for n := 0; n < 10; n++ {
|
||||
id := PPID(prefix)
|
||||
assert.True(t, IsPPID(id, prefix))
|
||||
}
|
||||
|
||||
assert.True(t, IsPPID("lt9k3pw1wowuy3c2", 'l'))
|
||||
}
|
||||
|
||||
func TestIsUID(t *testing.T) {
|
||||
assert.True(t, IsUID("lt9k3pw1wowuy3c2", 'l'))
|
||||
// xmp.iid:dafbfeb8-a129-4e7c-9cf0-e7996a701cdb
|
||||
assert.True(t, IsUID("dafbfeb8-a129-4e7c-9cf0-e7996a701cdb", 'l'))
|
||||
assert.True(t, IsUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8", 'l'))
|
||||
assert.True(t, IsUID("55785BAC-9A4B-4747-B090-EE123FFEE437", 'l'))
|
||||
assert.True(t, IsUID("550e8400-e29b-11d4-a716-446655440000", 'l'))
|
||||
assert.False(t, IsUID("4B1FEF2D1CF4A5BE38B263E0637EDEAD", 'l'))
|
||||
assert.False(t, IsUID("123", '1'))
|
||||
assert.False(t, IsUID("_", '_'))
|
||||
assert.False(t, IsUID("", '_'))
|
||||
}
|
||||
|
Loading…
Reference in a new issue