Docs: Improve code comments in internal/entity/photo*.go

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-10-02 15:52:05 +02:00
parent 0ca3310f0a
commit 59b54663d8
12 changed files with 74 additions and 63 deletions

View File

@@ -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

View File

@@ -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{}

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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
}