Photos: Add photo_type column and search filters for path / name

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-05-21 10:03:56 +02:00
parent e3c7f73ef1
commit 2efb0039e8
15 changed files with 225 additions and 174 deletions

View file

@ -9,7 +9,7 @@ PhotoPrism: Browse your life in pictures
[![Community Chat](https://img.shields.io/badge/chat-on%20gitter-4aa087.svg)][chat]
[![Twitter](https://img.shields.io/badge/follow-@browseyourlife-00acee.svg)][twitter]
PhotoPrism is a server-based application for browsing, organizing and sharing your personal photo collection.
PhotoPrism is a server-based application for browsing, organizing and sharing your personal photo collection.
It makes use of the latest technologies to automatically tag and find pictures without getting in your way.
Say goodbye to solutions that force you to upload your visual memories to the cloud.
@ -122,6 +122,10 @@ We'd like to remind everyone that we are not full-time marketing specialists but
enjoy a bit of sarcasm from time to time. Please let us know when there is an issue with our "nuance and tone"
and we'll find a solution.
PhotoPrism™ is a trademark of Michael Mayer.
You may use it as required to describe our software but not for offering commercial goods or services
without prior written permission.
[wiki:classification]: https://github.com/photoprism/photoprism/wiki/Image-Classification
[wiki:xmp]: https://github.com/photoprism/photoprism/wiki/XMP
[wiki:geocoding]: https://github.com/photoprism/photoprism/wiki/Geocoding

View file

@ -14,20 +14,21 @@ class Photo extends RestModel {
getDefaults() {
return {
ID: 0,
PhotoUUID: "",
PhotoType: "",
PhotoFavorite: false,
PhotoPrivate: false,
PhotoVideo: false,
TakenAt: "",
TakenAtLocal: "",
TakenSrc: "",
TimeZone: "",
PhotoUUID: "",
PhotoPath: "",
PhotoName: "",
PhotoTitle: "",
TitleSrc: "",
PhotoDescription: "",
DescriptionSrc: "",
PhotoFavorite: false,
PhotoPrivate: false,
PhotoVideo: false,
PhotoResolution: 0,
PhotoQuality: 0,
PhotoLat: 0.0,
@ -84,13 +85,13 @@ class Photo extends RestModel {
getColor() {
switch (this.PhotoColor) {
case "brown":
case "black":
case "white":
case "grey":
return "grey lighten-2";
default:
return this.PhotoColor + " lighten-4";
case "brown":
case "black":
case "white":
case "grey":
return "grey lighten-2";
default:
return this.PhotoColor + " lighten-4";
}
}

View file

@ -24,4 +24,10 @@ const (
YearUnknown = -1
MonthUnknown = -1
TitleUnknown = "Unknown"
TypeImage = "image"
TypeLive = "live"
TypeVideo = "video"
TypeRaw = "raw"
TypeText = "text"
)

View file

@ -19,6 +19,7 @@ import (
type Photo struct {
ID uint `gorm:"primary_key" yaml:"-"`
PhotoUUID string `gorm:"type:varbinary(36);unique_index;index:idx_photos_taken_uuid;" yaml:"PhotoID"`
PhotoType string `gorm:"type:varbinary(8);default:'image';" json:"PhotoType" yaml:"Type"`
TakenAt time.Time `gorm:"type:datetime;index:idx_photos_taken_uuid;" json:"TakenAt" yaml:"Taken"`
TakenAtLocal time.Time `gorm:"type:datetime;" yaml:"-"`
TakenSrc string `gorm:"type:varbinary(8);" json:"TakenSrc" yaml:"TakenSrc,omitempty"`

View file

@ -21,7 +21,7 @@ func TestGeoSearch(t *testing.T) {
log.Debugf("%+v\n", form)
assert.Equal(t, "foobar baz", form.Query)
assert.Equal(t, "fooBar baz", form.Query)
assert.Equal(t, time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), form.Before)
assert.Equal(t, uint(0x61a8), form.Dist)
assert.Equal(t, float32(33.45343), form.Lat)

View file

@ -18,6 +18,7 @@ type Details struct {
// Photo represents a photo edit form.
type Photo struct {
PhotoType string `json:"PhotoType"`
TakenAt time.Time `json:"TakenAt"`
TakenAtLocal time.Time `json:"TakenAtLocal"`
TakenSrc string `json:"TakenSrc"`

View file

@ -8,6 +8,9 @@ import (
type PhotoSearch struct {
Query string `form:"q"`
ID string `form:"id"`
Type string `form:"type"`
Path string `form:"path"`
Name string `form:"name"`
Title string `form:"title"`
Hash string `form:"hash"`
Video bool `form:"video"`

View file

@ -29,7 +29,7 @@ func TestParseQueryString(t *testing.T) {
}
assert.Equal(t, "cat", form.Label)
assert.Equal(t, "foobar baz", form.Query)
assert.Equal(t, "fooBar baz", form.Query)
assert.Equal(t, 23, form.Camera)
assert.Equal(t, time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), form.Before)
assert.Equal(t, false, form.Favorite)

View file

@ -89,7 +89,7 @@ func ParseQueryString(f SearchForm) (result error) {
} else if char == '"' {
escaped = !escaped
} else if isKeyValue {
value = append(value, unicode.ToLower(char))
value = append(value, char)
} else {
key = append(key, unicode.ToLower(char))
}

View file

@ -63,7 +63,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
file, primaryFile := entity.File{}, entity.File{}
photo := entity.Photo{}
photo := entity.Photo{PhotoType: entity.TypeImage}
metaData := meta.Data{}
description := entity.Details{}
labels := classify.Labels{}
@ -154,121 +154,33 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
return result
}
if m.IsVideo() {
photo.PhotoVideo = true
metaData, _ = m.MetaData()
file.FileCodec = metaData.Codec
file.FileWidth = metaData.Width
file.FileHeight = metaData.Height
file.FileDuration = metaData.Duration
file.FileAspectRatio = metaData.AspectRatio()
file.FilePortrait = metaData.Portrait()
if res := metaData.Megapixels(); res > photo.PhotoResolution {
photo.PhotoResolution = res
// Handle file types other than JPEG.
switch {
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())))
} else {
file.FileMainColor = p.MainColor.Name()
file.FileColors = p.Colors.Hex()
file.FileLuminance = p.Luminance.Hex()
file.FileDiff = p.Luminance.Diff()
file.FileChroma = p.Chroma.Value()
}
if file.FileWidth == 0 && primaryFile.FileWidth > 0 {
file.FileWidth = primaryFile.FileWidth
file.FileHeight = primaryFile.FileHeight
file.FileAspectRatio = primaryFile.FileAspectRatio
file.FilePortrait = primaryFile.FilePortrait
}
if m.Width() > 0 && m.Height() > 0 {
file.FileWidth = m.Width()
file.FileHeight = m.Height()
file.FileAspectRatio = m.AspectRatio()
file.FilePortrait = m.Width() < m.Height()
file.FileDiff = primaryFile.FileDiff
file.FileMainColor = primaryFile.FileMainColor
file.FileChroma = primaryFile.FileChroma
file.FileLuminance = primaryFile.FileLuminance
file.FileColors = primaryFile.FileColors
}
megapixels := int(math.Round(float64(file.FileWidth*file.FileHeight) / 1000000))
// file obviously exists: remove deleted and missing flags
file.DeletedAt = nil
file.FileMissing = false
file.FileError = ""
// primary files are used for rendering thumbnails and image classification (plus sidecar files if they exist)
if file.FilePrimary {
primaryFile = file
if !ind.conf.DisableTensorFlow() && (fileChanged || o.Rescan) {
// Image classification via TensorFlow
labels = ind.classifyImage(m)
if !photoExists && ind.conf.Settings().Features.Private && ind.conf.DetectNSFW() {
photo.PhotoPrivate = ind.NSFW(m)
if megapixels > photo.PhotoResolution {
photo.PhotoResolution = megapixels
}
}
if fileChanged || o.Rescan {
// read metadata from embedded Exif and JSON sidecar file (if exists)
if metaData, err := m.MetaData(); err == nil {
photo.SetTitle(metaData.Title, entity.SrcMeta)
photo.SetDescription(metaData.Description, entity.SrcMeta)
photo.SetTakenAt(metaData.TakenAt, metaData.TakenAtLocal, metaData.TimeZone, entity.SrcMeta)
photo.SetCoordinates(metaData.Lat, metaData.Lng, metaData.Altitude, entity.SrcMeta)
if photo.Details.NoNotes() {
photo.Details.Notes = metaData.Comment
}
if photo.Details.NoSubject() {
photo.Details.Subject = metaData.Subject
}
if photo.Details.NoKeywords() {
photo.Details.Keywords = metaData.Keywords
}
if photo.Details.NoArtist() && metaData.Artist != "" {
photo.Details.Artist = metaData.Artist
}
if photo.Details.NoArtist() && metaData.CameraOwner != "" {
photo.Details.Artist = metaData.CameraOwner
}
if photo.NoCameraSerial() {
photo.CameraSerial = metaData.CameraSerial
}
if len(metaData.UniqueID) > 15 {
log.Debugf("index: file uuid %s", txt.Quote(metaData.UniqueID))
file.FileUUID = metaData.UniqueID
}
}
}
if photo.CameraSrc == entity.SrcAuto && (fileChanged || o.Rescan) {
// Set UpdateCamera, Lens, Focal Length and F Number
photo.Camera = entity.NewCamera(m.CameraModel(), m.CameraMake()).FirstOrCreate()
photo.Lens = entity.NewLens(m.LensModel(), m.LensMake()).FirstOrCreate()
photo.PhotoFocalLength = m.FocalLength()
photo.PhotoFNumber = m.FNumber()
photo.PhotoIso = m.Iso()
photo.PhotoExposure = m.Exposure()
}
if photo.TakenAt.IsZero() || photo.TakenAtLocal.IsZero() {
takenUtc, takenSrc := m.TakenAt()
photo.SetTakenAt(takenUtc, takenUtc, "", takenSrc)
}
if fileChanged || o.Rescan || photo.NoTitle() {
if photo.HasLatLng() {
var locLabels classify.Labels
locKeywords, locLabels = photo.UpdateLocation(ind.conf.GeoCodingApi())
labels = append(labels, locLabels...)
} else {
log.Info("index: no latitude and longitude in metadata")
photo.Place = &entity.UnknownPlace
photo.PlaceID = entity.UnknownPlace.ID
}
}
} else if m.IsXMP() {
case m.IsXMP():
// TODO: Proof-of-concept for indexing XMP sidecar files
if data, err := meta.XMP(m.FileName()); err == nil {
photo.SetTitle(data.Title, entity.SrcXmp)
@ -286,6 +198,130 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
photo.Details.Copyright = data.Copyright
}
}
case m.IsRaw():
if photo.PhotoType == entity.TypeImage {
photo.PhotoType = entity.TypeRaw
}
case m.IsVideo():
metaData, _ = m.MetaData()
file.FileCodec = metaData.Codec
file.FileWidth = metaData.Width
file.FileHeight = metaData.Height
file.FileDuration = metaData.Duration
file.FileAspectRatio = metaData.AspectRatio()
file.FilePortrait = metaData.Portrait()
if file.FileDuration == 0 || file.FileDuration > time.Second*4 {
photo.PhotoVideo = true
photo.PhotoType = entity.TypeVideo
} else {
photo.PhotoType = entity.TypeLive
}
if res := metaData.Megapixels(); res > photo.PhotoResolution {
photo.PhotoResolution = res
}
if file.FileWidth == 0 && primaryFile.FileWidth > 0 {
file.FileWidth = primaryFile.FileWidth
file.FileHeight = primaryFile.FileHeight
file.FileAspectRatio = primaryFile.FileAspectRatio
file.FilePortrait = primaryFile.FilePortrait
}
if primaryFile.FileDiff > 0 {
file.FileDiff = primaryFile.FileDiff
file.FileMainColor = primaryFile.FileMainColor
file.FileChroma = primaryFile.FileChroma
file.FileLuminance = primaryFile.FileLuminance
file.FileColors = primaryFile.FileColors
}
}
// file obviously exists: remove deleted and missing flags
file.DeletedAt = nil
file.FileMissing = false
file.FileError = ""
// primary files are used for rendering thumbnails and image classification (plus sidecar files if they exist)
if file.FilePrimary {
primaryFile = file
if !ind.conf.DisableTensorFlow() {
// Image classification via TensorFlow.
labels = ind.classifyImage(m)
if !photoExists && ind.conf.Settings().Features.Private && ind.conf.DetectNSFW() {
photo.PhotoPrivate = ind.NSFW(m)
}
}
// read metadata from embedded Exif and JSON sidecar file (if exists)
if metaData, err := m.MetaData(); err == nil {
photo.SetTitle(metaData.Title, entity.SrcMeta)
photo.SetDescription(metaData.Description, entity.SrcMeta)
photo.SetTakenAt(metaData.TakenAt, metaData.TakenAtLocal, metaData.TimeZone, entity.SrcMeta)
photo.SetCoordinates(metaData.Lat, metaData.Lng, metaData.Altitude, entity.SrcMeta)
if photo.Details.NoNotes() {
photo.Details.Notes = metaData.Comment
}
if photo.Details.NoSubject() {
photo.Details.Subject = metaData.Subject
}
if photo.Details.NoKeywords() {
photo.Details.Keywords = metaData.Keywords
}
if photo.Details.NoArtist() && metaData.Artist != "" {
photo.Details.Artist = metaData.Artist
}
if photo.Details.NoArtist() && metaData.CameraOwner != "" {
photo.Details.Artist = metaData.CameraOwner
}
if photo.NoCameraSerial() {
photo.CameraSerial = metaData.CameraSerial
}
if len(metaData.UniqueID) > 15 {
log.Debugf("index: found file uuid %s for %s", txt.Quote(metaData.UniqueID), txt.Quote(m.RelativeName(ind.originalsPath())))
file.FileUUID = metaData.UniqueID
}
}
if photo.CameraSrc == entity.SrcAuto {
// Set UpdateCamera, Lens, Focal Length and F Number.
photo.Camera = entity.NewCamera(m.CameraModel(), m.CameraMake()).FirstOrCreate()
photo.Lens = entity.NewLens(m.LensModel(), m.LensMake()).FirstOrCreate()
photo.PhotoFocalLength = m.FocalLength()
photo.PhotoFNumber = m.FNumber()
photo.PhotoIso = m.Iso()
photo.PhotoExposure = m.Exposure()
}
if photo.TakenAt.IsZero() || photo.TakenAtLocal.IsZero() {
takenUtc, takenSrc := m.TakenAt()
photo.SetTakenAt(takenUtc, takenUtc, "", takenSrc)
}
if photo.NoTitle() {
if photo.HasLatLng() {
var locLabels classify.Labels
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())))
photo.Place = &entity.UnknownPlace
photo.PlaceID = entity.UnknownPlace.ID
}
}
}
if len(photo.PlaceID) < 2 {
@ -310,34 +346,6 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
file.FileMime = m.MimeType()
file.FileOrientation = m.Orientation()
if m.IsJpeg() && (fileChanged || o.Rescan) {
// Color information
if p, err := m.Colors(ind.thumbPath()); err != nil {
log.Errorf("index: %s", err.Error())
} else {
file.FileMainColor = p.MainColor.Name()
file.FileColors = p.Colors.Hex()
file.FileLuminance = p.Luminance.Hex()
file.FileDiff = p.Luminance.Diff()
file.FileChroma = p.Chroma.Value()
}
}
if m.IsJpeg() && (fileChanged || o.Rescan) {
if m.Width() > 0 && m.Height() > 0 {
file.FileWidth = m.Width()
file.FileHeight = m.Height()
file.FileAspectRatio = m.AspectRatio()
file.FilePortrait = m.Width() < m.Height()
megapixels := int(math.Round(float64(file.FileWidth*file.FileHeight) / 1000000))
if megapixels > photo.PhotoResolution {
photo.PhotoResolution = megapixels
}
}
}
if photoExists {
// Estimate location
if o.Rescan && photo.NoLocation() {
@ -345,7 +353,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", err)
log.Errorf("index: %s for %s", err.Error(), txt.Quote(m.RelativeName(ind.originalsPath())))
result.Status = IndexFailed
result.Error = err
return result
@ -353,7 +361,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)", err.Error())
log.Errorf("index: %s (restore from yaml) for %s", err.Error(), txt.Quote(m.RelativeName(ind.originalsPath())))
} else {
log.Infof("index: restored from %s", txt.Quote(fs.RelativeName(yamlName, ind.originalsPath())))
}
@ -395,11 +403,12 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
file.PhotoUUID = photo.PhotoUUID
result.PhotoUUID = photo.PhotoUUID
if file.FilePrimary && (fileChanged || o.Rescan) {
// Main JPEG file.
if file.FilePrimary {
labels := photo.ClassifyLabels()
if err := photo.UpdateTitle(labels); err != nil {
log.Warnf("%s (%s)", err.Error(), photo.PhotoUUID)
log.Warnf("%s for %s", err.Error(), txt.Quote(m.RelativeName(ind.originalsPath())))
}
w := txt.Keywords(photo.Details.Keywords)
@ -417,22 +426,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: updated photo keywords (%s)", photo.Details.Keywords)
log.Debugf("index: set keywords %s for %s", photo.Details.Keywords, txt.Quote(m.RelativeName(ind.originalsPath())))
} else {
log.Debug("index: no photo keywords")
log.Debugf("index: no keywords for %s", txt.Quote(m.RelativeName(ind.originalsPath())))
}
photo.PhotoQuality = photo.QualityScore()
if err := ind.db.Unscoped().Save(&photo).Error; err != nil {
log.Errorf("index: %s", err)
log.Errorf("index: %s for %s", err, txt.Quote(m.RelativeName(ind.originalsPath())))
result.Status = IndexFailed
result.Error = err
return result
}
if err := photo.IndexKeywords(); err != nil {
log.Error(err)
log.Errorf("%s for %s", err, txt.Quote(m.RelativeName(ind.originalsPath())))
}
} else {
if photo.PhotoQuality >= 0 {
@ -440,7 +449,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", err)
log.Errorf("index: %s for %s", err, txt.Quote(m.RelativeName(ind.originalsPath())))
result.Status = IndexFailed
result.Error = err
return result
@ -453,7 +462,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", err)
log.Errorf("index: %s for %s", err, txt.Quote(m.RelativeName(ind.originalsPath())))
result.Status = IndexFailed
result.Error = err
return result
@ -462,7 +471,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", err)
log.Errorf("index: %s for %s", err, txt.Quote(m.RelativeName(ind.originalsPath())))
result.Status = IndexFailed
result.Error = err
return result
@ -473,7 +482,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
if photo.PhotoVideo && file.FilePrimary {
if err := file.UpdateVideoInfos(); err != nil {
log.Errorf("index: %s", err)
log.Errorf("index: %s for %s", err, txt.Quote(m.RelativeName(ind.originalsPath())))
}
}
@ -487,7 +496,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
}
if err := query.SetDownloadFileID(downloadedAs, file.ID); err != nil {
log.Errorf("index: %s", err)
log.Errorf("index: %s for %s", err, txt.Quote(m.RelativeName(ind.originalsPath())))
}
// Write YAML sidecar file (optional).
@ -495,7 +504,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)", err.Error())
log.Errorf("index: %s (update yaml) for %s", err.Error(), txt.Quote(m.RelativeName(ind.originalsPath())))
} else {
log.Infof("index: updated yaml file %s", txt.Quote(fs.RelativeName(yamlFile, ind.originalsPath())))
}
@ -518,7 +527,7 @@ func (ind *Index) NSFW(jpeg *MediaFile) bool {
return false
} else {
if nsfwLabels.NSFW(nsfw.ThresholdHigh) {
log.Warnf("index: %s might contain offensive content", jpeg.RelativeName(ind.originalsPath()))
log.Warnf("index: %s might contain offensive content", txt.Quote(jpeg.RelativeName(ind.originalsPath())))
return true
}
}

View file

@ -23,10 +23,11 @@ func Geo(f form.GeoSearch) (results GeoResults, err error) {
s := UnscopedDb()
s = s.Table("photos").
Select(`photos.id, photos.photo_uuid, photos.photo_lat, photos.photo_lng, photos.photo_title,
photos.photo_favorite, photos.taken_at, files.file_hash, files.file_width, files.file_height`).
Joins(`JOIN files ON files.photo_id = photos.id
AND files.file_missing = 0 AND files.file_primary AND files.deleted_at IS NULL`).
Select(`photos.id, photos.photo_uuid,photos.photo_type, photos.photo_lat, photos.photo_lng,
photos.photo_title, photos.photo_favorite, photos.taken_at, files.file_hash, files.file_width,
files.file_height`).
Joins(`JOIN files ON files.photo_id = photos.id AND
files.file_missing = 0 AND files.file_primary AND files.deleted_at IS NULL`).
Where("photos.deleted_at IS NULL").
Where("photos.photo_lat <> 0").
Group("photos.id, files.id")

View file

@ -10,6 +10,7 @@ type GeoResult struct {
PhotoLat float32 `json:"Lat"`
PhotoLng float32 `json:"Lng"`
PhotoUUID string `json:"PhotoUUID"`
PhotoType string `json:"PhotoType"`
PhotoTitle string `json:"PhotoTitle"`
PhotoFavorite bool `json:"PhotoFavorite"`
FileHash string `json:"FileHash"`

View file

@ -74,6 +74,7 @@ func MissingPhotos(limit int, offset int) (entities []entity.Photo, err error) {
Joins("JOIN files a ON photos.id = a.photo_id ").
Joins("LEFT JOIN files b ON a.photo_id = b.photo_id AND a.id != b.id AND b.file_missing = 0").
Where("a.file_missing = 1 AND b.id IS NULL").
Where("photos.photo_type <> ?", entity.TypeText).
Group("photos.id").
Limit(limit).Offset(offset).Find(&entities).Error

View file

@ -186,8 +186,30 @@ func Photos(f form.PhotoSearch) (results PhotosResults, count int, err error) {
s = s.Where("photos.photo_country = ?", f.Country)
}
if f.Type != "" {
s = s.Where("photos.photo_type = ?", strings.ToLower(f.Type))
}
if f.Path != "" {
p := f.Path
if strings.HasPrefix(p, "/") {
p = p[1:]
}
if strings.HasSuffix(p, "/") {
s = s.Where("photos.photo_path = ?", p[:len(p)-1])
} else {
s = s.Where("photos.photo_path LIKE ?", strings.ReplaceAll(p, "*", "%"))
}
}
if f.Name != "" {
s = s.Where("photos.photo_name LIKE ?", strings.ReplaceAll(f.Name, "*", "%"))
}
if f.Title != "" {
s = s.Where("LOWER(photos.photo_title) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Title)))
s = s.Where("LOWER(photos.photo_title) LIKE ?", strings.ReplaceAll(strings.ToLower(f.Title), "*", "%"))
}
if f.Hash != "" {

View file

@ -15,6 +15,8 @@ import (
type PhotosResult struct {
// Photo
ID uint
PhotoUUID string
PhotoType string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt time.Time
@ -22,7 +24,6 @@ type PhotosResult struct {
TakenAtLocal time.Time
TakenSrc string
TimeZone string
PhotoUUID string
PhotoPath string
PhotoName string
PhotoTitle string