Add PHash distance matching to stash-box integration (#1858)

* Add PHash distance matching to stash-box integration
This commit is contained in:
InfiniteTF
2021-10-20 08:22:25 +02:00
committed by GitHub
parent 976038424b
commit 15acf91b90
21 changed files with 462 additions and 217 deletions

View File

@@ -31,6 +31,7 @@ type ScrapedStudio {
stored_id: ID stored_id: ID
name: String! name: String!
url: String url: String
image: String
remote_site_id: String remote_site_id: String
} }

View File

@@ -49,6 +49,7 @@ fragment PerformerFragment on Performer {
disambiguation disambiguation
aliases aliases
gender gender
merged_ids
urls { urls {
...URLFragment ...URLFragment
} }
@@ -75,11 +76,6 @@ fragment PerformerFragment on Performer {
piercings { piercings {
...BodyModificationFragment ...BodyModificationFragment
} }
details
death_date {
...FuzzyDateFragment
}
weight
} }
fragment PerformerAppearanceFragment on PerformerAppearance { fragment PerformerAppearanceFragment on PerformerAppearance {
@@ -127,8 +123,8 @@ query FindSceneByFingerprint($fingerprint: FingerprintQueryInput!) {
} }
} }
query FindScenesByFingerprints($fingerprints: [String!]!) { query FindScenesByFullFingerprints($fingerprints: [FingerprintQueryInput!]!) {
findScenesByFingerprints(fingerprints: $fingerprints) { findScenesByFullFingerprints(fingerprints: $fingerprints) {
...SceneFragment ...SceneFragment
} }
} }
@@ -151,6 +147,12 @@ query FindPerformerByID($id: ID!) {
} }
} }
query FindSceneByID($id: ID!) {
findScene(id: $id) {
...SceneFragment
}
}
mutation SubmitFingerprint($input: FingerprintSubmission!) { mutation SubmitFingerprint($input: FingerprintSubmission!) {
submitFingerprint(input: $input) submitFingerprint(input: $input)
} }

View File

@@ -18,26 +18,27 @@ func NewClient(cli *http.Client, baseURL string, options ...client.HTTPRequestOp
} }
type Query struct { type Query struct {
FindPerformer *Performer "json:\"findPerformer\" graphql:\"findPerformer\"" FindPerformer *Performer "json:\"findPerformer\" graphql:\"findPerformer\""
QueryPerformers QueryPerformersResultType "json:\"queryPerformers\" graphql:\"queryPerformers\"" QueryPerformers QueryPerformersResultType "json:\"queryPerformers\" graphql:\"queryPerformers\""
FindStudio *Studio "json:\"findStudio\" graphql:\"findStudio\"" FindStudio *Studio "json:\"findStudio\" graphql:\"findStudio\""
QueryStudios QueryStudiosResultType "json:\"queryStudios\" graphql:\"queryStudios\"" QueryStudios QueryStudiosResultType "json:\"queryStudios\" graphql:\"queryStudios\""
FindTag *Tag "json:\"findTag\" graphql:\"findTag\"" FindTag *Tag "json:\"findTag\" graphql:\"findTag\""
QueryTags QueryTagsResultType "json:\"queryTags\" graphql:\"queryTags\"" QueryTags QueryTagsResultType "json:\"queryTags\" graphql:\"queryTags\""
FindTagCategory *TagCategory "json:\"findTagCategory\" graphql:\"findTagCategory\"" FindTagCategory *TagCategory "json:\"findTagCategory\" graphql:\"findTagCategory\""
QueryTagCategories QueryTagCategoriesResultType "json:\"queryTagCategories\" graphql:\"queryTagCategories\"" QueryTagCategories QueryTagCategoriesResultType "json:\"queryTagCategories\" graphql:\"queryTagCategories\""
FindScene *Scene "json:\"findScene\" graphql:\"findScene\"" FindScene *Scene "json:\"findScene\" graphql:\"findScene\""
FindSceneByFingerprint []*Scene "json:\"findSceneByFingerprint\" graphql:\"findSceneByFingerprint\"" FindSceneByFingerprint []*Scene "json:\"findSceneByFingerprint\" graphql:\"findSceneByFingerprint\""
FindScenesByFingerprints []*Scene "json:\"findScenesByFingerprints\" graphql:\"findScenesByFingerprints\"" FindScenesByFingerprints []*Scene "json:\"findScenesByFingerprints\" graphql:\"findScenesByFingerprints\""
QueryScenes QueryScenesResultType "json:\"queryScenes\" graphql:\"queryScenes\"" FindScenesByFullFingerprints []*Scene "json:\"findScenesByFullFingerprints\" graphql:\"findScenesByFullFingerprints\""
FindEdit *Edit "json:\"findEdit\" graphql:\"findEdit\"" QueryScenes QueryScenesResultType "json:\"queryScenes\" graphql:\"queryScenes\""
QueryEdits QueryEditsResultType "json:\"queryEdits\" graphql:\"queryEdits\"" FindEdit *Edit "json:\"findEdit\" graphql:\"findEdit\""
FindUser *User "json:\"findUser\" graphql:\"findUser\"" QueryEdits QueryEditsResultType "json:\"queryEdits\" graphql:\"queryEdits\""
QueryUsers QueryUsersResultType "json:\"queryUsers\" graphql:\"queryUsers\"" FindUser *User "json:\"findUser\" graphql:\"findUser\""
Me *User "json:\"me\" graphql:\"me\"" QueryUsers QueryUsersResultType "json:\"queryUsers\" graphql:\"queryUsers\""
SearchPerformer []*Performer "json:\"searchPerformer\" graphql:\"searchPerformer\"" Me *User "json:\"me\" graphql:\"me\""
SearchScene []*Scene "json:\"searchScene\" graphql:\"searchScene\"" SearchPerformer []*Performer "json:\"searchPerformer\" graphql:\"searchPerformer\""
Version Version "json:\"version\" graphql:\"version\"" SearchScene []*Scene "json:\"searchScene\" graphql:\"searchScene\""
Version Version "json:\"version\" graphql:\"version\""
} }
type Mutation struct { type Mutation struct {
@@ -120,6 +121,7 @@ type PerformerFragment struct {
Disambiguation *string "json:\"disambiguation\" graphql:\"disambiguation\"" Disambiguation *string "json:\"disambiguation\" graphql:\"disambiguation\""
Aliases []string "json:\"aliases\" graphql:\"aliases\"" Aliases []string "json:\"aliases\" graphql:\"aliases\""
Gender *GenderEnum "json:\"gender\" graphql:\"gender\"" Gender *GenderEnum "json:\"gender\" graphql:\"gender\""
MergedIds []string "json:\"merged_ids\" graphql:\"merged_ids\""
Urls []*URLFragment "json:\"urls\" graphql:\"urls\"" Urls []*URLFragment "json:\"urls\" graphql:\"urls\""
Images []*ImageFragment "json:\"images\" graphql:\"images\"" Images []*ImageFragment "json:\"images\" graphql:\"images\""
Birthdate *FuzzyDateFragment "json:\"birthdate\" graphql:\"birthdate\"" Birthdate *FuzzyDateFragment "json:\"birthdate\" graphql:\"birthdate\""
@@ -160,8 +162,8 @@ type SceneFragment struct {
type FindSceneByFingerprint struct { type FindSceneByFingerprint struct {
FindSceneByFingerprint []*SceneFragment "json:\"findSceneByFingerprint\" graphql:\"findSceneByFingerprint\"" FindSceneByFingerprint []*SceneFragment "json:\"findSceneByFingerprint\" graphql:\"findSceneByFingerprint\""
} }
type FindScenesByFingerprints struct { type FindScenesByFullFingerprints struct {
FindScenesByFingerprints []*SceneFragment "json:\"findScenesByFingerprints\" graphql:\"findScenesByFingerprints\"" FindScenesByFullFingerprints []*SceneFragment "json:\"findScenesByFullFingerprints\" graphql:\"findScenesByFullFingerprints\""
} }
type SearchScene struct { type SearchScene struct {
SearchScene []*SceneFragment "json:\"searchScene\" graphql:\"searchScene\"" SearchScene []*SceneFragment "json:\"searchScene\" graphql:\"searchScene\""
@@ -172,6 +174,9 @@ type SearchPerformer struct {
type FindPerformerByID struct { type FindPerformerByID struct {
FindPerformer *PerformerFragment "json:\"findPerformer\" graphql:\"findPerformer\"" FindPerformer *PerformerFragment "json:\"findPerformer\" graphql:\"findPerformer\""
} }
type FindSceneByID struct {
FindScene *SceneFragment "json:\"findScene\" graphql:\"findScene\""
}
type SubmitFingerprintPayload struct { type SubmitFingerprintPayload struct {
SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\"" SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\""
} }
@@ -181,56 +186,10 @@ const FindSceneByFingerprintQuery = `query FindSceneByFingerprint ($fingerprint:
... SceneFragment ... SceneFragment
} }
} }
fragment TagFragment on Tag {
name
id
}
fragment PerformerFragment on Performer {
id
name
disambiguation
aliases
gender
urls {
... URLFragment
}
images {
... ImageFragment
}
birthdate {
... FuzzyDateFragment
}
ethnicity
country
eye_color
hair_color
height
measurements {
... MeasurementsFragment
}
breast_type
career_start_year
career_end_year
tattoos {
... BodyModificationFragment
}
piercings {
... BodyModificationFragment
}
}
fragment FuzzyDateFragment on FuzzyDate { fragment FuzzyDateFragment on FuzzyDate {
date date
accuracy accuracy
} }
fragment BodyModificationFragment on BodyModification {
location
description
}
fragment FingerprintFragment on Fingerprint {
algorithm
hash
duration
}
fragment SceneFragment on Scene { fragment SceneFragment on Scene {
id id
title title
@@ -270,24 +229,71 @@ fragment StudioFragment on Studio {
... ImageFragment ... ImageFragment
} }
} }
fragment ImageFragment on Image {
id
url
width
height
}
fragment PerformerAppearanceFragment on PerformerAppearance { fragment PerformerAppearanceFragment on PerformerAppearance {
as as
performer { performer {
... PerformerFragment ... PerformerFragment
} }
} }
fragment PerformerFragment on Performer {
id
name
disambiguation
aliases
gender
merged_ids
urls {
... URLFragment
}
images {
... ImageFragment
}
birthdate {
... FuzzyDateFragment
}
ethnicity
country
eye_color
hair_color
height
measurements {
... MeasurementsFragment
}
breast_type
career_start_year
career_end_year
tattoos {
... BodyModificationFragment
}
piercings {
... BodyModificationFragment
}
}
fragment ImageFragment on Image {
id
url
width
height
}
fragment TagFragment on Tag {
name
id
}
fragment MeasurementsFragment on Measurements { fragment MeasurementsFragment on Measurements {
band_size band_size
cup_size cup_size
waist waist
hip hip
} }
fragment BodyModificationFragment on BodyModification {
location
description
}
fragment FingerprintFragment on Fingerprint {
algorithm
hash
duration
}
` `
func (c *Client) FindSceneByFingerprint(ctx context.Context, fingerprint FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindSceneByFingerprint, error) { func (c *Client) FindSceneByFingerprint(ctx context.Context, fingerprint FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindSceneByFingerprint, error) {
@@ -303,31 +309,38 @@ func (c *Client) FindSceneByFingerprint(ctx context.Context, fingerprint Fingerp
return &res, nil return &res, nil
} }
const FindScenesByFingerprintsQuery = `query FindScenesByFingerprints ($fingerprints: [String!]!) { const FindScenesByFullFingerprintsQuery = `query FindScenesByFullFingerprints ($fingerprints: [FingerprintQueryInput!]!) {
findScenesByFingerprints(fingerprints: $fingerprints) { findScenesByFullFingerprints(fingerprints: $fingerprints) {
... SceneFragment ... SceneFragment
} }
} }
fragment ImageFragment on Image {
id
url
width
height
}
fragment StudioFragment on Studio {
name
id
urls {
... URLFragment
}
images {
... ImageFragment
}
}
fragment TagFragment on Tag { fragment TagFragment on Tag {
name name
id id
} }
fragment FuzzyDateFragment on FuzzyDate {
date
accuracy
}
fragment PerformerAppearanceFragment on PerformerAppearance {
as
performer {
... PerformerFragment
}
}
fragment PerformerFragment on Performer { fragment PerformerFragment on Performer {
id id
name name
disambiguation disambiguation
aliases aliases
gender gender
merged_ids
urls { urls {
... URLFragment ... URLFragment
} }
@@ -355,16 +368,16 @@ fragment PerformerFragment on Performer {
... BodyModificationFragment ... BodyModificationFragment
} }
} }
fragment FuzzyDateFragment on FuzzyDate {
date
accuracy
}
fragment MeasurementsFragment on Measurements { fragment MeasurementsFragment on Measurements {
band_size band_size
cup_size cup_size
waist waist
hip hip
} }
fragment BodyModificationFragment on BodyModification {
location
description
}
fragment SceneFragment on Scene { fragment SceneFragment on Scene {
id id
title title
@@ -390,40 +403,34 @@ fragment SceneFragment on Scene {
... FingerprintFragment ... FingerprintFragment
} }
} }
fragment URLFragment on URL { fragment PerformerAppearanceFragment on PerformerAppearance {
url as
type performer {
} ... PerformerFragment
fragment ImageFragment on Image {
id
url
width
height
}
fragment StudioFragment on Studio {
name
id
urls {
... URLFragment
}
images {
... ImageFragment
} }
} }
fragment BodyModificationFragment on BodyModification {
location
description
}
fragment FingerprintFragment on Fingerprint { fragment FingerprintFragment on Fingerprint {
algorithm algorithm
hash hash
duration duration
} }
fragment URLFragment on URL {
url
type
}
` `
func (c *Client) FindScenesByFingerprints(ctx context.Context, fingerprints []string, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesByFingerprints, error) { func (c *Client) FindScenesByFullFingerprints(ctx context.Context, fingerprints []*FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesByFullFingerprints, error) {
vars := map[string]interface{}{ vars := map[string]interface{}{
"fingerprints": fingerprints, "fingerprints": fingerprints,
} }
var res FindScenesByFingerprints var res FindScenesByFullFingerprints
if err := c.Client.Post(ctx, FindScenesByFingerprintsQuery, &res, vars, httpRequestOptions...); err != nil { if err := c.Client.Post(ctx, FindScenesByFullFingerprintsQuery, &res, vars, httpRequestOptions...); err != nil {
return nil, err return nil, err
} }
@@ -435,21 +442,6 @@ const SearchSceneQuery = `query SearchScene ($term: String!) {
... SceneFragment ... SceneFragment
} }
} }
fragment MeasurementsFragment on Measurements {
band_size
cup_size
waist
hip
}
fragment BodyModificationFragment on BodyModification {
location
description
}
fragment FingerprintFragment on Fingerprint {
algorithm
hash
duration
}
fragment URLFragment on URL { fragment URLFragment on URL {
url url
type type
@@ -468,6 +460,11 @@ fragment FuzzyDateFragment on FuzzyDate {
date date
accuracy accuracy
} }
fragment FingerprintFragment on Fingerprint {
algorithm
hash
duration
}
fragment SceneFragment on Scene { fragment SceneFragment on Scene {
id id
title title
@@ -515,6 +512,7 @@ fragment PerformerFragment on Performer {
disambiguation disambiguation
aliases aliases
gender gender
merged_ids
urls { urls {
... URLFragment ... URLFragment
} }
@@ -542,6 +540,16 @@ fragment PerformerFragment on Performer {
... BodyModificationFragment ... BodyModificationFragment
} }
} }
fragment MeasurementsFragment on Measurements {
band_size
cup_size
waist
hip
}
fragment BodyModificationFragment on BodyModification {
location
description
}
` `
func (c *Client) SearchScene(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchScene, error) { func (c *Client) SearchScene(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchScene, error) {
@@ -562,6 +570,16 @@ const SearchPerformerQuery = `query SearchPerformer ($term: String!) {
... PerformerFragment ... PerformerFragment
} }
} }
fragment URLFragment on URL {
url
type
}
fragment ImageFragment on Image {
id
url
width
height
}
fragment FuzzyDateFragment on FuzzyDate { fragment FuzzyDateFragment on FuzzyDate {
date date
accuracy accuracy
@@ -582,6 +600,7 @@ fragment PerformerFragment on Performer {
disambiguation disambiguation
aliases aliases
gender gender
merged_ids
urls { urls {
... URLFragment ... URLFragment
} }
@@ -609,16 +628,6 @@ fragment PerformerFragment on Performer {
... BodyModificationFragment ... BodyModificationFragment
} }
} }
fragment URLFragment on URL {
url
type
}
fragment ImageFragment on Image {
id
url
width
height
}
` `
func (c *Client) SearchPerformer(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchPerformer, error) { func (c *Client) SearchPerformer(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchPerformer, error) {
@@ -639,26 +648,13 @@ const FindPerformerByIDQuery = `query FindPerformerByID ($id: ID!) {
... PerformerFragment ... PerformerFragment
} }
} }
fragment FuzzyDateFragment on FuzzyDate {
date
accuracy
}
fragment MeasurementsFragment on Measurements {
band_size
cup_size
waist
hip
}
fragment BodyModificationFragment on BodyModification {
location
description
}
fragment PerformerFragment on Performer { fragment PerformerFragment on Performer {
id id
name name
disambiguation disambiguation
aliases aliases
gender gender
merged_ids
urls { urls {
... URLFragment ... URLFragment
} }
@@ -696,6 +692,20 @@ fragment ImageFragment on Image {
width width
height height
} }
fragment FuzzyDateFragment on FuzzyDate {
date
accuracy
}
fragment MeasurementsFragment on Measurements {
band_size
cup_size
waist
hip
}
fragment BodyModificationFragment on BodyModification {
location
description
}
` `
func (c *Client) FindPerformerByID(ctx context.Context, id string, httpRequestOptions ...client.HTTPRequestOption) (*FindPerformerByID, error) { func (c *Client) FindPerformerByID(ctx context.Context, id string, httpRequestOptions ...client.HTTPRequestOption) (*FindPerformerByID, error) {
@@ -711,6 +721,134 @@ func (c *Client) FindPerformerByID(ctx context.Context, id string, httpRequestOp
return &res, nil return &res, nil
} }
const FindSceneByIDQuery = `query FindSceneByID ($id: ID!) {
findScene(id: $id) {
... SceneFragment
}
}
fragment ImageFragment on Image {
id
url
width
height
}
fragment StudioFragment on Studio {
name
id
urls {
... URLFragment
}
images {
... ImageFragment
}
}
fragment TagFragment on Tag {
name
id
}
fragment PerformerFragment on Performer {
id
name
disambiguation
aliases
gender
merged_ids
urls {
... URLFragment
}
images {
... ImageFragment
}
birthdate {
... FuzzyDateFragment
}
ethnicity
country
eye_color
hair_color
height
measurements {
... MeasurementsFragment
}
breast_type
career_start_year
career_end_year
tattoos {
... BodyModificationFragment
}
piercings {
... BodyModificationFragment
}
}
fragment MeasurementsFragment on Measurements {
band_size
cup_size
waist
hip
}
fragment FingerprintFragment on Fingerprint {
algorithm
hash
duration
}
fragment SceneFragment on Scene {
id
title
details
duration
date
urls {
... URLFragment
}
images {
... ImageFragment
}
studio {
... StudioFragment
}
tags {
... TagFragment
}
performers {
... PerformerAppearanceFragment
}
fingerprints {
... FingerprintFragment
}
}
fragment URLFragment on URL {
url
type
}
fragment BodyModificationFragment on BodyModification {
location
description
}
fragment PerformerAppearanceFragment on PerformerAppearance {
as
performer {
... PerformerFragment
}
}
fragment FuzzyDateFragment on FuzzyDate {
date
accuracy
}
`
func (c *Client) FindSceneByID(ctx context.Context, id string, httpRequestOptions ...client.HTTPRequestOption) (*FindSceneByID, error) {
vars := map[string]interface{}{
"id": id,
}
var res FindSceneByID
if err := c.Client.Post(ctx, FindSceneByIDQuery, &res, vars, httpRequestOptions...); err != nil {
return nil, err
}
return &res, nil
}
const SubmitFingerprintQuery = `mutation SubmitFingerprint ($input: FingerprintSubmission!) { const SubmitFingerprintQuery = `mutation SubmitFingerprint ($input: FingerprintSubmission!) {
submitFingerprint(input: $input) submitFingerprint(input: $input)
} }

View File

@@ -81,6 +81,7 @@ type Edit struct {
Status VoteStatusEnum `json:"status"` Status VoteStatusEnum `json:"status"`
Applied bool `json:"applied"` Applied bool `json:"applied"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
} }
type EditComment struct { type EditComment struct {
@@ -134,9 +135,21 @@ type EyeColorCriterionInput struct {
} }
type Fingerprint struct { type Fingerprint struct {
Hash string `json:"hash"` Hash string `json:"hash"`
Algorithm FingerprintAlgorithm `json:"algorithm"` Algorithm FingerprintAlgorithm `json:"algorithm"`
Duration int `json:"duration"` Duration int `json:"duration"`
Submissions int `json:"submissions"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
}
type FingerprintEditInput struct {
Hash string `json:"hash"`
Algorithm FingerprintAlgorithm `json:"algorithm"`
Duration int `json:"duration"`
Submissions int `json:"submissions"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
} }
type FingerprintInput struct { type FingerprintInput struct {
@@ -255,6 +268,8 @@ type Performer struct {
Deleted bool `json:"deleted"` Deleted bool `json:"deleted"`
Edits []*Edit `json:"edits"` Edits []*Edit `json:"edits"`
SceneCount int `json:"scene_count"` SceneCount int `json:"scene_count"`
MergedIds []string `json:"merged_ids"`
Studios []*PerformerStudio `json:"studios"`
} }
func (Performer) IsEditTarget() {} func (Performer) IsEditTarget() {}
@@ -359,16 +374,16 @@ type PerformerEditInput struct {
} }
type PerformerEditOptions struct { type PerformerEditOptions struct {
// Set performer alias on scenes without alias to old name if name is changed // Set performer alias on scenes without alias to old name if name is changed
SetModifyAliases bool `json:"set_modify_aliases"` SetModifyAliases bool `json:"set_modify_aliases"`
// Set performer alias on scenes attached to merge sources to old name // Set performer alias on scenes attached to merge sources to old name
SetMergeAliases bool `json:"set_merge_aliases"` SetMergeAliases bool `json:"set_merge_aliases"`
} }
type PerformerEditOptionsInput struct { type PerformerEditOptionsInput struct {
// Set performer alias on scenes without alias to old name if name is changed // Set performer alias on scenes without alias to old name if name is changed
SetModifyAliases *bool `json:"set_modify_aliases"` SetModifyAliases *bool `json:"set_modify_aliases"`
// Set performer alias on scenes attached to merge sources to old name // Set performer alias on scenes attached to merge sources to old name
SetMergeAliases *bool `json:"set_merge_aliases"` SetMergeAliases *bool `json:"set_merge_aliases"`
} }
@@ -402,6 +417,11 @@ type PerformerFilterType struct {
Piercings *BodyModificationCriterionInput `json:"piercings"` Piercings *BodyModificationCriterionInput `json:"piercings"`
} }
type PerformerStudio struct {
Studio *Studio `json:"studio"`
SceneCount int `json:"scene_count"`
}
type PerformerUpdateInput struct { type PerformerUpdateInput struct {
ID string `json:"id"` ID string `json:"id"`
Name *string `json:"name"` Name *string `json:"name"`
@@ -507,7 +527,7 @@ type SceneCreateInput struct {
Performers []*PerformerAppearanceInput `json:"performers"` Performers []*PerformerAppearanceInput `json:"performers"`
TagIds []string `json:"tag_ids"` TagIds []string `json:"tag_ids"`
ImageIds []string `json:"image_ids"` ImageIds []string `json:"image_ids"`
Fingerprints []*FingerprintInput `json:"fingerprints"` Fingerprints []*FingerprintEditInput `json:"fingerprints"`
Duration *int `json:"duration"` Duration *int `json:"duration"`
Director *string `json:"director"` Director *string `json:"director"`
} }
@@ -547,7 +567,7 @@ type SceneEditDetailsInput struct {
Performers []*PerformerAppearanceInput `json:"performers"` Performers []*PerformerAppearanceInput `json:"performers"`
TagIds []string `json:"tag_ids"` TagIds []string `json:"tag_ids"`
ImageIds []string `json:"image_ids"` ImageIds []string `json:"image_ids"`
Fingerprints []*FingerprintInput `json:"fingerprints"` Fingerprints []*FingerprintEditInput `json:"fingerprints"`
Duration *int `json:"duration"` Duration *int `json:"duration"`
Director *string `json:"director"` Director *string `json:"director"`
} }
@@ -578,6 +598,8 @@ type SceneFilterType struct {
Performers *MultiIDCriterionInput `json:"performers"` Performers *MultiIDCriterionInput `json:"performers"`
// Filter to include scenes with performer appearing as alias // Filter to include scenes with performer appearing as alias
Alias *StringCriterionInput `json:"alias"` Alias *StringCriterionInput `json:"alias"`
// Filter to only include scenes with these fingerprints
Fingerprints *MultiIDCriterionInput `json:"fingerprints"`
} }
type SceneUpdateInput struct { type SceneUpdateInput struct {
@@ -590,7 +612,7 @@ type SceneUpdateInput struct {
Performers []*PerformerAppearanceInput `json:"performers"` Performers []*PerformerAppearanceInput `json:"performers"`
TagIds []string `json:"tag_ids"` TagIds []string `json:"tag_ids"`
ImageIds []string `json:"image_ids"` ImageIds []string `json:"image_ids"`
Fingerprints []*FingerprintInput `json:"fingerprints"` Fingerprints []*FingerprintEditInput `json:"fingerprints"`
Duration *int `json:"duration"` Duration *int `json:"duration"`
Director *string `json:"director"` Director *string `json:"director"`
} }

View File

@@ -75,7 +75,7 @@ func (c Client) FindStashBoxScenesByFingerprints(sceneIDs []string) ([][]*models
return nil, err return nil, err
} }
var fingerprints []string var fingerprints []*graphql.FingerprintQueryInput
// map fingerprints to their scene index // map fingerprints to their scene index
fpToScene := make(map[string][]int) fpToScene := make(map[string][]int)
@@ -93,18 +93,27 @@ func (c Client) FindStashBoxScenesByFingerprints(sceneIDs []string) ([][]*models
} }
if scene.Checksum.Valid { if scene.Checksum.Valid {
fingerprints = append(fingerprints, scene.Checksum.String) fingerprints = append(fingerprints, &graphql.FingerprintQueryInput{
Hash: scene.Checksum.String,
Algorithm: graphql.FingerprintAlgorithmMd5,
})
fpToScene[scene.Checksum.String] = append(fpToScene[scene.Checksum.String], index) fpToScene[scene.Checksum.String] = append(fpToScene[scene.Checksum.String], index)
} }
if scene.OSHash.Valid { if scene.OSHash.Valid {
fingerprints = append(fingerprints, scene.OSHash.String) fingerprints = append(fingerprints, &graphql.FingerprintQueryInput{
Hash: scene.OSHash.String,
Algorithm: graphql.FingerprintAlgorithmOshash,
})
fpToScene[scene.OSHash.String] = append(fpToScene[scene.OSHash.String], index) fpToScene[scene.OSHash.String] = append(fpToScene[scene.OSHash.String], index)
} }
if scene.Phash.Valid { if scene.Phash.Valid {
phashStr := utils.PhashToString(scene.Phash.Int64) phashStr := utils.PhashToString(scene.Phash.Int64)
fingerprints = append(fingerprints, phashStr) fingerprints = append(fingerprints, &graphql.FingerprintQueryInput{
Hash: phashStr,
Algorithm: graphql.FingerprintAlgorithmPhash,
})
fpToScene[phashStr] = append(fpToScene[phashStr], index) fpToScene[phashStr] = append(fpToScene[phashStr], index)
} }
} }
@@ -147,7 +156,7 @@ func (c Client) FindStashBoxScenesByFingerprintsFlat(sceneIDs []string) ([]*mode
return nil, err return nil, err
} }
var fingerprints []string var fingerprints []*graphql.FingerprintQueryInput
if err := c.txnManager.WithReadTxn(ctx, func(r models.ReaderRepository) error { if err := c.txnManager.WithReadTxn(ctx, func(r models.ReaderRepository) error {
qb := r.Scene() qb := r.Scene()
@@ -163,16 +172,24 @@ func (c Client) FindStashBoxScenesByFingerprintsFlat(sceneIDs []string) ([]*mode
} }
if scene.Checksum.Valid { if scene.Checksum.Valid {
fingerprints = append(fingerprints, scene.Checksum.String) fingerprints = append(fingerprints, &graphql.FingerprintQueryInput{
Hash: scene.Checksum.String,
Algorithm: graphql.FingerprintAlgorithmMd5,
})
} }
if scene.OSHash.Valid { if scene.OSHash.Valid {
fingerprints = append(fingerprints, scene.OSHash.String) fingerprints = append(fingerprints, &graphql.FingerprintQueryInput{
Hash: scene.OSHash.String,
Algorithm: graphql.FingerprintAlgorithmOshash,
})
} }
if scene.Phash.Valid { if scene.Phash.Valid {
phashStr := utils.PhashToString(scene.Phash.Int64) fingerprints = append(fingerprints, &graphql.FingerprintQueryInput{
fingerprints = append(fingerprints, phashStr) Hash: utils.PhashToString(scene.Phash.Int64),
Algorithm: graphql.FingerprintAlgorithmPhash,
})
} }
} }
@@ -184,20 +201,20 @@ func (c Client) FindStashBoxScenesByFingerprintsFlat(sceneIDs []string) ([]*mode
return c.findStashBoxScenesByFingerprints(ctx, fingerprints) return c.findStashBoxScenesByFingerprints(ctx, fingerprints)
} }
func (c Client) findStashBoxScenesByFingerprints(ctx context.Context, fingerprints []string) ([]*models.ScrapedScene, error) { func (c Client) findStashBoxScenesByFingerprints(ctx context.Context, fingerprints []*graphql.FingerprintQueryInput) ([]*models.ScrapedScene, error) {
var ret []*models.ScrapedScene var ret []*models.ScrapedScene
for i := 0; i < len(fingerprints); i += 100 { for i := 0; i < len(fingerprints); i += 100 {
end := i + 100 end := i + 100
if end > len(fingerprints) { if end > len(fingerprints) {
end = len(fingerprints) end = len(fingerprints)
} }
scenes, err := c.client.FindScenesByFingerprints(ctx, fingerprints[i:end]) scenes, err := c.client.FindScenesByFullFingerprints(ctx, fingerprints[i:end])
if err != nil { if err != nil {
return nil, err return nil, err
} }
sceneFragments := scenes.FindScenesByFingerprints sceneFragments := scenes.FindScenesByFullFingerprints
for _, s := range sceneFragments { for _, s := range sceneFragments {
ss, err := sceneFragmentToScrapedScene(ctx, c.getHTTPClient(), c.txnManager, s) ss, err := sceneFragmentToScrapedScene(ctx, c.getHTTPClient(), c.txnManager, s)

View File

@@ -46,6 +46,7 @@
"formik": "^2.2.6", "formik": "^2.2.6",
"graphql": "^15.4.0", "graphql": "^15.4.0",
"graphql-tag": "^2.11.0", "graphql-tag": "^2.11.0",
"hamming-distance": "^1.0.0",
"i18n-iso-countries": "^6.4.0", "i18n-iso-countries": "^6.4.0",
"intersection-observer": "^0.12.0", "intersection-observer": "^0.12.0",
"jimp": "^0.16.1", "jimp": "^0.16.1",

View File

@@ -1,4 +1,5 @@
### ✨ New Features ### ✨ New Features
* Added support for matching scenes using perceptual hashes when querying stash-box. ([#1858](https://github.com/stashapp/stash/pull/1858))
* Generalised Tagger view to support tagging using supported scene scrapers. ([#1812](https://github.com/stashapp/stash/pull/1812)) * Generalised Tagger view to support tagging using supported scene scrapers. ([#1812](https://github.com/stashapp/stash/pull/1812))
* Added built-in `Auto Tag` scene scraper to match performers, studio and tags from filename - using AutoTag logic. ([#1817](https://github.com/stashapp/stash/pull/1817)) * Added built-in `Auto Tag` scene scraper to match performers, studio and tags from filename - using AutoTag logic. ([#1817](https://github.com/stashapp/stash/pull/1817))
* Added interface options to disable creating performers/studios/tags from dropdown selectors. ([#1814](https://github.com/stashapp/stash/pull/1814)) * Added interface options to disable creating performers/studios/tags from dropdown selectors. ([#1814](https://github.com/stashapp/stash/pull/1814))

View File

@@ -17,3 +17,4 @@ export { GridCard } from "./GridCard";
export { RatingStars } from "./RatingStars"; export { RatingStars } from "./RatingStars";
export { ExportDialog } from "./ExportDialog"; export { ExportDialog } from "./ExportDialog";
export { default as DeleteEntityDialog } from "./DeleteEntityDialog"; export { default as DeleteEntityDialog } from "./DeleteEntityDialog";
export { OperationButton } from "./OperationButton";

View File

@@ -1,2 +1,2 @@
export { Tagger as default } from "./Tagger"; export { Tagger as default } from "./scenes/SceneTagger";
export { PerformerTagger } from "./performers/PerformerTagger"; export { PerformerTagger } from "./performers/PerformerTagger";

View File

@@ -8,9 +8,10 @@ import {
InputGroup, InputGroup,
} from "react-bootstrap"; } from "react-bootstrap";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { Icon } from "src/components/Shared"; import { Icon } from "src/components/Shared";
import { ParseMode, TagOperation } from "./constants"; import { ParseMode, TagOperation } from "../constants";
import { TaggerStateContext } from "./context"; import { TaggerStateContext } from "../context";
interface IConfigProps { interface IConfigProps {
show: boolean; show: boolean;

View File

@@ -3,12 +3,14 @@ import { Button, ButtonGroup } from "react-bootstrap";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import cx from "classnames"; import cx from "classnames";
import { Icon, PerformerSelect } from "src/components/Shared";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { ValidTypes } from "src/components/Shared/Select"; import {
Icon,
import { OptionalField } from "./IncludeButton"; OperationButton,
import { OperationButton } from "../Shared/OperationButton"; PerformerSelect,
ValidTypes,
} from "src/components/Shared";
import { OptionalField } from "../IncludeButton";
interface IPerformerResultProps { interface IPerformerResultProps {
performer: GQL.ScrapedPerformer; performer: GQL.ScrapedPerformer;

View File

@@ -3,9 +3,10 @@ import * as GQL from "src/core/generated-graphql";
import { SceneQueue } from "src/models/sceneQueue"; import { SceneQueue } from "src/models/sceneQueue";
import { Button, Form } from "react-bootstrap"; import { Button, Form } from "react-bootstrap";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { Icon, LoadingIndicator } from "src/components/Shared"; import { Icon, LoadingIndicator } from "src/components/Shared";
import { OperationButton } from "src/components/Shared/OperationButton"; import { OperationButton } from "src/components/Shared/OperationButton";
import { TaggerStateContext } from "./context"; import { TaggerStateContext } from "../context";
import Config from "./Config"; import Config from "./Config";
import { TaggerScene } from "./TaggerScene"; import { TaggerScene } from "./TaggerScene";
import { SceneTaggerModals } from "./sceneTaggerModals"; import { SceneTaggerModals } from "./sceneTaggerModals";

View File

@@ -2,22 +2,24 @@ import React, { useState, useEffect, useCallback, useMemo } from "react";
import cx from "classnames"; import cx from "classnames";
import { Badge, Button, Col, Form, Row } from "react-bootstrap"; import { Badge, Button, Col, Form, Row } from "react-bootstrap";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { uniq } from "lodash";
import { blobToBase64 } from "base64-blob";
import distance from "hamming-distance";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { import {
HoverPopover,
Icon, Icon,
LoadingIndicator, LoadingIndicator,
SuccessIcon, SuccessIcon,
TagSelect, TagSelect,
TruncatedText, TruncatedText,
OperationButton,
} from "src/components/Shared"; } from "src/components/Shared";
import { FormUtils } from "src/utils"; import { FormUtils } from "src/utils";
import { uniq } from "lodash";
import { blobToBase64 } from "base64-blob";
import { stringToGender } from "src/utils/gender"; import { stringToGender } from "src/utils/gender";
import { OptionalField } from "./IncludeButton"; import { IScrapedScene, TaggerStateContext } from "../context";
import { IScrapedScene, TaggerStateContext } from "./context"; import { OptionalField } from "../IncludeButton";
import { OperationButton } from "../Shared/OperationButton";
import { SceneTaggerModalsState } from "./sceneTaggerModals"; import { SceneTaggerModalsState } from "./sceneTaggerModals";
import PerformerResult from "./PerformerResult"; import PerformerResult from "./PerformerResult";
import StudioResult from "./StudioResult"; import StudioResult from "./StudioResult";
@@ -77,23 +79,58 @@ const getFingerprintStatus = (
const checksumMatch = scene.fingerprints?.some( const checksumMatch = scene.fingerprints?.some(
(f) => f.hash === stashScene.checksum || f.hash === stashScene.oshash (f) => f.hash === stashScene.checksum || f.hash === stashScene.oshash
); );
const phashMatch = scene.fingerprints?.some( const phashMatches =
(f) => f.hash === stashScene.phash scene.fingerprints?.filter(
(f) => f.algorithm === "PHASH" && distance(f.hash, stashScene.phash) <= 8
) ?? [];
const phashList = (
<div className="m-2">
{phashMatches.map((fp) => (
<div>
<b>{fp.hash}</b>
{fp.hash === stashScene.phash
? ", Exact match"
: `, distance ${distance(fp.hash, stashScene.phash)}`}
</div>
))}
</div>
); );
if (checksumMatch || phashMatch)
if (checksumMatch || phashMatches.length > 0)
return ( return (
<div className="font-weight-bold"> <div className="font-weight-bold">
<SuccessIcon className="mr-2" /> <SuccessIcon className="mr-2" />
<FormattedMessage {phashMatches.length > 0 ? (
id="component_tagger.results.hash_matches" <HoverPopover
values={{ placement="bottom"
hash_type: ( content={phashList}
className="PHashPopover"
>
{phashMatches.length > 1 ? (
<FormattedMessage <FormattedMessage
id={`media_info.${phashMatch ? "phash" : "checksum"}`} id="component_tagger.results.phash_matches"
values={{
count: phashMatches.length,
}}
/> />
), ) : (
}} <FormattedMessage
/> id="component_tagger.results.hash_matches"
values={{
hash_type: <FormattedMessage id="media_info.phash" />,
}}
/>
)}
</HoverPopover>
) : (
<FormattedMessage
id="component_tagger.results.hash_matches"
values={{
hash_type: <FormattedMessage id="media_info.checksum" />,
}}
/>
)}
</div> </div>
); );
}; };

View File

@@ -2,9 +2,9 @@ import React, { useContext } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { IconName } from "@fortawesome/fontawesome-svg-core"; import { IconName } from "@fortawesome/fontawesome-svg-core";
import { Icon, Modal, TruncatedText } from "src/components/Shared";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { TaggerStateContext } from "./context"; import { Icon, Modal, TruncatedText } from "src/components/Shared";
import { TaggerStateContext } from "../context";
interface IStudioModalProps { interface IStudioModalProps {
studio: GQL.ScrapedSceneStudioDataFragment; studio: GQL.ScrapedSceneStudioDataFragment;

View File

@@ -3,12 +3,15 @@ import { Button, ButtonGroup } from "react-bootstrap";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import cx from "classnames"; import cx from "classnames";
import { Icon, StudioSelect } from "src/components/Shared"; import {
Icon,
OperationButton,
StudioSelect,
ValidTypes,
} from "src/components/Shared";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { ValidTypes } from "src/components/Shared/Select";
import { OptionalField } from "./IncludeButton"; import { OptionalField } from "../IncludeButton";
import { OperationButton } from "../Shared/OperationButton";
interface IStudioResultProps { interface IStudioResultProps {
studio: GQL.ScrapedStudio; studio: GQL.ScrapedStudio;

View File

@@ -1,14 +1,19 @@
import React, { useState, useContext, PropsWithChildren } from "react"; import React, { useState, useContext, PropsWithChildren } from "react";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Icon, TagLink, TruncatedText } from "src/components/Shared";
import { Button, Collapse, Form, InputGroup } from "react-bootstrap"; import { Button, Collapse, Form, InputGroup } from "react-bootstrap";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { sortPerformers } from "src/core/performers"; import { sortPerformers } from "src/core/performers";
import {
Icon,
OperationButton,
TagLink,
TruncatedText,
} from "src/components/Shared";
import { parsePath, prepareQueryString } from "src/components/Tagger/utils"; import { parsePath, prepareQueryString } from "src/components/Tagger/utils";
import { OperationButton } from "src/components/Shared/OperationButton"; import { ScenePreview } from "src/components/Scenes/SceneCard";
import { TaggerStateContext } from "./context"; import { TaggerStateContext } from "../context";
import { ScenePreview } from "../Scenes/SceneCard";
interface ITaggerSceneDetails { interface ITaggerSceneDetails {
scene: GQL.SlimSceneDataFragment; scene: GQL.SlimSceneDataFragment;

View File

@@ -1,8 +1,9 @@
import React, { useState, useContext } from "react"; import React, { useState, useContext } from "react";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import PerformerModal from "./PerformerModal";
import StudioModal from "./StudioModal"; import StudioModal from "./StudioModal";
import { TaggerStateContext } from "./context"; import PerformerModal from "../PerformerModal";
import { TaggerStateContext } from "../context";
type PerformerModalCallback = (toCreate?: GQL.PerformerCreateInput) => void; type PerformerModalCallback = (toCreate?: GQL.PerformerCreateInput) => void;
type StudioModalCallback = (toCreate?: GQL.StudioCreateInput) => void; type StudioModalCallback = (toCreate?: GQL.StudioCreateInput) => void;

View File

@@ -270,3 +270,8 @@ li.active .optional-field.excluded .scene-link {
width: 100%; width: 100%;
} }
} }
.PHashPopover {
display: inline-block;
text-decoration: underline dotted;
}

View File

@@ -3,6 +3,7 @@ declare var STASH_BASE_URL: string;
declare module "*.md"; declare module "*.md";
declare module "string.prototype.replaceall"; declare module "string.prototype.replaceall";
declare module "mousetrap-pause"; declare module "mousetrap-pause";
declare module "hamming-distance";
declare module "@formatjs/intl-pluralrules/locale-data/en"; declare module "@formatjs/intl-pluralrules/locale-data/en";
declare module "@formatjs/intl-numberformat/locale-data/en"; declare module "@formatjs/intl-numberformat/locale-data/en";
declare module "@formatjs/intl-numberformat/locale-data/en-GB"; declare module "@formatjs/intl-numberformat/locale-data/en-GB";

View File

@@ -137,7 +137,8 @@
"match_success": "Scene successfully tagged", "match_success": "Scene successfully tagged",
"unnamed": "Unnamed", "unnamed": "Unnamed",
"duration_off": "Duration off by at least {number}s", "duration_off": "Duration off by at least {number}s",
"duration_unknown": "Duration unknown" "duration_unknown": "Duration unknown",
"phash_matches": "{count} PHashes match"
}, },
"verb_match_fp": "Match Fingerprints", "verb_match_fp": "Match Fingerprints",
"verb_matched": "Matched", "verb_matched": "Matched",

View File

@@ -7326,6 +7326,11 @@ gzip-size@5.1.1:
duplexer "^0.1.1" duplexer "^0.1.1"
pify "^4.0.1" pify "^4.0.1"
hamming-distance@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/hamming-distance/-/hamming-distance-1.0.0.tgz#39bfa46c61f39e87421e4035a1be4f725dd7b931"
integrity sha1-Ob+kbGHznodCHkA1ob5Pcl3XuTE=
handle-thing@^2.0.0: handle-thing@^2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"