This commit is contained in:
Gykes 2026-01-30 10:33:23 -08:00
parent cd3c25ccc5
commit 6df099a4bd
3 changed files with 86 additions and 61 deletions

View file

@ -82,7 +82,7 @@ func (qb *fileFilterHandler) criterionHandler() criterionHandler {
qb.hashesCriterionHandler(fileFilter.Hashes),
qb.phashDuplicatedCriterionHandler(fileFilter.Duplicated),
qb.duplicatedCriterionHandler(fileFilter.Duplicated),
&timestampCriterionHandler{fileFilter.CreatedAt, "files.created_at", nil},
&timestampCriterionHandler{fileFilter.UpdatedAt, "files.updated_at", nil},
@ -205,12 +205,26 @@ func (qb *fileFilterHandler) galleryCountCriterionHandler(c *models.IntCriterion
return h.handler(c)
}
func (qb *fileFilterHandler) phashDuplicatedCriterionHandler(duplicatedFilter *models.PHashDuplicationCriterionInput) criterionHandlerFunc {
func (qb *fileFilterHandler) duplicatedCriterionHandler(duplicatedFilter *models.DuplicationCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
// TODO: Wishlist item: Implement Distance matching
if duplicatedFilter != nil {
// For files, only phash duplication applies
if duplicatedFilter == nil {
return
}
var phashValue *bool
// Handle legacy 'duplicated' field for backwards compatibility
if duplicatedFilter.Duplicated != nil && duplicatedFilter.Phash == nil {
phashValue = duplicatedFilter.Duplicated
} else if duplicatedFilter.Phash != nil {
phashValue = duplicatedFilter.Phash
}
if phashValue != nil {
var v string
if *duplicatedFilter.Duplicated {
if *phashValue {
v = ">"
} else {
v = "="

View file

@ -155,10 +155,7 @@ func (qb *sceneFilterHandler) criterionHandler() criterionHandler {
qb.performerTagsCriterionHandler(sceneFilter.PerformerTags),
qb.performerFavoriteCriterionHandler(sceneFilter.PerformerFavorite),
qb.performerAgeCriterionHandler(sceneFilter.PerformerAge),
qb.phashDuplicatedCriterionHandler(sceneFilter.Duplicated, qb.addSceneFilesTable),
qb.stashIDDuplicatedCriterionHandler(sceneFilter.DuplicatedStashID),
qb.titleDuplicatedCriterionHandler(sceneFilter.DuplicatedTitle),
qb.urlDuplicatedCriterionHandler(sceneFilter.DuplicatedURL),
qb.duplicatedCriterionHandler(sceneFilter.Duplicated),
&dateCriterionHandler{sceneFilter.Date, "scenes.date", nil},
&timestampCriterionHandler{sceneFilter.CreatedAt, "scenes.created_at", nil},
&timestampCriterionHandler{sceneFilter.UpdatedAt, "scenes.updated_at", nil},
@ -280,72 +277,86 @@ func (qb *sceneFilterHandler) fileCountCriterionHandler(fileCount *models.IntCri
return h.handler(fileCount)
}
func (qb *sceneFilterHandler) phashDuplicatedCriterionHandler(duplicatedFilter *models.PHashDuplicationCriterionInput, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {
func (qb *sceneFilterHandler) duplicatedCriterionHandler(duplicatedFilter *models.DuplicationCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
// TODO: Wishlist item: Implement Distance matching
if duplicatedFilter != nil {
if addJoinFn != nil {
addJoinFn(f)
}
if duplicatedFilter == nil {
return
}
var v string
if *duplicatedFilter.Duplicated {
v = ">"
} else {
v = "="
}
// Handle legacy 'duplicated' field for backwards compatibility
// Only use it if set AND none of the new fields are set
if duplicatedFilter.Duplicated != nil &&
duplicatedFilter.Phash == nil &&
duplicatedFilter.URL == nil &&
duplicatedFilter.StashID == nil &&
duplicatedFilter.Title == nil {
qb.addSceneFilesTable(f)
qb.applyPhashDuplication(f, *duplicatedFilter.Duplicated)
return
}
f.addInnerJoin("(SELECT file_id FROM files_fingerprints INNER JOIN (SELECT fingerprint FROM files_fingerprints WHERE type = 'phash' GROUP BY fingerprint HAVING COUNT (fingerprint) "+v+" 1) dupes on files_fingerprints.fingerprint = dupes.fingerprint)", "scph", "scenes_files.file_id = scph.file_id")
// Handle new explicit fields
if duplicatedFilter.Phash != nil {
qb.addSceneFilesTable(f)
qb.applyPhashDuplication(f, *duplicatedFilter.Phash)
}
if duplicatedFilter.StashID != nil {
qb.applyStashIDDuplication(f, *duplicatedFilter.StashID)
}
if duplicatedFilter.Title != nil {
qb.applyTitleDuplication(f, *duplicatedFilter.Title)
}
if duplicatedFilter.URL != nil {
qb.applyURLDuplication(f, *duplicatedFilter.URL)
}
}
}
func (qb *sceneFilterHandler) stashIDDuplicatedCriterionHandler(duplicatedFilter *models.StashIDDuplicationCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if duplicatedFilter != nil && duplicatedFilter.Duplicated != nil {
var v string
if *duplicatedFilter.Duplicated {
v = ">"
} else {
v = "="
}
// Find stash_ids that appear on more than one scene
f.addInnerJoin("(SELECT scene_id FROM scene_stash_ids INNER JOIN (SELECT stash_id FROM scene_stash_ids GROUP BY stash_id HAVING COUNT(DISTINCT scene_id) "+v+" 1) dupes ON scene_stash_ids.stash_id = dupes.stash_id)", "scsi", "scenes.id = scsi.scene_id")
}
func (qb *sceneFilterHandler) applyPhashDuplication(f *filterBuilder, duplicated bool) {
// TODO: Wishlist item: Implement Distance matching
var v string
if duplicated {
v = ">"
} else {
v = "="
}
f.addInnerJoin("(SELECT file_id FROM files_fingerprints INNER JOIN (SELECT fingerprint FROM files_fingerprints WHERE type = 'phash' GROUP BY fingerprint HAVING COUNT (fingerprint) "+v+" 1) dupes on files_fingerprints.fingerprint = dupes.fingerprint)", "scph", "scenes_files.file_id = scph.file_id")
}
func (qb *sceneFilterHandler) titleDuplicatedCriterionHandler(duplicatedFilter *models.TitleDuplicationCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if duplicatedFilter != nil && duplicatedFilter.Duplicated != nil {
var v string
if *duplicatedFilter.Duplicated {
v = ">"
} else {
v = "="
}
// Find titles that appear on more than one scene (excluding empty titles)
f.addInnerJoin("(SELECT id FROM scenes WHERE title != '' AND title IS NOT NULL AND title IN (SELECT title FROM scenes WHERE title != '' AND title IS NOT NULL GROUP BY title HAVING COUNT(*) "+v+" 1))", "sctitle", "scenes.id = sctitle.id")
}
func (qb *sceneFilterHandler) applyStashIDDuplication(f *filterBuilder, duplicated bool) {
var v string
if duplicated {
v = ">"
} else {
v = "="
}
// Find stash_ids that appear on more than one scene
f.addInnerJoin("(SELECT scene_id FROM scene_stash_ids INNER JOIN (SELECT stash_id FROM scene_stash_ids GROUP BY stash_id HAVING COUNT(DISTINCT scene_id) "+v+" 1) dupes ON scene_stash_ids.stash_id = dupes.stash_id)", "scsi", "scenes.id = scsi.scene_id")
}
func (qb *sceneFilterHandler) urlDuplicatedCriterionHandler(duplicatedFilter *models.URLDuplicationCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) {
if duplicatedFilter != nil && duplicatedFilter.Duplicated != nil {
var v string
if *duplicatedFilter.Duplicated {
v = ">"
} else {
v = "="
}
// Find URLs that appear on more than one scene
f.addInnerJoin("(SELECT scene_id FROM scene_urls INNER JOIN (SELECT url FROM scene_urls GROUP BY url HAVING COUNT(DISTINCT scene_id) "+v+" 1) dupes ON scene_urls.url = dupes.url)", "scurl", "scenes.id = scurl.scene_id")
}
func (qb *sceneFilterHandler) applyTitleDuplication(f *filterBuilder, duplicated bool) {
var v string
if duplicated {
v = ">"
} else {
v = "="
}
// Find titles that appear on more than one scene (excluding empty titles)
f.addInnerJoin("(SELECT id FROM scenes WHERE title != '' AND title IS NOT NULL AND title IN (SELECT title FROM scenes WHERE title != '' AND title IS NOT NULL GROUP BY title HAVING COUNT(*) "+v+" 1))", "sctitle", "scenes.id = sctitle.id")
}
func (qb *sceneFilterHandler) applyURLDuplication(f *filterBuilder, duplicated bool) {
var v string
if duplicated {
v = ">"
} else {
v = "="
}
// Find URLs that appear on more than one scene
f.addInnerJoin("(SELECT scene_id FROM scene_urls INNER JOIN (SELECT url FROM scene_urls GROUP BY url HAVING COUNT(DISTINCT scene_id) "+v+" 1) dupes ON scene_urls.url = dupes.url)", "scurl", "scenes.id = scurl.scene_id")
}
func (qb *sceneFilterHandler) codecCriterionHandler(codec *models.StringCriterionInput, codecColumn string, addJoinFn func(f *filterBuilder)) criterionHandlerFunc {

View file

@ -4039,7 +4039,7 @@ func TestSceneQueryPhashDuplicated(t *testing.T) {
withTxn(func(ctx context.Context) error {
sqb := db.Scene
duplicated := true
phashCriterion := models.PHashDuplicationCriterionInput{
phashCriterion := models.DuplicationCriterionInput{
Duplicated: &duplicated,
}