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 @@
|
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 != "" {
|