diff --git a/graphql/schema/types/scraper.graphql b/graphql/schema/types/scraper.graphql index e567cbe50..81925a6dc 100644 --- a/graphql/schema/types/scraper.graphql +++ b/graphql/schema/types/scraper.graphql @@ -31,6 +31,7 @@ type ScrapedStudio { stored_id: ID name: String! url: String + image: String remote_site_id: String } diff --git a/graphql/stash-box/query.graphql b/graphql/stash-box/query.graphql index ad1c937f5..9bc24f70a 100644 --- a/graphql/stash-box/query.graphql +++ b/graphql/stash-box/query.graphql @@ -49,6 +49,7 @@ fragment PerformerFragment on Performer { disambiguation aliases gender + merged_ids urls { ...URLFragment } @@ -75,11 +76,6 @@ fragment PerformerFragment on Performer { piercings { ...BodyModificationFragment } - details - death_date { - ...FuzzyDateFragment - } - weight } fragment PerformerAppearanceFragment on PerformerAppearance { @@ -127,8 +123,8 @@ query FindSceneByFingerprint($fingerprint: FingerprintQueryInput!) { } } -query FindScenesByFingerprints($fingerprints: [String!]!) { - findScenesByFingerprints(fingerprints: $fingerprints) { +query FindScenesByFullFingerprints($fingerprints: [FingerprintQueryInput!]!) { + findScenesByFullFingerprints(fingerprints: $fingerprints) { ...SceneFragment } } @@ -151,6 +147,12 @@ query FindPerformerByID($id: ID!) { } } +query FindSceneByID($id: ID!) { + findScene(id: $id) { + ...SceneFragment + } +} + mutation SubmitFingerprint($input: FingerprintSubmission!) { submitFingerprint(input: $input) } diff --git a/pkg/scraper/stashbox/graphql/generated_client.go b/pkg/scraper/stashbox/graphql/generated_client.go index e3f4b45dd..8e0b31429 100644 --- a/pkg/scraper/stashbox/graphql/generated_client.go +++ b/pkg/scraper/stashbox/graphql/generated_client.go @@ -18,26 +18,27 @@ func NewClient(cli *http.Client, baseURL string, options ...client.HTTPRequestOp } type Query struct { - FindPerformer *Performer "json:\"findPerformer\" graphql:\"findPerformer\"" - QueryPerformers QueryPerformersResultType "json:\"queryPerformers\" graphql:\"queryPerformers\"" - FindStudio *Studio "json:\"findStudio\" graphql:\"findStudio\"" - QueryStudios QueryStudiosResultType "json:\"queryStudios\" graphql:\"queryStudios\"" - FindTag *Tag "json:\"findTag\" graphql:\"findTag\"" - QueryTags QueryTagsResultType "json:\"queryTags\" graphql:\"queryTags\"" - FindTagCategory *TagCategory "json:\"findTagCategory\" graphql:\"findTagCategory\"" - QueryTagCategories QueryTagCategoriesResultType "json:\"queryTagCategories\" graphql:\"queryTagCategories\"" - FindScene *Scene "json:\"findScene\" graphql:\"findScene\"" - FindSceneByFingerprint []*Scene "json:\"findSceneByFingerprint\" graphql:\"findSceneByFingerprint\"" - FindScenesByFingerprints []*Scene "json:\"findScenesByFingerprints\" graphql:\"findScenesByFingerprints\"" - QueryScenes QueryScenesResultType "json:\"queryScenes\" graphql:\"queryScenes\"" - FindEdit *Edit "json:\"findEdit\" graphql:\"findEdit\"" - QueryEdits QueryEditsResultType "json:\"queryEdits\" graphql:\"queryEdits\"" - FindUser *User "json:\"findUser\" graphql:\"findUser\"" - QueryUsers QueryUsersResultType "json:\"queryUsers\" graphql:\"queryUsers\"" - Me *User "json:\"me\" graphql:\"me\"" - SearchPerformer []*Performer "json:\"searchPerformer\" graphql:\"searchPerformer\"" - SearchScene []*Scene "json:\"searchScene\" graphql:\"searchScene\"" - Version Version "json:\"version\" graphql:\"version\"" + FindPerformer *Performer "json:\"findPerformer\" graphql:\"findPerformer\"" + QueryPerformers QueryPerformersResultType "json:\"queryPerformers\" graphql:\"queryPerformers\"" + FindStudio *Studio "json:\"findStudio\" graphql:\"findStudio\"" + QueryStudios QueryStudiosResultType "json:\"queryStudios\" graphql:\"queryStudios\"" + FindTag *Tag "json:\"findTag\" graphql:\"findTag\"" + QueryTags QueryTagsResultType "json:\"queryTags\" graphql:\"queryTags\"" + FindTagCategory *TagCategory "json:\"findTagCategory\" graphql:\"findTagCategory\"" + QueryTagCategories QueryTagCategoriesResultType "json:\"queryTagCategories\" graphql:\"queryTagCategories\"" + FindScene *Scene "json:\"findScene\" graphql:\"findScene\"" + FindSceneByFingerprint []*Scene "json:\"findSceneByFingerprint\" graphql:\"findSceneByFingerprint\"" + FindScenesByFingerprints []*Scene "json:\"findScenesByFingerprints\" graphql:\"findScenesByFingerprints\"" + FindScenesByFullFingerprints []*Scene "json:\"findScenesByFullFingerprints\" graphql:\"findScenesByFullFingerprints\"" + QueryScenes QueryScenesResultType "json:\"queryScenes\" graphql:\"queryScenes\"" + FindEdit *Edit "json:\"findEdit\" graphql:\"findEdit\"" + QueryEdits QueryEditsResultType "json:\"queryEdits\" graphql:\"queryEdits\"" + FindUser *User "json:\"findUser\" graphql:\"findUser\"" + QueryUsers QueryUsersResultType "json:\"queryUsers\" graphql:\"queryUsers\"" + Me *User "json:\"me\" graphql:\"me\"" + SearchPerformer []*Performer "json:\"searchPerformer\" graphql:\"searchPerformer\"" + SearchScene []*Scene "json:\"searchScene\" graphql:\"searchScene\"" + Version Version "json:\"version\" graphql:\"version\"" } type Mutation struct { @@ -120,6 +121,7 @@ type PerformerFragment struct { Disambiguation *string "json:\"disambiguation\" graphql:\"disambiguation\"" Aliases []string "json:\"aliases\" graphql:\"aliases\"" Gender *GenderEnum "json:\"gender\" graphql:\"gender\"" + MergedIds []string "json:\"merged_ids\" graphql:\"merged_ids\"" Urls []*URLFragment "json:\"urls\" graphql:\"urls\"" Images []*ImageFragment "json:\"images\" graphql:\"images\"" Birthdate *FuzzyDateFragment "json:\"birthdate\" graphql:\"birthdate\"" @@ -160,8 +162,8 @@ type SceneFragment struct { type FindSceneByFingerprint struct { FindSceneByFingerprint []*SceneFragment "json:\"findSceneByFingerprint\" graphql:\"findSceneByFingerprint\"" } -type FindScenesByFingerprints struct { - FindScenesByFingerprints []*SceneFragment "json:\"findScenesByFingerprints\" graphql:\"findScenesByFingerprints\"" +type FindScenesByFullFingerprints struct { + FindScenesByFullFingerprints []*SceneFragment "json:\"findScenesByFullFingerprints\" graphql:\"findScenesByFullFingerprints\"" } type SearchScene struct { SearchScene []*SceneFragment "json:\"searchScene\" graphql:\"searchScene\"" @@ -172,6 +174,9 @@ type SearchPerformer struct { type FindPerformerByID struct { FindPerformer *PerformerFragment "json:\"findPerformer\" graphql:\"findPerformer\"" } +type FindSceneByID struct { + FindScene *SceneFragment "json:\"findScene\" graphql:\"findScene\"" +} type SubmitFingerprintPayload struct { SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\"" } @@ -181,56 +186,10 @@ const FindSceneByFingerprintQuery = `query FindSceneByFingerprint ($fingerprint: ... 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 { date accuracy } -fragment BodyModificationFragment on BodyModification { - location - description -} -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration -} fragment SceneFragment on Scene { id title @@ -270,24 +229,71 @@ fragment StudioFragment on Studio { ... ImageFragment } } -fragment ImageFragment on Image { - id - url - width - height -} fragment PerformerAppearanceFragment on PerformerAppearance { as performer { ... 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 { band_size cup_size waist 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) { @@ -303,31 +309,38 @@ func (c *Client) FindSceneByFingerprint(ctx context.Context, fingerprint Fingerp return &res, nil } -const FindScenesByFingerprintsQuery = `query FindScenesByFingerprints ($fingerprints: [String!]!) { - findScenesByFingerprints(fingerprints: $fingerprints) { +const FindScenesByFullFingerprintsQuery = `query FindScenesByFullFingerprints ($fingerprints: [FingerprintQueryInput!]!) { + findScenesByFullFingerprints(fingerprints: $fingerprints) { ... 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 FuzzyDateFragment on FuzzyDate { - date - accuracy -} -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } -} fragment PerformerFragment on Performer { id name disambiguation aliases gender + merged_ids urls { ... URLFragment } @@ -355,16 +368,16 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} fragment MeasurementsFragment on Measurements { band_size cup_size waist hip } -fragment BodyModificationFragment on BodyModification { - location - description -} fragment SceneFragment on Scene { id title @@ -390,40 +403,34 @@ fragment SceneFragment on Scene { ... FingerprintFragment } } -fragment URLFragment on URL { - url - type -} -fragment ImageFragment on Image { - id - url - width - height -} -fragment StudioFragment on Studio { - name - id - urls { - ... URLFragment - } - images { - ... ImageFragment +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment } } +fragment BodyModificationFragment on BodyModification { + location + description +} fragment FingerprintFragment on Fingerprint { algorithm hash 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{}{ "fingerprints": fingerprints, } - var res FindScenesByFingerprints - if err := c.Client.Post(ctx, FindScenesByFingerprintsQuery, &res, vars, httpRequestOptions...); err != nil { + var res FindScenesByFullFingerprints + if err := c.Client.Post(ctx, FindScenesByFullFingerprintsQuery, &res, vars, httpRequestOptions...); err != nil { return nil, err } @@ -435,21 +442,6 @@ const SearchSceneQuery = `query SearchScene ($term: String!) { ... 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 { url type @@ -468,6 +460,11 @@ fragment FuzzyDateFragment on FuzzyDate { date accuracy } +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration +} fragment SceneFragment on Scene { id title @@ -515,6 +512,7 @@ fragment PerformerFragment on Performer { disambiguation aliases gender + merged_ids urls { ... URLFragment } @@ -542,6 +540,16 @@ fragment PerformerFragment on Performer { ... 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) { @@ -562,6 +570,16 @@ const SearchPerformerQuery = `query SearchPerformer ($term: String!) { ... PerformerFragment } } +fragment URLFragment on URL { + url + type +} +fragment ImageFragment on Image { + id + url + width + height +} fragment FuzzyDateFragment on FuzzyDate { date accuracy @@ -582,6 +600,7 @@ fragment PerformerFragment on Performer { disambiguation aliases gender + merged_ids urls { ... URLFragment } @@ -609,16 +628,6 @@ fragment PerformerFragment on Performer { ... 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) { @@ -639,26 +648,13 @@ const FindPerformerByIDQuery = `query FindPerformerByID ($id: ID!) { ... 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 { id name disambiguation aliases gender + merged_ids urls { ... URLFragment } @@ -696,6 +692,20 @@ fragment ImageFragment on Image { width 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) { @@ -711,6 +721,134 @@ func (c *Client) FindPerformerByID(ctx context.Context, id string, httpRequestOp 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!) { submitFingerprint(input: $input) } diff --git a/pkg/scraper/stashbox/graphql/generated_models.go b/pkg/scraper/stashbox/graphql/generated_models.go index 9fa66170f..932acbe6b 100644 --- a/pkg/scraper/stashbox/graphql/generated_models.go +++ b/pkg/scraper/stashbox/graphql/generated_models.go @@ -81,6 +81,7 @@ type Edit struct { Status VoteStatusEnum `json:"status"` Applied bool `json:"applied"` Created time.Time `json:"created"` + Updated time.Time `json:"updated"` } type EditComment struct { @@ -134,9 +135,21 @@ type EyeColorCriterionInput struct { } type Fingerprint struct { - Hash string `json:"hash"` - Algorithm FingerprintAlgorithm `json:"algorithm"` - Duration int `json:"duration"` + 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 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 { @@ -255,6 +268,8 @@ type Performer struct { Deleted bool `json:"deleted"` Edits []*Edit `json:"edits"` SceneCount int `json:"scene_count"` + MergedIds []string `json:"merged_ids"` + Studios []*PerformerStudio `json:"studios"` } func (Performer) IsEditTarget() {} @@ -359,16 +374,16 @@ type PerformerEditInput 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"` - // 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"` } 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"` - // 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"` } @@ -402,6 +417,11 @@ type PerformerFilterType struct { Piercings *BodyModificationCriterionInput `json:"piercings"` } +type PerformerStudio struct { + Studio *Studio `json:"studio"` + SceneCount int `json:"scene_count"` +} + type PerformerUpdateInput struct { ID string `json:"id"` Name *string `json:"name"` @@ -507,7 +527,7 @@ type SceneCreateInput struct { Performers []*PerformerAppearanceInput `json:"performers"` TagIds []string `json:"tag_ids"` ImageIds []string `json:"image_ids"` - Fingerprints []*FingerprintInput `json:"fingerprints"` + Fingerprints []*FingerprintEditInput `json:"fingerprints"` Duration *int `json:"duration"` Director *string `json:"director"` } @@ -547,7 +567,7 @@ type SceneEditDetailsInput struct { Performers []*PerformerAppearanceInput `json:"performers"` TagIds []string `json:"tag_ids"` ImageIds []string `json:"image_ids"` - Fingerprints []*FingerprintInput `json:"fingerprints"` + Fingerprints []*FingerprintEditInput `json:"fingerprints"` Duration *int `json:"duration"` Director *string `json:"director"` } @@ -578,6 +598,8 @@ type SceneFilterType struct { Performers *MultiIDCriterionInput `json:"performers"` // Filter to include scenes with performer appearing as alias Alias *StringCriterionInput `json:"alias"` + // Filter to only include scenes with these fingerprints + Fingerprints *MultiIDCriterionInput `json:"fingerprints"` } type SceneUpdateInput struct { @@ -590,7 +612,7 @@ type SceneUpdateInput struct { Performers []*PerformerAppearanceInput `json:"performers"` TagIds []string `json:"tag_ids"` ImageIds []string `json:"image_ids"` - Fingerprints []*FingerprintInput `json:"fingerprints"` + Fingerprints []*FingerprintEditInput `json:"fingerprints"` Duration *int `json:"duration"` Director *string `json:"director"` } diff --git a/pkg/scraper/stashbox/stash_box.go b/pkg/scraper/stashbox/stash_box.go index 7556b9426..1cf3e4d97 100644 --- a/pkg/scraper/stashbox/stash_box.go +++ b/pkg/scraper/stashbox/stash_box.go @@ -75,7 +75,7 @@ func (c Client) FindStashBoxScenesByFingerprints(sceneIDs []string) ([][]*models return nil, err } - var fingerprints []string + var fingerprints []*graphql.FingerprintQueryInput // map fingerprints to their scene index fpToScene := make(map[string][]int) @@ -93,18 +93,27 @@ func (c Client) FindStashBoxScenesByFingerprints(sceneIDs []string) ([][]*models } 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) } 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) } if scene.Phash.Valid { 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) } } @@ -147,7 +156,7 @@ func (c Client) FindStashBoxScenesByFingerprintsFlat(sceneIDs []string) ([]*mode return nil, err } - var fingerprints []string + var fingerprints []*graphql.FingerprintQueryInput if err := c.txnManager.WithReadTxn(ctx, func(r models.ReaderRepository) error { qb := r.Scene() @@ -163,16 +172,24 @@ func (c Client) FindStashBoxScenesByFingerprintsFlat(sceneIDs []string) ([]*mode } 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 { - fingerprints = append(fingerprints, scene.OSHash.String) + fingerprints = append(fingerprints, &graphql.FingerprintQueryInput{ + Hash: scene.OSHash.String, + Algorithm: graphql.FingerprintAlgorithmOshash, + }) } if scene.Phash.Valid { - phashStr := utils.PhashToString(scene.Phash.Int64) - fingerprints = append(fingerprints, phashStr) + fingerprints = append(fingerprints, &graphql.FingerprintQueryInput{ + 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) } -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 for i := 0; i < len(fingerprints); i += 100 { end := i + 100 if 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 { return nil, err } - sceneFragments := scenes.FindScenesByFingerprints + sceneFragments := scenes.FindScenesByFullFingerprints for _, s := range sceneFragments { ss, err := sceneFragmentToScrapedScene(ctx, c.getHTTPClient(), c.txnManager, s) diff --git a/ui/v2.5/package.json b/ui/v2.5/package.json index 3eb6464c7..3a2716171 100644 --- a/ui/v2.5/package.json +++ b/ui/v2.5/package.json @@ -46,6 +46,7 @@ "formik": "^2.2.6", "graphql": "^15.4.0", "graphql-tag": "^2.11.0", + "hamming-distance": "^1.0.0", "i18n-iso-countries": "^6.4.0", "intersection-observer": "^0.12.0", "jimp": "^0.16.1", diff --git a/ui/v2.5/src/components/Changelog/versions/v0110.md b/ui/v2.5/src/components/Changelog/versions/v0110.md index f4f1bd188..1847241d4 100644 --- a/ui/v2.5/src/components/Changelog/versions/v0110.md +++ b/ui/v2.5/src/components/Changelog/versions/v0110.md @@ -1,4 +1,5 @@ ### ✨ 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)) * 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)) diff --git a/ui/v2.5/src/components/Shared/index.ts b/ui/v2.5/src/components/Shared/index.ts index 834d1d3e8..704be7135 100644 --- a/ui/v2.5/src/components/Shared/index.ts +++ b/ui/v2.5/src/components/Shared/index.ts @@ -17,3 +17,4 @@ export { GridCard } from "./GridCard"; export { RatingStars } from "./RatingStars"; export { ExportDialog } from "./ExportDialog"; export { default as DeleteEntityDialog } from "./DeleteEntityDialog"; +export { OperationButton } from "./OperationButton"; diff --git a/ui/v2.5/src/components/Tagger/index.ts b/ui/v2.5/src/components/Tagger/index.ts index cbf7a9f20..9869909f4 100644 --- a/ui/v2.5/src/components/Tagger/index.ts +++ b/ui/v2.5/src/components/Tagger/index.ts @@ -1,2 +1,2 @@ -export { Tagger as default } from "./Tagger"; +export { Tagger as default } from "./scenes/SceneTagger"; export { PerformerTagger } from "./performers/PerformerTagger"; diff --git a/ui/v2.5/src/components/Tagger/Config.tsx b/ui/v2.5/src/components/Tagger/scenes/Config.tsx similarity index 98% rename from ui/v2.5/src/components/Tagger/Config.tsx rename to ui/v2.5/src/components/Tagger/scenes/Config.tsx index 8c8662d76..1c1702687 100644 --- a/ui/v2.5/src/components/Tagger/Config.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/Config.tsx @@ -8,9 +8,10 @@ import { InputGroup, } from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; + import { Icon } from "src/components/Shared"; -import { ParseMode, TagOperation } from "./constants"; -import { TaggerStateContext } from "./context"; +import { ParseMode, TagOperation } from "../constants"; +import { TaggerStateContext } from "../context"; interface IConfigProps { show: boolean; diff --git a/ui/v2.5/src/components/Tagger/PerformerResult.tsx b/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx similarity index 93% rename from ui/v2.5/src/components/Tagger/PerformerResult.tsx rename to ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx index 07b615cc7..dc0ca049a 100755 --- a/ui/v2.5/src/components/Tagger/PerformerResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx @@ -3,12 +3,14 @@ import { Button, ButtonGroup } from "react-bootstrap"; import { FormattedMessage } from "react-intl"; import cx from "classnames"; -import { Icon, PerformerSelect } from "src/components/Shared"; import * as GQL from "src/core/generated-graphql"; -import { ValidTypes } from "src/components/Shared/Select"; - -import { OptionalField } from "./IncludeButton"; -import { OperationButton } from "../Shared/OperationButton"; +import { + Icon, + OperationButton, + PerformerSelect, + ValidTypes, +} from "src/components/Shared"; +import { OptionalField } from "../IncludeButton"; interface IPerformerResultProps { performer: GQL.ScrapedPerformer; diff --git a/ui/v2.5/src/components/Tagger/Tagger.tsx b/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx similarity index 99% rename from ui/v2.5/src/components/Tagger/Tagger.tsx rename to ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx index b6ec37257..139f99766 100755 --- a/ui/v2.5/src/components/Tagger/Tagger.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx @@ -3,9 +3,10 @@ import * as GQL from "src/core/generated-graphql"; import { SceneQueue } from "src/models/sceneQueue"; import { Button, Form } from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; + import { Icon, LoadingIndicator } from "src/components/Shared"; import { OperationButton } from "src/components/Shared/OperationButton"; -import { TaggerStateContext } from "./context"; +import { TaggerStateContext } from "../context"; import Config from "./Config"; import { TaggerScene } from "./TaggerScene"; import { SceneTaggerModals } from "./sceneTaggerModals"; diff --git a/ui/v2.5/src/components/Tagger/StashSearchResult.tsx b/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx similarity index 91% rename from ui/v2.5/src/components/Tagger/StashSearchResult.tsx rename to ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx index 177b8d73c..7168e93f1 100755 --- a/ui/v2.5/src/components/Tagger/StashSearchResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/StashSearchResult.tsx @@ -2,22 +2,24 @@ import React, { useState, useEffect, useCallback, useMemo } from "react"; import cx from "classnames"; import { Badge, Button, Col, Form, Row } from "react-bootstrap"; 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 { + HoverPopover, Icon, LoadingIndicator, SuccessIcon, TagSelect, TruncatedText, + OperationButton, } from "src/components/Shared"; import { FormUtils } from "src/utils"; -import { uniq } from "lodash"; -import { blobToBase64 } from "base64-blob"; import { stringToGender } from "src/utils/gender"; -import { OptionalField } from "./IncludeButton"; -import { IScrapedScene, TaggerStateContext } from "./context"; -import { OperationButton } from "../Shared/OperationButton"; +import { IScrapedScene, TaggerStateContext } from "../context"; +import { OptionalField } from "../IncludeButton"; import { SceneTaggerModalsState } from "./sceneTaggerModals"; import PerformerResult from "./PerformerResult"; import StudioResult from "./StudioResult"; @@ -77,23 +79,58 @@ const getFingerprintStatus = ( const checksumMatch = scene.fingerprints?.some( (f) => f.hash === stashScene.checksum || f.hash === stashScene.oshash ); - const phashMatch = scene.fingerprints?.some( - (f) => f.hash === stashScene.phash + const phashMatches = + scene.fingerprints?.filter( + (f) => f.algorithm === "PHASH" && distance(f.hash, stashScene.phash) <= 8 + ) ?? []; + + const phashList = ( +