Index: Prevent two primary files in photo stacks #1823

This commit is contained in:
Michael Mayer 2022-01-06 14:33:49 +01:00
parent d03e28d88e
commit f5b7ef834e
12 changed files with 119 additions and 77 deletions

View file

@ -1,4 +1,4 @@
FROM photoprism/develop:20211218
FROM photoprism/develop:20220106
# Copy latest entrypoint script
COPY --chown=root:root /docker/develop/entrypoint.sh /entrypoint.sh

View file

@ -1,5 +1,5 @@
##################################################### BUILD STAGE ######################################################
FROM photoprism/develop:20211218 as build
FROM photoprism/develop:20220106 as build
ARG TARGETARCH
ARG TARGETPLATFORM

View file

@ -4254,9 +4254,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.35",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.35.tgz",
"integrity": "sha512-wzTOMh6HGFWeALMI3bif0mzgRrVGyP1BdFRx7IvWukFrSC5QVQELENuy+Fm2dCrAdQH9T3nuqr07n94nPDFBWA=="
"version": "1.4.36",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.36.tgz",
"integrity": "sha512-MbLlbF39vKrXWlFEFpCgDHwdlz4O3LmHM5W4tiLRHjSmEUXjJjz8sZkMgWgvYxlZw3N1iDTmCEtOkkESb5TMCg=="
},
"node_modules/emoji-regex": {
"version": "8.0.0",
@ -6483,9 +6483,9 @@
}
},
"node_modules/import-local": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.3.tgz",
"integrity": "sha512-bE9iaUY3CXH8Cwfan/abDKAxe1KGT9kyGsBPqf6DMK/z0a2OzAsrukeYNgIH6cH5Xr452jb1TUL8rSfCLjZ9uA==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",
"integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==",
"dependencies": {
"pkg-dir": "^4.2.0",
"resolve-cwd": "^3.0.0"
@ -6495,6 +6495,9 @@
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/import-local/node_modules/pkg-dir": {
@ -6674,9 +6677,9 @@
}
},
"node_modules/is-core-module": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz",
"integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==",
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
"integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==",
"dependencies": {
"has": "^1.0.3"
},
@ -10284,9 +10287,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sass": {
"version": "1.45.2",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.45.2.tgz",
"integrity": "sha512-cKfs+F9AMPAFlbbTXNsbGvg3y58nV0mXA3E94jqaySKcC8Kq3/8983zVKQ0TLMUrHw7hF9Tnd3Bz9z5Xgtrl9g==",
"version": "1.46.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.46.0.tgz",
"integrity": "sha512-Z4BYTgioAOlMmo4LU3Ky2txR8KR0GRPLXxO38kklaYxgo7qMTgy+mpNN4eKsrXDTFlwS5vdruvazG4cihxHRVQ==",
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
@ -10775,9 +10778,9 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/socket.io": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.0.tgz",
"integrity": "sha512-bnpJxswR9ov0Bw6ilhCvO38/1WPtE3eA2dtxi2Iq4/sFebiDJQzgKNYA7AuVVdGW09nrESXd90NbZqtDd9dzRQ==",
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz",
"integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
@ -15658,9 +15661,9 @@
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA=="
},
"electron-to-chromium": {
"version": "1.4.35",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.35.tgz",
"integrity": "sha512-wzTOMh6HGFWeALMI3bif0mzgRrVGyP1BdFRx7IvWukFrSC5QVQELENuy+Fm2dCrAdQH9T3nuqr07n94nPDFBWA=="
"version": "1.4.36",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.36.tgz",
"integrity": "sha512-MbLlbF39vKrXWlFEFpCgDHwdlz4O3LmHM5W4tiLRHjSmEUXjJjz8sZkMgWgvYxlZw3N1iDTmCEtOkkESb5TMCg=="
},
"emoji-regex": {
"version": "8.0.0",
@ -17284,9 +17287,9 @@
}
},
"import-local": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.3.tgz",
"integrity": "sha512-bE9iaUY3CXH8Cwfan/abDKAxe1KGT9kyGsBPqf6DMK/z0a2OzAsrukeYNgIH6cH5Xr452jb1TUL8rSfCLjZ9uA==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",
"integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==",
"requires": {
"pkg-dir": "^4.2.0",
"resolve-cwd": "^3.0.0"
@ -17406,9 +17409,9 @@
"integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w=="
},
"is-core-module": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz",
"integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==",
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
"integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==",
"requires": {
"has": "^1.0.3"
}
@ -19999,9 +20002,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sass": {
"version": "1.45.2",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.45.2.tgz",
"integrity": "sha512-cKfs+F9AMPAFlbbTXNsbGvg3y58nV0mXA3E94jqaySKcC8Kq3/8983zVKQ0TLMUrHw7hF9Tnd3Bz9z5Xgtrl9g==",
"version": "1.46.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.46.0.tgz",
"integrity": "sha512-Z4BYTgioAOlMmo4LU3Ky2txR8KR0GRPLXxO38kklaYxgo7qMTgy+mpNN4eKsrXDTFlwS5vdruvazG4cihxHRVQ==",
"requires": {
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
@ -20375,9 +20378,9 @@
}
},
"socket.io": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.0.tgz",
"integrity": "sha512-bnpJxswR9ov0Bw6ilhCvO38/1WPtE3eA2dtxi2Iq4/sFebiDJQzgKNYA7AuVVdGW09nrESXd90NbZqtDd9dzRQ==",
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz",
"integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==",
"requires": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",

View file

@ -126,6 +126,14 @@
<translate>Yes</translate>
</td>
</tr>
<tr v-if="file.HDR">
<td>
<translate>High Dynamic Range (HDR)</translate>
</td>
<td>
<translate>Yes</translate>
</td>
</tr>
<tr v-if="file.Portrait">
<td>
<translate>Portrait</translate>
@ -154,14 +162,6 @@
<translate>{{ file.Orientation }}</translate>
</td>
</tr>
<tr v-if="file.HDR">
<td>
<translate>HDR</translate>
</td>
<td>
<translate>Yes</translate>
</td>
</tr>
<tr v-if="file.ColorProfile">
<td>
<translate>Color Profile</translate>

View file

@ -9,8 +9,8 @@ msgstr ""
msgid ""
msgstr ""
#: src/dialog/photo/files.vue:132
#: src/dialog/photo/files.vue:129
#: src/dialog/photo/files.vue:140
#: src/dialog/photo/files.vue:137
msgid "{{ file.Orientation }}"
msgstr ""
@ -329,8 +329,8 @@ msgstr ""
msgid "Artist"
msgstr ""
#: src/dialog/photo/files.vue:123
#: src/dialog/photo/files.vue:120
#: src/dialog/photo/files.vue:131
#: src/dialog/photo/files.vue:128
msgid "Aspect Ratio"
msgstr ""
@ -1080,11 +1080,6 @@ msgstr ""
msgid "Hash"
msgstr ""
#: src/dialog/photo/files.vue:137
#: src/dialog/photo/files.vue:134
msgid "HDR"
msgstr ""
#: src/options/options.js:115
msgid "Hebrew"
msgstr ""
@ -1110,6 +1105,11 @@ msgstr ""
msgid "Hide photos that have been moved to archive."
msgstr ""
#: src/dialog/photo/files.vue:109
#: src/dialog/photo/files.vue:106
msgid "High Dynamic Range (HDR)"
msgstr ""
#: src/options/options.js:121
msgid "Hindi"
msgstr ""
@ -1719,8 +1719,8 @@ msgstr ""
msgid "Orange"
msgstr ""
#: src/dialog/photo/files.vue:129
#: src/dialog/photo/files.vue:126
#: src/dialog/photo/files.vue:137
#: src/dialog/photo/files.vue:134
msgid "Orientation"
msgstr ""
@ -1858,8 +1858,8 @@ msgstr ""
msgid "Polish"
msgstr ""
#: src/dialog/photo/files.vue:109
#: src/dialog/photo/files.vue:106
#: src/dialog/photo/files.vue:117
#: src/dialog/photo/files.vue:114
msgid "Portrait"
msgstr ""
@ -1909,8 +1909,8 @@ msgstr ""
msgid "Product Feedback"
msgstr ""
#: src/dialog/photo/files.vue:117
#: src/dialog/photo/files.vue:114
#: src/dialog/photo/files.vue:125
#: src/dialog/photo/files.vue:122
msgid "Projection"
msgstr ""
@ -2673,11 +2673,11 @@ msgstr ""
#: src/dialog/photo/archive.vue:18
#: src/dialog/photo/files.vue:104
#: src/dialog/photo/files.vue:112
#: src/dialog/photo/files.vue:140
#: src/dialog/photo/files.vue:120
#: src/dialog/photo/files.vue:166
#: src/dialog/photo/files.vue:101
#: src/dialog/photo/files.vue:109
#: src/dialog/photo/files.vue:137
#: src/dialog/photo/files.vue:117
#: src/dialog/photo/files.vue:163
#: src/dialog/photo/info.vue:284
#: src/dialog/photo/info.vue:305

View file

@ -66,6 +66,10 @@ func PhotoUnstack(router *gin.RouterGroup) {
log.Errorf("photo: %s (unstack %s)", err, sanitize.Log(baseName))
AbortEntityNotFound(c)
return
} else if file.Photo == nil {
log.Errorf("photo: cannot find photo for file uid %s (unstack)", fileUID)
AbortEntityNotFound(c)
return
}
stackPhoto := *file.Photo
@ -77,6 +81,9 @@ func PhotoUnstack(router *gin.RouterGroup) {
return
}
// Flag original photo as unstacked / not stackable.
stackPhoto.SetStack(entity.IsUnstacked)
related, err := unstackFile.RelatedFiles(false)
if err != nil {
@ -117,6 +124,7 @@ func PhotoUnstack(router *gin.RouterGroup) {
files = related.Files
}
// Create new photo, also flagged as unstacked / not stackable.
newPhoto := entity.NewPhoto(false)
newPhoto.PhotoPath = unstackFile.RootRelPath()
newPhoto.PhotoName = unstackFile.BasePrefix(false)

View file

@ -31,7 +31,7 @@ var ResetCommand = cli.Command{
},
cli.BoolFlag{
Name: "yes, y",
Usage: "assume \"yes\" as answer to all prompts and run non-interactively",
Usage: "assume \"yes\" and run non-interactively",
},
},
Action: resetAction,
@ -86,7 +86,7 @@ func resetAction(ctx *cli.Context) error {
}
// Reset index only?
if ctx.Bool("index") {
if ctx.Bool("index") || ctx.Bool("yes") {
return nil
}

View file

@ -5,6 +5,7 @@ import (
"path/filepath"
"sort"
"strings"
"sync"
"time"
"github.com/dustin/go-humanize/english"
@ -30,6 +31,8 @@ const (
type Files []File
var primaryFileMutex = sync.Mutex{}
// File represents an image or sidecar file that belongs to a photo.
type File struct {
ID uint `gorm:"primary_key" json:"-" yaml:"-"`
@ -331,11 +334,14 @@ func (m *File) Create() error {
return err
}
return nil
return m.ResolvePrimary()
}
// ResolvePrimary ensures there is only one primary file for a photo..
// ResolvePrimary ensures there is only one primary file for a photo.
func (m *File) ResolvePrimary() error {
primaryFileMutex.Lock()
defer primaryFileMutex.Unlock()
if m.FilePrimary {
return UnscopedDb().Exec("UPDATE `files` SET file_primary = (id = ?) WHERE photo_id = ?", m.ID, m.PhotoID).Error
}

View file

@ -788,7 +788,7 @@ func (m *Photo) Updates(values interface{}) error {
return UnscopedDb().Model(m).UpdateColumns(values).Error
}
// SetFavorite updates the favorite status of a photo.
// SetFavorite updates the favorite flag of a photo.
func (m *Photo) SetFavorite(favorite bool) error {
changed := m.PhotoFavorite != favorite
m.PhotoFavorite = favorite
@ -814,6 +814,14 @@ func (m *Photo) SetFavorite(favorite bool) error {
return nil
}
// SetStack updates the stack flag of a photo.
func (m *Photo) SetStack(stack int8) {
if m.PhotoStack != stack {
m.PhotoStack = stack
Log("photo", "update stack flag", m.Update("PhotoStack", m.PhotoStack))
}
}
// Approve approves a photo in review.
func (m *Photo) Approve() error {
if m.PhotoQuality >= 3 {

View file

@ -545,6 +545,23 @@ func TestPhoto_SetFavorite(t *testing.T) {
})
}
func TestPhoto_SetStack(t *testing.T) {
t.Run("Ignore", func(t *testing.T) {
m := PhotoFixtures.Get("Photo27")
assert.Equal(t, IsStackable, m.PhotoStack)
m.SetStack(IsStackable)
assert.Equal(t, IsStackable, m.PhotoStack)
})
t.Run("Update", func(t *testing.T) {
m := PhotoFixtures.Get("Photo27")
assert.Equal(t, IsStackable, m.PhotoStack)
m.SetStack(IsUnstacked)
assert.Equal(t, IsUnstacked, m.PhotoStack)
m.SetStack(IsStackable)
assert.Equal(t, IsStackable, m.PhotoStack)
})
}
func TestPhoto_Approve(t *testing.T) {
t.Run("quality = 4", func(t *testing.T) {
photo := Photo{PhotoQuality: 4}

View file

@ -131,7 +131,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
}
// Find existing photo if a photo uid was provided or file has not been indexed yet...
if photoUID != "" {
if !fileExists && photoUID != "" {
// Find existing photo by UID.
photoQuery = entity.UnscopedDb().First(&photo, "photo_uid = ?", photoUID)
@ -154,7 +154,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
} else if photoQuery = entity.UnscopedDb().First(&photo, "photo_path = ? AND photo_name = ? AND photo_stack > -1", filePath, fileBase); photoQuery.Error == nil {
// Found.
fileStacked = true
} else if photoQuery = entity.UnscopedDb().First(&photo, "id IN (SELECT photo_id FROM files WHERE file_name = LIKE ? AND file_root = ? AND file_sidecar = 0 AND file_missing = 0) AND photo_path = ?", fs.StripKnownExt(fileName)+".%", entity.RootOriginals, filePath); photoQuery.Error == nil {
} else if photoQuery = entity.UnscopedDb().First(&photo, "id IN (SELECT photo_id FROM files WHERE file_name = LIKE ? AND file_root = ? AND file_sidecar = 0 AND file_missing = 0) AND photo_path = ? AND photo_stack > -1", fs.StripKnownExt(fileName)+".%", entity.RootOriginals, filePath); photoQuery.Error == nil {
// Found.
fileStacked = true
}
@ -269,17 +269,6 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
log.Errorf("index: %s while updating covers of %s", err, logName)
}
// Update photo path and name based on the main filename.
if photoUID == "" && !fileStacked && (fileRenamed || photo.PhotoName == "") {
photo.PhotoPath = filePath
if !o.Stack || !stripSequence || photo.PhotoStack == entity.IsUnstacked {
photo.PhotoName = fullBase
} else {
photo.PhotoName = fileBase
}
}
// Clear (previous) file error.
file.FileError = ""
@ -294,6 +283,17 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
}
}
// Update photo path and name based on the main filename.
if !fileStacked && (file.FilePrimary || photo.PhotoName == "") {
photo.PhotoPath = filePath
if !o.Stack || !stripSequence || photo.PhotoStack == entity.IsUnstacked {
photo.PhotoName = fullBase
} else {
photo.PhotoName = fileBase
}
}
// Set basic file information.
file.FileRoot = fileRoot
file.FileName = fileName

View file

@ -95,12 +95,12 @@ func IndexRelated(related RelatedFiles, ind *Index, opt IndexOptions) (result In
} else if !result.Indexed() {
// Skip related files if main file was not indexed but for example skipped.
if related.Len() > 1 {
log.Warnf("index: %s main %s file %s has %s", result, related.MainFileType(), related.MainLogName(), english.Plural(related.Count(), "related file", "related files"))
log.Warnf("index: %s has %s", related.MainLogName(), english.Plural(related.Count(), "related file", "related files"))
}
return result
} else if result.Stacked() && related.Len() > 1 {
// Show info if main file was stacked and has additional related files.
log.Infof("index: %s main %s file %s has %s", result, related.MainFileType(), related.MainLogName(), english.Plural(related.Count(), "related file", "related files"))
log.Infof("index: %s has %s", related.MainLogName(), english.Plural(related.Count(), "related file", "related files"))
}
done[related.Main.FileName()] = true