From 604849e92c4b671f479844872c70204f87227065 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Fri, 6 Oct 2023 02:22:48 +0200 Subject: [PATCH] Search: Include RAW files in results by default #2040 With these changes the size and type of the RAW file as well as other details can be displayed in the Cards View. This also improves the indexing of camera and lens metadata. Signed-off-by: Michael Mayer --- frontend/src/model/photo.js | 50 ++++++++++++++++++++++++---- internal/api/thumbnails.go | 4 +-- internal/entity/camera.go | 6 ++++ internal/entity/camera_makes.go | 10 ++++++ internal/entity/camera_test.go | 6 ++-- internal/entity/entity_const.go | 1 - internal/entity/lens.go | 5 +++ internal/meta/data.go | 4 +-- internal/meta/exif.go | 18 ++++++---- internal/meta/json_exiftool.go | 5 +++ internal/meta/json_test.go | 1 + internal/meta/testdata/altitude.json | 3 +- internal/query/file_selection.go | 2 -- internal/query/photo.go | 2 -- internal/search/photos.go | 12 +++---- pkg/media/filename.go | 4 +-- pkg/media/filename_test.go | 8 ++--- pkg/media/formats.go | 2 +- pkg/media/types.go | 2 -- 19 files changed, 102 insertions(+), 43 deletions(-) diff --git a/frontend/src/model/photo.js b/frontend/src/model/photo.js index eb5b26dd4..48cc58108 100644 --- a/frontend/src/model/photo.js +++ b/frontend/src/model/photo.js @@ -491,16 +491,17 @@ export class Photo extends RestModel { if (file) { let videoFormat = FormatAvc; + const fileCodec = file.Codec ? file.Codec : ""; - if (canUseHevc && (file.Codec === CodecHvc1 || file.Codec === CodecHev1)) { + if (canUseHevc && (fileCodec === CodecHvc1 || fileCodec === CodecHev1)) { videoFormat = FormatHevc; - } else if (canUseOGV && file.Codec === CodecOGV) { + } else if (canUseOGV && fileCodec === CodecOGV) { videoFormat = CodecOGV; - } else if (canUseVP8 && file.Codec === CodecVP8) { + } else if (canUseVP8 && fileCodec === CodecVP8) { videoFormat = CodecVP8; - } else if (canUseVP9 && file.Codec === CodecVP9) { + } else if (canUseVP9 && fileCodec === CodecVP9) { videoFormat = CodecVP9; - } else if (canUseAv1 && (file.Codec === CodecAv01 || file.Codec === CodecAv1C)) { + } else if (canUseAv1 && (fileCodec === CodecAv01 || fileCodec === CodecAv1C)) { videoFormat = FormatAv1; } else if (canUseWebM && file.FileType === FormatWebM) { videoFormat = FormatWebM; @@ -523,12 +524,21 @@ export class Photo extends RestModel { // Return the primary image, if found. let file = files.find((f) => !!f.Primary); + + // Found? if (file) { return file; } // Find and return the first JPEG or PNG image otherwise. - return files.find((f) => f.FileType === FormatJpeg || f.FileType === FormatPng); + file = files.find((f) => f.FileType === FormatJpeg || f.FileType === FormatPng); + + // Found? + if (file) { + return file; + } + + return files.find((f) => !f.Sidecar); }); originalFile() { @@ -547,8 +557,34 @@ export class Photo extends RestModel { return this; } + let file; + + // Find file with matching media type. + switch (this.Type) { + case MediaAnimated: + file = files.find((f) => f.MediaType === MediaImage && f.Root === "/"); + break; + case MediaLive: + file = files.find( + (f) => (f.MediaType === MediaVideo || f.MediaType === MediaLive) && f.Root === "/" + ); + break; + case MediaRaw: + case MediaVideo: + case MediaVector: + file = files.find((f) => f.MediaType === this.Type && f.Root === "/"); + break; + } + + // Found? + if (file) { + return file; + } + // Find first original media file with a format other than JPEG. - let file = files.find((f) => !f.Sidecar && f.Root === "/" && f.FileType !== FormatJpeg); + file = files.find((f) => !f.Sidecar && f.FileType !== FormatJpeg && f.Root === "/"); + + // Found? if (file) { return file; } diff --git a/internal/api/thumbnails.go b/internal/api/thumbnails.go index f86451015..b7b608d85 100644 --- a/internal/api/thumbnails.go +++ b/internal/api/thumbnails.go @@ -143,9 +143,7 @@ func GetThumb(router *gin.RouterGroup) { // Find supported preview image if media file is not a JPEG or PNG. if f.NoJPEG() && f.NoPNG() { - f, err = query.FileByPhotoUID(f.PhotoUID) - - if err != nil { + if f, err = query.FileByPhotoUID(f.PhotoUID); err != nil { c.Data(http.StatusOK, "image/svg+xml", fileIconSvg) return } diff --git a/internal/entity/camera.go b/internal/entity/camera.go index b9bcf2ee1..a85192d87 100644 --- a/internal/entity/camera.go +++ b/internal/entity/camera.go @@ -58,14 +58,20 @@ func NewCamera(modelName string, makeName string) *Camera { modelName = strings.TrimSpace(modelName[len(makeName):]) } + // Normalize make name. if n, ok := CameraMakes[makeName]; ok { makeName = n } + // Normalize model name. if n, ok := CameraModels[modelName]; ok { modelName = n } + if strings.HasPrefix(modelName, makeName) { + modelName = strings.TrimSpace(modelName[len(makeName):]) + } + var name []string if makeName != "" { diff --git a/internal/entity/camera_makes.go b/internal/entity/camera_makes.go index 132ec5b6b..8fba46527 100644 --- a/internal/entity/camera_makes.go +++ b/internal/entity/camera_makes.go @@ -1,6 +1,8 @@ package entity var CameraMakes = map[string]string{ + "Asus": "ASUS", + "ASUS_AI2302": "ASUS", "apple": "Apple", "google": "Google", "samsung": "Samsung", @@ -9,9 +11,17 @@ var CameraMakes = map[string]string{ "OLYMPUS DIGITAL CAMERA": "Olympus", "OLYMPUS IMAGING CORP.": "Olympus", "OLYMPUS OPTICAL CO.,LTD": "Olympus", + "Nikon": "NIKON", + "NIKON CORPORATION": "NIKON", + "Fujifilm": "FUJIFILM", + "FUJIFILM CORPORATION": "FUJIFILM", + "Huawei": "HUAWEI", + "RaspberryPi": "Raspberry Pi", } var CameraModels = map[string]string{ + "AI2302": "ASUS Zenfone 10", + "ASUS_AI2302": "ASUS Zenfone 10", "iPhone SE (1st generation)": "iPhone SE", "iPhone SE (2nd generation)": "iPhone SE", "iPhone SE (3rd generation)": "iPhone SE", diff --git a/internal/entity/camera_test.go b/internal/entity/camera_test.go index cbb002a37..3faa16a7b 100644 --- a/internal/entity/camera_test.go +++ b/internal/entity/camera_test.go @@ -109,8 +109,8 @@ func TestNewCamera(t *testing.T) { camera := NewCamera("ELE-AL00", "Huawei") assert.Equal(t, "huawei-p30", camera.CameraSlug) - assert.Equal(t, "Huawei P30", camera.CameraName) - assert.Equal(t, "Huawei", camera.CameraMake) + assert.Equal(t, "HUAWEI P30", camera.CameraName) + assert.Equal(t, "HUAWEI", camera.CameraMake) assert.Equal(t, "P30", camera.CameraModel) }) } @@ -119,7 +119,7 @@ func TestCamera_String(t *testing.T) { t.Run("model XXX make Nikon", func(t *testing.T) { camera := NewCamera("XXX", "Nikon") cameraString := camera.String() - assert.Equal(t, "'Nikon XXX'", cameraString) + assert.Equal(t, "'NIKON XXX'", cameraString) }) t.Run("model XXX make Unknown", func(t *testing.T) { camera := NewCamera("XXX", "") diff --git a/internal/entity/entity_const.go b/internal/entity/entity_const.go index 61fa78f65..563ba62a5 100644 --- a/internal/entity/entity_const.go +++ b/internal/entity/entity_const.go @@ -24,7 +24,6 @@ const ( MediaLive = string(media.Live) MediaVideo = string(media.Video) MediaVector = string(media.Vector) - MediaText = string(media.Text) ) // Base folders. diff --git a/internal/entity/lens.go b/internal/entity/lens.go index 4a3958387..463481cb9 100644 --- a/internal/entity/lens.go +++ b/internal/entity/lens.go @@ -58,10 +58,15 @@ func NewLens(modelName string, makeName string) *Lens { modelName = strings.TrimSpace(modelName[len(makeName):]) } + // Normalize make name. if n, ok := CameraMakes[makeName]; ok { makeName = n } + if strings.HasPrefix(modelName, makeName) { + modelName = strings.TrimSpace(modelName[len(makeName):]) + } + var name []string if makeName != "" { diff --git a/internal/meta/data.go b/internal/meta/data.go index bba8daa29..f625d1a2e 100644 --- a/internal/meta/data.go +++ b/internal/meta/data.go @@ -44,11 +44,11 @@ type Data struct { Projection string `meta:"ProjectionType"` ColorProfile string `meta:"ICCProfileName,ProfileDescription"` CameraMake string `meta:"CameraMake,Make" xmp:"Make"` - CameraModel string `meta:"CameraModel,Model" xmp:"Model"` + CameraModel string `meta:"CameraModel,Model,CameraID,UniqueCameraModel" xmp:"CameraModel,Model"` CameraOwner string `meta:"OwnerName"` CameraSerial string `meta:"SerialNumber"` LensMake string `meta:"LensMake"` - LensModel string `meta:"Lens,LensModel" xmp:"LensModel"` + LensModel string `meta:"LensModel,Lens,LensID," xmp:"LensModel,Lens"` Software string `meta:"Software,CreatorTool,HistorySoftwareAgent,ProcessingSoftware"` Flash bool `meta:"FlashFired"` FocalLength int `meta:"FocalLength,FocalLengthIn35mmFormat"` diff --git a/internal/meta/exif.go b/internal/meta/exif.go index 5848f2078..bf8b50eca 100644 --- a/internal/meta/exif.go +++ b/internal/meta/exif.go @@ -131,15 +131,18 @@ func (data *Data) Exif(fileName string, fileFormat fs.Type, bruteForce bool) (er data.Copyright = SanitizeString(value) } - if value, ok := data.exif["Model"]; ok { + // Ignore numeric model names as they are probably invalid. + if value, ok := data.exif["CameraModel"]; ok && !txt.IsUInt(value) { data.CameraModel = SanitizeString(value) - } else if value, ok := data.exif["CameraModel"]; ok { + } else if value, ok = data.exif["Model"]; ok && !txt.IsUInt(value) { + data.CameraModel = SanitizeString(value) + } else if value, ok = data.exif["UniqueCameraModel"]; ok && !txt.IsUInt(value) { data.CameraModel = SanitizeString(value) } - if value, ok := data.exif["Make"]; ok { + if value, ok := data.exif["CameraMake"]; ok && !txt.IsUInt(value) { data.CameraMake = SanitizeString(value) - } else if value, ok := data.exif["CameraMake"]; ok { + } else if value, ok = data.exif["Make"]; ok && !txt.IsUInt(value) { data.CameraMake = SanitizeString(value) } @@ -151,11 +154,14 @@ func (data *Data) Exif(fileName string, fileFormat fs.Type, bruteForce bool) (er data.CameraSerial = SanitizeString(value) } - if value, ok := data.exif["LensMake"]; ok { + if value, ok := data.exif["LensMake"]; ok && !txt.IsUInt(value) { data.LensMake = SanitizeString(value) } - if value, ok := data.exif["LensModel"]; ok { + // Ignore numeric model names as they are probably invalid. + if value, ok := data.exif["LensModel"]; ok && !txt.IsUInt(value) { + data.LensModel = SanitizeString(value) + } else if value, ok = data.exif["Lens"]; ok && !txt.IsUInt(value) { data.LensModel = SanitizeString(value) } diff --git a/internal/meta/json_exiftool.go b/internal/meta/json_exiftool.go index 3af058861..099df3a2a 100644 --- a/internal/meta/json_exiftool.go +++ b/internal/meta/json_exiftool.go @@ -380,6 +380,11 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) { data.Subject = SanitizeMeta(data.Subject) data.Artist = SanitizeMeta(data.Artist) + // Ignore numeric model names as they are probably invalid. + if txt.IsUInt(data.LensModel) { + data.LensModel = "" + } + // Flag Samsung/Google Motion Photos as live media. if data.EmbeddedVideo && (data.MimeType == fs.MimeTypeJPEG || data.MimeType == fs.MimeTypeHEIC) { data.MediaType = media.Live diff --git a/internal/meta/json_test.go b/internal/meta/json_test.go index 58012c5af..e25d7004b 100644 --- a/internal/meta/json_test.go +++ b/internal/meta/json_test.go @@ -1318,6 +1318,7 @@ func TestJSON(t *testing.T) { t.Fatal(err) } + assert.Equal(t, "", data.LensModel) assert.Equal(t, float32(45.75285), data.Lat) assert.Equal(t, float32(33.221977), data.Lng) assert.InEpsilon(t, 4294967284, data.Altitude, 1000) diff --git a/internal/meta/testdata/altitude.json b/internal/meta/testdata/altitude.json index 7b16eef1b..3eff5d642 100644 --- a/internal/meta/testdata/altitude.json +++ b/internal/meta/testdata/altitude.json @@ -86,5 +86,6 @@ "DateTimeCreated": "2013:08:25 14:54:05+00:00", "FocalLength35efl": 3.5, "GPSPosition": "45.7528495555556 33.2219772222222", - "LightValue": 13.9150945354232 + "LightValue": 13.9150945354232, + "LensModel": "135" }] diff --git a/internal/query/file_selection.go b/internal/query/file_selection.go index fa1c3f2f5..1b281d116 100644 --- a/internal/query/file_selection.go +++ b/internal/query/file_selection.go @@ -58,8 +58,6 @@ func ShareSelection(originals bool) FileSelection { media.Unknown.String(), media.Raw.String(), media.Sidecar.String(), - media.Text.String(), - media.Other.String(), } omitTypes = []string{ diff --git a/internal/query/photo.go b/internal/query/photo.go index e19e46001..a73265f47 100644 --- a/internal/query/photo.go +++ b/internal/query/photo.go @@ -77,7 +77,6 @@ func MissingPhotos(limit int, offset int) (entities entity.Photos, err error) { err = Db(). Select("photos.*"). Where("id NOT IN (SELECT photo_id FROM files WHERE file_missing = 0 AND file_root = '/' AND deleted_at IS NULL)"). - Where("photos.photo_type <> ?", entity.MediaText). Order("photos.id"). Limit(limit).Offset(offset).Find(&entities).Error @@ -90,7 +89,6 @@ func ArchivedPhotos(limit int, offset int) (entities entity.Photos, err error) { Select("photos.*"). Where("photos.photo_quality > -1"). Where("photos.deleted_at IS NOT NULL"). - Where("photos.photo_type <> ?", entity.MediaText). Order("photos.id"). Limit(limit).Offset(offset).Find(&entities).Error diff --git a/internal/search/photos.go b/internal/search/photos.go index 3399f4f54..f3c93ebc9 100644 --- a/internal/search/photos.go +++ b/internal/search/photos.go @@ -29,9 +29,6 @@ var PhotosColsAll = SelectString(Photo{}, []string{"*"}) // PhotosColsView contains the result column names necessary for the photo viewer. var PhotosColsView = SelectString(Photo{}, SelectCols(GeoResult{}, []string{"*"})) -// FileTypes contains a list of browser-compatible file formats returned by search queries. -var FileTypes = []string{fs.ImageJPEG.String(), fs.ImagePNG.String(), fs.ImageGIF.String(), fs.ImageHEIC.String(), fs.ImageAVIF.String(), fs.ImageAVIFS.String(), fs.ImageWebP.String(), fs.VectorSVG.String()} - // Photos finds PhotoResults based on the search form without checking rights or permissions. func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) { return searchPhotos(f, nil, PhotosColsAll) @@ -200,10 +197,8 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string) return PhotoResults{}, 0, ErrBadSortOrder } - // Limit the result file types if hidden images/videos should not be found. + // Exclude files with errors by default. if !f.Hidden { - s = s.Where("files.file_type IN (?) OR files.media_type IN ('vector','video')", FileTypes) - if f.Error { s = s.Where("files.file_error <> ''") } else { @@ -211,9 +206,12 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string) } } - // Primary files only. + // Find primary files only? if f.Primary { s = s.Where("files.file_primary = 1") + } else { + // Otherwise, find all matching media except sidecar files. + s = s.Where("files.file_sidecar = 0") } // Find specific UIDs only. diff --git a/pkg/media/filename.go b/pkg/media/filename.go index 705281347..3104e8e79 100644 --- a/pkg/media/filename.go +++ b/pkg/media/filename.go @@ -15,8 +15,8 @@ func FromName(fileName string) Type { return result } - // Default. - return Other + // Default to sidecar. + return Sidecar } // MainFile checks if the filename belongs to a main content type. diff --git a/pkg/media/filename_test.go b/pkg/media/filename_test.go index 9d53bce96..ed898412d 100644 --- a/pkg/media/filename_test.go +++ b/pkg/media/filename_test.go @@ -23,14 +23,14 @@ func TestFromName(t *testing.T) { result := FromName("/IMG_4120.AAE") assert.Equal(t, Sidecar, result) }) + t.Run("other", func(t *testing.T) { + result := FromName("/IMG_4120.XXX") + assert.Equal(t, Sidecar, result) + }) t.Run("empty", func(t *testing.T) { result := FromName("") assert.Equal(t, Unknown, result) }) - t.Run("invalid type", func(t *testing.T) { - result := FromName("/IMG_4120.XXX") - assert.Equal(t, Other, result) - }) } func TestMainFile(t *testing.T) { diff --git a/pkg/media/formats.go b/pkg/media/formats.go index 15a57f149..8d20ceee8 100644 --- a/pkg/media/formats.go +++ b/pkg/media/formats.go @@ -52,5 +52,5 @@ var Formats = map[fs.Type]Type{ fs.SidecarText: Sidecar, fs.SidecarJSON: Sidecar, fs.SidecarMarkdown: Sidecar, - fs.TypeUnknown: Other, + fs.TypeUnknown: Sidecar, } diff --git a/pkg/media/types.go b/pkg/media/types.go index fd370d9df..827eaaa61 100644 --- a/pkg/media/types.go +++ b/pkg/media/types.go @@ -9,6 +9,4 @@ const ( Video Type = "video" Vector Type = "vector" Sidecar Type = "sidecar" - Text Type = "text" - Other Type = "other" )