Photos: Add photo_type column and search filters for path / name
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
e3c7f73ef1
commit
2efb0039e8
|
@ -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
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,4 +24,10 @@ const (
|
|||
YearUnknown = -1
|
||||
MonthUnknown = -1
|
||||
TitleUnknown = "Unknown"
|
||||
|
||||
TypeImage = "image"
|
||||
TypeLive = "live"
|
||||
TypeVideo = "video"
|
||||
TypeRaw = "raw"
|
||||
TypeText = "text"
|
||||
)
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 != "" {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue