package entity import ( "fmt" "path/filepath" "strings" "github.com/photoprism/photoprism/internal/classify" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/txt" ) // HasTitle checks if the photo has a title. func (m *Photo) HasTitle() bool { return m.PhotoTitle != "" } // NoTitle checks if the photo has no Title func (m *Photo) NoTitle() bool { return m.PhotoTitle == "" } // SetTitle changes the photo title and clips it to 300 characters. func (m *Photo) SetTitle(title, source string) { newTitle := txt.Clip(title, txt.ClipDefault) if newTitle == "" { return } if (SrcPriority[source] < SrcPriority[m.TitleSrc]) && m.HasTitle() { return } m.PhotoTitle = newTitle m.TitleSrc = source } // UpdateTitle updated the photo title based on location and labels. func (m *Photo) UpdateTitle(labels classify.Labels) error { if m.TitleSrc != SrcAuto && m.HasTitle() { return fmt.Errorf("photo: won't update title, %s was modified", m.PhotoUID) } var names string var knownLocation bool oldTitle := m.PhotoTitle fileTitle := m.FileTitle() people := m.SubjectNames() m.UpdateDescription(people) if n := len(people); n > 0 && n < 4 { names = txt.JoinNames(people, true) } if m.LocationLoaded() { knownLocation = true loc := m.Cell // TODO: User defined title format if names != "" { log.Debugf("photo: using %s to create title for %s", txt.Quote(names), m.PhotoUID) if l := len([]rune(names)); l > 35 { m.SetTitle(names, SrcAuto) } else if l > 20 && (loc.NoCity() || loc.LongCity()) { m.SetTitle(fmt.Sprintf("%s / %s", names, m.TakenAt.Format("2006")), SrcAuto) } else if l > 20 { m.SetTitle(fmt.Sprintf("%s / %s", names, loc.City()), SrcAuto) } else if loc.NoCity() || loc.LongCity() { m.SetTitle(fmt.Sprintf("%s / %s / %s", names, loc.CountryName(), m.TakenAt.Format("2006")), SrcAuto) } else { m.SetTitle(fmt.Sprintf("%s / %s / %s", names, loc.City(), m.TakenAt.Format("2006")), SrcAuto) } } else if title := labels.Title(loc.Name()); title != "" { log.Debugf("photo: using label %s to create title for %s", txt.Quote(title), m.PhotoUID) if loc.NoCity() || loc.LongCity() || loc.CityContains(title) { m.SetTitle(fmt.Sprintf("%s / %s / %s", txt.Title(title), loc.CountryName(), m.TakenAt.Format("2006")), SrcAuto) } else { m.SetTitle(fmt.Sprintf("%s / %s / %s", txt.Title(title), loc.City(), m.TakenAt.Format("2006")), SrcAuto) } } else if loc.Name() != "" && loc.City() != "" { if len(loc.Name()) > 45 { m.SetTitle(txt.Title(loc.Name()), SrcAuto) } else if len(loc.Name()) > 20 || len(loc.City()) > 16 || strings.Contains(loc.Name(), loc.City()) { m.SetTitle(fmt.Sprintf("%s / %s", loc.Name(), m.TakenAt.Format("2006")), SrcAuto) } else { m.SetTitle(fmt.Sprintf("%s / %s / %s", loc.Name(), loc.City(), m.TakenAt.Format("2006")), SrcAuto) } } else if loc.City() != "" && loc.CountryName() != "" { if len(loc.City()) > 20 { m.SetTitle(fmt.Sprintf("%s / %s", loc.City(), m.TakenAt.Format("2006")), SrcAuto) } else { m.SetTitle(fmt.Sprintf("%s / %s / %s", loc.City(), loc.CountryName(), m.TakenAt.Format("2006")), SrcAuto) } } } else if m.PlaceLoaded() { knownLocation = true if names != "" { log.Debugf("photo: using %s to create title for %s", txt.Quote(names), m.PhotoUID) if l := len([]rune(names)); l > 35 { m.SetTitle(names, SrcAuto) } else if l > 20 && (m.Place.NoCity() || m.Place.LongCity()) { m.SetTitle(fmt.Sprintf("%s / %s", names, m.TakenAt.Format("2006")), SrcAuto) } else if l > 20 { m.SetTitle(fmt.Sprintf("%s / %s", names, m.Place.City()), SrcAuto) } else if m.Place.NoCity() || m.Place.LongCity() { m.SetTitle(fmt.Sprintf("%s / %s / %s", names, m.Place.CountryName(), m.TakenAt.Format("2006")), SrcAuto) } else { m.SetTitle(fmt.Sprintf("%s / %s / %s", names, m.Place.City(), m.TakenAt.Format("2006")), SrcAuto) } } else if title := labels.Title(fileTitle); title != "" { log.Debugf("photo: using label %s to create title for %s", txt.Quote(title), m.PhotoUID) if m.Place.NoCity() || m.Place.LongCity() || m.Place.CityContains(title) { m.SetTitle(fmt.Sprintf("%s / %s / %s", txt.Title(title), m.Place.CountryName(), m.TakenAt.Format("2006")), SrcAuto) } else { m.SetTitle(fmt.Sprintf("%s / %s / %s", txt.Title(title), m.Place.City(), m.TakenAt.Format("2006")), SrcAuto) } } else if m.Place.City() != "" && m.Place.CountryName() != "" { if len(m.Place.City()) > 20 { m.SetTitle(fmt.Sprintf("%s / %s", m.Place.City(), m.TakenAt.Format("2006")), SrcAuto) } else { m.SetTitle(fmt.Sprintf("%s / %s / %s", m.Place.City(), m.Place.CountryName(), m.TakenAt.Format("2006")), SrcAuto) } } } if !knownLocation || m.NoTitle() { if names != "" { if len([]rune(names)) <= 35 && m.TakenSrc != SrcAuto { m.SetTitle(fmt.Sprintf("%s / %s", names, m.TakenAt.Format("2006")), SrcAuto) } else { m.SetTitle(names, SrcAuto) } } else if fileTitle == "" && len(labels) > 0 && labels[0].Priority >= -1 && labels[0].Uncertainty <= 85 && labels[0].Name != "" { if m.TakenSrc != SrcAuto { m.SetTitle(fmt.Sprintf("%s / %s", txt.Title(labels[0].Name), m.TakenAt.Format("2006")), SrcAuto) } else { m.SetTitle(txt.Title(labels[0].Name), SrcAuto) } } else if fileTitle != "" && len(fileTitle) <= 20 && !m.TakenAtLocal.IsZero() && m.TakenSrc != SrcAuto { m.SetTitle(fmt.Sprintf("%s / %s", fileTitle, m.TakenAtLocal.Format("2006")), SrcAuto) } else if fileTitle != "" { m.SetTitle(fileTitle, SrcAuto) } else { if m.TakenSrc != SrcAuto { m.SetTitle(fmt.Sprintf("%s / %s", UnknownTitle, m.TakenAt.Format("2006")), SrcAuto) } else { m.SetTitle(UnknownTitle, SrcAuto) } } } if m.PhotoTitle != oldTitle { log.Debugf("photo: changed title of %s to %s", m.PhotoUID, txt.Quote(m.PhotoTitle)) } return nil } // UpdateAndSaveTitle updates the photo title and saves it. func (m *Photo) UpdateAndSaveTitle() error { if !m.HasID() { return fmt.Errorf("photo: can't save to database, id is empty") } m.PhotoFaces = m.FaceCount() labels := m.ClassifyLabels() m.UpdateDateFields() if err := m.UpdateTitle(labels); err != nil { log.Info(err) } details := m.GetDetails() w := txt.UniqueWords(txt.Words(details.Keywords)) w = append(w, labels.Keywords()...) details.Keywords = strings.Join(txt.UniqueWords(w), ", ") if err := m.IndexKeywords(); err != nil { log.Errorf("photo: %s", err.Error()) } if err := m.Save(); err != nil { return err } return nil } // UpdateDescription updates the photo descriptions based on available metadata. func (m *Photo) UpdateDescription(people []string) { if m.DescriptionSrc != SrcAuto { return } // Add subject names to description when there's more than one person. if len(people) > 3 { m.PhotoDescription = txt.JoinNames(people, false) } else { m.PhotoDescription = "" } } // 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: if !fs.IsGenerated(m.PhotoName) { if title := txt.FileTitle(m.PhotoName); title != "" { return title } } // Generate title based on original file name, if any: if m.OriginalName != "" { if title := txt.FileTitle(m.OriginalName); !fs.IsGenerated(m.OriginalName) && title != "" { return title } else if title := txt.FileTitle(filepath.Dir(m.OriginalName)); title != "" { return title } } // Generate title based on photo path, if any: if m.PhotoPath != "" && !fs.IsGenerated(m.PhotoPath) { return txt.FileTitle(m.PhotoPath) } return "" } // SubjectNames returns all known subject names. func (m *Photo) SubjectNames() []string { if f, err := m.PrimaryFile(); err == nil { return f.SubjectNames() } return nil }