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 <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2023-10-06 02:22:48 +02:00
parent b45b20aa53
commit 604849e92c
19 changed files with 102 additions and 43 deletions

View file

@ -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;
}

View file

@ -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
}

View file

@ -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 != "" {

View file

@ -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",

View file

@ -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", "")

View file

@ -24,7 +24,6 @@ const (
MediaLive = string(media.Live)
MediaVideo = string(media.Video)
MediaVector = string(media.Vector)
MediaText = string(media.Text)
)
// Base folders.

View file

@ -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 != "" {

View file

@ -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"`

View file

@ -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)
}

View file

@ -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

View file

@ -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)

View file

@ -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"
}]

View file

@ -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{

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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) {

View file

@ -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,
}

View file

@ -9,6 +9,4 @@ const (
Video Type = "video"
Vector Type = "vector"
Sidecar Type = "sidecar"
Text Type = "text"
Other Type = "other"
)