mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Docs: Improve code comments in internal/entity/photo*.go
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -490,7 +490,7 @@ func (m *Photo) BeforeCreate(scope *gorm.Scope) error {
|
||||
return scope.SetColumn("PhotoUID", m.PhotoUID)
|
||||
}
|
||||
|
||||
// BeforeSave ensures the existence of TakenAt properties before indexing or updating a photo
|
||||
// BeforeSave ensures the existence of TakenAt properties before indexing or updating a photo.
|
||||
func (m *Photo) BeforeSave(scope *gorm.Scope) error {
|
||||
if m.TakenAt.IsZero() || m.TakenAtLocal.IsZero() {
|
||||
now := Now()
|
||||
@@ -648,7 +648,7 @@ func (m *Photo) IndexKeywords() error {
|
||||
return db.Where("photo_id = ? AND keyword_id NOT IN (?)", m.ID, keywordIds).Delete(&PhotoKeyword{}).Error
|
||||
}
|
||||
|
||||
// PreloadFiles prepares gorm scope to retrieve photo file
|
||||
// PreloadFiles loads the non-deleted file records associated with the photo.
|
||||
func (m *Photo) PreloadFiles() {
|
||||
q := Db().
|
||||
Table("files").
|
||||
@@ -659,7 +659,7 @@ func (m *Photo) PreloadFiles() {
|
||||
Log("photo", "preload files", q.Scan(&m.Files).Error)
|
||||
}
|
||||
|
||||
// PreloadKeywords prepares gorm scope to retrieve photo keywords
|
||||
// PreloadKeywords loads keyword entities linked to the photo.
|
||||
func (m *Photo) PreloadKeywords() {
|
||||
q := Db().NewScope(nil).DB().
|
||||
Table("keywords").
|
||||
@@ -670,7 +670,7 @@ func (m *Photo) PreloadKeywords() {
|
||||
Log("photo", "preload files", q.Scan(&m.Keywords).Error)
|
||||
}
|
||||
|
||||
// PreloadAlbums prepares gorm scope to retrieve photo albums
|
||||
// PreloadAlbums loads albums related to the photo using the standard visibility filters.
|
||||
func (m *Photo) PreloadAlbums() {
|
||||
q := Db().NewScope(nil).DB().
|
||||
Table("albums").
|
||||
@@ -682,7 +682,7 @@ func (m *Photo) PreloadAlbums() {
|
||||
Log("photo", "preload albums", q.Scan(&m.Albums).Error)
|
||||
}
|
||||
|
||||
// PreloadMany prepares gorm scope to retrieve photo file, albums and keywords
|
||||
// PreloadMany loads the primary supporting associations (files, keywords, albums).
|
||||
func (m *Photo) PreloadMany() {
|
||||
m.PreloadFiles()
|
||||
m.PreloadKeywords()
|
||||
@@ -707,17 +707,17 @@ func (m *Photo) NormalizeValues() (normalized bool) {
|
||||
return normalized
|
||||
}
|
||||
|
||||
// NoCameraSerial checks if the photo has no CameraSerial
|
||||
// NoCameraSerial reports whether the photo has no camera serial assigned.
|
||||
func (m *Photo) NoCameraSerial() bool {
|
||||
return m.CameraSerial == ""
|
||||
}
|
||||
|
||||
// UnknownCamera test if the camera is unknown.
|
||||
// UnknownCamera tests whether the camera reference is the placeholder entry.
|
||||
func (m *Photo) UnknownCamera() bool {
|
||||
return m.CameraID == 0 || m.CameraID == UnknownCamera.ID
|
||||
}
|
||||
|
||||
// UnknownLens test if the lens is unknown.
|
||||
// UnknownLens tests whether the lens reference is the placeholder entry.
|
||||
func (m *Photo) UnknownLens() bool {
|
||||
return m.LensID == 0 || m.LensID == UnknownLens.ID
|
||||
}
|
||||
@@ -850,7 +850,7 @@ func (m *Photo) AddLabels(labels classify.Labels) {
|
||||
Db().Set("gorm:auto_preload", true).Model(m).Related(&m.Labels)
|
||||
}
|
||||
|
||||
// SetCamera updates the camera.
|
||||
// SetCamera updates the camera reference if the source priority allows the change.
|
||||
func (m *Photo) SetCamera(camera *Camera, source string) {
|
||||
if camera == nil {
|
||||
log.Warnf("photo: %s failed to update camera from source %s", m.String(), SrcString(source))
|
||||
@@ -874,7 +874,7 @@ func (m *Photo) SetCamera(camera *Camera, source string) {
|
||||
}
|
||||
}
|
||||
|
||||
// SetLens updates the lens.
|
||||
// SetLens updates the lens reference when the source outranks the existing metadata.
|
||||
func (m *Photo) SetLens(lens *Lens, source string) {
|
||||
if lens == nil {
|
||||
log.Warnf("photo: %s failed to update lens from source %s", m.String(), SrcString(source))
|
||||
@@ -918,7 +918,7 @@ func (m *Photo) SetExposure(focalLength int, fNumber float32, iso int, exposure,
|
||||
}
|
||||
}
|
||||
|
||||
// AllFilesMissing returns true, if all files for this photo are missing.
|
||||
// AllFilesMissing reports whether all files for this photo are marked missing.
|
||||
func (m *Photo) AllFilesMissing() bool {
|
||||
count := 0
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// PhotoAlbums is a helper alias for collections of PhotoAlbum relations.
|
||||
type PhotoAlbums []PhotoAlbum
|
||||
|
||||
// PhotoAlbum represents the many_to_many relation between Photo and Album
|
||||
// PhotoAlbum represents the many-to-many relation between Photo and Album.
|
||||
type PhotoAlbum struct {
|
||||
PhotoUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false" json:"PhotoUID" yaml:"UID"`
|
||||
AlbumUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;index" json:"AlbumUID" yaml:"-"`
|
||||
@@ -24,7 +25,7 @@ func (PhotoAlbum) TableName() string {
|
||||
return "photos_albums"
|
||||
}
|
||||
|
||||
// NewPhotoAlbum creates a new photo and album mapping with UIDs.
|
||||
// NewPhotoAlbum creates a new photo-to-album relation with the provided UIDs.
|
||||
func NewPhotoAlbum(photoUid, albumUid string) *PhotoAlbum {
|
||||
result := &PhotoAlbum{
|
||||
PhotoUID: photoUid,
|
||||
@@ -34,17 +35,17 @@ func NewPhotoAlbum(photoUid, albumUid string) *PhotoAlbum {
|
||||
return result
|
||||
}
|
||||
|
||||
// Create inserts a new row to the database.
|
||||
// Create inserts a new row into the database.
|
||||
func (m *PhotoAlbum) Create() error {
|
||||
return Db().Create(m).Error
|
||||
}
|
||||
|
||||
// Save updates or inserts a row.
|
||||
// Save updates an existing relation or inserts a new one if needed.
|
||||
func (m *PhotoAlbum) Save() error {
|
||||
return Db().Save(m).Error
|
||||
}
|
||||
|
||||
// FirstOrCreatePhotoAlbum returns the existing row, inserts a new row or nil in case of errors.
|
||||
// FirstOrCreatePhotoAlbum returns the persisted relation, creating it when necessary, or nil on failure.
|
||||
func FirstOrCreatePhotoAlbum(m *PhotoAlbum) *PhotoAlbum {
|
||||
result := PhotoAlbum{}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ func (m *Photo) GetCaptionSrc() string {
|
||||
return m.CaptionSrc
|
||||
}
|
||||
|
||||
// SetCaption sets the specified caption if is not empty and from the same source.
|
||||
// SetCaption stores the supplied caption when it is non-empty and the source priority is sufficient.
|
||||
func (m *Photo) SetCaption(caption, source string) {
|
||||
newCaption := txt.Clip(caption, txt.ClipLongText)
|
||||
|
||||
@@ -50,7 +50,7 @@ func (m *Photo) SetCaption(caption, source string) {
|
||||
m.CaptionSrc = source
|
||||
}
|
||||
|
||||
// GenerateCaption generates the caption from the specified list of at least 3 names if CaptionSrc is auto.
|
||||
// GenerateCaption builds an automatic caption from the supplied names when CaptionSrc is auto and enough names are known.
|
||||
func (m *Photo) GenerateCaption(names []string) {
|
||||
if m.CaptionSrc != SrcAuto {
|
||||
return
|
||||
|
||||
@@ -114,7 +114,7 @@ func (m *Photo) TimeZoneLocal() bool {
|
||||
return tz.IsLocal(m.TimeZone)
|
||||
}
|
||||
|
||||
// UpdateTimeZone updates the time zone.
|
||||
// UpdateTimeZone applies a new time zone when the source priority allows it and recalculates derived times.
|
||||
func (m *Photo) UpdateTimeZone(zone string) {
|
||||
if zone == "" {
|
||||
return
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// Accuracy1Km defines the maximum tolerated inaccuracy (in meters) for estimated coordinates.
|
||||
const Accuracy1Km = 1000
|
||||
|
||||
// EstimateCountry updates the photo with an estimated country if possible.
|
||||
@@ -50,7 +51,7 @@ func (m *Photo) EstimateCountry() {
|
||||
}
|
||||
}
|
||||
|
||||
// Set new country?
|
||||
// Assign the estimated country when we found a match.
|
||||
if countryCode != unknown {
|
||||
m.PhotoCountry = countryCode
|
||||
m.PlaceSrc = SrcEstimate
|
||||
@@ -94,6 +95,7 @@ func (m *Photo) EstimateLocation(force bool) {
|
||||
rangeMin := m.TakenAt.Add(-1 * time.Hour * 37)
|
||||
rangeMax := m.TakenAt.Add(time.Hour * 37)
|
||||
|
||||
// Collect up to two recent photos that match the time window and have known locations.
|
||||
var mostRecent Photos
|
||||
|
||||
switch DbDialect() {
|
||||
@@ -120,14 +122,14 @@ func (m *Photo) EstimateLocation(force bool) {
|
||||
log.Warnf("photo: %s while estimating position", err)
|
||||
}
|
||||
|
||||
// Found?
|
||||
// Abort if no nearby photos with reliable locations were found.
|
||||
if len(mostRecent) == 0 {
|
||||
log.Debugf("photo: unknown position at %s", m.TakenAt)
|
||||
m.RemoveLocation(SrcEstimate, false)
|
||||
m.RemoveLocationLabels()
|
||||
m.EstimateCountry()
|
||||
} else if recentPhoto := mostRecent[0]; recentPhoto.HasLocation() && recentPhoto.HasPlace() {
|
||||
// Too much time difference?
|
||||
// Abort if the time difference to the reference photo is outside the allowed window.
|
||||
if hours := recentPhoto.TakenAt.Sub(m.TakenAt) / time.Hour; hours < -36 || hours > 36 {
|
||||
log.Debugf("photo: skipping %s, %d hours time difference to recent position", m, hours)
|
||||
m.RemoveLocation(SrcEstimate, false)
|
||||
@@ -141,7 +143,7 @@ func (m *Photo) EstimateLocation(force bool) {
|
||||
|
||||
movement := geo.NewMovement(p1.Position(), p2.Position())
|
||||
|
||||
// Ignore inaccurate coordinate estimates.
|
||||
// Ignore coordinate estimates with poor accuracy or implausible travel distance.
|
||||
if estimate := movement.EstimatePosition(m.TakenAt); movement.Km() < 100 && estimate.Accuracy < Accuracy1Km {
|
||||
m.SetPosition(estimate, SrcEstimate, false)
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package entity
|
||||
|
||||
// PhotoKeyword represents the many-to-many relation between Photo and Keyword
|
||||
// PhotoKeyword represents the many-to-many relation between Photo and Keyword.
|
||||
type PhotoKeyword struct {
|
||||
PhotoID uint `gorm:"primary_key;auto_increment:false"`
|
||||
KeywordID uint `gorm:"primary_key;auto_increment:false;index"`
|
||||
@@ -11,7 +11,7 @@ func (PhotoKeyword) TableName() string {
|
||||
return "photos_keywords"
|
||||
}
|
||||
|
||||
// NewPhotoKeyword registers a new PhotoKeyword relation
|
||||
// NewPhotoKeyword returns a new PhotoKeyword relation ready for persistence.
|
||||
func NewPhotoKeyword(photoID, keywordID uint) *PhotoKeyword {
|
||||
result := &PhotoKeyword{
|
||||
PhotoID: photoID,
|
||||
|
||||
@@ -6,10 +6,11 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/ai/classify"
|
||||
)
|
||||
|
||||
// PhotoLabels is a convenience alias for lists of PhotoLabel relations.
|
||||
type PhotoLabels []PhotoLabel
|
||||
|
||||
// PhotoLabel represents the many-to-many relation between Photo and label.
|
||||
// Labels are weighted by uncertainty (100 - confidence)
|
||||
// PhotoLabel represents the many-to-many relation between Photo and Label.
|
||||
// Labels are weighted by uncertainty (100 - confidence).
|
||||
type PhotoLabel struct {
|
||||
PhotoID uint `gorm:"primary_key;auto_increment:false"`
|
||||
LabelID uint `gorm:"primary_key;auto_increment:false;index"`
|
||||
@@ -20,12 +21,12 @@ type PhotoLabel struct {
|
||||
Label *Label `gorm:"PRELOAD:true"`
|
||||
}
|
||||
|
||||
// TableName returns the entity table name.
|
||||
// TableName returns the database table name for PhotoLabel.
|
||||
func (PhotoLabel) TableName() string {
|
||||
return "photos_labels"
|
||||
}
|
||||
|
||||
// NewPhotoLabel registers a new PhotoLabel relation with an uncertainty and a source of label
|
||||
// NewPhotoLabel registers a new PhotoLabel relation with an uncertainty and source.
|
||||
func NewPhotoLabel(photoID, labelID uint, uncertainty int, source string) *PhotoLabel {
|
||||
result := &PhotoLabel{
|
||||
PhotoID: photoID,
|
||||
@@ -37,7 +38,7 @@ func NewPhotoLabel(photoID, labelID uint, uncertainty int, source string) *Photo
|
||||
return result
|
||||
}
|
||||
|
||||
// Updates multiple columns in the database.
|
||||
// Updates mutates multiple columns in the database and clears cached copies.
|
||||
func (m *PhotoLabel) Updates(values interface{}) error {
|
||||
if err := UnscopedDb().Model(m).UpdateColumns(values).Error; err != nil {
|
||||
return err
|
||||
@@ -46,7 +47,7 @@ func (m *PhotoLabel) Updates(values interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update a column in the database.
|
||||
// Update mutates a single column in the database and clears cached copies.
|
||||
func (m *PhotoLabel) Update(attr string, value interface{}) error {
|
||||
if err := UnscopedDb().Model(m).UpdateColumn(attr, value).Error; err != nil {
|
||||
return err
|
||||
@@ -55,7 +56,7 @@ func (m *PhotoLabel) Update(attr string, value interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AfterUpdate flushes the label cache when a label is updated.
|
||||
// AfterUpdate flushes the label cache after a relation change.
|
||||
func (m *PhotoLabel) AfterUpdate(tx *gorm.DB) (err error) {
|
||||
FlushCachedPhotoLabel(m)
|
||||
return
|
||||
@@ -64,6 +65,7 @@ func (m *PhotoLabel) AfterUpdate(tx *gorm.DB) (err error) {
|
||||
// Save updates the record in the database or inserts a new record if it does not already exist.
|
||||
func (m *PhotoLabel) Save() error {
|
||||
if m.Photo != nil {
|
||||
// Clear the eager-loaded Photo pointer so GORM does not attempt to persist it again.
|
||||
m.Photo = nil
|
||||
}
|
||||
|
||||
@@ -76,18 +78,18 @@ func (m *PhotoLabel) Save() error {
|
||||
return Db().Save(m).Error
|
||||
}
|
||||
|
||||
// Create inserts a new row to the database.
|
||||
// Create inserts a new row into the database without touching cache state.
|
||||
func (m *PhotoLabel) Create() error {
|
||||
return Db().Create(m).Error
|
||||
}
|
||||
|
||||
// AfterCreate sets the New column used for database callback
|
||||
// AfterCreate flushes the label cache once a relation has been persisted.
|
||||
func (m *PhotoLabel) AfterCreate(scope *gorm.Scope) error {
|
||||
FlushCachedPhotoLabel(m)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes the label reference.
|
||||
// Delete removes the label reference and clears the cache.
|
||||
func (m *PhotoLabel) Delete() error {
|
||||
FlushCachedPhotoLabel(m)
|
||||
return Db().Delete(m).Error
|
||||
@@ -113,7 +115,7 @@ func (m *PhotoLabel) CacheKey() string {
|
||||
return photoLabelCacheKey(m.PhotoID, m.LabelID)
|
||||
}
|
||||
|
||||
// FirstOrCreatePhotoLabel returns the existing row, inserts a new row or nil in case of errors.
|
||||
// FirstOrCreatePhotoLabel returns the existing row, inserts a new row, or nil in case of errors.
|
||||
func FirstOrCreatePhotoLabel(m *PhotoLabel) *PhotoLabel {
|
||||
if m == nil {
|
||||
return nil
|
||||
@@ -135,7 +137,7 @@ func FirstOrCreatePhotoLabel(m *PhotoLabel) *PhotoLabel {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClassifyLabel returns the label as classify.Label
|
||||
// ClassifyLabel returns the label as a classify.Label.
|
||||
func (m *PhotoLabel) ClassifyLabel() classify.Label {
|
||||
if m.Label == nil {
|
||||
log.Errorf("photo-label: classify label is nil (photo id %d, label id %d) - you may have found a bug", m.PhotoID, m.LabelID)
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// SetCoordinates changes the photo lat, lng and altitude if not empty and from an acceptable source.
|
||||
// SetCoordinates updates latitude/longitude while honoring source priority and keeping altitude in sync.
|
||||
func (m *Photo) SetCoordinates(lat, lng, altitude float64, source string) {
|
||||
m.SetAltitude(altitude, source)
|
||||
|
||||
@@ -32,7 +32,7 @@ func (m *Photo) SetCoordinates(lat, lng, altitude float64, source string) {
|
||||
m.PlaceSrc = source
|
||||
}
|
||||
|
||||
// SetAltitude sets the photo altitude if not empty and from an acceptable source.
|
||||
// SetAltitude updates the stored altitude when the new value passes cleanup and the source outranks the current one.
|
||||
func (m *Photo) SetAltitude(altitude float64, source string) {
|
||||
a := clean.Altitude(altitude)
|
||||
|
||||
@@ -52,7 +52,7 @@ func (m *Photo) UnknownLocation() bool {
|
||||
return m.CellID == "" || m.CellID == UnknownLocation.ID || m.NoLatLng()
|
||||
}
|
||||
|
||||
// SetPosition sets a position estimate.
|
||||
// SetPosition records an estimated position, randomizing estimates slightly and refreshing location metadata.
|
||||
func (m *Photo) SetPosition(pos geo.Position, source string, force bool) {
|
||||
if SrcPriority[m.PlaceSrc] > SrcPriority[source] && !force {
|
||||
return
|
||||
@@ -85,7 +85,7 @@ func (m *Photo) SetPosition(pos geo.Position, source string, force bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// AdoptPlace sets the place based on another photo.
|
||||
// AdoptPlace copies place metadata from another photo when the source priority allows it.
|
||||
func (m *Photo) AdoptPlace(other *Photo, source string, force bool) {
|
||||
if other == nil {
|
||||
return
|
||||
@@ -114,7 +114,7 @@ func (m *Photo) AdoptPlace(other *Photo, source string, force bool) {
|
||||
log.Debugf("photo: %s now located at %s (id %s)", m.String(), clean.Log(m.Place.Label()), m.PlaceID)
|
||||
}
|
||||
|
||||
// RemoveLocation removes the current location.
|
||||
// RemoveLocation clears the current location metadata when the caller is authorized via source priority.
|
||||
func (m *Photo) RemoveLocation(source string, force bool) {
|
||||
if SrcPriority[m.PlaceSrc] > SrcPriority[source] && !force {
|
||||
return
|
||||
@@ -140,7 +140,7 @@ func (m *Photo) RemoveLocation(source string, force bool) {
|
||||
m.PlaceSrc = SrcAuto
|
||||
}
|
||||
|
||||
// RemoveLocationLabels removes existing location labels.
|
||||
// RemoveLocationLabels removes labels created from prior location data to keep annotations consistent.
|
||||
func (m *Photo) RemoveLocationLabels() {
|
||||
if len(m.Labels) == 0 {
|
||||
res := Db().Delete(PhotoLabel{}, "photo_id = ? AND label_src = ?", m.ID, SrcLocation)
|
||||
@@ -198,7 +198,7 @@ func (m *Photo) LocationLoaded() bool {
|
||||
return !m.Cell.Unknown() && m.Cell.ID == m.CellID
|
||||
}
|
||||
|
||||
// LoadLocation loads the photo location from the database if not done already.
|
||||
// LoadLocation fetches the full Cell record (including Place) from the database when needed.
|
||||
func (m *Photo) LoadLocation() error {
|
||||
if m.LocationLoaded() {
|
||||
return nil
|
||||
@@ -235,7 +235,7 @@ func (m *Photo) PlaceLoaded() bool {
|
||||
return !m.Place.Unknown() && m.Place.ID == m.PlaceID
|
||||
}
|
||||
|
||||
// LoadPlace loads the photo place from the database if not done already.
|
||||
// LoadPlace fetches the associated Place from the database when needed.
|
||||
func (m *Photo) LoadPlace() error {
|
||||
if m.PlaceLoaded() {
|
||||
return nil
|
||||
@@ -348,7 +348,7 @@ func (m *Photo) GetTakenAtLocal() time.Time {
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateLocation updates location and labels based on latitude and longitude.
|
||||
// UpdateLocation resolves cells, places, time zones, and labels from the current coordinates.
|
||||
func (m *Photo) UpdateLocation() (keywords []string, labels classify.Labels) {
|
||||
if m.HasLatLng() {
|
||||
var loc = NewCell(m.PhotoLat, m.PhotoLng)
|
||||
@@ -432,7 +432,7 @@ func (m *Photo) UpdateLocation() (keywords []string, labels classify.Labels) {
|
||||
return keywords, labels
|
||||
}
|
||||
|
||||
// SaveLocation updates location data and saves the photo metadata back to the index.
|
||||
// SaveLocation applies UpdateLocation, synchronizes derived labels/keywords, and persists the photo.
|
||||
func (m *Photo) SaveLocation() error {
|
||||
locKeywords, labels := m.UpdateLocation()
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
var photoMergeMutex = sync.Mutex{}
|
||||
|
||||
// ResolvePrimary ensures there is only one primary file for a photo.
|
||||
// ResolvePrimary ensures only one associated file remains marked as primary, delegating to the file helper.
|
||||
func (m *Photo) ResolvePrimary() error {
|
||||
var file File
|
||||
|
||||
@@ -23,7 +23,7 @@ func (m *Photo) ResolvePrimary() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stackable tests if the photo may be stacked.
|
||||
// Stackable reports whether the photo participates in stacking workflows.
|
||||
func (m *Photo) Stackable() bool {
|
||||
if !m.HasID() || m.PhotoStack == IsUnstacked || m.PhotoName == "" {
|
||||
return false
|
||||
@@ -32,7 +32,7 @@ func (m *Photo) Stackable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Identical returns identical photos that can be merged.
|
||||
// Identical returns candidate photos that can be merged with the current one based on metadata and/or UUID.
|
||||
func (m *Photo) Identical(includeMeta, includeUuid bool) (identical Photos, err error) {
|
||||
if !m.Stackable() {
|
||||
return identical, nil
|
||||
@@ -77,7 +77,7 @@ func (m *Photo) Identical(includeMeta, includeUuid bool) (identical Photos, err
|
||||
return identical, nil
|
||||
}
|
||||
|
||||
// Merge photo with identical ones.
|
||||
// Merge collapses identical photos into a single original, reassigning files and associations while marking duplicates deleted.
|
||||
func (m *Photo) Merge(mergeMeta, mergeUuid bool) (original Photo, merged Photos, err error) {
|
||||
photoMergeMutex.Lock()
|
||||
defer photoMergeMutex.Unlock()
|
||||
|
||||
@@ -8,12 +8,13 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// Optimize the picture metadata based on the specified parameters.
|
||||
// Optimize updates picture metadata, enriching titles, keywords, and locations according to the supplied flags.
|
||||
func (m *Photo) Optimize(mergeMeta, mergeUuid, estimateLocation, force bool) (updated bool, merged Photos, err error) {
|
||||
if !m.HasID() {
|
||||
return false, merged, errors.New("photo: cannot maintain, id is empty")
|
||||
}
|
||||
|
||||
// Keep a snapshot so we can detect whether anything changed.
|
||||
current := *m
|
||||
|
||||
if m.HasLatLng() && !m.HasLocation() {
|
||||
@@ -55,11 +56,13 @@ func (m *Photo) Optimize(mergeMeta, mergeUuid, estimateLocation, force bool) (up
|
||||
|
||||
checked := Now()
|
||||
|
||||
// Skip persistence when nothing changed besides the CheckedAt timestamp.
|
||||
if reflect.DeepEqual(*m, current) {
|
||||
return false, merged, m.Update("CheckedAt", &checked)
|
||||
}
|
||||
|
||||
m.CheckedAt = &checked
|
||||
|
||||
// Persist the updated metadata to the database.
|
||||
return true, merged, m.Save()
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ var (
|
||||
year2012 = time.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
)
|
||||
|
||||
// QualityScore returns a score based on photo properties like size and metadata.
|
||||
// QualityScore returns the heuristic review score derived from favorites, trusted metadata, age, and resolution.
|
||||
func (m *Photo) QualityScore() (score int) {
|
||||
if m.PhotoFavorite {
|
||||
score += 3
|
||||
@@ -51,7 +51,7 @@ func (m *Photo) QualityScore() (score int) {
|
||||
return score
|
||||
}
|
||||
|
||||
// UpdateQuality updates the photo quality attribute.
|
||||
// UpdateQuality recomputes the quality score and persists it unless the photo is already deleted/invalid.
|
||||
func (m *Photo) UpdateQuality() error {
|
||||
if m.DeletedAt != nil || m.PhotoQuality < 0 {
|
||||
return nil
|
||||
@@ -62,7 +62,7 @@ func (m *Photo) UpdateQuality() error {
|
||||
return m.Update("PhotoQuality", m.PhotoQuality)
|
||||
}
|
||||
|
||||
// IsNonPhotographic checks whether the image appears to be non-photographic.
|
||||
// IsNonPhotographic checks whether the image looks like a non-photographic asset based on type and keywords.
|
||||
func (m *Photo) IsNonPhotographic() (result bool) {
|
||||
if m.PhotoType == MediaUnknown || m.PhotoType == MediaVector || m.PhotoType == MediaAnimated || m.PhotoType == MediaDocument {
|
||||
return true
|
||||
|
||||
@@ -19,7 +19,7 @@ func (m *Photo) HasTitle() bool {
|
||||
return m.PhotoTitle != ""
|
||||
}
|
||||
|
||||
// NoTitle checks if the photo has no Title
|
||||
// NoTitle reports whether the photo has no title.
|
||||
func (m *Photo) NoTitle() bool {
|
||||
return m.PhotoTitle == ""
|
||||
}
|
||||
@@ -29,7 +29,8 @@ func (m *Photo) GetTitle() string {
|
||||
return m.PhotoTitle
|
||||
}
|
||||
|
||||
// SetTitle changes the photo title and clips it to 300 characters.
|
||||
// SetTitle updates the photo title when the supplied source outranks the current one.
|
||||
// The title is normalized, quotes are unified, and the final value is clipped to 300 characters.
|
||||
func (m *Photo) SetTitle(title, source string) {
|
||||
title = strings.Trim(title, "_&|{}<>: \n\r\t\\")
|
||||
title = strings.ReplaceAll(title, "\"", "'")
|
||||
@@ -39,6 +40,7 @@ func (m *Photo) SetTitle(title, source string) {
|
||||
p := SrcPriority[source]
|
||||
|
||||
// Compare the source priority with the priority of the current title source.
|
||||
// Ignore requests from lower ranked sources so manual and trusted titles stay in place.
|
||||
if (p < SrcPriority[m.TitleSrc]) && m.HasTitle() {
|
||||
return
|
||||
}
|
||||
@@ -52,8 +54,8 @@ func (m *Photo) SetTitle(title, source string) {
|
||||
m.TitleSrc = source
|
||||
}
|
||||
|
||||
// GenerateTitle tries to generate a title based on the picture
|
||||
// location and labels if no other title is currently set.
|
||||
// GenerateTitle derives an automatic title using location, labels, and subject metadata
|
||||
// when the current title source allows auto-generation.
|
||||
func (m *Photo) GenerateTitle(labels classify.Labels) error {
|
||||
if m.TitleSrc != SrcAuto {
|
||||
return fmt.Errorf("photo: %s keeps existing %s title", m.String(), SrcString(m.TitleSrc))
|
||||
@@ -165,7 +167,7 @@ func (m *Photo) GenerateTitle(labels classify.Labels) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Log changes.
|
||||
// Log changes for debugging and auditing.
|
||||
if m.PhotoTitle != oldTitle {
|
||||
log.Debugf("photo: %s has new title %s [%s]", m.String(), clean.Log(m.PhotoTitle), time.Since(start))
|
||||
}
|
||||
@@ -208,14 +210,14 @@ func (m *Photo) GenerateAndSaveTitle() error {
|
||||
|
||||
// FileTitle returns a photo title based on the file name and/or path.
|
||||
func (m *Photo) FileTitle() string {
|
||||
// Generate title based on photo name, if not generated:
|
||||
// Generate a title from the photo name when the name was not generated automatically.
|
||||
if !fs.IsGenerated(m.PhotoName) {
|
||||
if title := txt.FileTitle(m.PhotoName); title != "" {
|
||||
return title
|
||||
}
|
||||
}
|
||||
|
||||
// Generate title based on original file name, if any:
|
||||
// Generate a title from the original file name, if available.
|
||||
if m.OriginalName != "" {
|
||||
if title := txt.FileTitle(m.OriginalName); !fs.IsGenerated(m.OriginalName) && title != "" {
|
||||
return title
|
||||
@@ -224,7 +226,7 @@ func (m *Photo) FileTitle() string {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate title based on photo path, if any:
|
||||
// Fall back to the photo path when no other title could be inferred.
|
||||
if m.PhotoPath != "" && !fs.IsGenerated(m.PhotoPath) {
|
||||
return txt.FileTitle(m.PhotoPath)
|
||||
}
|
||||
@@ -257,5 +259,6 @@ func (m *Photo) UpdateTitleLabels() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove stale title-based labels so the photo reflects the current title.
|
||||
return Db().Where("label_src = ? AND photo_id = ? AND label_id NOT IN (?)", classify.SrcTitle, m.ID, labelIds).Delete(&PhotoLabel{}).Error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user