Subjects: Do not use or count people tagged on private pictures #4238

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2024-05-08 15:35:30 +02:00
parent 9d04275d18
commit 22cb0ac1a9
9 changed files with 77 additions and 44 deletions

View File

@@ -64,7 +64,7 @@ export class Subject extends RestModel {
return { name: view, query: { q: `person:${this.Slug}` } }; return { name: view, query: { q: `person:${this.Slug}` } };
} }
return { name: view, query: { q: "subject:" + this.UID } }; return { name: view, query: { q: `subject:${this.UID}` } };
} }
classes(selected) { classes(selected) {

View File

@@ -142,11 +142,11 @@ func CreateMarker(router *gin.RouterGroup) {
AbortSaveFailed(c) AbortSaveFailed(c)
return return
} else if changed { } else if changed {
if updateErr := query.UpdateSubjectCovers(); updateErr != nil { if updateErr := query.UpdateSubjectCovers(true); updateErr != nil {
log.Errorf("faces: %s (update covers)", updateErr) log.Errorf("faces: %s (update covers)", updateErr)
} }
if updateErr := entity.UpdateSubjectCounts(); updateErr != nil { if updateErr := entity.UpdateSubjectCounts(true); updateErr != nil {
log.Errorf("faces: %s (update counts)", updateErr) log.Errorf("faces: %s (update counts)", updateErr)
} }
} }
@@ -234,11 +234,11 @@ func UpdateMarker(router *gin.RouterGroup) {
} }
} }
if updateErr := query.UpdateSubjectCovers(); updateErr != nil { if updateErr := query.UpdateSubjectCovers(true); updateErr != nil {
log.Errorf("faces: %s (update covers)", updateErr) log.Errorf("faces: %s (update covers)", updateErr)
} }
if updateErr := entity.UpdateSubjectCounts(); updateErr != nil { if updateErr := entity.UpdateSubjectCounts(true); updateErr != nil {
log.Errorf("faces: %s (update counts)", updateErr) log.Errorf("faces: %s (update counts)", updateErr)
} }
} }
@@ -298,9 +298,9 @@ func ClearMarkerSubject(router *gin.RouterGroup) {
log.Errorf("faces: %s (clear marker subject)", err) log.Errorf("faces: %s (clear marker subject)", err)
AbortSaveFailed(c) AbortSaveFailed(c)
return return
} else if err := query.UpdateSubjectCovers(); err != nil { } else if err := query.UpdateSubjectCovers(true); err != nil {
log.Errorf("faces: %s (update covers)", err) log.Errorf("faces: %s (update covers)", err)
} else if err := entity.UpdateSubjectCounts(); err != nil { } else if err := entity.UpdateSubjectCounts(true); err != nil {
log.Errorf("faces: %s (update counts)", err) log.Errorf("faces: %s (update counts)", err)
} }

View File

@@ -72,7 +72,7 @@ func UpdatePlacesCounts() (err error) {
} }
// UpdateSubjectCounts updates the subject file counts. // UpdateSubjectCounts updates the subject file counts.
func UpdateSubjectCounts() (err error) { func UpdateSubjectCounts(public bool) (err error) {
mutex.Index.Lock() mutex.Index.Lock()
defer mutex.Index.Unlock() defer mutex.Index.Unlock()
@@ -81,36 +81,49 @@ func UpdateSubjectCounts() (err error) {
var res *gorm.DB var res *gorm.DB
subjTable := Subject{}.TableName() subjTable := Subject{}.TableName()
filesTable := File{}.TableName()
markerTable := Marker{}.TableName() var photosJoin *gorm.SqlExpr
// Count people tagged on private pictures?
// see https://github.com/photoprism/photoprism/issues/4238
// and https://github.com/photoprism/photoprism/issues/2570#issuecomment-1231690056
if public {
photosJoin = gorm.Expr("p.id = f.photo_id AND p.deleted_at IS NULL AND p.photo_private = 0")
} else {
photosJoin = gorm.Expr("p.id = f.photo_id AND p.deleted_at IS NULL")
}
condition := gorm.Expr("subj_type = ?", SubjPerson) condition := gorm.Expr("subj_type = ?", SubjPerson)
switch DbDialect() { switch DbDialect() {
case MySQL: case MySQL:
res = Db().Exec(`UPDATE ? LEFT JOIN ( res = Db().Exec(`UPDATE ? LEFT JOIN (
SELECT m.subj_uid, COUNT(DISTINCT f.id) AS subj_files, COUNT(DISTINCT f.photo_id) AS subj_photos FROM ? f SELECT m.subj_uid, COUNT(DISTINCT f.id) AS subj_files, COUNT(DISTINCT f.photo_id) AS subj_photos
JOIN ? m ON f.file_uid = m.file_uid AND m.subj_uid IS NOT NULL AND m.subj_uid <> '' AND m.subj_uid IS NOT NULL FROM files f
JOIN photos p ON ?
JOIN markers m ON f.file_uid = m.file_uid AND m.subj_uid IS NOT NULL AND m.subj_uid <> '' AND m.subj_uid IS NOT NULL
WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL GROUP BY m.subj_uid WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL GROUP BY m.subj_uid
) b ON b.subj_uid = subjects.subj_uid ) b ON b.subj_uid = subjects.subj_uid
SET subjects.file_count = CASE WHEN b.subj_files IS NULL THEN 0 ELSE b.subj_files END, SET subjects.file_count = CASE WHEN b.subj_files IS NULL THEN 0 ELSE b.subj_files END,
subjects.photo_count = CASE WHEN b.subj_photos IS NULL THEN 0 ELSE b.subj_photos END subjects.photo_count = CASE WHEN b.subj_photos IS NULL THEN 0 ELSE b.subj_photos END
WHERE ?`, gorm.Expr(subjTable), gorm.Expr(filesTable), gorm.Expr(markerTable), condition) WHERE ?`, gorm.Expr(subjTable), photosJoin, condition)
case SQLite3: case SQLite3:
// Update files count. // Update files count.
res = Db().Table(subjTable). res = Db().Table(subjTable).
UpdateColumn("file_count", gorm.Expr("(SELECT COUNT(DISTINCT f.id) FROM files f "+ UpdateColumn("file_count", gorm.Expr("(SELECT COUNT(DISTINCT f.id)"+
fmt.Sprintf("JOIN %s m ON f.file_uid = m.file_uid AND m.subj_uid = %s.subj_uid ", " FROM files f JOIN photos p ON ?"+
markerTable, subjTable)+" WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL) WHERE ?", condition)) " JOIN markers m ON f.file_uid = m.file_uid AND m.subj_uid = subjects.subj_uid"+
" WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL) WHERE ?", photosJoin, condition))
// Update photo count. // Update photo count.
if res.Error != nil { if res.Error != nil {
return res.Error return res.Error
} else { } else {
photosRes := Db().Table(subjTable). photosRes := Db().Table(subjTable).
UpdateColumn("photo_count", gorm.Expr("(SELECT COUNT(DISTINCT f.photo_id) FROM files f "+ UpdateColumn("photo_count", gorm.Expr("(SELECT COUNT(DISTINCT f.photo_id)"+
fmt.Sprintf("JOIN %s m ON f.file_uid = m.file_uid AND m.subj_uid = %s.subj_uid ", " FROM files f JOIN photos p ON ?"+
markerTable, subjTable)+" WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL) WHERE ?", condition)) " JOIN markers m ON f.file_uid = m.file_uid AND m.subj_uid = subjects.subj_uid"+
" WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL) WHERE ?", photosJoin, condition))
res.RowsAffected += photosRes.RowsAffected res.RowsAffected += photosRes.RowsAffected
} }
default: default:
@@ -195,7 +208,7 @@ func UpdateCounts() (err error) {
return fmt.Errorf("%s while updating places counts", err) return fmt.Errorf("%s while updating places counts", err)
} }
if err = UpdateSubjectCounts(); err != nil { if err = UpdateSubjectCounts(true); err != nil {
if strings.Contains(err.Error(), "Error 1054") { if strings.Contains(err.Error(), "Error 1054") {
log.Errorf("counts: failed to update subjects, potentially incompatible database version") log.Errorf("counts: failed to update subjects, potentially incompatible database version")
log.Errorf("%s see https://jira.mariadb.org/browse/MDEV-25362", err) log.Errorf("%s see https://jira.mariadb.org/browse/MDEV-25362", err)

View File

@@ -16,7 +16,7 @@ func TestLabelCounts(t *testing.T) {
} }
} }
func TestUpdatePhotoCounts(t *testing.T) { func TestUpdateCounts(t *testing.T) {
err := UpdateCounts() err := UpdateCounts()
if err != nil { if err != nil {

View File

@@ -87,7 +87,7 @@ func NewMarker(file File, area crop.Area, subjUID, markerSrc, markerType string,
Y: area.Y, Y: area.Y,
W: area.W, W: area.W,
H: area.H, H: area.H,
Q: int(float32(math.Log(float64(score))) * float32(size) * area.W), Q: int(math.Log(float64(score)) * ((float64(size) * float64(area.W)) / 2)),
Size: size, Size: size,
Score: score, Score: score,
Thumb: area.Thumb(file.FileHash), Thumb: area.Thumb(file.FileHash),

View File

@@ -52,7 +52,7 @@ func TestNewMarker(t *testing.T) {
assert.Equal(t, "2cad9168fa6acc5c5c2965ddf6ec465ca42fd818-1340ce163163", m.Thumb) assert.Equal(t, "2cad9168fa6acc5c5c2965ddf6ec465ca42fd818-1340ce163163", m.Thumb)
assert.Equal(t, "ls6sg6b1wowuy3c3", m.SubjUID) assert.Equal(t, "ls6sg6b1wowuy3c3", m.SubjUID)
assert.True(t, m.MarkerReview) assert.True(t, m.MarkerReview)
assert.Equal(t, 119, m.Q) assert.Equal(t, 59, m.Q)
assert.Equal(t, 29, m.Score) assert.Equal(t, 29, m.Score)
assert.Equal(t, SrcImage, m.MarkerSrc) assert.Equal(t, SrcImage, m.MarkerSrc)
assert.Equal(t, MarkerLabel, m.MarkerType) assert.Equal(t, MarkerLabel, m.MarkerType)

View File

@@ -245,37 +245,57 @@ func UpdateLabelCovers() (err error) {
} }
// UpdateSubjectCovers updates subject cover thumbs. // UpdateSubjectCovers updates subject cover thumbs.
func UpdateSubjectCovers() (err error) { func UpdateSubjectCovers(public bool) (err error) {
mutex.Index.Lock() mutex.Index.Lock()
defer mutex.Index.Unlock() defer mutex.Index.Unlock()
start := time.Now() start := time.Now()
var res *gorm.DB var res *gorm.DB
var photosJoin *gorm.SqlExpr
subjTable := entity.Subject{}.TableName() // Use faces tagged on private pictures as cover images?
markerTable := entity.Marker{}.TableName() // see https://github.com/photoprism/photoprism/issues/4238
// and https://github.com/photoprism/photoprism/issues/2570#issuecomment-1231690056
if public {
photosJoin = gorm.Expr("p.id = f.photo_id AND p.deleted_at IS NULL AND p.photo_private = 0")
} else {
photosJoin = gorm.Expr("p.id = f.photo_id AND p.deleted_at IS NULL")
}
condition := gorm.Expr( condition := gorm.Expr("subjects.subj_type = ? AND thumb_src = ?", entity.SubjPerson, entity.SrcAuto)
fmt.Sprintf("%s.subj_type = ? AND thumb_src = ?", subjTable),
entity.SubjPerson, entity.SrcAuto)
// TODO: Avoid using private photos as subject covers. // Compose SQL update query.
// See https://github.com/photoprism/photoprism/issues/2570#issuecomment-1231690056
switch DbDialect() { switch DbDialect() {
case MySQL: case MySQL:
res = Db().Exec(`UPDATE ? LEFT JOIN ( res = Db().Exec(`UPDATE subjects LEFT JOIN (
SELECT m.subj_uid, m.q, MAX(m.thumb) AS marker_thumb FROM ? m SELECT m.subj_uid, m.q, MAX(m.thumb) AS marker_thumb
FROM markers m
JOIN files f ON f.file_uid = m.file_uid AND f.deleted_at IS NULL
JOIN photos p ON ?
WHERE m.subj_uid <> '' AND m.subj_uid IS NOT NULL WHERE m.subj_uid <> '' AND m.subj_uid IS NOT NULL
AND m.marker_invalid = 0 AND m.thumb IS NOT NULL AND m.thumb <> '' AND m.marker_invalid = 0 AND m.thumb IS NOT NULL AND m.thumb <> ''
GROUP BY m.subj_uid, m.q GROUP BY m.subj_uid, m.q
) b ON b.subj_uid = subjects.subj_uid ) b ON b.subj_uid = subjects.subj_uid
SET thumb = marker_thumb WHERE ?`, gorm.Expr(subjTable), gorm.Expr(markerTable), condition) SET thumb = marker_thumb WHERE ?`,
photosJoin,
condition,
)
case SQLite3: case SQLite3:
from := gorm.Expr(fmt.Sprintf("%s m WHERE m.subj_uid = %s.subj_uid ", markerTable, subjTable)) // from := gorm.Expr(fmt.Sprintf("%s m WHERE m.subj_uid = %s.subj_uid ", markerTable, subjTable))
res = Db().Table(entity.Subject{}.TableName()).UpdateColumn("thumb", gorm.Expr(`( res = Db().Table(entity.Subject{}.TableName()).UpdateColumn("thumb",
SELECT m.thumb FROM ? AND m.thumb <> '' ORDER BY m.subj_src DESC, m.q DESC LIMIT 1 gorm.Expr(`(
) WHERE ?`, from, condition)) SELECT m.thumb
FROM markers m
JOIN files f ON f.file_uid = m.file_uid AND f.deleted_at IS NULL
JOIN photos p ON ?
WHERE m.subj_uid = subjects.subj_uid AND m.thumb <> ''
ORDER BY m.subj_src DESC, m.q DESC LIMIT 1
) WHERE ?`,
photosJoin,
condition,
),
)
default: default:
log.Warnf("sql: unsupported dialect %s", DbDialect()) log.Warnf("sql: unsupported dialect %s", DbDialect())
return nil return nil
@@ -309,7 +329,7 @@ func UpdateCovers() (err error) {
} }
// Update Subjects. // Update Subjects.
if err = UpdateSubjectCovers(); err != nil { if err = UpdateSubjectCovers(true); err != nil {
return fmt.Errorf("%s while updating subject covers", err) return fmt.Errorf("%s while updating subject covers", err)
} }

View File

@@ -27,7 +27,8 @@ func TestUpdateLabelCovers(t *testing.T) {
} }
func TestUpdateSubjectCovers(t *testing.T) { func TestUpdateSubjectCovers(t *testing.T) {
assert.NoError(t, UpdateSubjectCovers()) assert.NoError(t, UpdateSubjectCovers(false))
assert.NoError(t, UpdateSubjectCovers(true))
} }
func TestUpdateCovers(t *testing.T) { func TestUpdateCovers(t *testing.T) {

View File

@@ -4,13 +4,12 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/txt"
) )
// Subjects searches subjects and returns them. // Subjects searches subjects and returns them.