Stacks: Use "Stackable" int8 instead of "Unstacked" bool #616 #667

This commit is contained in:
Michael Mayer 2020-12-19 19:15:32 +01:00
parent 4891377d35
commit 12cb89eca5
16 changed files with 126 additions and 105 deletions

View file

@ -88,6 +88,21 @@
<td>{{ model.CameraSerial }}
</td>
</tr>
<tr v-if="model.Stack < 1">
<td>
<translate>Stackable</translate>
</td>
<td>
<v-switch
@change="save"
hide-details
:true-value="0"
:false-value="-1"
v-model="model.Stack"
:label="model.Stack ? $gettext('Yes') : $gettext('No')"
></v-switch>
</td>
</tr>
<tr>
<td>
<translate>Favorite</translate>
@ -114,19 +129,6 @@
></v-switch>
</td>
</tr>
<tr>
<td>
<translate>Unstacked</translate>
</td>
<td>
<v-switch
@change="save"
hide-details
v-model="model.Single"
:label="model.Single ? $gettext('Yes') : $gettext('No')"
></v-switch>
</td>
</tr>
<tr>
<td>
<translate>Scan</translate>

View file

@ -57,6 +57,7 @@ export class Photo extends RestModel {
DocumentID: "",
Type: TypeImage,
TypeSrc: "",
Stack: 0,
Favorite: false,
Private: false,
Scan: false,

View file

@ -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 {

View file

@ -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

View file

@ -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)

View file

@ -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
)

View file

@ -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)

View file

@ -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.

View file

@ -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

View file

@ -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"`

View file

@ -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 {

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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"`

View file

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