Refresh titles, labels and locations
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
af3945d2fe
commit
f07064c2c3
18 changed files with 1420 additions and 301 deletions
|
@ -117,6 +117,7 @@
|
|||
|
||||
<v-flex xs12 sm6 md3 class="pa-2 p-timezone-select">
|
||||
<v-autocomplete
|
||||
@change="updateTime"
|
||||
:disabled="disabled"
|
||||
:label="labels.timezone"
|
||||
hide-details
|
||||
|
@ -198,10 +199,10 @@
|
|||
hide-details
|
||||
browser-autocomplete="off"
|
||||
color="secondary-dark"
|
||||
item-value="code"
|
||||
item-text="name"
|
||||
item-value="Code"
|
||||
item-text="Name"
|
||||
v-model="model.PhotoCountry"
|
||||
:items="countryOptions">
|
||||
:items="countries">
|
||||
</v-select>
|
||||
</v-flex>
|
||||
|
||||
|
@ -376,6 +377,7 @@
|
|||
import options from "resources/options.json";
|
||||
import {DateTime} from "luxon";
|
||||
import moment from "moment-timezone"
|
||||
import countries from "resources/countries.json";
|
||||
|
||||
export default {
|
||||
name: 'p-tab-photo-edit-details',
|
||||
|
@ -387,11 +389,11 @@
|
|||
disabled: !this.$config.feature("edit"),
|
||||
config: this.$config.values,
|
||||
all: {
|
||||
countries: [{code: "", name: ""}],
|
||||
colors: [{label: "Unknown", name: ""}],
|
||||
},
|
||||
readonly: this.$config.getValue("readonly"),
|
||||
options: options,
|
||||
countries: countries,
|
||||
labels: {
|
||||
search: this.$gettext("Search"),
|
||||
view: this.$gettext("View"),
|
||||
|
@ -411,28 +413,48 @@
|
|||
showTimePicker: false,
|
||||
date: "",
|
||||
time: "",
|
||||
dateFormatted: "",
|
||||
timeFormatted: "",
|
||||
timeLocalFormatted: "",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
dateFormatted() {
|
||||
watch: {
|
||||
date() {
|
||||
if (!this.date) {
|
||||
return "";
|
||||
this.dateFormatted = "";
|
||||
this.timeLocalFormatted = "";
|
||||
return
|
||||
}
|
||||
|
||||
return DateTime.fromISO(this.date).toLocaleString(DateTime.DATE_FULL);
|
||||
this.dateFormatted = DateTime.fromISO(this.date).toLocaleString(DateTime.DATE_FULL);
|
||||
},
|
||||
timeFormatted() {
|
||||
if (!this.time) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return DateTime.fromISO(this.time).toLocaleString(DateTime.TIME_24_WITH_SECONDS);
|
||||
time() {
|
||||
this.updateTime();
|
||||
},
|
||||
timeLocalFormatted() {
|
||||
},
|
||||
computed: {
|
||||
cameraOptions() {
|
||||
return this.config.cameras;
|
||||
},
|
||||
lensOptions() {
|
||||
return this.config.lenses;
|
||||
},
|
||||
colorOptions() {
|
||||
return this.all.colors.concat(this.config.colors);
|
||||
},
|
||||
timeZones() {
|
||||
return moment.tz.names();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateTime() {
|
||||
if (!this.time || !this.date) {
|
||||
return ""
|
||||
this.timeFormatted = ""
|
||||
this.timeLocalFormatted = ""
|
||||
}
|
||||
|
||||
this.timeFormatted = DateTime.fromISO(this.time).toLocaleString(DateTime.TIME_24_WITH_SECONDS);
|
||||
|
||||
const utcDate = this.date + "T" + this.time + "Z";
|
||||
|
||||
this.model.TakenAt = utcDate;
|
||||
|
@ -450,25 +472,8 @@
|
|||
includeOffset: false,
|
||||
}) + "Z";
|
||||
|
||||
return localDate.toLocaleString(DateTime.TIME_24_WITH_SECONDS);
|
||||
this.timeLocalFormatted = localDate.toLocaleString(DateTime.TIME_24_WITH_SECONDS);
|
||||
},
|
||||
countryOptions() {
|
||||
return this.all.countries.concat(this.config.countries);
|
||||
},
|
||||
cameraOptions() {
|
||||
return this.config.cameras;
|
||||
},
|
||||
lensOptions() {
|
||||
return this.config.lenses;
|
||||
},
|
||||
colorOptions() {
|
||||
return this.all.colors.concat(this.config.colors);
|
||||
},
|
||||
timeZones() {
|
||||
return moment.tz.names();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
left() {
|
||||
this.$emit('next');
|
||||
},
|
||||
|
@ -485,6 +490,8 @@
|
|||
const date = DateTime.fromISO(model.TakenAt).toUTC();
|
||||
this.date = date.toISODate();
|
||||
this.time = date.toFormat("HH:mm:ss");
|
||||
|
||||
this.updateTime();
|
||||
}
|
||||
},
|
||||
save(close) {
|
||||
|
@ -495,6 +502,8 @@
|
|||
this.model.update().then(() => {
|
||||
if (close) {
|
||||
this.$emit('close');
|
||||
} else {
|
||||
this.refresh(this.model);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -2,47 +2,51 @@ import RestModel from "model/rest";
|
|||
import Api from "common/api";
|
||||
import {DateTime} from "luxon";
|
||||
|
||||
const SrcAuto = ""
|
||||
const SrcManual = "manual"
|
||||
const SrcImg = "img"
|
||||
const SrcXmp = "xmp"
|
||||
const SrcYml = "yml"
|
||||
|
||||
class Photo extends RestModel {
|
||||
getDefaults() {
|
||||
return {
|
||||
ID: 0,
|
||||
TakenAt: "",
|
||||
TakenAtLocal: "",
|
||||
TakenSrc: "",
|
||||
TimeZone: "",
|
||||
PhotoUUID: "",
|
||||
PhotoPath: "",
|
||||
PhotoName: "",
|
||||
PhotoTitle: "",
|
||||
TitleSrc: "",
|
||||
PhotoFavorite: false,
|
||||
PhotoPrivate: false,
|
||||
PhotoNSFW: false,
|
||||
PhotoStory: false,
|
||||
PhotoReview: false,
|
||||
PhotoLat: 0.0,
|
||||
PhotoLng: 0.0,
|
||||
PhotoAltitude: 0,
|
||||
PhotoFocalLength: 0,
|
||||
PhotoIso: 0,
|
||||
PhotoFocalLength: 0,
|
||||
PhotoFNumber: 0.0,
|
||||
PhotoExposure: "",
|
||||
PhotoViews: 0,
|
||||
Camera: {},
|
||||
CameraID: 0,
|
||||
CameraSrc: "",
|
||||
Lens: {},
|
||||
LensID: 0,
|
||||
CountryChanged: false,
|
||||
Location: null,
|
||||
LocationID: "",
|
||||
LocationSrc: "",
|
||||
Place: null,
|
||||
PlaceID: "",
|
||||
LocationEstimated: false,
|
||||
PhotoCountry: "",
|
||||
PhotoYear: 0,
|
||||
PhotoMonth: 0,
|
||||
TakenAtLocal: "",
|
||||
ModifiedTitle: false,
|
||||
ModifiedDescription: false,
|
||||
ModifiedDate: false,
|
||||
ModifiedLocation: false,
|
||||
ModifiedCamera: false,
|
||||
TimeZone: "",
|
||||
Description: {
|
||||
PhotoDescription: "",
|
||||
PhotoKeywords: "",
|
||||
|
@ -52,6 +56,7 @@ class Photo extends RestModel {
|
|||
PhotoCopyright: "",
|
||||
PhotoLicense: "",
|
||||
},
|
||||
DescriptionSrc: "",
|
||||
Files: [],
|
||||
Labels: [],
|
||||
Keywords: [],
|
||||
|
@ -241,23 +246,23 @@ class Photo extends RestModel {
|
|||
const values = this.getValues(true);
|
||||
|
||||
if(values.PhotoTitle) {
|
||||
values.ModifiedTitle = true;
|
||||
values.TitleSrc = SrcManual;
|
||||
}
|
||||
|
||||
if(values.Description) {
|
||||
values.ModifiedDescription = true;
|
||||
values.DescriptionSrc = SrcManual;
|
||||
}
|
||||
|
||||
if(values.PhotoLat || values.PhotoLng || values.PhotoAltitude || values.PhotoCountry) {
|
||||
values.ModifiedLocation = true;
|
||||
if(values.PhotoLat || values.PhotoLng) {
|
||||
values.LocationSrc = SrcManual;
|
||||
}
|
||||
|
||||
if(values.TakenAt || values.TimeZone) {
|
||||
values.ModifiedDate = true;
|
||||
values.TakenSrc = SrcManual;
|
||||
}
|
||||
|
||||
if(values.CameraID || values.LensID) {
|
||||
values.ModifiedCamera = true;
|
||||
if(values.CameraID || values.LensID || values.PhotoFocalLength || values.PhotoFNumber || values.PhotoIso || values.PhotoExposure) {
|
||||
values.CameraSrc = SrcManual;
|
||||
}
|
||||
|
||||
return Api.put(this.getEntityResource(), values).then((response) => Promise.resolve(this.setValues(response.data)));
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
slot-scope="{ hover }"
|
||||
:dark="selection.includes(label.LabelUUID)"
|
||||
:class="selection.includes(label.LabelUUID) ? 'elevation-10 ma-0 accent darken-1 white--text' : 'elevation-0 ma-1 accent lighten-3'"
|
||||
:to="{name: 'photos', query: {q: 'label:' + label.CustomSlug}}">
|
||||
:to="{name: 'photos', query: {q: 'label:' + (label.CustomSlug ? label.CustomSlug : label.LabelSlug)}}">
|
||||
<v-img
|
||||
:src="label.getThumbnailUrl('tile_500')"
|
||||
aspect-ratio="1"
|
||||
|
|
1002
frontend/src/resources/countries.json
Normal file
1002
frontend/src/resources/countries.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -52,7 +52,7 @@ func AddPhotoLabel(router *gin.RouterGroup, conf *config.Config) {
|
|||
|
||||
if plm.LabelUncertainty > f.LabelUncertainty {
|
||||
plm.LabelUncertainty = f.LabelUncertainty
|
||||
plm.LabelSource = entity.LabelSourceManual
|
||||
plm.LabelSource = entity.SrcManual
|
||||
|
||||
if err := db.Save(&plm).Error; err != nil {
|
||||
log.Errorf("label: %s", err)
|
||||
|
@ -112,7 +112,7 @@ func RemovePhotoLabel(router *gin.RouterGroup, conf *config.Config) {
|
|||
return
|
||||
}
|
||||
|
||||
if label.LabelSource == entity.LabelSourceManual {
|
||||
if label.LabelSource == entity.SrcManual {
|
||||
db.Delete(&label)
|
||||
} else {
|
||||
label.LabelUncertainty = 100
|
||||
|
|
|
@ -115,9 +115,19 @@ func (m *Label) Update(label classify.Label, db *gorm.DB) error {
|
|||
save = true
|
||||
}
|
||||
|
||||
if !save {
|
||||
return nil
|
||||
if save {
|
||||
if err := db.Save(m).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return db.Save(m).Error
|
||||
// Add categories
|
||||
for _, category := range label.Categories {
|
||||
sn := NewLabel(txt.Title(category), -3).FirstOrCreate(db)
|
||||
if err := db.Model(m).Association("LabelCategories").Append(sn).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/internal/classify"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
|
@ -15,52 +17,52 @@ import (
|
|||
|
||||
// Photo represents a photo, all its properties, and link to all its images and sidecar files.
|
||||
type Photo struct {
|
||||
ID uint `gorm:"primary_key"`
|
||||
TakenAt time.Time `gorm:"type:datetime;index:idx_photos_taken_uuid;" json:"TakenAt"`
|
||||
PhotoUUID string `gorm:"type:varbinary(36);unique_index;index:idx_photos_taken_uuid;"`
|
||||
PhotoPath string `gorm:"type:varbinary(512);index;"`
|
||||
PhotoName string `gorm:"type:varbinary(256);"`
|
||||
PhotoTitle string `json:"PhotoTitle"`
|
||||
PhotoFavorite bool `json:"PhotoFavorite"`
|
||||
PhotoPrivate bool `json:"PhotoPrivate"`
|
||||
PhotoNSFW bool `json:"PhotoNSFW"`
|
||||
PhotoStory bool `json:"PhotoStory"`
|
||||
PhotoLat float64 `gorm:"index;" json:"PhotoLat"`
|
||||
PhotoLng float64 `gorm:"index;" json:"PhotoLng"`
|
||||
PhotoAltitude int `json:"PhotoAltitude"`
|
||||
PhotoFocalLength int `json:"PhotoFocalLength"`
|
||||
PhotoIso int `json:"PhotoIso"`
|
||||
PhotoFNumber float64 `json:"PhotoFNumber"`
|
||||
PhotoExposure string `gorm:"type:varbinary(64);" json:"PhotoExposure"`
|
||||
CameraID uint `gorm:"index:idx_photos_camera_lens;" json:"CameraID"`
|
||||
CameraSerial string `gorm:"type:varbinary(128);" json:"CameraSerial"`
|
||||
LensID uint `gorm:"index:idx_photos_camera_lens;" json:"LensID"`
|
||||
PlaceID string `gorm:"type:varbinary(16);index;" json:"PlaceID"`
|
||||
LocationID string `gorm:"type:varbinary(16);index;" json:"LocationID"`
|
||||
LocationEstimated bool `json:"LocationEstimated"`
|
||||
PhotoCountry string `gorm:"index:idx_photos_country_year_month;" json:"PhotoCountry"`
|
||||
PhotoYear int `gorm:"index:idx_photos_country_year_month;"`
|
||||
PhotoMonth int `gorm:"index:idx_photos_country_year_month;"`
|
||||
TimeZone string `gorm:"type:varbinary(64);" json:"TimeZone"`
|
||||
TakenAtLocal time.Time `gorm:"type:datetime;"`
|
||||
ModifiedTitle bool `json:"ModifiedTitle"`
|
||||
ModifiedDescription bool `json:"ModifiedDescription"`
|
||||
ModifiedDate bool `json:"ModifiedDate"`
|
||||
ModifiedLocation bool `json:"ModifiedLocation"`
|
||||
ModifiedCamera bool `json:"ModifiedCamera"`
|
||||
Description Description `json:"Description"`
|
||||
Camera *Camera `json:"Camera"`
|
||||
Lens *Lens `json:"Lens"`
|
||||
Location *Location `json:"Location"`
|
||||
Place *Place `json:"-"`
|
||||
Links []Link `gorm:"foreignkey:ShareUUID;association_foreignkey:PhotoUUID"`
|
||||
Keywords []Keyword `json:"-"`
|
||||
Albums []Album `json:"-"`
|
||||
Files []File
|
||||
Labels []PhotoLabel
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time `sql:"index"`
|
||||
ID uint `gorm:"primary_key"`
|
||||
TakenAt time.Time `gorm:"type:datetime;index:idx_photos_taken_uuid;" json:"TakenAt"`
|
||||
TakenSrc string `gorm:"type:varbinary(8);" json:"TakenSrc"`
|
||||
PhotoUUID string `gorm:"type:varbinary(36);unique_index;index:idx_photos_taken_uuid;"`
|
||||
PhotoPath string `gorm:"type:varbinary(512);index;"`
|
||||
PhotoName string `gorm:"type:varbinary(256);"`
|
||||
PhotoTitle string `json:"PhotoTitle"`
|
||||
TitleSrc string `gorm:"type:varbinary(8);" json:"TitleSrc"`
|
||||
PhotoFavorite bool `json:"PhotoFavorite"`
|
||||
PhotoPrivate bool `json:"PhotoPrivate"`
|
||||
PhotoNSFW bool `json:"PhotoNSFW"`
|
||||
PhotoStory bool `json:"PhotoStory"`
|
||||
PhotoReview bool `json:"PhotoReview"`
|
||||
PhotoLat float64 `gorm:"index;" json:"PhotoLat"`
|
||||
PhotoLng float64 `gorm:"index;" json:"PhotoLng"`
|
||||
PhotoAltitude int `json:"PhotoAltitude"`
|
||||
PhotoIso int `json:"PhotoIso"`
|
||||
PhotoFocalLength int `json:"PhotoFocalLength"`
|
||||
PhotoFNumber float64 `json:"PhotoFNumber"`
|
||||
PhotoExposure string `gorm:"type:varbinary(64);" json:"PhotoExposure"`
|
||||
CameraID uint `gorm:"index:idx_photos_camera_lens;" json:"CameraID"`
|
||||
CameraSerial string `gorm:"type:varbinary(128);" json:"CameraSerial"`
|
||||
CameraSrc string `gorm:"type:varbinary(8);" json:"CameraSrc"`
|
||||
LensID uint `gorm:"index:idx_photos_camera_lens;" json:"LensID"`
|
||||
PlaceID string `gorm:"type:varbinary(16);index;" json:"PlaceID"`
|
||||
LocationID string `gorm:"type:varbinary(16);index;" json:"LocationID"`
|
||||
LocationSrc string `gorm:"type:varbinary(8);" json:"LocationSrc"`
|
||||
PhotoCountry string `gorm:"index:idx_photos_country_year_month;" json:"PhotoCountry"`
|
||||
PhotoYear int `gorm:"index:idx_photos_country_year_month;"`
|
||||
PhotoMonth int `gorm:"index:idx_photos_country_year_month;"`
|
||||
TimeZone string `gorm:"type:varbinary(64);" json:"TimeZone"`
|
||||
TakenAtLocal time.Time `gorm:"type:datetime;"`
|
||||
Description Description `json:"Description"`
|
||||
DescriptionSrc string `gorm:"type:varbinary(8);" json:"DescriptionSrc"`
|
||||
Camera *Camera `json:"Camera"`
|
||||
Lens *Lens `json:"Lens"`
|
||||
Location *Location `json:"Location"`
|
||||
Place *Place `json:"-"`
|
||||
Links []Link `gorm:"foreignkey:ShareUUID;association_foreignkey:PhotoUUID"`
|
||||
Keywords []Keyword `json:"-"`
|
||||
Albums []Album `json:"-"`
|
||||
Files []File
|
||||
Labels []PhotoLabel
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt *time.Time `sql:"index"`
|
||||
}
|
||||
|
||||
// SavePhotoForm updates a model using form data and persists it in the database.
|
||||
|
@ -79,22 +81,23 @@ func SavePhotoForm(model Photo, form form.Photo, db *gorm.DB, geoApi string) err
|
|||
model.Description.PhotoKeywords = strings.Join(txt.UniqueKeywords(model.Description.PhotoKeywords), ", ")
|
||||
}
|
||||
|
||||
if model.HasLatLng() && locChanged && model.ModifiedLocation {
|
||||
if model.HasLatLng() && locChanged && model.LocationSrc == SrcManual {
|
||||
locKeywords, labels := model.UpdateLocation(db, geoApi)
|
||||
|
||||
model.AddLabels(labels, db)
|
||||
|
||||
w := txt.UniqueKeywords(model.Description.PhotoKeywords)
|
||||
|
||||
var locKeywords []string
|
||||
labels := model.ClassifyLabels()
|
||||
|
||||
locKeywords, labels = model.IndexLocation(db, geoApi, labels)
|
||||
|
||||
w = append(w, locKeywords...)
|
||||
w = append(w, labels.Keywords()...)
|
||||
|
||||
model.Description.PhotoKeywords = strings.Join(txt.UniqueWords(w), ", ")
|
||||
}
|
||||
|
||||
if err := model.UpdateTitle(model.ClassifyLabels()); err != nil {
|
||||
log.Warnf("%s (%s)", err.Error(), model.PhotoUUID)
|
||||
}
|
||||
|
||||
if err := model.IndexKeywords(db); err != nil {
|
||||
log.Error(err)
|
||||
log.Warnf("%s (%s)", err.Error(), model.PhotoUUID)
|
||||
}
|
||||
|
||||
return db.Unscoped().Save(&model).Error
|
||||
|
@ -305,3 +308,91 @@ func (m *Photo) HasTitle() bool {
|
|||
func (m *Photo) DescriptionLoaded() bool {
|
||||
return m.Description.PhotoID == m.ID
|
||||
}
|
||||
|
||||
// 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 errors.New("photo: won't update title, was modified")
|
||||
}
|
||||
|
||||
m.TitleSrc = SrcAuto
|
||||
|
||||
hasLocation := m.Location != nil && m.Location.Place != nil
|
||||
|
||||
if hasLocation {
|
||||
loc := m.Location
|
||||
|
||||
if title := labels.Title(loc.Name()); title != "" { // TODO: User defined title format
|
||||
log.Infof("photo: using label \"%s\" to create photo title", title)
|
||||
if loc.NoCity() || loc.LongCity() || loc.CityContains(title) {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s / %s", txt.Title(title), loc.CountryName(), m.TakenAt.Format("2006"))
|
||||
} else {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s / %s", txt.Title(title), loc.City(), m.TakenAt.Format("2006"))
|
||||
}
|
||||
} else if loc.Name() != "" && loc.City() != "" {
|
||||
if len(loc.Name()) > 45 {
|
||||
m.PhotoTitle = txt.Title(loc.Name())
|
||||
} else if len(loc.Name()) > 20 || len(loc.City()) > 16 || strings.Contains(loc.Name(), loc.City()) {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s", loc.Name(), m.TakenAt.Format("2006"))
|
||||
} else {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s / %s", loc.Name(), loc.City(), m.TakenAt.Format("2006"))
|
||||
}
|
||||
} else if loc.City() != "" && loc.CountryName() != "" {
|
||||
if len(loc.City()) > 20 {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s", loc.City(), m.TakenAt.Format("2006"))
|
||||
} else {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s / %s", loc.City(), loc.CountryName(), m.TakenAt.Format("2006"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.NoTitle() {
|
||||
if len(labels) > 0 && labels[0].Priority >= -1 && labels[0].Uncertainty <= 85 && labels[0].Name != "" {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s", txt.Title(labels[0].Name), m.TakenAt.Format("2006"))
|
||||
} else if !m.TakenAtLocal.IsZero() {
|
||||
m.PhotoTitle = fmt.Sprintf("Unknown / %s", m.TakenAtLocal.Format("2006"))
|
||||
} else {
|
||||
m.PhotoTitle = "Unknown"
|
||||
}
|
||||
|
||||
log.Infof("photo: changed empty photo title to \"%s\"", m.PhotoTitle)
|
||||
} else {
|
||||
log.Infof("photo: new title is \"%s\"", m.PhotoTitle)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddLabels updates the entity with additional or updated label information.
|
||||
func (m *Photo) AddLabels(labels classify.Labels, db *gorm.DB) {
|
||||
// TODO: Update classify labels from database
|
||||
for _, label := range labels {
|
||||
lm := NewLabel(label.Title(), label.Priority).FirstOrCreate(db)
|
||||
|
||||
if lm.New {
|
||||
event.EntitiesCreated("labels", []*Label{lm})
|
||||
|
||||
if label.Priority >= 0 {
|
||||
event.Publish("count.labels", event.Data{
|
||||
"count": 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if err := lm.Update(label, db); err != nil {
|
||||
log.Errorf("index: %s", err)
|
||||
}
|
||||
|
||||
plm := NewPhotoLabel(m.ID, lm.ID, label.Uncertainty, label.Source).FirstOrCreate(db)
|
||||
|
||||
if plm.LabelUncertainty > label.Uncertainty && plm.LabelUncertainty > 100 {
|
||||
plm.LabelUncertainty = label.Uncertainty
|
||||
plm.LabelSource = label.Source
|
||||
if err := db.Save(&plm).Error; err != nil {
|
||||
log.Errorf("index: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
db.Set("gorm:auto_preload", true).Model(m).Related(&m.Labels)
|
||||
}
|
||||
|
|
|
@ -6,19 +6,15 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/mutex"
|
||||
)
|
||||
|
||||
const (
|
||||
LabelSourceManual = "manual"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
LabelUncertainty int
|
||||
LabelSource string
|
||||
Photo *Photo
|
||||
Label *Label
|
||||
LabelSource string `gorm:"type:varbinary(8);"`
|
||||
Photo *Photo `gorm:"PRELOAD:false"`
|
||||
Label *Label `gorm:"PRELOAD:true"`
|
||||
}
|
||||
|
||||
// TableName returns PhotoLabel table identifier "photos_labels"
|
||||
|
|
|
@ -1,25 +1,54 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/internal/classify"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
"gopkg.in/ugjka/go-tz.v2/tz"
|
||||
)
|
||||
|
||||
// IndexLocation updates location and labels based on latitude and longitude.
|
||||
func (m *Photo) IndexLocation(db *gorm.DB, geoApi string, labels classify.Labels) ([]string, classify.Labels) {
|
||||
// GetTimeZone uses PhotoLat and PhotoLng to guess the time zone of the photo.
|
||||
func (m *Photo) GetTimeZone() string {
|
||||
result := "UTC"
|
||||
|
||||
if m.HasLatLng() {
|
||||
zones, err := tz.GetZone(tz.Point{
|
||||
Lat: m.PhotoLat,
|
||||
Lon: m.PhotoLng,
|
||||
})
|
||||
|
||||
if err == nil && len(zones) > 0 {
|
||||
result = zones[0]
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetTakenAt returns UTC time for TakenAtLocal.
|
||||
func (m *Photo) GetTakenAt() time.Time {
|
||||
loc, err := time.LoadLocation(m.TimeZone)
|
||||
|
||||
if err != nil {
|
||||
return m.TakenAt
|
||||
}
|
||||
|
||||
if takenAt, err := time.ParseInLocation("2006-01-02T15:04:05", m.TakenAtLocal.Format("2006-01-02T15:04:05"), loc); err != nil {
|
||||
return m.TakenAt
|
||||
} else {
|
||||
return takenAt.UTC()
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateLocation updates location and labels based on latitude and longitude.
|
||||
func (m *Photo) UpdateLocation(db *gorm.DB, geoApi string) (keywords []string, labels classify.Labels) {
|
||||
var location = NewLocation(m.PhotoLat, m.PhotoLng)
|
||||
|
||||
location.Lock()
|
||||
defer location.Unlock()
|
||||
|
||||
var keywords []string
|
||||
|
||||
err := location.Find(db, geoApi)
|
||||
|
||||
if err == nil {
|
||||
|
@ -33,7 +62,12 @@ func (m *Photo) IndexLocation(db *gorm.DB, geoApi string, labels classify.Labels
|
|||
m.LocationID = location.ID
|
||||
m.Place = location.Place
|
||||
m.PlaceID = location.PlaceID
|
||||
m.LocationEstimated = false
|
||||
m.PhotoCountry = location.CountryCode()
|
||||
|
||||
if m.TakenSrc != SrcManual {
|
||||
m.TimeZone = m.GetTimeZone()
|
||||
m.TakenAt = m.GetTakenAt()
|
||||
}
|
||||
|
||||
country := NewCountry(location.CountryCode(), location.CountryName()).FirstOrCreate(db)
|
||||
|
||||
|
@ -50,10 +84,6 @@ func (m *Photo) IndexLocation(db *gorm.DB, geoApi string, labels classify.Labels
|
|||
if locCategory != "" {
|
||||
labels = append(labels, classify.LocationLabel(locCategory, 0, -1))
|
||||
}
|
||||
|
||||
if err := m.UpdateTitle(labels); err != nil {
|
||||
log.Warn(err)
|
||||
}
|
||||
} else {
|
||||
log.Warn(err)
|
||||
|
||||
|
@ -61,61 +91,9 @@ func (m *Photo) IndexLocation(db *gorm.DB, geoApi string, labels classify.Labels
|
|||
m.PlaceID = UnknownPlace.ID
|
||||
}
|
||||
|
||||
if m.Place != nil && (!m.ModifiedLocation || m.PhotoCountry == "" || m.PhotoCountry == "zz") {
|
||||
if m.Place != nil && (m.PhotoCountry == "" || m.PhotoCountry == "zz") {
|
||||
m.PhotoCountry = m.Place.LocCountry
|
||||
}
|
||||
|
||||
return keywords, labels
|
||||
}
|
||||
|
||||
// UpdateTitle updated the photo title based on location and labels.
|
||||
func (m *Photo) UpdateTitle(labels classify.Labels) error {
|
||||
if m.ModifiedTitle && m.HasTitle() {
|
||||
return errors.New("photo: won't update title, was modified")
|
||||
}
|
||||
|
||||
hasLocation := m.Location != nil && m.Location.Place != nil
|
||||
|
||||
if hasLocation {
|
||||
loc := m.Location
|
||||
|
||||
if title := labels.Title(loc.Name()); title != "" { // TODO: User defined title format
|
||||
log.Infof("photo: using label \"%s\" to create photo title", title)
|
||||
if loc.NoCity() || loc.LongCity() || loc.CityContains(title) {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s / %s", txt.Title(title), loc.CountryName(), m.TakenAt.Format("2006"))
|
||||
} else {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s / %s", txt.Title(title), loc.City(), m.TakenAt.Format("2006"))
|
||||
}
|
||||
} else if loc.Name() != "" && loc.City() != "" {
|
||||
if len(loc.Name()) > 45 {
|
||||
m.PhotoTitle = txt.Title(loc.Name())
|
||||
} else if len(loc.Name()) > 20 || len(loc.City()) > 16 || strings.Contains(loc.Name(), loc.City()) {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s", loc.Name(), m.TakenAt.Format("2006"))
|
||||
} else {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s / %s", loc.Name(), loc.City(), m.TakenAt.Format("2006"))
|
||||
}
|
||||
} else if loc.City() != "" && loc.CountryName() != "" {
|
||||
if len(loc.City()) > 20 {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s", loc.City(), m.TakenAt.Format("2006"))
|
||||
} else {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s / %s", loc.City(), loc.CountryName(), m.TakenAt.Format("2006"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !hasLocation || m.NoTitle() {
|
||||
if len(labels) > 0 && labels[0].Priority >= -1 && labels[0].Uncertainty <= 85 && labels[0].Name != "" {
|
||||
m.PhotoTitle = fmt.Sprintf("%s / %s", txt.Title(labels[0].Name), m.TakenAt.Format("2006"))
|
||||
} else if !m.TakenAtLocal.IsZero() {
|
||||
m.PhotoTitle = fmt.Sprintf("Unknown / %s", m.TakenAtLocal.Format("2006"))
|
||||
} else {
|
||||
m.PhotoTitle = "Unknown"
|
||||
}
|
||||
|
||||
log.Infof("photo: changed empty photo title to \"%s\"", m.PhotoTitle)
|
||||
} else {
|
||||
log.Infof("photo: new title is \"%s\"", m.PhotoTitle)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
43
internal/entity/photo_location_test.go
Normal file
43
internal/entity/photo_location_test.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPhoto_GetTimeZone(t *testing.T) {
|
||||
m := Photo{}
|
||||
m.PhotoLat = 48.533905555
|
||||
m.PhotoLng = 9.01
|
||||
|
||||
result := m.GetTimeZone()
|
||||
|
||||
if result != "Europe/Berlin" {
|
||||
t.Fatalf("time zone should be Europe/Berlin: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhoto_GetTakenAt(t *testing.T) {
|
||||
m := Photo{}
|
||||
m.PhotoLat = 48.533905555
|
||||
m.PhotoLng = 9.01
|
||||
m.TakenAt, _ = time.Parse(time.RFC3339, "2020-02-04T11:54:34Z")
|
||||
m.TakenAtLocal, _ = time.Parse(time.RFC3339, "2020-02-04T11:54:34Z")
|
||||
m.TimeZone = m.GetTimeZone()
|
||||
|
||||
if m.TimeZone != "Europe/Berlin" {
|
||||
t.Fatalf("time zone should be Europe/Berlin: %s", m.TimeZone)
|
||||
}
|
||||
|
||||
localTime := m.TakenAtLocal.Format("2006-01-02T15:04:05")
|
||||
|
||||
if localTime != "2020-02-04T11:54:34" {
|
||||
t.Fatalf("local time should be 2020-02-04T11:54:34: %s", localTime)
|
||||
}
|
||||
|
||||
utcTime := m.GetTakenAt().Format("2006-01-02T15:04:05")
|
||||
|
||||
if utcTime != "2020-02-04T10:54:34" {
|
||||
t.Fatalf("utc time should be 2020-02-04T10:54:34: %s", utcTime)
|
||||
}
|
||||
}
|
9
internal/entity/src.go
Normal file
9
internal/entity/src.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package entity
|
||||
|
||||
const (
|
||||
SrcAuto = ""
|
||||
SrcManual = "manual"
|
||||
SrcImg = "img"
|
||||
SrcXmp = "xmp"
|
||||
SrcYml = "yml"
|
||||
)
|
|
@ -8,9 +8,13 @@ import (
|
|||
|
||||
// Photo represents a photo edit form.
|
||||
type Photo struct {
|
||||
TakenAt time.Time `json:"TakenAt"`
|
||||
PhotoTitle string `json:"PhotoTitle"`
|
||||
Description struct {
|
||||
TakenAt time.Time `json:"TakenAt"`
|
||||
TakenAtLocal time.Time `json:"TakenAtLocal"`
|
||||
TakenSrc string `json:"TakenSrc"`
|
||||
TimeZone string `json:"TimeZone"`
|
||||
PhotoTitle string `json:"PhotoTitle"`
|
||||
TitleSrc string `json:"TitleSrc"`
|
||||
Description struct {
|
||||
PhotoID uint `json:"PhotoID" deepcopier:"skip"`
|
||||
PhotoDescription string `json:"PhotoDescription"`
|
||||
PhotoKeywords string `json:"PhotoKeywords"`
|
||||
|
@ -20,29 +24,26 @@ type Photo struct {
|
|||
PhotoCopyright string `json:"PhotoCopyright"`
|
||||
PhotoLicense string `json:"PhotoLicense"`
|
||||
} `json:"Description"`
|
||||
PhotoFavorite bool `json:"PhotoFavorite"`
|
||||
PhotoPrivate bool `json:"PhotoPrivate"`
|
||||
PhotoNSFW bool `json:"PhotoNSFW"`
|
||||
PhotoStory bool `json:"PhotoStory"`
|
||||
PhotoLat float64 `json:"PhotoLat"`
|
||||
PhotoLng float64 `json:"PhotoLng"`
|
||||
PhotoAltitude int `json:"PhotoAltitude"`
|
||||
PhotoFocalLength int `json:"PhotoFocalLength"`
|
||||
PhotoIso int `json:"PhotoIso"`
|
||||
PhotoFNumber float64 `json:"PhotoFNumber"`
|
||||
PhotoExposure string `json:"PhotoExposure"`
|
||||
CameraID uint `json:"CameraID"`
|
||||
LensID uint `json:"LensID"`
|
||||
LocationID string `json:"LocationID"`
|
||||
PlaceID string `json:"PlaceID"`
|
||||
PhotoCountry string `json:"PhotoCountry"`
|
||||
TimeZone string `json:"TimeZone"`
|
||||
TakenAtLocal time.Time `json:"TakenAtLocal"`
|
||||
ModifiedTitle bool `json:"ModifiedTitle"`
|
||||
ModifiedDescription bool `json:"ModifiedDescription"`
|
||||
ModifiedDate bool `json:"ModifiedDate"`
|
||||
ModifiedLocation bool `json:"ModifiedLocation"`
|
||||
ModifiedCamera bool `json:"ModifiedCamera"`
|
||||
DescriptionSrc string `json:"DescriptionSrc"`
|
||||
PhotoFavorite bool `json:"PhotoFavorite"`
|
||||
PhotoPrivate bool `json:"PhotoPrivate"`
|
||||
PhotoNSFW bool `json:"PhotoNSFW"`
|
||||
PhotoStory bool `json:"PhotoStory"`
|
||||
PhotoReview bool `json:"PhotoReview"`
|
||||
PhotoLat float64 `json:"PhotoLat"`
|
||||
PhotoLng float64 `json:"PhotoLng"`
|
||||
PhotoAltitude int `json:"PhotoAltitude"`
|
||||
PhotoIso int `json:"PhotoIso"`
|
||||
PhotoFocalLength int `json:"PhotoFocalLength"`
|
||||
PhotoFNumber float64 `json:"PhotoFNumber"`
|
||||
PhotoExposure string `json:"PhotoExposure"`
|
||||
CameraID uint `json:"CameraID"`
|
||||
CameraSrc string `json:"CameraSrc"`
|
||||
LensID uint `json:"LensID"`
|
||||
LocationID string `json:"LocationID"`
|
||||
LocationSrc string `json:"LocationSrc"`
|
||||
PlaceID string `json:"PlaceID"`
|
||||
PhotoCountry string `json:"PhotoCountry"`
|
||||
}
|
||||
|
||||
func NewPhoto(m interface{}) (f Photo, err error) {
|
||||
|
|
|
@ -243,7 +243,7 @@ var CountryNames = map[string]string{
|
|||
"uz": "Uzbekistan",
|
||||
"vu": "Vanuatu",
|
||||
"ve": "Venezuela",
|
||||
"vn": "Viet Nam",
|
||||
"vn": "Vietnam",
|
||||
"vg": "British Virgin Islands",
|
||||
"vi": "US Virgin Islands",
|
||||
"wf": "Wallis and Futuna",
|
||||
|
|
|
@ -965,7 +965,7 @@
|
|||
},
|
||||
{
|
||||
"Code": "VN",
|
||||
"Name": "Viet Nam"
|
||||
"Name": "Vietnam"
|
||||
},
|
||||
{
|
||||
"Code": "VG",
|
||||
|
|
|
@ -273,7 +273,7 @@ func Exif(filename string) (data Data, err error) {
|
|||
} else if tl, err := time.ParseInLocation("2006:01:02 15:04:05", value, loc); err == nil {
|
||||
data.TakenAt = tl.UTC()
|
||||
} else {
|
||||
log.Warnf("could parse time: %s", err.Error())
|
||||
log.Warnf("could not parse time: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ func (ind *Index) estimateLocation(photo *entity.Photo) {
|
|||
if recentPhoto.HasPlace() {
|
||||
photo.Place = recentPhoto.Place
|
||||
photo.PhotoCountry = photo.Place.LocCountry
|
||||
photo.LocationEstimated = true
|
||||
photo.LocationSrc = entity.SrcAuto
|
||||
log.Debugf("index: approximate location is \"%s\"", recentPhoto.Place.Label())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -149,24 +149,28 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
if fileChanged || o.UpdateExif {
|
||||
// Read UpdateExif data
|
||||
if metaData, err := m.MetaData(); err == nil {
|
||||
if !photo.ModifiedLocation {
|
||||
if photo.LocationSrc == entity.SrcAuto || photo.LocationSrc == entity.SrcImg {
|
||||
photo.PhotoLat = metaData.Lat
|
||||
photo.PhotoLng = metaData.Lng
|
||||
photo.PhotoAltitude = metaData.Altitude
|
||||
photo.LocationSrc = entity.SrcImg
|
||||
}
|
||||
|
||||
if !photo.ModifiedDate {
|
||||
if photo.TakenSrc == entity.SrcAuto || photo.TakenSrc == entity.SrcImg {
|
||||
photo.TakenAt = metaData.TakenAt
|
||||
photo.TakenAtLocal = metaData.TakenAtLocal
|
||||
photo.TimeZone = metaData.TimeZone
|
||||
photo.TakenSrc = entity.SrcImg
|
||||
}
|
||||
|
||||
if photo.NoTitle() {
|
||||
if metaData.Title != "" && (photo.NoTitle() || photo.TitleSrc == entity.SrcImg) {
|
||||
photo.PhotoTitle = metaData.Title
|
||||
photo.TitleSrc = entity.SrcImg
|
||||
}
|
||||
|
||||
if photo.Description.NoDescription() {
|
||||
if metaData.Description != "" && (photo.Description.NoDescription() || photo.DescriptionSrc == entity.SrcImg) {
|
||||
photo.Description.PhotoDescription = metaData.Description
|
||||
photo.DescriptionSrc = entity.SrcImg
|
||||
}
|
||||
|
||||
if photo.Description.NoNotes() {
|
||||
|
@ -201,7 +205,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
}
|
||||
}
|
||||
|
||||
if !photo.ModifiedCamera && (fileChanged || o.UpdateCamera) {
|
||||
if photo.CameraSrc == entity.SrcAuto && (fileChanged || o.UpdateCamera) {
|
||||
// Set UpdateCamera, Lens, Focal Length and F Number
|
||||
photo.Camera = entity.NewCamera(m.CameraModel(), m.CameraMake()).FirstOrCreate(ind.db)
|
||||
photo.Lens = entity.NewLens(m.LensModel(), m.LensMake()).FirstOrCreate(ind.db)
|
||||
|
@ -218,14 +222,12 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
|
||||
if fileChanged || o.UpdateKeywords || o.UpdateLocation || o.UpdateTitle || photo.NoTitle() {
|
||||
if photo.HasLatLng() {
|
||||
locKeywords, labels = photo.IndexLocation(ind.db, ind.conf.GeoCodingApi(), labels)
|
||||
var locLabels classify.Labels
|
||||
locKeywords, locLabels = photo.UpdateLocation(ind.db, ind.conf.GeoCodingApi())
|
||||
labels = append(labels, locLabels...)
|
||||
} else {
|
||||
log.Info("index: no latitude and longitude in metadata")
|
||||
|
||||
if err := photo.UpdateTitle(labels); err != nil {
|
||||
log.Warn(err)
|
||||
}
|
||||
|
||||
photo.Place = entity.UnknownPlace
|
||||
photo.PlaceID = entity.UnknownPlace.ID
|
||||
}
|
||||
|
@ -233,8 +235,9 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
} else if m.IsXMP() {
|
||||
// TODO: Proof-of-concept for indexing XMP sidecar files
|
||||
if data, err := meta.XMP(m.FileName()); err == nil {
|
||||
if data.Title != "" && !photo.ModifiedTitle {
|
||||
if data.Title != "" && photo.TitleSrc == entity.SrcAuto {
|
||||
photo.PhotoTitle = data.Title
|
||||
photo.TitleSrc = entity.SrcXmp
|
||||
}
|
||||
|
||||
if photo.Description.NoCopyright() && data.Copyright != "" {
|
||||
|
@ -255,6 +258,10 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
}
|
||||
}
|
||||
|
||||
if photo.Place != nil && (photo.PhotoCountry == "" || photo.PhotoCountry == "zz") {
|
||||
photo.PhotoCountry = photo.Place.LocCountry
|
||||
}
|
||||
|
||||
if !photo.TakenAtLocal.IsZero() {
|
||||
photo.PhotoYear = photo.TakenAtLocal.Year()
|
||||
photo.PhotoMonth = int(photo.TakenAtLocal.Month())
|
||||
|
@ -297,28 +304,6 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
}
|
||||
}
|
||||
|
||||
if file.FilePrimary && (fileChanged || o.UpdateKeywords) {
|
||||
w := txt.Keywords(photo.Description.PhotoKeywords)
|
||||
|
||||
if NonCanonical(fileBase) {
|
||||
w = append(w, txt.FilenameKeywords(filePath)...)
|
||||
w = append(w, txt.FilenameKeywords(fileBase)...)
|
||||
}
|
||||
|
||||
w = append(w, locKeywords...)
|
||||
w = append(w, txt.FilenameKeywords(file.OriginalName)...)
|
||||
w = append(w, file.FileMainColor)
|
||||
w = append(w, labels.Keywords()...)
|
||||
|
||||
photo.Description.PhotoKeywords = strings.Join(txt.UniqueWords(w), ", ")
|
||||
|
||||
if photo.Description.PhotoKeywords != "" {
|
||||
log.Debugf("index: updated photo keywords (%s)", photo.Description.PhotoKeywords)
|
||||
} else {
|
||||
log.Debug("index: no photo keywords")
|
||||
}
|
||||
}
|
||||
|
||||
if photoExists {
|
||||
// Estimate location
|
||||
if o.UpdateLocation && photo.NoLocation() {
|
||||
|
@ -348,10 +333,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
event.EntitiesCreated("photos", []entity.Photo{photo})
|
||||
}
|
||||
|
||||
if len(labels) > 0 {
|
||||
log.Infof("index: adding labels %+v", labels)
|
||||
ind.addLabels(photo.ID, labels)
|
||||
}
|
||||
photo.AddLabels(labels, ind.db)
|
||||
|
||||
file.PhotoID = photo.ID
|
||||
result.PhotoID = photo.ID
|
||||
|
@ -359,9 +341,42 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
|
|||
file.PhotoUUID = photo.PhotoUUID
|
||||
result.PhotoUUID = photo.PhotoUUID
|
||||
|
||||
if file.FilePrimary && (fileChanged || o.UpdateKeywords) {
|
||||
if file.FilePrimary && (fileChanged || o.UpdateKeywords || o.UpdateTitle || o.UpdateLabels) {
|
||||
labels := photo.ClassifyLabels()
|
||||
|
||||
if err := photo.UpdateTitle(labels); err != nil {
|
||||
log.Warnf("%s (%s)", err.Error(), photo.PhotoUUID)
|
||||
}
|
||||
|
||||
w := txt.Keywords(photo.Description.PhotoKeywords)
|
||||
|
||||
if NonCanonical(fileBase) {
|
||||
w = append(w, txt.FilenameKeywords(filePath)...)
|
||||
w = append(w, txt.FilenameKeywords(fileBase)...)
|
||||
}
|
||||
|
||||
w = append(w, locKeywords...)
|
||||
w = append(w, txt.FilenameKeywords(file.OriginalName)...)
|
||||
w = append(w, file.FileMainColor)
|
||||
w = append(w, labels.Keywords()...)
|
||||
|
||||
photo.Description.PhotoKeywords = strings.Join(txt.UniqueWords(w), ", ")
|
||||
|
||||
if photo.Description.PhotoKeywords != "" {
|
||||
log.Debugf("index: updated photo keywords (%s)", photo.Description.PhotoKeywords)
|
||||
} else {
|
||||
log.Debug("index: no photo keywords")
|
||||
}
|
||||
|
||||
if err := ind.db.Unscoped().Save(&photo).Error; err != nil {
|
||||
log.Errorf("index: %s", err)
|
||||
result.Status = IndexFailed
|
||||
result.Error = err
|
||||
return result
|
||||
}
|
||||
|
||||
if err := photo.IndexKeywords(ind.db); err != nil {
|
||||
log.Error(err)
|
||||
log.Warnf("%s (%s)", err.Error(), photo.PhotoUUID)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -484,41 +499,3 @@ func (ind *Index) classifyImage(jpeg *MediaFile) (results classify.Labels) {
|
|||
|
||||
return results
|
||||
}
|
||||
|
||||
func (ind *Index) addLabels(photoId uint, labels classify.Labels) {
|
||||
for _, label := range labels {
|
||||
lm := entity.NewLabel(label.Title(), label.Priority).FirstOrCreate(ind.db)
|
||||
|
||||
if lm.New {
|
||||
event.EntitiesCreated("labels", []*entity.Label{lm})
|
||||
|
||||
if label.Priority >= 0 {
|
||||
event.Publish("count.labels", event.Data{
|
||||
"count": 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if err := lm.Update(label, ind.db); err != nil {
|
||||
log.Errorf("index: %s", err)
|
||||
}
|
||||
|
||||
plm := entity.NewPhotoLabel(photoId, lm.ID, label.Uncertainty, label.Source).FirstOrCreate(ind.db)
|
||||
|
||||
// Add categories
|
||||
for _, category := range label.Categories {
|
||||
sn := entity.NewLabel(txt.Title(category), -3).FirstOrCreate(ind.db)
|
||||
if err := ind.db.Model(&lm).Association("LabelCategories").Append(sn).Error; err != nil {
|
||||
log.Errorf("index: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if plm.LabelUncertainty > label.Uncertainty && plm.LabelUncertainty > 100 {
|
||||
plm.LabelUncertainty = label.Uncertainty
|
||||
plm.LabelSource = label.Source
|
||||
if err := ind.db.Save(&plm).Error; err != nil {
|
||||
log.Errorf("index: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,8 +36,8 @@ type PhotoResult struct {
|
|||
PhotoLat float64
|
||||
PhotoLng float64
|
||||
PhotoAltitude int
|
||||
PhotoFocalLength int
|
||||
PhotoIso int
|
||||
PhotoFocalLength int
|
||||
PhotoFNumber float64
|
||||
PhotoExposure string
|
||||
|
||||
|
@ -52,14 +52,12 @@ type PhotoResult struct {
|
|||
LensMake string
|
||||
|
||||
// Location
|
||||
LocationID string
|
||||
PlaceID string
|
||||
LocLabel string
|
||||
LocCity string
|
||||
LocState string
|
||||
LocCountry string
|
||||
LocationChanged bool
|
||||
LocationEstimated bool
|
||||
LocationID string
|
||||
PlaceID string
|
||||
LocLabel string
|
||||
LocCity string
|
||||
LocState string
|
||||
LocCountry string
|
||||
|
||||
// File
|
||||
FileID uint
|
||||
|
|
Loading…
Reference in a new issue