package repo import ( "fmt" "strings" "time" "github.com/gosimple/slug" "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/models" "github.com/photoprism/photoprism/internal/util" ) // PhotoResult contains found photos and their main file plus other meta data. type PhotoResult struct { // Photo ID uint CreatedAt time.Time UpdatedAt time.Time DeletedAt time.Time TakenAt time.Time TakenAtLocal time.Time TimeZone string PhotoUUID string PhotoPath string PhotoName string PhotoTitle string PhotoDescription string PhotoArtist string PhotoKeywords string PhotoColors string PhotoColor string PhotoFavorite bool PhotoPrivate bool PhotoSensitive bool PhotoStory bool PhotoLat float64 PhotoLong float64 PhotoAltitude int PhotoFocalLength int PhotoIso int PhotoFNumber float64 PhotoExposure string // Camera CameraID uint CameraModel string CameraMake string // Lens LensID uint LensModel string LensMake string // Country CountryID string CountryName string // Location LocationID uint LocDisplayName string LocName string LocCity string LocPostcode string LocCounty string LocState string LocCountry string LocCountryCode string LocCategory string LocType string LocationChanged bool LocationEstimated bool // File FileID uint FileUUID string FilePrimary bool FileMissing bool FileName string FileHash string FilePerceptualHash string FileType string FileMime string FileWidth int FileHeight int FileOrientation int FileAspectRatio float64 // List of matching labels and keywords Labels string Keywords string } func (m *PhotoResult) DownloadFileName() string { var name string if m.PhotoTitle != "" { name = strings.Title(slug.MakeLang(m.PhotoTitle, "en")) } else { name = m.PhotoUUID } taken := m.TakenAt.Format("20060102-150405") result := fmt.Sprintf("%s-%s.%s", taken, name, m.FileType) return result } // Photos searches for photos based on a Form and returns a PhotoResult slice. func (s *Repo) Photos(f form.PhotoSearch) (results []PhotoResult, err error) { if err := f.ParseQueryString(); err != nil { return results, err } defer util.ProfileTime(time.Now(), fmt.Sprintf("search: %+v", f)) q := s.db.NewScope(nil).DB() // q.LogMode(true) q = q.Table("photos"). Select(`photos.*, files.id AS file_id, files.file_uuid, files.file_primary, files.file_missing, files.file_name, files.file_hash, files.file_type, files.file_mime, files.file_width, files.file_height, files.file_aspect_ratio, files.file_orientation, files.file_main_color, files.file_colors, files.file_luminance, files.file_chroma, cameras.camera_make, cameras.camera_model, lenses.lens_make, lenses.lens_model, countries.country_name, locations.loc_display_name, locations.loc_name, locations.loc_city, locations.loc_postcode, locations.loc_county, locations.loc_state, locations.loc_country, locations.loc_country_code, locations.loc_category, locations.loc_type, GROUP_CONCAT(DISTINCT labels.label_name) AS labels, GROUP_CONCAT(DISTINCT keywords.keyword) AS keywords`). Joins("JOIN files ON files.photo_id = photos.id AND files.file_primary AND files.deleted_at IS NULL"). Joins("JOIN cameras ON cameras.id = photos.camera_id"). Joins("JOIN lenses ON lenses.id = photos.lens_id"). Joins("LEFT JOIN countries ON countries.id = photos.country_id"). Joins("LEFT JOIN locations ON locations.id = photos.location_id"). Joins("LEFT JOIN photos_labels ON photos_labels.photo_id = photos.id"). Joins("LEFT JOIN labels ON photos_labels.label_id = labels.id"). Joins("LEFT JOIN photos_keywords ON photos_keywords.photo_id = photos.id"). Joins("LEFT JOIN keywords ON photos_keywords.keyword_id = keywords.id"). Where("photos.deleted_at IS NULL AND files.file_missing = 0"). Group("photos.id, files.id") var categories []models.Category var label models.Label var labelIds []uint if f.Label != "" { if result := s.db.First(&label, "label_slug = ?", strings.ToLower(f.Label)); result.Error != nil { log.Errorf("search: label \"%s\" not found", f.Label) return results, fmt.Errorf("label \"%s\" not found", f.Label) } else { labelIds = append(labelIds, label.ID) s.db.Where("category_id = ?", label.ID).Find(&categories) for _, category := range categories { labelIds = append(labelIds, category.LabelID) } q = q.Where("labels.id IN (?)", labelIds) } } if f.Location == true { q = q.Where("location_id > 0") if f.Query != "" { likeString := "%" + strings.ToLower(f.Query) + "%" q = q.Where("LOWER(locations.loc_display_name) LIKE ?", likeString) } } else if f.Query != "" { slugString := slug.Make(f.Query) lowerString := strings.ToLower(f.Query) likeString := lowerString + "%" if result := s.db.First(&label, "label_slug = ?", slugString); result.Error != nil { log.Infof("search: label \"%s\" not found", f.Query) q = q.Where("labels.label_slug = ? OR keywords.keyword LIKE ? OR files.file_main_color = ?", slugString, likeString, lowerString) } else { labelIds = append(labelIds, label.ID) s.db.Where("category_id = ?", label.ID).Find(&categories) for _, category := range categories { labelIds = append(labelIds, category.LabelID) } log.Infof("search: label \"%s\" includes %d categories", label.LabelName, len(labelIds)) q = q.Where("labels.id IN (?) OR keywords.keyword LIKE ? OR files.file_main_color = ?", labelIds, likeString, lowerString) } } if f.Album != "" { q = q.Joins("JOIN photos_albums ON photos_albums.photo_uuid = photos.photo_uuid").Where("photos_albums.album_uuid = ?", f.Album) } if f.Camera > 0 { q = q.Where("photos.camera_id = ?", f.Camera) } if f.Color != "" { q = q.Where("files.file_main_color = ?", strings.ToLower(f.Color)) } if f.Favorites { q = q.Where("photos.photo_favorite = 1") } if f.Country != "" { q = q.Where("locations.loc_country_code = ?", f.Country) } if f.Title != "" { q = q.Where("LOWER(photos.photo_title) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Title))) } if f.Description != "" { q = q.Where("LOWER(photos.photo_description) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Description))) } if f.Notes != "" { q = q.Where("LOWER(photos.photo_notes) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(f.Notes))) } if f.Hash != "" { q = q.Where("files.file_hash = ?", f.Hash) } if f.Duplicate { q = q.Where("files.file_duplicate = 1") } if f.Portrait { q = q.Where("files.file_portrait = 1") } if f.Mono { q = q.Where("files.file_chroma = 0") } else if f.Chroma > 9 { q = q.Where("files.file_chroma > ?", f.Chroma) } else if f.Chroma > 0 { q = q.Where("files.file_chroma > 0 AND files.file_chroma <= ?", f.Chroma) } if f.Fmin > 0 { q = q.Where("photos.photo_f_number >= ?", f.Fmin) } if f.Fmax > 0 { q = q.Where("photos.photo_f_number <= ?", f.Fmax) } if f.Dist == 0 { f.Dist = 20 } else if f.Dist > 1000 { f.Dist = 1000 } // Inaccurate distance search, but probably 'good enough' for now if f.Lat > 0 { latMin := f.Lat - SearchRadius*float64(f.Dist) latMax := f.Lat + SearchRadius*float64(f.Dist) q = q.Where("photos.photo_lat BETWEEN ? AND ?", latMin, latMax) } if f.Long > 0 { longMin := f.Long - SearchRadius*float64(f.Dist) longMax := f.Long + SearchRadius*float64(f.Dist) q = q.Where("photos.photo_long BETWEEN ? AND ?", longMin, longMax) } if !f.Before.IsZero() { q = q.Where("photos.taken_at <= ?", f.Before.Format("2006-01-02")) } if !f.After.IsZero() { q = q.Where("photos.taken_at >= ?", f.After.Format("2006-01-02")) } switch f.Order { case "newest": q = q.Order("taken_at DESC") case "oldest": q = q.Order("taken_at") case "imported": q = q.Order("created_at DESC") default: q = q.Order("taken_at DESC") } if f.Count > 0 && f.Count <= 1000 { q = q.Limit(f.Count).Offset(f.Offset) } else { q = q.Limit(100).Offset(0) } if result := q.Scan(&results); result.Error != nil { return results, result.Error } return results, nil } // FindPhotoByID returns a Photo based on the ID. func (s *Repo) FindPhotoByID(photoID uint64) (photo models.Photo, err error) { if err := s.db.Where("id = ?", photoID).First(&photo).Error; err != nil { return photo, err } return photo, nil } // FindPhotoByUUID returns a Photo based on the UUID. func (s *Repo) FindPhotoByUUID(photoUUID string) (photo models.Photo, err error) { if err := s.db.Where("photo_uuid = ?", photoUUID).First(&photo).Error; err != nil { return photo, err } return photo, nil }