From 12cb89eca59df11d10a8a4945b14d25a1a0fc8e8 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Sat, 19 Dec 2020 19:15:32 +0100 Subject: [PATCH] Stacks: Use "Stackable" int8 instead of "Unstacked" bool #616 #667 --- frontend/src/dialog/photo/info.vue | 28 ++++--- frontend/src/model/photo.js | 1 + internal/api/index.go | 2 +- internal/api/photo_unstack.go | 4 +- internal/commands/index.go | 2 +- internal/entity/const.go | 17 ++-- internal/entity/details_test.go | 2 +- internal/entity/photo.go | 15 +++- internal/entity/photo_merge.go | 10 +-- internal/form/photo.go | 2 +- internal/form/photo_search.go | 109 +++++++++++++------------ internal/form/photo_test.go | 4 +- internal/photoprism/index_mediafile.go | 21 +++-- internal/photoprism/index_options.go | 6 +- internal/query/photo_results.go | 2 +- internal/query/photo_search.go | 6 +- 16 files changed, 126 insertions(+), 105 deletions(-) diff --git a/frontend/src/dialog/photo/info.vue b/frontend/src/dialog/photo/info.vue index dd1b237de..ebcf2008c 100644 --- a/frontend/src/dialog/photo/info.vue +++ b/frontend/src/dialog/photo/info.vue @@ -88,6 +88,21 @@ {{ model.CameraSerial }} + + + Stackable + + + + + Favorite @@ -114,19 +129,6 @@ > - - - Unstacked - - - - - Scan diff --git a/frontend/src/model/photo.js b/frontend/src/model/photo.js index 62a19d7bb..ba2e25268 100644 --- a/frontend/src/model/photo.js +++ b/frontend/src/model/photo.js @@ -57,6 +57,7 @@ export class Photo extends RestModel { DocumentID: "", Type: TypeImage, TypeSrc: "", + Stack: 0, Favorite: false, Private: false, Scan: false, diff --git a/internal/api/index.go b/internal/api/index.go index 5c22371c5..cdb4a12b5 100644 --- a/internal/api/index.go +++ b/internal/api/index.go @@ -50,7 +50,7 @@ func StartIndexing(router *gin.RouterGroup) { Rescan: f.Rescan, Convert: conf.Settings().Index.Convert && conf.SidecarWritable(), Path: filepath.Clean(f.Path), - Single: false, + Stack: true, } if len(indOpt.Path) > 1 { diff --git a/internal/api/photo_unstack.go b/internal/api/photo_unstack.go index 48a8d955f..cdc7fc9f0 100644 --- a/internal/api/photo_unstack.go +++ b/internal/api/photo_unstack.go @@ -114,7 +114,7 @@ func PhotoUnstack(router *gin.RouterGroup) { files = related.Files } - newPhoto := entity.NewPhoto(true) + newPhoto := entity.NewPhoto(false) newPhoto.PhotoPath = unstackFile.RootRelPath() newPhoto.PhotoName = unstackFile.BasePrefix(false) @@ -175,7 +175,7 @@ func PhotoUnstack(router *gin.RouterGroup) { } // Re-index existing photo stack. - if res := ind.FileName(photoprism.FileName(stackPrimary.FileRoot, stackPrimary.FileName), photoprism.IndexOptionsAll()); res.Failed() { + if res := ind.FileName(photoprism.FileName(stackPrimary.FileRoot, stackPrimary.FileName), photoprism.IndexOptionsSingle()); res.Failed() { log.Errorf("photo: %s (unstack %s)", res.Err, txt.Quote(baseName)) AbortSaveFailed(c) return diff --git a/internal/commands/index.go b/internal/commands/index.go index 82b553a51..ead84d1f8 100644 --- a/internal/commands/index.go +++ b/internal/commands/index.go @@ -63,7 +63,7 @@ func indexAction(ctx *cli.Context) error { Path: subPath, Rescan: ctx.Bool("all"), Convert: conf.Settings().Index.Convert && conf.SidecarWritable(), - Single: false, + Stack: true, } indexed := ind.Start(indOpt) diff --git a/internal/entity/const.go b/internal/entity/const.go index 4461f68e7..e8769ccac 100644 --- a/internal/entity/const.go +++ b/internal/entity/const.go @@ -1,7 +1,7 @@ package entity const ( - // Sort orders. + // Sort orders: SortOrderAdded = "added" SortOrderNewest = "newest" SortOrderOldest = "oldest" @@ -10,13 +10,13 @@ const ( SortOrderRelevance = "relevance" SortOrderEdited = "edited" - // Unknown values. + // Unknown values: YearUnknown = -1 MonthUnknown = -1 DayUnknown = -1 TitleUnknown = "Unknown" - // Content types. + // Content types: TypeDefault = "" TypeImage = "image" TypeLive = "live" @@ -24,7 +24,7 @@ const ( TypeRaw = "raw" TypeText = "text" - // Root directories. + // Root directories: RootUnknown = "" RootOriginals = "/" RootExamples = "examples" @@ -32,14 +32,19 @@ const ( RootImport = "import" RootPath = "/" - // Panorama projections. + // Panorama projections: ProjectionDefault = "" ProjectionEquirectangular = "equirectangular" ProjectionCubestrip = "cubestrip" ProjectionCylindrical = "cylindrical" - // Event names. + // Event names: Updated = "updated" Created = "created" Deleted = "deleted" + + // Photo stacks: + IsStacked int8 = 1 + IsStackable int8 = 0 + IsUnstacked int8 = -1 ) diff --git a/internal/entity/details_test.go b/internal/entity/details_test.go index a950bccf6..e39ba3125 100644 --- a/internal/entity/details_test.go +++ b/internal/entity/details_test.go @@ -97,7 +97,7 @@ func TestDetails_NoCopyright(t *testing.T) { func TestNewDetails(t *testing.T) { t.Run("add to photo", func(t *testing.T) { - p := NewPhoto(false) + p := NewPhoto(true) assert.Equal(t, TitleUnknown, p.PhotoTitle) diff --git a/internal/entity/photo.go b/internal/entity/photo.go index ca936d255..cb9c1569a 100644 --- a/internal/entity/photo.go +++ b/internal/entity/photo.go @@ -57,8 +57,8 @@ type Photo struct { PhotoPath string `gorm:"type:VARBINARY(500);index:idx_photos_path_name;" json:"Path" yaml:"-"` PhotoName string `gorm:"type:VARBINARY(255);index:idx_photos_path_name;" json:"Name" yaml:"-"` OriginalName string `gorm:"type:VARBINARY(755);" json:"OriginalName" yaml:"OriginalName,omitempty"` + PhotoStack int8 `json:"Stack" yaml:"Stack"` PhotoFavorite bool `json:"Favorite" yaml:"Favorite,omitempty"` - PhotoSingle bool `json:"Single" yaml:"Single,omitempty"` PhotoPrivate bool `json:"Private" yaml:"Private,omitempty"` PhotoScan bool `json:"Scan" yaml:"Scan,omitempty"` PhotoPanorama bool `json:"Panorama" yaml:"Panorama,omitempty"` @@ -102,11 +102,10 @@ type Photo struct { } // NewPhoto creates a photo entity. -func NewPhoto(single bool) Photo { - return Photo{ +func NewPhoto(stackable bool) Photo { + m := Photo{ PhotoTitle: TitleUnknown, PhotoType: TypeImage, - PhotoSingle: single, PhotoCountry: UnknownCountry.ID, CameraID: UnknownCamera.ID, LensID: UnknownLens.ID, @@ -117,6 +116,14 @@ func NewPhoto(single bool) Photo { Cell: &UnknownLocation, Place: &UnknownPlace, } + + if stackable { + m.PhotoStack = IsStackable + } else { + m.PhotoStack = IsUnstacked + } + + return m } // SavePhotoForm saves a model in the database using form data. diff --git a/internal/entity/photo_merge.go b/internal/entity/photo_merge.go index 68d2110a1..1e2218a9b 100644 --- a/internal/entity/photo_merge.go +++ b/internal/entity/photo_merge.go @@ -18,15 +18,15 @@ func (m *Photo) ResolvePrimary() error { // Identical returns identical photos that can be merged. func (m *Photo) Identical(includeMeta, includeUuid bool) (identical Photos, err error) { - if m.PhotoSingle || m.PhotoName == "" { + if m.PhotoStack == IsUnstacked || m.PhotoName == "" { return identical, nil } switch { case includeMeta && includeUuid && m.HasLocation() && m.HasLatLng() && m.TakenSrc == SrcMeta && rnd.IsUUID(m.UUID): if err := Db(). - Where("(taken_at = ? AND taken_src = 'meta' AND photo_single = 0 AND cell_id = ? AND camera_serial = ? AND camera_id = ?) "+ - "OR (uuid = ? AND photo_single = 0)"+ + Where("(taken_at = ? AND taken_src = 'meta' AND photo_stack > -1 AND cell_id = ? AND camera_serial = ? AND camera_id = ?) "+ + "OR (uuid = ? AND photo_stack > -1)"+ "OR (photo_path = ? AND photo_name = ?)", m.TakenAt, m.CellID, m.CameraSerial, m.CameraID, m.UUID, m.PhotoPath, m.PhotoName). Order("id ASC").Find(&identical).Error; err != nil { @@ -34,7 +34,7 @@ func (m *Photo) Identical(includeMeta, includeUuid bool) (identical Photos, err } case includeMeta && m.HasLocation() && m.HasLatLng() && m.TakenSrc == SrcMeta: if err := Db(). - Where("(taken_at = ? AND taken_src = 'meta' AND photo_single = 0 AND cell_id = ? AND camera_serial = ? AND camera_id = ?) "+ + Where("(taken_at = ? AND taken_src = 'meta' AND photo_stack > -1 AND cell_id = ? AND camera_serial = ? AND camera_id = ?) "+ "OR (photo_path = ? AND photo_name = ?)", m.TakenAt, m.CellID, m.CameraSerial, m.CameraID, m.PhotoPath, m.PhotoName). Order("id ASC").Find(&identical).Error; err != nil { @@ -42,7 +42,7 @@ func (m *Photo) Identical(includeMeta, includeUuid bool) (identical Photos, err } case includeUuid && rnd.IsUUID(m.UUID): if err := Db(). - Where("(uuid = ? AND photo_single = 0) OR (photo_path = ? AND photo_name = ?)", + Where("(uuid = ? AND photo_stack > -1) OR (photo_path = ? AND photo_name = ?)", m.UUID, m.PhotoPath, m.PhotoName). Order("id ASC").Find(&identical).Error; err != nil { return identical, err diff --git a/internal/form/photo.go b/internal/form/photo.go index 14e0e6ab9..9b8c0ad9e 100644 --- a/internal/form/photo.go +++ b/internal/form/photo.go @@ -33,9 +33,9 @@ type Photo struct { PhotoDescription string `json:"Description"` DescriptionSrc string `json:"DescriptionSrc"` Details Details `json:"Details"` + PhotoStack int8 `json:"Stack"` PhotoFavorite bool `json:"Favorite"` PhotoPrivate bool `json:"Private"` - PhotoSingle bool `json:"Single"` PhotoScan bool `json:"Scan"` PhotoPanorama bool `json:"Panorama"` PhotoAltitude int `json:"Altitude"` diff --git a/internal/form/photo_search.go b/internal/form/photo_search.go index 627cc385c..0facc69e5 100644 --- a/internal/form/photo_search.go +++ b/internal/form/photo_search.go @@ -6,60 +6,61 @@ import ( // PhotoSearch represents search form fields for "/api/v1/photos". type PhotoSearch struct { - Query string `form:"q"` - Filter string `form:"filter"` - ID string `form:"id"` - Type string `form:"type"` - Path string `form:"path"` - Folder string `form:"folder"` // Alias for Path - Name string `form:"name"` - Filename string `form:"filename"` - Original string `form:"original"` - Title string `form:"title"` - Hash string `form:"hash"` - Primary bool `form:"primary"` - Single bool `form:"single"` - Video bool `form:"video"` - Photo bool `form:"photo"` - Scan bool `form:"scan"` - Panorama bool `form:"panorama"` - Error bool `form:"error"` - Hidden bool `form:"hidden"` - Archived bool `form:"archived"` - Public bool `form:"public"` - Private bool `form:"private"` - Favorite bool `form:"favorite"` - Unsorted bool `form:"unsorted"` - Stack bool `form:"stack"` - Lat float32 `form:"lat"` - Lng float32 `form:"lng"` - Dist uint `form:"dist"` - Fmin float32 `form:"fmin"` - Fmax float32 `form:"fmax"` - Chroma uint8 `form:"chroma"` - Diff uint32 `form:"diff"` - Mono bool `form:"mono"` - Portrait bool `form:"portrait"` - Geo bool `form:"geo"` - Album string `form:"album"` - Label string `form:"label"` - Category string `form:"category"` // Moments - Country string `form:"country"` // Moments - State string `form:"state"` // Moments - Year int `form:"year"` // Moments - Month int `form:"month"` // Moments - Day int `form:"day"` // Moments - Color string `form:"color"` - Quality int `form:"quality"` - Review bool `form:"review"` - Camera int `form:"camera"` - Lens int `form:"lens"` - Before time.Time `form:"before" time_format:"2006-01-02"` - After time.Time `form:"after" time_format:"2006-01-02"` - Count int `form:"count" binding:"required" serialize:"-"` - Offset int `form:"offset" serialize:"-"` - Order string `form:"order" serialize:"-"` - Merged bool `form:"merged" serialize:"-"` + Query string `form:"q"` + Filter string `form:"filter"` + ID string `form:"id"` + Type string `form:"type"` + Path string `form:"path"` + Folder string `form:"folder"` // Alias for Path + Name string `form:"name"` + Filename string `form:"filename"` + Original string `form:"original"` + Title string `form:"title"` + Hash string `form:"hash"` + Primary bool `form:"primary"` + Stack bool `form:"stack"` + Unstacked bool `form:"unstacked"` + Stackable bool `form:"stackable"` + Video bool `form:"video"` + Photo bool `form:"photo"` + Scan bool `form:"scan"` + Panorama bool `form:"panorama"` + Error bool `form:"error"` + Hidden bool `form:"hidden"` + Archived bool `form:"archived"` + Public bool `form:"public"` + Private bool `form:"private"` + Favorite bool `form:"favorite"` + Unsorted bool `form:"unsorted"` + Lat float32 `form:"lat"` + Lng float32 `form:"lng"` + Dist uint `form:"dist"` + Fmin float32 `form:"fmin"` + Fmax float32 `form:"fmax"` + Chroma uint8 `form:"chroma"` + Diff uint32 `form:"diff"` + Mono bool `form:"mono"` + Portrait bool `form:"portrait"` + Geo bool `form:"geo"` + Album string `form:"album"` + Label string `form:"label"` + Category string `form:"category"` // Moments + Country string `form:"country"` // Moments + State string `form:"state"` // Moments + Year int `form:"year"` // Moments + Month int `form:"month"` // Moments + Day int `form:"day"` // Moments + Color string `form:"color"` + Quality int `form:"quality"` + Review bool `form:"review"` + Camera int `form:"camera"` + Lens int `form:"lens"` + Before time.Time `form:"before" time_format:"2006-01-02"` + After time.Time `form:"after" time_format:"2006-01-02"` + Count int `form:"count" binding:"required" serialize:"-"` + Offset int `form:"offset" serialize:"-"` + Order string `form:"order" serialize:"-"` + Merged bool `form:"merged" serialize:"-"` } func (f *PhotoSearch) GetQuery() string { diff --git a/internal/form/photo_test.go b/internal/form/photo_test.go index 8d89d0f49..16bbaa9ee 100644 --- a/internal/form/photo_test.go +++ b/internal/form/photo_test.go @@ -19,7 +19,7 @@ func TestNewPhoto(t *testing.T) { PhotoFavorite: false, PhotoPrivate: false, PhotoType: "image", - PhotoSingle: false, + PhotoStack: int8(1), PhotoLat: 9.9999, PhotoLng: 8.8888, PhotoAltitude: 2, @@ -50,7 +50,7 @@ func TestNewPhoto(t *testing.T) { assert.Equal(t, false, r.PhotoFavorite) assert.Equal(t, false, r.PhotoPrivate) assert.Equal(t, "image", r.PhotoType) - assert.Equal(t, false, r.PhotoSingle) + assert.Equal(t, int8(1), r.PhotoStack) assert.Equal(t, float32(9.9999), r.PhotoLat) assert.Equal(t, float32(8.8888), r.PhotoLng) assert.Equal(t, 2, r.PhotoAltitude) diff --git a/internal/photoprism/index_mediafile.go b/internal/photoprism/index_mediafile.go index 1c272a8e2..e1e829392 100644 --- a/internal/photoprism/index_mediafile.go +++ b/internal/photoprism/index_mediafile.go @@ -89,10 +89,10 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( file, primaryFile := entity.File{}, entity.File{} - photo := entity.NewPhoto(o.Single) + photo := entity.NewPhoto(o.Stack) metaData := meta.NewData() labels := classify.Labels{} - stripSequence := Config().Settings().StackSequences() && !o.Single + stripSequence := Config().Settings().StackSequences() && o.Stack fileRoot, fileBase, filePath, fileName := m.PathNameInfo(stripSequence) fullBase := m.BasePrefix(false) @@ -173,14 +173,14 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( // Look for existing photo if file wasn't indexed yet... if !fileExists { - if photoQuery = entity.UnscopedDb().First(&photo, "photo_path = ? AND photo_name = ?", filePath, fullBase); photoQuery.Error == nil || fileBase == fullBase || o.Single { + if photoQuery = entity.UnscopedDb().First(&photo, "photo_path = ? AND photo_name = ?", filePath, fullBase); photoQuery.Error == nil || fileBase == fullBase || !o.Stack { // Skip next query. - } else if photoQuery = entity.UnscopedDb().First(&photo, "photo_path = ? AND photo_name = ? AND photo_single = 0", filePath, fileBase); photoQuery.Error == nil { + } else if photoQuery = entity.UnscopedDb().First(&photo, "photo_path = ? AND photo_name = ? AND photo_stack > -1", filePath, fileBase); photoQuery.Error == nil { fileStacked = true } // Stack file based on matching location and time metadata? - if !o.Single && photoQuery.Error != nil && Config().Settings().StackMeta() && m.MetaData().HasTimeAndPlace() { + if o.Stack && photoQuery.Error != nil && Config().Settings().StackMeta() && m.MetaData().HasTimeAndPlace() { metaData = m.MetaData() photoQuery = entity.UnscopedDb().First(&photo, "photo_lat = ? AND photo_lng = ? AND taken_at = ? AND taken_src = 'meta' AND camera_serial = ?", metaData.Lat, metaData.Lng, metaData.TakenAt, metaData.CameraSerial) @@ -190,7 +190,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( } // Stack file based on the same unique ID? - if !o.Single && photoQuery.Error != nil && Config().Settings().StackUUID() && m.MetaData().HasDocumentID() { + if o.Stack && photoQuery.Error != nil && Config().Settings().StackUUID() && m.MetaData().HasDocumentID() { photoQuery = entity.UnscopedDb().First(&photo, "uuid <> '' AND uuid = ?", m.MetaData().DocumentID) if photoQuery.Error == nil { @@ -229,7 +229,10 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( // Try to recover photo metadata from backup if not exists. if !photoExists { photo.PhotoQuality = -1 - photo.PhotoSingle = o.Single + + if o.Stack { + photo.PhotoStack = entity.IsStackable + } if yamlName := fs.FormatYaml.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); yamlName != "" { if err := photo.LoadFromYaml(yamlName); err != nil { @@ -250,7 +253,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( photo.PhotoPath = filePath - if o.Single || photo.PhotoSingle || !stripSequence { + if !o.Stack || !stripSequence || photo.PhotoStack == entity.IsUnstacked { photo.PhotoName = fullBase } else { photo.PhotoName = fileBase @@ -823,7 +826,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) ( log.Errorf("index: %s in %s (set download id)", err, logName) } - if o.Single || photo.PhotoSingle { + if !o.Stack || photo.PhotoStack == entity.IsUnstacked { // Do nothing. } else if original, merged, err := photo.Merge(Config().Settings().StackMeta(), Config().Settings().StackUUID()); err != nil { log.Errorf("index: %s in %s (merge)", err.Error(), logName) diff --git a/internal/photoprism/index_options.go b/internal/photoprism/index_options.go index 6f800d289..e6e1dad91 100644 --- a/internal/photoprism/index_options.go +++ b/internal/photoprism/index_options.go @@ -4,7 +4,7 @@ type IndexOptions struct { Path string Rescan bool Convert bool - Single bool + Stack bool } func (o *IndexOptions) SkipUnchanged() bool { @@ -17,7 +17,7 @@ func IndexOptionsAll() IndexOptions { Path: "/", Rescan: true, Convert: true, - Single: false, + Stack: true, } return result @@ -29,7 +29,7 @@ func IndexOptionsSingle() IndexOptions { Path: "/", Rescan: true, Convert: true, - Single: true, + Stack: false, } return result diff --git a/internal/query/photo_results.go b/internal/query/photo_results.go index e98bbaed2..6be983d76 100644 --- a/internal/query/photo_results.go +++ b/internal/query/photo_results.go @@ -31,8 +31,8 @@ type PhotoResult struct { PhotoMonth int `json:"Month"` PhotoDay int `json:"Day"` PhotoCountry string `json:"Country"` + PhotoStack int8 `json:"Stack"` PhotoFavorite bool `json:"Favorite"` - PhotoSingle bool `json:"Single"` PhotoPrivate bool `json:"Private"` PhotoIso int `json:"Iso"` PhotoFocalLength int `json:"FocalLength"` diff --git a/internal/query/photo_search.go b/internal/query/photo_search.go index 4aed5dc88..77d52815f 100644 --- a/internal/query/photo_search.go +++ b/internal/query/photo_search.go @@ -201,8 +201,10 @@ func PhotoSearch(f form.PhotoSearch) (results PhotoResults, count int, err error s = s.Where("photos.photo_panorama = 1") } - if f.Single { - s = s.Where("photos.photo_single = 1") + if f.Stackable { + s = s.Where("photos.photo_stack > -1") + } else if f.Unstacked { + s = s.Where("photos.photo_stack = -1") } if f.Country != "" {