mirror of
https://github.com/photoprism/photoprism.git
synced 2026-01-23 02:24:24 +00:00
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
ad2470ca04
commit
117c8db73b
5 changed files with 435 additions and 230 deletions
|
|
@ -84,7 +84,7 @@ func NewPhotosFormWithEntities(photos search.PhotoResults, preloaded map[string]
|
|||
return nil
|
||||
}
|
||||
|
||||
// Populate Albums and Labels from selected photos (no raw SQL; use preload helpers)
|
||||
// Populate Albums and Labels from selected photos (no raw SQL; use preload helpers).
|
||||
total := len(photos)
|
||||
if total > 0 {
|
||||
type albumAgg struct {
|
||||
|
|
@ -120,7 +120,7 @@ func NewPhotosFormWithEntities(photos search.PhotoResults, preloaded map[string]
|
|||
albumCount[a.AlbumUID] = v
|
||||
}
|
||||
|
||||
// Labels on this photo (only visible ones: uncertainty < 100)
|
||||
// Labels on this photo (only visible ones: uncertainty < 100).
|
||||
for _, pl := range p.Labels {
|
||||
if pl.Uncertainty >= 100 || pl.Label == nil || !pl.Label.HasID() {
|
||||
continue
|
||||
|
|
@ -136,7 +136,7 @@ func NewPhotosFormWithEntities(photos search.PhotoResults, preloaded map[string]
|
|||
}
|
||||
}
|
||||
|
||||
// Build Albums items
|
||||
// Build Albums items.
|
||||
frm.Albums.Items = make([]Item, 0, len(albumCount))
|
||||
anyAlbumMixed := false
|
||||
|
||||
|
|
@ -148,7 +148,7 @@ func NewPhotosFormWithEntities(photos search.PhotoResults, preloaded map[string]
|
|||
frm.Albums.Items = append(frm.Albums.Items, Item{Value: uid, Title: agg.title, Mixed: mixed, Action: ActionNone})
|
||||
}
|
||||
|
||||
// Sort shared-first (Mixed=false), then by Title alphabetically
|
||||
// Sort shared-first (Mixed=false), then by Title alphabetically.
|
||||
sort.Slice(frm.Albums.Items, func(i, j int) bool {
|
||||
if frm.Albums.Items[i].Mixed != frm.Albums.Items[j].Mixed {
|
||||
return !frm.Albums.Items[i].Mixed && frm.Albums.Items[j].Mixed
|
||||
|
|
@ -159,7 +159,7 @@ func NewPhotosFormWithEntities(photos search.PhotoResults, preloaded map[string]
|
|||
frm.Albums.Mixed = anyAlbumMixed
|
||||
frm.Albums.Action = ActionNone
|
||||
|
||||
// Build Labels items
|
||||
// Build Labels items.
|
||||
frm.Labels.Items = make([]Item, 0, len(labelCount))
|
||||
anyLabelMixed := false
|
||||
for uid, agg := range labelCount {
|
||||
|
|
@ -170,7 +170,7 @@ func NewPhotosFormWithEntities(photos search.PhotoResults, preloaded map[string]
|
|||
frm.Labels.Items = append(frm.Labels.Items, Item{Value: uid, Title: agg.name, Mixed: mixed, Action: ActionNone})
|
||||
}
|
||||
|
||||
// Sort shared-first (Mixed=false), then by Title alphabetically
|
||||
// Sort shared-first (Mixed=false), then by Title alphabetically.
|
||||
sort.Slice(frm.Labels.Items, func(i, j int) bool {
|
||||
if frm.Labels.Items[i].Mixed != frm.Labels.Items[j].Mixed {
|
||||
return !frm.Labels.Items[i].Mixed && frm.Labels.Items[j].Mixed
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package batch
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
|
|
@ -62,6 +61,9 @@ func PreparePhotoSaveRequests(photos search.PhotoResults, preloaded map[string]*
|
|||
return nil, preloaded, MutationStats{}
|
||||
}
|
||||
|
||||
// Pre-create any album/label add targets so Apply* can reuse resolved UIDs per photo.
|
||||
resolveBatchItemValues(values)
|
||||
|
||||
if preloaded == nil {
|
||||
preloaded = map[string]*entity.Photo{}
|
||||
}
|
||||
|
|
@ -224,226 +226,3 @@ func SavePhotos(requests []*PhotoSaveRequest) ([]bool, error) {
|
|||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// savePhoto applies the batched form values to a single entity.Photo and writes
|
||||
// only the changed columns back to the database.
|
||||
func savePhoto(req *PhotoSaveRequest) (bool, error) {
|
||||
if req == nil || req.Photo == nil || req.Form == nil || req.Values == nil {
|
||||
return false, fmt.Errorf("batch: invalid save request")
|
||||
}
|
||||
|
||||
p := req.Photo
|
||||
formValues := req.Values
|
||||
formData := req.Form
|
||||
|
||||
if !p.HasID() {
|
||||
return false, fmt.Errorf("batch: photo has no id")
|
||||
}
|
||||
|
||||
original := *p
|
||||
details := p.GetDetails()
|
||||
origDetails := *details
|
||||
|
||||
if shouldUpdateString(formValues.PhotoTitle) {
|
||||
p.PhotoTitle = formData.PhotoTitle
|
||||
p.TitleSrc = formData.TitleSrc
|
||||
}
|
||||
|
||||
if shouldUpdateString(formValues.PhotoCaption) {
|
||||
p.PhotoCaption = formData.PhotoCaption
|
||||
p.CaptionSrc = formData.CaptionSrc
|
||||
}
|
||||
|
||||
if formValues.PhotoType.Action == ActionUpdate {
|
||||
p.PhotoType = formData.PhotoType
|
||||
p.TypeSrc = formData.TypeSrc
|
||||
}
|
||||
|
||||
if formValues.PhotoFavorite.Action == ActionUpdate {
|
||||
p.PhotoFavorite = formValues.PhotoFavorite.Value
|
||||
}
|
||||
|
||||
if formValues.PhotoPrivate.Action == ActionUpdate {
|
||||
p.PhotoPrivate = formValues.PhotoPrivate.Value
|
||||
}
|
||||
|
||||
if formValues.PhotoScan.Action == ActionUpdate {
|
||||
p.PhotoScan = formValues.PhotoScan.Value
|
||||
}
|
||||
|
||||
if formValues.PhotoPanorama.Action == ActionUpdate {
|
||||
p.PhotoPanorama = formValues.PhotoPanorama.Value
|
||||
}
|
||||
|
||||
timeChanged := formValues.PhotoDay.Action == ActionUpdate ||
|
||||
formValues.PhotoMonth.Action == ActionUpdate ||
|
||||
formValues.PhotoYear.Action == ActionUpdate ||
|
||||
formValues.TimeZone.Action == ActionUpdate
|
||||
|
||||
if timeChanged {
|
||||
p.PhotoYear = formData.PhotoYear
|
||||
p.PhotoMonth = formData.PhotoMonth
|
||||
p.PhotoDay = formData.PhotoDay
|
||||
if formValues.TimeZone.Action == ActionUpdate {
|
||||
p.TimeZone = formData.TimeZone
|
||||
}
|
||||
p.TakenAtLocal = formData.TakenAtLocal
|
||||
p.TakenSrc = formData.TakenSrc
|
||||
|
||||
p.NormalizeValues()
|
||||
|
||||
if p.TimeZoneUTC() {
|
||||
p.TakenAt = p.TakenAtLocal
|
||||
} else {
|
||||
p.TakenAt = p.GetTakenAt()
|
||||
}
|
||||
|
||||
p.UpdateDateFields()
|
||||
}
|
||||
|
||||
locationChanged := formValues.PhotoLat.Action == ActionUpdate ||
|
||||
formValues.PhotoLng.Action == ActionUpdate ||
|
||||
formValues.PhotoCountry.Action == ActionUpdate ||
|
||||
formValues.PhotoAltitude.Action == ActionUpdate
|
||||
|
||||
if formValues.PhotoLat.Action == ActionUpdate {
|
||||
p.PhotoLat = formValues.PhotoLat.Value
|
||||
}
|
||||
|
||||
if formValues.PhotoLng.Action == ActionUpdate {
|
||||
p.PhotoLng = formValues.PhotoLng.Value
|
||||
}
|
||||
|
||||
if formValues.PhotoAltitude.Action == ActionUpdate {
|
||||
p.PhotoAltitude = formValues.PhotoAltitude.Value
|
||||
}
|
||||
|
||||
if formValues.PhotoCountry.Action == ActionUpdate {
|
||||
p.PhotoCountry = formValues.PhotoCountry.Value
|
||||
}
|
||||
|
||||
if locationChanged {
|
||||
p.PlaceSrc = entity.SrcBatch
|
||||
locKeywords, locLabels := p.UpdateLocation()
|
||||
if len(locLabels) > 0 {
|
||||
p.AddLabels(locLabels)
|
||||
}
|
||||
if len(locKeywords) > 0 {
|
||||
words := txt.UniqueWords(txt.Words(details.Keywords))
|
||||
words = append(words, locKeywords...)
|
||||
details.Keywords = strings.Join(txt.UniqueWords(words), ", ")
|
||||
}
|
||||
}
|
||||
|
||||
if formValues.DetailsSubject.Action == ActionUpdate || formValues.DetailsSubject.Action == ActionRemove {
|
||||
details.Subject = formData.Details.Subject
|
||||
details.SubjectSrc = formData.Details.SubjectSrc
|
||||
}
|
||||
|
||||
if formValues.DetailsArtist.Action == ActionUpdate || formValues.DetailsArtist.Action == ActionRemove {
|
||||
details.Artist = formData.Details.Artist
|
||||
details.ArtistSrc = formData.Details.ArtistSrc
|
||||
}
|
||||
|
||||
if formValues.DetailsCopyright.Action == ActionUpdate || formValues.DetailsCopyright.Action == ActionRemove {
|
||||
details.Copyright = formData.Details.Copyright
|
||||
details.CopyrightSrc = formData.Details.CopyrightSrc
|
||||
}
|
||||
|
||||
if formValues.DetailsLicense.Action == ActionUpdate || formValues.DetailsLicense.Action == ActionRemove {
|
||||
details.License = formData.Details.License
|
||||
details.LicenseSrc = formData.Details.LicenseSrc
|
||||
}
|
||||
|
||||
updates := entity.Values{}
|
||||
addUpdate := func(column string, changed bool, value interface{}) {
|
||||
if changed {
|
||||
updates[column] = value
|
||||
}
|
||||
}
|
||||
|
||||
addUpdate("photo_title", p.PhotoTitle != original.PhotoTitle, p.PhotoTitle)
|
||||
addUpdate("title_src", p.TitleSrc != original.TitleSrc, p.TitleSrc)
|
||||
addUpdate("photo_caption", p.PhotoCaption != original.PhotoCaption, p.PhotoCaption)
|
||||
addUpdate("caption_src", p.CaptionSrc != original.CaptionSrc, p.CaptionSrc)
|
||||
addUpdate("photo_type", p.PhotoType != original.PhotoType, p.PhotoType)
|
||||
addUpdate("type_src", p.TypeSrc != original.TypeSrc, p.TypeSrc)
|
||||
addUpdate("photo_favorite", p.PhotoFavorite != original.PhotoFavorite, p.PhotoFavorite)
|
||||
addUpdate("photo_private", p.PhotoPrivate != original.PhotoPrivate, p.PhotoPrivate)
|
||||
addUpdate("photo_scan", p.PhotoScan != original.PhotoScan, p.PhotoScan)
|
||||
addUpdate("photo_panorama", p.PhotoPanorama != original.PhotoPanorama, p.PhotoPanorama)
|
||||
addUpdate("photo_year", p.PhotoYear != original.PhotoYear, p.PhotoYear)
|
||||
addUpdate("photo_month", p.PhotoMonth != original.PhotoMonth, p.PhotoMonth)
|
||||
addUpdate("photo_day", p.PhotoDay != original.PhotoDay, p.PhotoDay)
|
||||
addUpdate("time_zone", p.TimeZone != original.TimeZone, p.TimeZone)
|
||||
addUpdate("taken_src", p.TakenSrc != original.TakenSrc, p.TakenSrc)
|
||||
addUpdate("taken_at", !p.TakenAt.Equal(original.TakenAt), p.TakenAt)
|
||||
addUpdate("taken_at_local", !p.TakenAtLocal.Equal(original.TakenAtLocal), p.TakenAtLocal)
|
||||
addUpdate("photo_lat", p.PhotoLat != original.PhotoLat, p.PhotoLat)
|
||||
addUpdate("photo_lng", p.PhotoLng != original.PhotoLng, p.PhotoLng)
|
||||
addUpdate("photo_altitude", p.PhotoAltitude != original.PhotoAltitude, p.PhotoAltitude)
|
||||
addUpdate("photo_country", p.PhotoCountry != original.PhotoCountry, p.PhotoCountry)
|
||||
addUpdate("place_id", p.PlaceID != original.PlaceID, p.PlaceID)
|
||||
addUpdate("cell_id", p.CellID != original.CellID, p.CellID)
|
||||
addUpdate("place_src", p.PlaceSrc != original.PlaceSrc, p.PlaceSrc)
|
||||
|
||||
detailUpdates := entity.Values{}
|
||||
if details.Subject != origDetails.Subject {
|
||||
detailUpdates["subject"] = details.Subject
|
||||
}
|
||||
if details.SubjectSrc != origDetails.SubjectSrc {
|
||||
detailUpdates["subject_src"] = details.SubjectSrc
|
||||
}
|
||||
if details.Artist != origDetails.Artist {
|
||||
detailUpdates["artist"] = details.Artist
|
||||
}
|
||||
if details.ArtistSrc != origDetails.ArtistSrc {
|
||||
detailUpdates["artist_src"] = details.ArtistSrc
|
||||
}
|
||||
if details.Copyright != origDetails.Copyright {
|
||||
detailUpdates["copyright"] = details.Copyright
|
||||
}
|
||||
if details.CopyrightSrc != origDetails.CopyrightSrc {
|
||||
detailUpdates["copyright_src"] = details.CopyrightSrc
|
||||
}
|
||||
if details.License != origDetails.License {
|
||||
detailUpdates["license"] = details.License
|
||||
}
|
||||
if details.LicenseSrc != origDetails.LicenseSrc {
|
||||
detailUpdates["license_src"] = details.LicenseSrc
|
||||
}
|
||||
if details.Keywords != origDetails.Keywords {
|
||||
detailUpdates["keywords"] = details.Keywords
|
||||
}
|
||||
|
||||
if len(updates) == 0 && len(detailUpdates) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
log.Debugf("batch: saving photo %s with updates=%v details=%v", p.PhotoUID, updates, detailUpdates)
|
||||
|
||||
edited := entity.Now()
|
||||
updates["edited_at"] = edited
|
||||
p.EditedAt = &edited
|
||||
updates["updated_at"] = edited
|
||||
p.UpdatedAt = edited
|
||||
updates["checked_at"] = nil
|
||||
p.CheckedAt = nil
|
||||
|
||||
if err := p.Updates(updates); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(detailUpdates) > 0 {
|
||||
if err := details.Updates(detailUpdates); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// shouldUpdateString reports whether the provided field requests an update or removal.
|
||||
func shouldUpdateString(v String) bool {
|
||||
return v.Action == ActionUpdate || v.Action == ActionRemove
|
||||
}
|
||||
|
|
|
|||
232
internal/photoprism/batch/save_photo.go
Normal file
232
internal/photoprism/batch/save_photo.go
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
package batch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// savePhoto applies the batched form values to a single entity.Photo and writes
|
||||
// only the changed columns back to the database.
|
||||
func savePhoto(req *PhotoSaveRequest) (bool, error) {
|
||||
if req == nil || req.Photo == nil || req.Form == nil || req.Values == nil {
|
||||
return false, fmt.Errorf("batch: invalid save request")
|
||||
}
|
||||
|
||||
p := req.Photo
|
||||
formValues := req.Values
|
||||
formData := req.Form
|
||||
|
||||
if !p.HasID() {
|
||||
return false, fmt.Errorf("batch: photo has no id")
|
||||
}
|
||||
|
||||
original := *p
|
||||
details := p.GetDetails()
|
||||
origDetails := *details
|
||||
|
||||
if shouldUpdateString(formValues.PhotoTitle) {
|
||||
p.PhotoTitle = formData.PhotoTitle
|
||||
p.TitleSrc = formData.TitleSrc
|
||||
}
|
||||
|
||||
if shouldUpdateString(formValues.PhotoCaption) {
|
||||
p.PhotoCaption = formData.PhotoCaption
|
||||
p.CaptionSrc = formData.CaptionSrc
|
||||
}
|
||||
|
||||
if formValues.PhotoType.Action == ActionUpdate {
|
||||
p.PhotoType = formData.PhotoType
|
||||
p.TypeSrc = formData.TypeSrc
|
||||
}
|
||||
|
||||
if formValues.PhotoFavorite.Action == ActionUpdate {
|
||||
p.PhotoFavorite = formValues.PhotoFavorite.Value
|
||||
}
|
||||
|
||||
if formValues.PhotoPrivate.Action == ActionUpdate {
|
||||
p.PhotoPrivate = formValues.PhotoPrivate.Value
|
||||
}
|
||||
|
||||
if formValues.PhotoScan.Action == ActionUpdate {
|
||||
p.PhotoScan = formValues.PhotoScan.Value
|
||||
}
|
||||
|
||||
if formValues.PhotoPanorama.Action == ActionUpdate {
|
||||
p.PhotoPanorama = formValues.PhotoPanorama.Value
|
||||
}
|
||||
|
||||
timeChanged := formValues.PhotoDay.Action == ActionUpdate ||
|
||||
formValues.PhotoMonth.Action == ActionUpdate ||
|
||||
formValues.PhotoYear.Action == ActionUpdate ||
|
||||
formValues.TimeZone.Action == ActionUpdate
|
||||
|
||||
if timeChanged {
|
||||
p.PhotoYear = formData.PhotoYear
|
||||
p.PhotoMonth = formData.PhotoMonth
|
||||
p.PhotoDay = formData.PhotoDay
|
||||
if formValues.TimeZone.Action == ActionUpdate {
|
||||
p.TimeZone = formData.TimeZone
|
||||
}
|
||||
p.TakenAtLocal = formData.TakenAtLocal
|
||||
p.TakenSrc = formData.TakenSrc
|
||||
|
||||
p.NormalizeValues()
|
||||
|
||||
if p.TimeZoneUTC() {
|
||||
p.TakenAt = p.TakenAtLocal
|
||||
} else {
|
||||
p.TakenAt = p.GetTakenAt()
|
||||
}
|
||||
|
||||
p.UpdateDateFields()
|
||||
}
|
||||
|
||||
locationChanged := formValues.PhotoLat.Action == ActionUpdate ||
|
||||
formValues.PhotoLng.Action == ActionUpdate ||
|
||||
formValues.PhotoCountry.Action == ActionUpdate ||
|
||||
formValues.PhotoAltitude.Action == ActionUpdate
|
||||
|
||||
if formValues.PhotoLat.Action == ActionUpdate {
|
||||
p.PhotoLat = formValues.PhotoLat.Value
|
||||
}
|
||||
|
||||
if formValues.PhotoLng.Action == ActionUpdate {
|
||||
p.PhotoLng = formValues.PhotoLng.Value
|
||||
}
|
||||
|
||||
if formValues.PhotoAltitude.Action == ActionUpdate {
|
||||
p.PhotoAltitude = formValues.PhotoAltitude.Value
|
||||
}
|
||||
|
||||
if formValues.PhotoCountry.Action == ActionUpdate {
|
||||
p.PhotoCountry = formValues.PhotoCountry.Value
|
||||
}
|
||||
|
||||
if locationChanged {
|
||||
p.PlaceSrc = entity.SrcBatch
|
||||
locKeywords, locLabels := p.UpdateLocation()
|
||||
if len(locLabels) > 0 {
|
||||
p.AddLabels(locLabels)
|
||||
}
|
||||
if len(locKeywords) > 0 {
|
||||
words := txt.UniqueWords(txt.Words(details.Keywords))
|
||||
words = append(words, locKeywords...)
|
||||
details.Keywords = strings.Join(txt.UniqueWords(words), ", ")
|
||||
}
|
||||
}
|
||||
|
||||
if formValues.DetailsSubject.Action == ActionUpdate || formValues.DetailsSubject.Action == ActionRemove {
|
||||
details.Subject = formData.Details.Subject
|
||||
details.SubjectSrc = formData.Details.SubjectSrc
|
||||
}
|
||||
|
||||
if formValues.DetailsArtist.Action == ActionUpdate || formValues.DetailsArtist.Action == ActionRemove {
|
||||
details.Artist = formData.Details.Artist
|
||||
details.ArtistSrc = formData.Details.ArtistSrc
|
||||
}
|
||||
|
||||
if formValues.DetailsCopyright.Action == ActionUpdate || formValues.DetailsCopyright.Action == ActionRemove {
|
||||
details.Copyright = formData.Details.Copyright
|
||||
details.CopyrightSrc = formData.Details.CopyrightSrc
|
||||
}
|
||||
|
||||
if formValues.DetailsLicense.Action == ActionUpdate || formValues.DetailsLicense.Action == ActionRemove {
|
||||
details.License = formData.Details.License
|
||||
details.LicenseSrc = formData.Details.LicenseSrc
|
||||
}
|
||||
|
||||
updates := entity.Values{}
|
||||
addUpdate := func(column string, changed bool, value interface{}) {
|
||||
if changed {
|
||||
updates[column] = value
|
||||
}
|
||||
}
|
||||
|
||||
addUpdate("photo_title", p.PhotoTitle != original.PhotoTitle, p.PhotoTitle)
|
||||
addUpdate("title_src", p.TitleSrc != original.TitleSrc, p.TitleSrc)
|
||||
addUpdate("photo_caption", p.PhotoCaption != original.PhotoCaption, p.PhotoCaption)
|
||||
addUpdate("caption_src", p.CaptionSrc != original.CaptionSrc, p.CaptionSrc)
|
||||
addUpdate("photo_type", p.PhotoType != original.PhotoType, p.PhotoType)
|
||||
addUpdate("type_src", p.TypeSrc != original.TypeSrc, p.TypeSrc)
|
||||
addUpdate("photo_favorite", p.PhotoFavorite != original.PhotoFavorite, p.PhotoFavorite)
|
||||
addUpdate("photo_private", p.PhotoPrivate != original.PhotoPrivate, p.PhotoPrivate)
|
||||
addUpdate("photo_scan", p.PhotoScan != original.PhotoScan, p.PhotoScan)
|
||||
addUpdate("photo_panorama", p.PhotoPanorama != original.PhotoPanorama, p.PhotoPanorama)
|
||||
addUpdate("photo_year", p.PhotoYear != original.PhotoYear, p.PhotoYear)
|
||||
addUpdate("photo_month", p.PhotoMonth != original.PhotoMonth, p.PhotoMonth)
|
||||
addUpdate("photo_day", p.PhotoDay != original.PhotoDay, p.PhotoDay)
|
||||
addUpdate("time_zone", p.TimeZone != original.TimeZone, p.TimeZone)
|
||||
addUpdate("taken_src", p.TakenSrc != original.TakenSrc, p.TakenSrc)
|
||||
addUpdate("taken_at", !p.TakenAt.Equal(original.TakenAt), p.TakenAt)
|
||||
addUpdate("taken_at_local", !p.TakenAtLocal.Equal(original.TakenAtLocal), p.TakenAtLocal)
|
||||
addUpdate("photo_lat", p.PhotoLat != original.PhotoLat, p.PhotoLat)
|
||||
addUpdate("photo_lng", p.PhotoLng != original.PhotoLng, p.PhotoLng)
|
||||
addUpdate("photo_altitude", p.PhotoAltitude != original.PhotoAltitude, p.PhotoAltitude)
|
||||
addUpdate("photo_country", p.PhotoCountry != original.PhotoCountry, p.PhotoCountry)
|
||||
addUpdate("place_id", p.PlaceID != original.PlaceID, p.PlaceID)
|
||||
addUpdate("cell_id", p.CellID != original.CellID, p.CellID)
|
||||
addUpdate("place_src", p.PlaceSrc != original.PlaceSrc, p.PlaceSrc)
|
||||
|
||||
detailUpdates := entity.Values{}
|
||||
if details.Subject != origDetails.Subject {
|
||||
detailUpdates["subject"] = details.Subject
|
||||
}
|
||||
if details.SubjectSrc != origDetails.SubjectSrc {
|
||||
detailUpdates["subject_src"] = details.SubjectSrc
|
||||
}
|
||||
if details.Artist != origDetails.Artist {
|
||||
detailUpdates["artist"] = details.Artist
|
||||
}
|
||||
if details.ArtistSrc != origDetails.ArtistSrc {
|
||||
detailUpdates["artist_src"] = details.ArtistSrc
|
||||
}
|
||||
if details.Copyright != origDetails.Copyright {
|
||||
detailUpdates["copyright"] = details.Copyright
|
||||
}
|
||||
if details.CopyrightSrc != origDetails.CopyrightSrc {
|
||||
detailUpdates["copyright_src"] = details.CopyrightSrc
|
||||
}
|
||||
if details.License != origDetails.License {
|
||||
detailUpdates["license"] = details.License
|
||||
}
|
||||
if details.LicenseSrc != origDetails.LicenseSrc {
|
||||
detailUpdates["license_src"] = details.LicenseSrc
|
||||
}
|
||||
if details.Keywords != origDetails.Keywords {
|
||||
detailUpdates["keywords"] = details.Keywords
|
||||
}
|
||||
|
||||
if len(updates) == 0 && len(detailUpdates) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
log.Debugf("batch: saving photo %s with updates=%v details=%v", p.PhotoUID, updates, detailUpdates)
|
||||
|
||||
edited := entity.Now()
|
||||
updates["edited_at"] = edited
|
||||
p.EditedAt = &edited
|
||||
updates["updated_at"] = edited
|
||||
p.UpdatedAt = edited
|
||||
updates["checked_at"] = nil
|
||||
p.CheckedAt = nil
|
||||
|
||||
if err := p.Updates(updates); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if len(detailUpdates) > 0 {
|
||||
if err := details.Updates(detailUpdates); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// shouldUpdateString reports whether the provided field requests an update or removal.
|
||||
func shouldUpdateString(v String) bool {
|
||||
return v.Action == ActionUpdate || v.Action == ActionRemove
|
||||
}
|
||||
105
internal/photoprism/batch/save_resolve.go
Normal file
105
internal/photoprism/batch/save_resolve.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package batch
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
)
|
||||
|
||||
// resolveBatchItemValues pre-creates album and label references so later per-photo work only
|
||||
// performs relation changes instead of issuing duplicate lookup/create queries for every photo.
|
||||
func resolveBatchItemValues(values *PhotosForm) {
|
||||
if values == nil {
|
||||
return
|
||||
}
|
||||
|
||||
resolveAlbumValues(&values.Albums)
|
||||
resolveLabelValues(&values.Labels)
|
||||
}
|
||||
|
||||
// resolveAlbumValues pre-creates album UIDs for add actions so ApplyAlbums only performs
|
||||
// membership changes instead of repeated title lookups.
|
||||
func resolveAlbumValues(items *Items) {
|
||||
if items == nil || items.Action != ActionUpdate {
|
||||
return
|
||||
}
|
||||
|
||||
items.ResolveValuesByTitle(func(title, action string) string {
|
||||
if action != ActionAdd || title == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return ensureAlbumUID(title)
|
||||
})
|
||||
}
|
||||
|
||||
// resolveLabelValues pre-creates label UIDs for add actions to avoid recreating the same
|
||||
// labels per photo when batch updates run.
|
||||
func resolveLabelValues(items *Items) {
|
||||
if items == nil || items.Action != ActionUpdate {
|
||||
return
|
||||
}
|
||||
|
||||
items.ResolveValuesByTitle(func(title, action string) string {
|
||||
if action != ActionAdd || title == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return ensureLabelUID(title)
|
||||
})
|
||||
}
|
||||
|
||||
// ensureAlbumUID returns the UID of the album with the provided title, creating or restoring
|
||||
// the album on demand when it does not already exist. Returns an empty string on failure.
|
||||
func ensureAlbumUID(title string) string {
|
||||
if title == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
album := entity.NewUserAlbum(title, entity.AlbumManual, entity.DefaultOrderAlbum, entity.OwnerUnknown)
|
||||
|
||||
if existing := album.Find(); existing != nil && existing.HasID() {
|
||||
if existing.Deleted() {
|
||||
if err := existing.Restore(); err != nil {
|
||||
log.Errorf("batch: failed to restore album %s: %s", clean.Log(title), err)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return existing.AlbumUID
|
||||
}
|
||||
|
||||
if err := album.Create(); err != nil {
|
||||
log.Errorf("batch: failed to create album %s: %s", clean.Log(title), err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return album.AlbumUID
|
||||
}
|
||||
|
||||
// ensureLabelUID resolves or creates a label for the given title and returns its UID,
|
||||
// restoring deleted labels when necessary.
|
||||
func ensureLabelUID(title string) string {
|
||||
if title == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
label, err := entity.FindLabel(title, true)
|
||||
|
||||
if err != nil || label == nil || !label.HasUID() {
|
||||
label = entity.FirstOrCreateLabel(entity.NewLabel(title, 0))
|
||||
err = nil
|
||||
}
|
||||
|
||||
if label == nil || !label.HasUID() {
|
||||
log.Errorf("batch: failed to resolve label %s", clean.Log(title))
|
||||
return ""
|
||||
}
|
||||
|
||||
if label.Deleted() {
|
||||
if restoreErr := label.Restore(); restoreErr != nil {
|
||||
log.Errorf("batch: failed to restore label %s: %s", clean.Log(title), restoreErr)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
return label.LabelUID
|
||||
}
|
||||
89
internal/photoprism/batch/save_resolve_test.go
Normal file
89
internal/photoprism/batch/save_resolve_test.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package batch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/entity/search"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
func TestPreparePhotoSaveRequestsResolvesItemTitles(t *testing.T) {
|
||||
albumTitle := fmt.Sprintf("Batch Album %d", time.Now().UnixNano())
|
||||
labelTitle := fmt.Sprintf("Batch Label %d", time.Now().UnixNano())
|
||||
|
||||
values := &PhotosForm{
|
||||
Albums: Items{
|
||||
Action: ActionUpdate,
|
||||
Items: []Item{
|
||||
{Title: albumTitle, Action: ActionAdd},
|
||||
{Title: albumTitle, Action: ActionAdd},
|
||||
{Title: albumTitle, Action: ActionRemove},
|
||||
},
|
||||
},
|
||||
Labels: Items{
|
||||
Action: ActionUpdate,
|
||||
Items: []Item{
|
||||
{Title: labelTitle, Action: ActionAdd},
|
||||
{Title: labelTitle, Action: ActionRemove},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
requests, _, _ := PreparePhotoSaveRequests(search.PhotoResults{}, nil, values)
|
||||
require.Len(t, requests, 0)
|
||||
|
||||
albumUID := values.Albums.Items[0].Value
|
||||
labelUID := values.Labels.Items[0].Value
|
||||
require.True(t, rnd.IsUID(albumUID, entity.AlbumUID))
|
||||
require.True(t, rnd.IsUID(labelUID, entity.LabelUID))
|
||||
require.Equal(t, albumUID, values.Albums.Items[1].Value)
|
||||
require.Equal(t, "", values.Albums.Items[2].Value)
|
||||
require.Equal(t, "", values.Labels.Items[1].Value)
|
||||
|
||||
t.Cleanup(func() {
|
||||
if albumUID != "" {
|
||||
if album := entity.FindAlbum(entity.Album{AlbumUID: albumUID}); album != nil {
|
||||
_ = album.Delete()
|
||||
}
|
||||
}
|
||||
if labelUID != "" {
|
||||
var label entity.Label
|
||||
if err := entity.Db().Where("label_uid = ?", labelUID).First(&label).Error; err == nil {
|
||||
_ = label.Delete()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestEnsureAlbumUIDCreatesAndReuses(t *testing.T) {
|
||||
title := fmt.Sprintf("Resolver Album %d", time.Now().UnixNano())
|
||||
|
||||
uid := ensureAlbumUID(title)
|
||||
require.True(t, rnd.IsUID(uid, entity.AlbumUID))
|
||||
assert.Equal(t, uid, ensureAlbumUID(title))
|
||||
|
||||
if album := entity.FindAlbum(entity.Album{AlbumUID: uid}); album != nil {
|
||||
t.Cleanup(func() { _ = album.Delete() })
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureLabelUIDCreatesAndReuses(t *testing.T) {
|
||||
title := fmt.Sprintf("Resolver Label %d", time.Now().UnixNano())
|
||||
|
||||
uid := ensureLabelUID(title)
|
||||
require.True(t, rnd.IsUID(uid, entity.LabelUID))
|
||||
assert.Equal(t, uid, ensureLabelUID(title))
|
||||
|
||||
t.Cleanup(func() {
|
||||
var label entity.Label
|
||||
if err := entity.Db().Where("label_uid = ?", uid).First(&label).Error; err == nil {
|
||||
_ = label.Delete()
|
||||
}
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue