package search import ( "fmt" "time" "github.com/gosimple/slug" "github.com/ulule/deepcopier" "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/pkg/txt" ) // Photo represents a photo search result. type Photo struct { ID uint `json:"-" select:"photos.id"` CompositeID string `json:"ID" select:"files.photo_id AS composite_id"` UUID string `json:"DocumentID,omitempty" select:"photos.uuid"` PhotoUID string `json:"UID" select:"photos.photo_uid"` PhotoType string `json:"Type" select:"photos.photo_type"` TypeSrc string `json:"TypeSrc" select:"photos.taken_src"` TakenAt time.Time `json:"TakenAt" select:"photos.taken_at"` TakenAtLocal time.Time `json:"TakenAtLocal" select:"photos.taken_at_local"` TakenSrc string `json:"TakenSrc" select:"photos.taken_src"` TimeZone string `json:"TimeZone" select:"photos.time_zone"` PhotoPath string `json:"Path" select:"photos.photo_path"` PhotoName string `json:"Name" select:"photos.photo_name"` OriginalName string `json:"OriginalName" select:"photos.original_name"` PhotoTitle string `json:"Title" select:"photos.photo_title"` PhotoDescription string `json:"Description" select:"photos.photo_description"` PhotoYear int `json:"Year" select:"photos.photo_year"` PhotoMonth int `json:"Month" select:"photos.photo_month"` PhotoDay int `json:"Day" select:"photos.photo_day"` PhotoCountry string `json:"Country" select:"photos.photo_country"` PhotoStack int8 `json:"Stack" select:"photos.photo_stack"` PhotoFavorite bool `json:"Favorite" select:"photos.photo_favorite"` PhotoPrivate bool `json:"Private" select:"photos.photo_private"` PhotoIso int `json:"Iso" select:"photos.photo_iso"` PhotoFocalLength int `json:"FocalLength" select:"photos.photo_focal_length"` PhotoFNumber float32 `json:"FNumber" select:"photos.photo_f_number"` PhotoExposure string `json:"Exposure" select:"photos.photo_exposure"` PhotoFaces int `json:"Faces,omitempty" select:"photos.photo_faces"` PhotoQuality int `json:"Quality" select:"photos.photo_quality"` PhotoResolution int `json:"Resolution" select:"photos.photo_resolution"` PhotoColor uint8 `json:"Color" select:"photos.photo_color"` PhotoScan bool `json:"Scan" select:"photos.photo_scan"` PhotoPanorama bool `json:"Panorama" select:"photos.photo_panorama"` CameraID uint `json:"CameraID" select:"photos.camera_id"` // Camera CameraSrc string `json:"CameraSrc,omitempty" select:"photos.camera_src"` CameraSerial string `json:"CameraSerial,omitempty" select:"photos.camera_serial"` CameraModel string `json:"CameraModel,omitempty" select:"cameras.camera_model"` CameraMake string `json:"CameraMake,omitempty" select:"cameras.camera_make"` LensID uint `json:"LensID" select:"photos.lens_id"` // Lens LensModel string `json:"LensModel,omitempty" select:"lenses.lens_make"` LensMake string `json:"LensMake,omitempty" select:"lenses.lens_model"` PhotoAltitude int `json:"Altitude,omitempty" select:"photos.photo_altitude"` PhotoLat float32 `json:"Lat" select:"photos.photo_lat"` PhotoLng float32 `json:"Lng" select:"photos.photo_lng"` CellID string `json:"CellID" select:"photos.cell_id"` // Cell CellAccuracy int `json:"CellAccuracy,omitempty" select:"photos.cell_accuracy"` PlaceID string `json:"PlaceID" select:"photos.place_id"` PlaceSrc string `json:"PlaceSrc" select:"photos.place_src"` PlaceLabel string `json:"PlaceLabel" select:"places.place_label"` PlaceCity string `json:"PlaceCity" select:"places.place_city"` PlaceState string `json:"PlaceState" select:"places.place_state"` PlaceCountry string `json:"PlaceCountry" select:"places.place_country"` InstanceID string `json:"InstanceID" select:"files.instance_id"` FileID uint `json:"-" select:"files.id AS file_id"` // File FileUID string `json:"FileUID" select:"files.file_uid"` FileRoot string `json:"FileRoot" select:"files.file_root"` FileName string `json:"FileName" select:"files.file_name"` FileHash string `json:"Hash" select:"files.file_hash"` FileWidth int `json:"Width" select:"files.file_width"` FileHeight int `json:"Height" select:"files.file_height"` FilePortrait bool `json:"Portrait" select:"files.file_portrait"` FilePrimary bool `json:"-" select:"files.file_primary"` FileSidecar bool `json:"-" select:"files.file_sidecar"` FileMissing bool `json:"-" select:"files.file_missing"` FileVideo bool `json:"-" select:"files.file_video"` FileDuration time.Duration `json:"-" select:"files.file_duration"` FileFPS float64 `json:"-" select:"files.file_fps"` FileFrames int `json:"-" select:"files.file_frames"` FileCodec string `json:"-" select:"files.file_codec"` FileType string `json:"-" select:"files.file_type"` MediaType string `json:"-" select:"files.media_type"` FileMime string `json:"-" select:"files.file_mime"` FileSize int64 `json:"-" select:"files.file_size"` FileOrientation int `json:"-" select:"files.file_orientation"` FileProjection string `json:"-" select:"files.file_projection"` FileAspectRatio float32 `json:"-" select:"files.file_aspect_ratio"` FileColors string `json:"-" select:"files.file_colors"` FileChroma uint8 `json:"-" select:"files.file_chroma"` FileLuminance string `json:"-" select:"files.file_luminance"` FileDiff uint32 `json:"-" select:"files.file_diff"` Merged bool `json:"Merged" select:"-"` CreatedAt time.Time `json:"CreatedAt" select:"photos.created_at"` UpdatedAt time.Time `json:"UpdatedAt" select:"photos.updated_at"` EditedAt time.Time `json:"EditedAt,omitempty" select:"photos.edited_at"` CheckedAt time.Time `json:"CheckedAt,omitempty" select:"photos.checked_at"` DeletedAt time.Time `json:"DeletedAt,omitempty" select:"photos.deleted_at"` Files []entity.File `json:"Files"` } // IsPlayable returns true if the photo has a related video/animation that is playable. func (photo *Photo) IsPlayable() bool { switch photo.PhotoType { case entity.MediaVideo, entity.MediaLive, entity.MediaAnimated: return true default: return false } } // ShareBase returns a meaningful file name for sharing. func (photo *Photo) ShareBase(seq int) string { var name string if photo.PhotoTitle != "" { name = txt.Title(slug.MakeLang(photo.PhotoTitle, "en")) } else { name = photo.PhotoUID } taken := photo.TakenAtLocal.Format("20060102-150405") if seq > 0 { return fmt.Sprintf("%s-%s (%d).%s", taken, name, seq, photo.FileType) } return fmt.Sprintf("%s-%s.%s", taken, name, photo.FileType) } type PhotoResults []Photo // UIDs returns a slice of photo UIDs. func (photos PhotoResults) UIDs() []string { result := make([]string, len(photos)) for i, el := range photos { result[i] = el.PhotoUID } return result } // Merge consecutive file results that belong to the same photo. func (photos PhotoResults) Merge() (merged PhotoResults, count int, err error) { count = len(photos) merged = make(PhotoResults, 0, count) var i int var photoId uint for _, photo := range photos { file := entity.File{} if err = deepcopier.Copy(&file).From(photo); err != nil { return merged, count, err } file.ID = photo.FileID if photoId == photo.ID && i > 0 { merged[i-1].Files = append(merged[i-1].Files, file) merged[i-1].Merged = true continue } i++ photoId = photo.ID photo.CompositeID = fmt.Sprintf("%d-%d", photoId, file.ID) photo.Files = append(photo.Files, file) merged = append(merged, photo) } return merged, count, nil }