diff --git a/graphql/documents/mutations/stash-box.graphql b/graphql/documents/mutations/stash-box.graphql index c20cdd25f..55c508737 100644 --- a/graphql/documents/mutations/stash-box.graphql +++ b/graphql/documents/mutations/stash-box.graphql @@ -5,3 +5,11 @@ mutation SubmitStashBoxFingerprints($input: StashBoxFingerprintSubmissionInput!) mutation StashBoxBatchPerformerTag($input: StashBoxBatchPerformerTagInput!) { stashBoxBatchPerformerTag(input: $input) } + +mutation SubmitStashBoxSceneDraft($input: StashBoxDraftSubmissionInput!) { + submitStashBoxSceneDraft(input: $input) +} + +mutation SubmitStashBoxPerformerDraft($input: StashBoxDraftSubmissionInput!) { + submitStashBoxPerformerDraft(input: $input) +} diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index 044dbf49a..5139c66c3 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -282,6 +282,11 @@ type Mutation { """Submit fingerprints to stash-box instance""" submitStashBoxFingerprints(input: StashBoxFingerprintSubmissionInput!): Boolean! + """Submit scene as draft to stash-box instance""" + submitStashBoxSceneDraft(input: StashBoxDraftSubmissionInput!): ID + """Submit performer as draft to stash-box instance""" + submitStashBoxPerformerDraft(input: StashBoxDraftSubmissionInput!): ID + """Backup the database. Optionally returns a link to download the database file""" backupDatabase(input: BackupDatabaseInput!): String diff --git a/graphql/schema/types/stash-box.graphql b/graphql/schema/types/stash-box.graphql index 471db19b1..7614a2fae 100644 --- a/graphql/schema/types/stash-box.graphql +++ b/graphql/schema/types/stash-box.graphql @@ -24,3 +24,8 @@ input StashBoxFingerprintSubmissionInput { scene_ids: [String!]! stash_box_index: Int! } + +input StashBoxDraftSubmissionInput { + id: String! + stash_box_index: Int! +} diff --git a/graphql/stash-box/query.graphql b/graphql/stash-box/query.graphql index 12db15ca5..39bce5d3c 100644 --- a/graphql/stash-box/query.graphql +++ b/graphql/stash-box/query.graphql @@ -162,3 +162,15 @@ query Me { name } } + +mutation SubmitSceneDraft($input: SceneDraftInput!) { + submitSceneDraft(input: $input) { + id + } +} + +mutation SubmitPerformerDraft($input: PerformerDraftInput!) { + submitPerformerDraft(input: $input) { + id + } +} diff --git a/pkg/api/resolver_mutation_stash_box.go b/pkg/api/resolver_mutation_stash_box.go index 9c489e8de..1c9d6d34b 100644 --- a/pkg/api/resolver_mutation_stash_box.go +++ b/pkg/api/resolver_mutation_stash_box.go @@ -27,3 +27,62 @@ func (r *mutationResolver) StashBoxBatchPerformerTag(ctx context.Context, input jobID := manager.GetInstance().StashBoxBatchPerformerTag(ctx, input) return strconv.Itoa(jobID), nil } + +func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input models.StashBoxDraftSubmissionInput) (*string, error) { + boxes := config.GetInstance().GetStashBoxes() + + if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) { + return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex) + } + + client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager) + + id, err := strconv.Atoi(input.ID) + if err != nil { + return nil, err + } + + var res *string + err = r.withReadTxn(ctx, func(repo models.ReaderRepository) error { + qb := repo.Scene() + scene, err := qb.Find(id) + if err != nil { + return err + } + filepath := manager.GetInstance().Paths.Scene.GetScreenshotPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm())) + + res, err = client.SubmitSceneDraft(ctx, id, boxes[input.StashBoxIndex].Endpoint, filepath) + return err + }) + + return res, err +} + +func (r *mutationResolver) SubmitStashBoxPerformerDraft(ctx context.Context, input models.StashBoxDraftSubmissionInput) (*string, error) { + boxes := config.GetInstance().GetStashBoxes() + + if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) { + return nil, fmt.Errorf("invalid stash_box_index %d", input.StashBoxIndex) + } + + client := stashbox.NewClient(*boxes[input.StashBoxIndex], r.txnManager) + + id, err := strconv.Atoi(input.ID) + if err != nil { + return nil, err + } + + var res *string + err = r.withReadTxn(ctx, func(repo models.ReaderRepository) error { + qb := repo.Performer() + performer, err := qb.Find(id) + if err != nil { + return err + } + + res, err = client.SubmitPerformerDraft(ctx, performer, boxes[input.StashBoxIndex].Endpoint) + return err + }) + + return res, err +} diff --git a/pkg/scraper/stashbox/graphql/generated_client.go b/pkg/scraper/stashbox/graphql/generated_client.go index fd48d6528..39bf3f91e 100644 --- a/pkg/scraper/stashbox/graphql/generated_client.go +++ b/pkg/scraper/stashbox/graphql/generated_client.go @@ -31,6 +31,8 @@ type Query struct { FindScenesByFingerprints []*Scene "json:\"findScenesByFingerprints\" graphql:\"findScenesByFingerprints\"" FindScenesByFullFingerprints []*Scene "json:\"findScenesByFullFingerprints\" graphql:\"findScenesByFullFingerprints\"" QueryScenes QueryScenesResultType "json:\"queryScenes\" graphql:\"queryScenes\"" + FindSite *Site "json:\"findSite\" graphql:\"findSite\"" + QuerySites QuerySitesResultType "json:\"querySites\" graphql:\"querySites\"" FindEdit *Edit "json:\"findEdit\" graphql:\"findEdit\"" QueryEdits QueryEditsResultType "json:\"queryEdits\" graphql:\"queryEdits\"" FindUser *User "json:\"findUser\" graphql:\"findUser\"" @@ -38,48 +40,57 @@ type Query struct { Me *User "json:\"me\" graphql:\"me\"" SearchPerformer []*Performer "json:\"searchPerformer\" graphql:\"searchPerformer\"" SearchScene []*Scene "json:\"searchScene\" graphql:\"searchScene\"" + FindDraft *Draft "json:\"findDraft\" graphql:\"findDraft\"" + FindDrafts []*Draft "json:\"findDrafts\" graphql:\"findDrafts\"" Version Version "json:\"version\" graphql:\"version\"" + GetConfig StashBoxConfig "json:\"getConfig\" graphql:\"getConfig\"" } type Mutation struct { - SceneCreate *Scene "json:\"sceneCreate\" graphql:\"sceneCreate\"" - SceneUpdate *Scene "json:\"sceneUpdate\" graphql:\"sceneUpdate\"" - SceneDestroy bool "json:\"sceneDestroy\" graphql:\"sceneDestroy\"" - PerformerCreate *Performer "json:\"performerCreate\" graphql:\"performerCreate\"" - PerformerUpdate *Performer "json:\"performerUpdate\" graphql:\"performerUpdate\"" - PerformerDestroy bool "json:\"performerDestroy\" graphql:\"performerDestroy\"" - StudioCreate *Studio "json:\"studioCreate\" graphql:\"studioCreate\"" - StudioUpdate *Studio "json:\"studioUpdate\" graphql:\"studioUpdate\"" - StudioDestroy bool "json:\"studioDestroy\" graphql:\"studioDestroy\"" - TagCreate *Tag "json:\"tagCreate\" graphql:\"tagCreate\"" - TagUpdate *Tag "json:\"tagUpdate\" graphql:\"tagUpdate\"" - TagDestroy bool "json:\"tagDestroy\" graphql:\"tagDestroy\"" - UserCreate *User "json:\"userCreate\" graphql:\"userCreate\"" - UserUpdate *User "json:\"userUpdate\" graphql:\"userUpdate\"" - UserDestroy bool "json:\"userDestroy\" graphql:\"userDestroy\"" - ImageCreate *Image "json:\"imageCreate\" graphql:\"imageCreate\"" - ImageDestroy bool "json:\"imageDestroy\" graphql:\"imageDestroy\"" - NewUser *string "json:\"newUser\" graphql:\"newUser\"" - ActivateNewUser *User "json:\"activateNewUser\" graphql:\"activateNewUser\"" - GenerateInviteCode string "json:\"generateInviteCode\" graphql:\"generateInviteCode\"" - RescindInviteCode bool "json:\"rescindInviteCode\" graphql:\"rescindInviteCode\"" - GrantInvite int "json:\"grantInvite\" graphql:\"grantInvite\"" - RevokeInvite int "json:\"revokeInvite\" graphql:\"revokeInvite\"" - TagCategoryCreate *TagCategory "json:\"tagCategoryCreate\" graphql:\"tagCategoryCreate\"" - TagCategoryUpdate *TagCategory "json:\"tagCategoryUpdate\" graphql:\"tagCategoryUpdate\"" - TagCategoryDestroy bool "json:\"tagCategoryDestroy\" graphql:\"tagCategoryDestroy\"" - RegenerateAPIKey string "json:\"regenerateAPIKey\" graphql:\"regenerateAPIKey\"" - ResetPassword bool "json:\"resetPassword\" graphql:\"resetPassword\"" - ChangePassword bool "json:\"changePassword\" graphql:\"changePassword\"" - SceneEdit Edit "json:\"sceneEdit\" graphql:\"sceneEdit\"" - PerformerEdit Edit "json:\"performerEdit\" graphql:\"performerEdit\"" - StudioEdit Edit "json:\"studioEdit\" graphql:\"studioEdit\"" - TagEdit Edit "json:\"tagEdit\" graphql:\"tagEdit\"" - EditVote Edit "json:\"editVote\" graphql:\"editVote\"" - EditComment Edit "json:\"editComment\" graphql:\"editComment\"" - ApplyEdit Edit "json:\"applyEdit\" graphql:\"applyEdit\"" - CancelEdit Edit "json:\"cancelEdit\" graphql:\"cancelEdit\"" - SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\"" + SceneCreate *Scene "json:\"sceneCreate\" graphql:\"sceneCreate\"" + SceneUpdate *Scene "json:\"sceneUpdate\" graphql:\"sceneUpdate\"" + SceneDestroy bool "json:\"sceneDestroy\" graphql:\"sceneDestroy\"" + PerformerCreate *Performer "json:\"performerCreate\" graphql:\"performerCreate\"" + PerformerUpdate *Performer "json:\"performerUpdate\" graphql:\"performerUpdate\"" + PerformerDestroy bool "json:\"performerDestroy\" graphql:\"performerDestroy\"" + StudioCreate *Studio "json:\"studioCreate\" graphql:\"studioCreate\"" + StudioUpdate *Studio "json:\"studioUpdate\" graphql:\"studioUpdate\"" + StudioDestroy bool "json:\"studioDestroy\" graphql:\"studioDestroy\"" + TagCreate *Tag "json:\"tagCreate\" graphql:\"tagCreate\"" + TagUpdate *Tag "json:\"tagUpdate\" graphql:\"tagUpdate\"" + TagDestroy bool "json:\"tagDestroy\" graphql:\"tagDestroy\"" + UserCreate *User "json:\"userCreate\" graphql:\"userCreate\"" + UserUpdate *User "json:\"userUpdate\" graphql:\"userUpdate\"" + UserDestroy bool "json:\"userDestroy\" graphql:\"userDestroy\"" + ImageCreate *Image "json:\"imageCreate\" graphql:\"imageCreate\"" + ImageDestroy bool "json:\"imageDestroy\" graphql:\"imageDestroy\"" + NewUser *string "json:\"newUser\" graphql:\"newUser\"" + ActivateNewUser *User "json:\"activateNewUser\" graphql:\"activateNewUser\"" + GenerateInviteCode *string "json:\"generateInviteCode\" graphql:\"generateInviteCode\"" + RescindInviteCode bool "json:\"rescindInviteCode\" graphql:\"rescindInviteCode\"" + GrantInvite int "json:\"grantInvite\" graphql:\"grantInvite\"" + RevokeInvite int "json:\"revokeInvite\" graphql:\"revokeInvite\"" + TagCategoryCreate *TagCategory "json:\"tagCategoryCreate\" graphql:\"tagCategoryCreate\"" + TagCategoryUpdate *TagCategory "json:\"tagCategoryUpdate\" graphql:\"tagCategoryUpdate\"" + TagCategoryDestroy bool "json:\"tagCategoryDestroy\" graphql:\"tagCategoryDestroy\"" + SiteCreate *Site "json:\"siteCreate\" graphql:\"siteCreate\"" + SiteUpdate *Site "json:\"siteUpdate\" graphql:\"siteUpdate\"" + SiteDestroy bool "json:\"siteDestroy\" graphql:\"siteDestroy\"" + RegenerateAPIKey string "json:\"regenerateAPIKey\" graphql:\"regenerateAPIKey\"" + ResetPassword bool "json:\"resetPassword\" graphql:\"resetPassword\"" + ChangePassword bool "json:\"changePassword\" graphql:\"changePassword\"" + SceneEdit Edit "json:\"sceneEdit\" graphql:\"sceneEdit\"" + PerformerEdit Edit "json:\"performerEdit\" graphql:\"performerEdit\"" + StudioEdit Edit "json:\"studioEdit\" graphql:\"studioEdit\"" + TagEdit Edit "json:\"tagEdit\" graphql:\"tagEdit\"" + EditVote Edit "json:\"editVote\" graphql:\"editVote\"" + EditComment Edit "json:\"editComment\" graphql:\"editComment\"" + ApplyEdit Edit "json:\"applyEdit\" graphql:\"applyEdit\"" + CancelEdit Edit "json:\"cancelEdit\" graphql:\"cancelEdit\"" + SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\"" + SubmitSceneDraft DraftSubmissionStatus "json:\"submitSceneDraft\" graphql:\"submitSceneDraft\"" + SubmitPerformerDraft DraftSubmissionStatus "json:\"submitPerformerDraft\" graphql:\"submitPerformerDraft\"" + DestroyDraft bool "json:\"destroyDraft\" graphql:\"destroyDraft\"" } type URLFragment struct { URL string "json:\"url\" graphql:\"url\"" @@ -185,12 +196,26 @@ type Me struct { Name string "json:\"name\" graphql:\"name\"" } "json:\"me\" graphql:\"me\"" } +type SubmitSceneDraftPayload struct { + SubmitSceneDraft struct { + ID *string "json:\"id\" graphql:\"id\"" + } "json:\"submitSceneDraft\" graphql:\"submitSceneDraft\"" +} +type SubmitPerformerDraftPayload struct { + SubmitPerformerDraft struct { + ID *string "json:\"id\" graphql:\"id\"" + } "json:\"submitPerformerDraft\" graphql:\"submitPerformerDraft\"" +} const FindSceneByFingerprintQuery = `query FindSceneByFingerprint ($fingerprint: FingerprintQueryInput!) { findSceneByFingerprint(fingerprint: $fingerprint) { ... SceneFragment } } +fragment BodyModificationFragment on BodyModification { + location + description +} fragment FingerprintFragment on Fingerprint { algorithm hash @@ -200,27 +225,9 @@ fragment URLFragment on URL { url type } -fragment ImageFragment on Image { - id - url - width - height -} -fragment StudioFragment on Studio { +fragment TagFragment on Tag { name id - urls { - ... URLFragment - } - images { - ... ImageFragment - } -} -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } } fragment PerformerFragment on Performer { id @@ -256,9 +263,15 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } -fragment BodyModificationFragment on BodyModification { - location - description +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy +} +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip } fragment SceneFragment on Scene { id @@ -285,19 +298,27 @@ fragment SceneFragment on Scene { ... FingerprintFragment } } -fragment TagFragment on Tag { +fragment ImageFragment on Image { + id + url + width + height +} +fragment StudioFragment on Studio { name id + urls { + ... URLFragment + } + images { + ... ImageFragment + } } -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy -} -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } } ` @@ -367,10 +388,36 @@ fragment FuzzyDateFragment on FuzzyDate { date accuracy } +fragment MeasurementsFragment on Measurements { + band_size + cup_size + waist + hip +} fragment BodyModificationFragment on BodyModification { location description } +fragment ImageFragment on Image { + id + url + width + height +} +fragment URLFragment on URL { + url + type +} +fragment TagFragment on Tag { + name + id +} +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } +} fragment FingerprintFragment on Fingerprint { algorithm hash @@ -401,32 +448,6 @@ fragment SceneFragment on Scene { ... FingerprintFragment } } -fragment URLFragment on URL { - url - type -} -fragment ImageFragment on Image { - id - url - width - height -} -fragment TagFragment on Tag { - name - id -} -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } -} -fragment MeasurementsFragment on Measurements { - band_size - cup_size - waist - hip -} ` func (c *Client) FindScenesByFullFingerprints(ctx context.Context, fingerprints []*FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindScenesByFullFingerprints, error) { @@ -447,6 +468,11 @@ const SearchSceneQuery = `query SearchScene ($term: String!) { ... SceneFragment } } +fragment FingerprintFragment on Fingerprint { + algorithm + hash + duration +} fragment URLFragment on URL { url type @@ -457,9 +483,15 @@ fragment ImageFragment on Image { width height } -fragment FuzzyDateFragment on FuzzyDate { - date - accuracy +fragment TagFragment on Tag { + name + id +} +fragment PerformerAppearanceFragment on PerformerAppearance { + as + performer { + ... PerformerFragment + } } fragment MeasurementsFragment on Measurements { band_size @@ -506,16 +538,6 @@ fragment StudioFragment on Studio { ... ImageFragment } } -fragment TagFragment on Tag { - name - id -} -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } -} fragment PerformerFragment on Performer { id name @@ -550,10 +572,9 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } -fragment FingerprintFragment on Fingerprint { - algorithm - hash - duration +fragment FuzzyDateFragment on FuzzyDate { + date + accuracy } ` @@ -653,6 +674,30 @@ const FindPerformerByIDQuery = `query FindPerformerByID ($id: ID!) { ... PerformerFragment } } +fragment URLFragment on URL { + url + type +} +fragment ImageFragment on Image { + id + url + width + height +} +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 @@ -687,30 +732,6 @@ fragment PerformerFragment on Performer { ... BodyModificationFragment } } -fragment URLFragment on URL { - url - type -} -fragment ImageFragment on Image { - id - url - 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) { @@ -731,22 +752,10 @@ const FindSceneByIDQuery = `query FindSceneByID ($id: ID!) { ... SceneFragment } } -fragment ImageFragment on Image { - id - url - width - height -} fragment TagFragment on Tag { name id } -fragment PerformerAppearanceFragment on PerformerAppearance { - as - performer { - ... PerformerFragment - } -} fragment FuzzyDateFragment on FuzzyDate { date accuracy @@ -762,58 +771,6 @@ fragment FingerprintFragment on Fingerprint { hash duration } -fragment URLFragment on URL { - url - type -} -fragment StudioFragment on Studio { - name - id - urls { - ... URLFragment - } - images { - ... ImageFragment - } -} -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 BodyModificationFragment on BodyModification { - location - description -} fragment SceneFragment on Scene { id title @@ -839,6 +796,70 @@ 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 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 BodyModificationFragment on BodyModification { + location + description +} ` func (c *Client) FindSceneByID(ctx context.Context, id string, httpRequestOptions ...client.HTTPRequestOption) (*FindSceneByID, error) { @@ -889,3 +910,43 @@ func (c *Client) Me(ctx context.Context, httpRequestOptions ...client.HTTPReques return &res, nil } + +const SubmitSceneDraftQuery = `mutation SubmitSceneDraft ($input: SceneDraftInput!) { + submitSceneDraft(input: $input) { + id + } +} +` + +func (c *Client) SubmitSceneDraft(ctx context.Context, input SceneDraftInput, httpRequestOptions ...client.HTTPRequestOption) (*SubmitSceneDraftPayload, error) { + vars := map[string]interface{}{ + "input": input, + } + + var res SubmitSceneDraftPayload + if err := c.Client.Post(ctx, SubmitSceneDraftQuery, &res, vars, httpRequestOptions...); err != nil { + return nil, err + } + + return &res, nil +} + +const SubmitPerformerDraftQuery = `mutation SubmitPerformerDraft ($input: PerformerDraftInput!) { + submitPerformerDraft(input: $input) { + id + } +} +` + +func (c *Client) SubmitPerformerDraft(ctx context.Context, input PerformerDraftInput, httpRequestOptions ...client.HTTPRequestOption) (*SubmitPerformerDraftPayload, error) { + vars := map[string]interface{}{ + "input": input, + } + + var res SubmitPerformerDraftPayload + if err := c.Client.Post(ctx, SubmitPerformerDraftQuery, &res, vars, httpRequestOptions...); err != nil { + return nil, err + } + + return &res, nil +} diff --git a/pkg/scraper/stashbox/graphql/generated_models.go b/pkg/scraper/stashbox/graphql/generated_models.go index 932acbe6b..bdbdfca6e 100644 --- a/pkg/scraper/stashbox/graphql/generated_models.go +++ b/pkg/scraper/stashbox/graphql/generated_models.go @@ -11,6 +11,10 @@ import ( "github.com/99designs/gqlgen/graphql" ) +type DraftData interface { + IsDraftData() +} + type EditDetails interface { IsEditDetails() } @@ -19,6 +23,18 @@ type EditTarget interface { IsEditTarget() } +type SceneDraftPerformer interface { + IsSceneDraftPerformer() +} + +type SceneDraftStudio interface { + IsSceneDraftStudio() +} + +type SceneDraftTag interface { + IsSceneDraftTag() +} + type ActivateNewUserInput struct { Name string `json:"name"` Email string `json:"email"` @@ -60,6 +76,37 @@ type DateCriterionInput struct { Modifier CriterionModifier `json:"modifier"` } +type Draft struct { + ID string `json:"id"` + Created time.Time `json:"created"` + Expires time.Time `json:"expires"` + Data DraftData `json:"data"` +} + +type DraftEntity struct { + Name string `json:"name"` + ID *string `json:"id"` +} + +func (DraftEntity) IsSceneDraftPerformer() {} +func (DraftEntity) IsSceneDraftStudio() {} +func (DraftEntity) IsSceneDraftTag() {} + +type DraftEntityInput struct { + Name string `json:"name"` + ID *string `json:"id"` +} + +type DraftFingerprint struct { + Hash string `json:"hash"` + Algorithm FingerprintAlgorithm `json:"algorithm"` + Duration int `json:"duration"` +} + +type DraftSubmissionStatus struct { + ID *string `json:"id"` +} + type Edit struct { ID string `json:"id"` User *User `json:"user"` @@ -75,13 +122,15 @@ type Edit struct { // Entity specific options Options *PerformerEditOptions `json:"options"` Comments []*EditComment `json:"comments"` - Votes []*VoteComment `json:"votes"` + Votes []*EditVote `json:"votes"` // = Accepted - Rejected - VoteCount int `json:"vote_count"` - Status VoteStatusEnum `json:"status"` - Applied bool `json:"applied"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + VoteCount int `json:"vote_count"` + // Is the edit considered destructive. + Destructive bool `json:"destructive"` + Status VoteStatusEnum `json:"status"` + Applied bool `json:"applied"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` } type EditComment struct { @@ -123,10 +172,15 @@ type EditInput struct { Comment *string `json:"comment"` } +type EditVote struct { + User *User `json:"user"` + Date time.Time `json:"date"` + Vote VoteTypeEnum `json:"vote"` +} + type EditVoteInput struct { - ID string `json:"id"` - Comment *string `json:"comment"` - Type VoteTypeEnum `json:"type"` + ID string `json:"id"` + Vote VoteTypeEnum `json:"vote"` } type EyeColorCriterionInput struct { @@ -135,24 +189,30 @@ type EyeColorCriterionInput struct { } type Fingerprint 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"` + 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"` + UserSubmitted bool `json:"user_submitted"` } 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"` + UserIds []string `json:"user_ids"` + Hash string `json:"hash"` + Algorithm FingerprintAlgorithm `json:"algorithm"` + Duration int `json:"duration"` + Created time.Time `json:"created"` + // @deprecated(reason: "unused") + Submissions *int `json:"submissions"` + // @deprecated(reason: "unused") + Updated *time.Time `json:"updated"` } type FingerprintInput struct { + // assumes current user if omitted. Ignored for non-modify Users + UserIds []string `json:"user_ids"` Hash string `json:"hash"` Algorithm FingerprintAlgorithm `json:"algorithm"` Duration int `json:"duration"` @@ -166,6 +226,7 @@ type FingerprintQueryInput struct { type FingerprintSubmission struct { SceneID string `json:"scene_id"` Fingerprint *FingerprintInput `json:"fingerprint"` + Unmatch *bool `json:"unmatch"` } type FuzzyDate struct { @@ -238,6 +299,11 @@ type MultiIDCriterionInput struct { Modifier CriterionModifier `json:"modifier"` } +type MultiStringCriterionInput struct { + Value []string `json:"value"` + Modifier CriterionModifier `json:"modifier"` +} + type NewUserInput struct { Email string `json:"email"` InviteKey *string `json:"invite_key"` @@ -272,7 +338,8 @@ type Performer struct { Studios []*PerformerStudio `json:"studios"` } -func (Performer) IsEditTarget() {} +func (Performer) IsEditTarget() {} +func (Performer) IsSceneDraftPerformer() {} type PerformerAppearance struct { Performer *Performer `json:"performer"` @@ -305,12 +372,55 @@ type PerformerCreateInput struct { Tattoos []*BodyModificationInput `json:"tattoos"` Piercings []*BodyModificationInput `json:"piercings"` ImageIds []string `json:"image_ids"` + DraftID *string `json:"draft_id"` } type PerformerDestroyInput struct { ID string `json:"id"` } +type PerformerDraft struct { + Name string `json:"name"` + Aliases *string `json:"aliases"` + Gender *string `json:"gender"` + Birthdate *string `json:"birthdate"` + Urls []string `json:"urls"` + Ethnicity *string `json:"ethnicity"` + Country *string `json:"country"` + EyeColor *string `json:"eye_color"` + HairColor *string `json:"hair_color"` + Height *string `json:"height"` + Measurements *string `json:"measurements"` + BreastType *string `json:"breast_type"` + Tattoos *string `json:"tattoos"` + Piercings *string `json:"piercings"` + CareerStartYear *int `json:"career_start_year"` + CareerEndYear *int `json:"career_end_year"` + Image *Image `json:"image"` +} + +func (PerformerDraft) IsDraftData() {} + +type PerformerDraftInput struct { + Name string `json:"name"` + Aliases *string `json:"aliases"` + Gender *string `json:"gender"` + Birthdate *string `json:"birthdate"` + Urls []string `json:"urls"` + Ethnicity *string `json:"ethnicity"` + Country *string `json:"country"` + EyeColor *string `json:"eye_color"` + HairColor *string `json:"hair_color"` + Height *string `json:"height"` + Measurements *string `json:"measurements"` + BreastType *string `json:"breast_type"` + Tattoos *string `json:"tattoos"` + Piercings *string `json:"piercings"` + CareerStartYear *int `json:"career_start_year"` + CareerEndYear *int `json:"career_end_year"` + Image *graphql.Upload `json:"image"` +} + type PerformerEdit struct { Name *string `json:"name"` Disambiguation *string `json:"disambiguation"` @@ -340,6 +450,7 @@ type PerformerEdit struct { RemovedPiercings []*BodyModification `json:"removed_piercings"` AddedImages []*Image `json:"added_images"` RemovedImages []*Image `json:"removed_images"` + DraftID *string `json:"draft_id"` } func (PerformerEdit) IsEditDetails() {} @@ -363,6 +474,7 @@ type PerformerEditDetailsInput struct { Tattoos []*BodyModificationInput `json:"tattoos"` Piercings []*BodyModificationInput `json:"piercings"` ImageIds []string `json:"image_ids"` + DraftID *string `json:"draft_id"` } type PerformerEditInput struct { @@ -459,6 +571,11 @@ type QueryScenesResultType struct { Scenes []*Scene `json:"scenes"` } +type QuerySitesResultType struct { + Count int `json:"count"` + Sites []*Site `json:"sites"` +} + type QuerySpec struct { Page *int `json:"page"` PerPage *int `json:"per_page"` @@ -514,6 +631,7 @@ type Scene struct { Duration *int `json:"duration"` Director *string `json:"director"` Deleted bool `json:"deleted"` + Edits []*Edit `json:"edits"` } func (Scene) IsEditTarget() {} @@ -536,13 +654,39 @@ type SceneDestroyInput struct { ID string `json:"id"` } +type SceneDraft struct { + Title *string `json:"title"` + Details *string `json:"details"` + URL *URL `json:"url"` + Date *string `json:"date"` + Studio SceneDraftStudio `json:"studio"` + Performers []SceneDraftPerformer `json:"performers"` + Tags []SceneDraftTag `json:"tags"` + Image *Image `json:"image"` + Fingerprints []*DraftFingerprint `json:"fingerprints"` +} + +func (SceneDraft) IsDraftData() {} + +type SceneDraftInput struct { + Title *string `json:"title"` + Details *string `json:"details"` + URL *string `json:"url"` + Date *string `json:"date"` + Studio *DraftEntityInput `json:"studio"` + Performers []*DraftEntityInput `json:"performers"` + Tags []*DraftEntityInput `json:"tags"` + Image *graphql.Upload `json:"image"` + Fingerprints []*FingerprintInput `json:"fingerprints"` +} + type SceneEdit struct { Title *string `json:"title"` Details *string `json:"details"` AddedUrls []*URL `json:"added_urls"` RemovedUrls []*URL `json:"removed_urls"` Date *string `json:"date"` - StudioID *string `json:"studio_id"` + Studio *Studio `json:"studio"` // Added or modified performer appearance entries AddedPerformers []*PerformerAppearance `json:"added_performers"` RemovedPerformers []*PerformerAppearance `json:"removed_performers"` @@ -554,6 +698,7 @@ type SceneEdit struct { RemovedFingerprints []*Fingerprint `json:"removed_fingerprints"` Duration *int `json:"duration"` Director *string `json:"director"` + DraftID *string `json:"draft_id"` } func (SceneEdit) IsEditDetails() {} @@ -567,9 +712,10 @@ type SceneEditDetailsInput struct { Performers []*PerformerAppearanceInput `json:"performers"` TagIds []string `json:"tag_ids"` ImageIds []string `json:"image_ids"` - Fingerprints []*FingerprintEditInput `json:"fingerprints"` Duration *int `json:"duration"` Director *string `json:"director"` + Fingerprints []*FingerprintInput `json:"fingerprints"` + DraftID *string `json:"draft_id"` } type SceneEditInput struct { @@ -599,7 +745,7 @@ type SceneFilterType struct { // 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"` + Fingerprints *MultiStringCriterionInput `json:"fingerprints"` } type SceneUpdateInput struct { @@ -617,6 +763,50 @@ type SceneUpdateInput struct { Director *string `json:"director"` } +type Site struct { + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + URL *string `json:"url"` + Regex *string `json:"regex"` + ValidTypes []ValidSiteTypeEnum `json:"valid_types"` + Icon string `json:"icon"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` +} + +type SiteCreateInput struct { + Name string `json:"name"` + Description *string `json:"description"` + URL *string `json:"url"` + Regex *string `json:"regex"` + ValidTypes []ValidSiteTypeEnum `json:"valid_types"` +} + +type SiteDestroyInput struct { + ID string `json:"id"` +} + +type SiteUpdateInput struct { + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + URL *string `json:"url"` + Regex *string `json:"regex"` + ValidTypes []ValidSiteTypeEnum `json:"valid_types"` +} + +type StashBoxConfig struct { + HostURL string `json:"host_url"` + RequireInvite bool `json:"require_invite"` + RequireActivation bool `json:"require_activation"` + VotePromotionThreshold *int `json:"vote_promotion_threshold"` + VoteApplicationThreshold int `json:"vote_application_threshold"` + VotingPeriod int `json:"voting_period"` + MinDestructiveVotingPeriod int `json:"min_destructive_voting_period"` + VoteCronInterval string `json:"vote_cron_interval"` +} + type StringCriterionInput struct { Value string `json:"value"` Modifier CriterionModifier `json:"modifier"` @@ -632,14 +822,14 @@ type Studio struct { Deleted bool `json:"deleted"` } -func (Studio) IsEditTarget() {} +func (Studio) IsEditTarget() {} +func (Studio) IsSceneDraftStudio() {} type StudioCreateInput struct { - Name string `json:"name"` - Urls []*URLInput `json:"urls"` - ParentID *string `json:"parent_id"` - ChildStudioIds []string `json:"child_studio_ids"` - ImageIds []string `json:"image_ids"` + Name string `json:"name"` + Urls []*URLInput `json:"urls"` + ParentID *string `json:"parent_id"` + ImageIds []string `json:"image_ids"` } type StudioDestroyInput struct { @@ -649,23 +839,20 @@ type StudioDestroyInput struct { type StudioEdit struct { Name *string `json:"name"` // Added and modified URLs - AddedUrls []*URL `json:"added_urls"` - RemovedUrls []*URL `json:"removed_urls"` - Parent *Studio `json:"parent"` - AddedChildStudios []*Studio `json:"added_child_studios"` - RemovedChildStudios []*Studio `json:"removed_child_studios"` - AddedImages []*Image `json:"added_images"` - RemovedImages []*Image `json:"removed_images"` + AddedUrls []*URL `json:"added_urls"` + RemovedUrls []*URL `json:"removed_urls"` + Parent *Studio `json:"parent"` + AddedImages []*Image `json:"added_images"` + RemovedImages []*Image `json:"removed_images"` } func (StudioEdit) IsEditDetails() {} type StudioEditDetailsInput struct { - Name *string `json:"name"` - Urls []*URLInput `json:"urls"` - ParentID *string `json:"parent_id"` - ChildStudioIds []string `json:"child_studio_ids"` - ImageIds []string `json:"image_ids"` + Name *string `json:"name"` + Urls []*URLInput `json:"urls"` + ParentID *string `json:"parent_id"` + ImageIds []string `json:"image_ids"` } type StudioEditInput struct { @@ -686,12 +873,11 @@ type StudioFilterType struct { } type StudioUpdateInput struct { - ID string `json:"id"` - Name *string `json:"name"` - Urls []*URLInput `json:"urls"` - ParentID *string `json:"parent_id"` - ChildStudioIds []string `json:"child_studio_ids"` - ImageIds []string `json:"image_ids"` + ID string `json:"id"` + Name *string `json:"name"` + Urls []*URLInput `json:"urls"` + ParentID *string `json:"parent_id"` + ImageIds []string `json:"image_ids"` } type Tag struct { @@ -704,7 +890,8 @@ type Tag struct { Category *TagCategory `json:"category"` } -func (Tag) IsEditTarget() {} +func (Tag) IsEditTarget() {} +func (Tag) IsSceneDraftTag() {} type TagCategory struct { ID string `json:"id"` @@ -742,11 +929,11 @@ type TagDestroyInput struct { } type TagEdit struct { - Name *string `json:"name"` - Description *string `json:"description"` - AddedAliases []string `json:"added_aliases"` - RemovedAliases []string `json:"removed_aliases"` - CategoryID *string `json:"category_id"` + Name *string `json:"name"` + Description *string `json:"description"` + AddedAliases []string `json:"added_aliases"` + RemovedAliases []string `json:"removed_aliases"` + Category *TagCategory `json:"category"` } func (TagEdit) IsEditDetails() {} @@ -786,11 +973,12 @@ type TagUpdateInput struct { type URL struct { URL string `json:"url"` Type string `json:"type"` + Site *Site `json:"site"` } type URLInput struct { - URL string `json:"url"` - Type string `json:"type"` + URL string `json:"url"` + SiteID string `json:"site_id"` } type User struct { @@ -801,12 +989,11 @@ type User struct { // Should not be visible to other users Email *string `json:"email"` // Should not be visible to other users - APIKey *string `json:"api_key"` - SuccessfulEdits int `json:"successful_edits"` - UnsuccessfulEdits int `json:"unsuccessful_edits"` - SuccessfulVotes int `json:"successful_votes"` - // Votes on unsuccessful edits - UnsuccessfulVotes int `json:"unsuccessful_votes"` + APIKey *string `json:"api_key"` + // Vote counts by type + VoteCount *UserVoteCount `json:"vote_count"` + // Edit counts by status + EditCount *UserEditCount `json:"edit_count"` // Calls to the API from this user over a configurable time period APICalls int `json:"api_calls"` InvitedBy *User `json:"invited_by"` @@ -834,6 +1021,16 @@ type UserDestroyInput struct { ID string `json:"id"` } +type UserEditCount struct { + Accepted int `json:"accepted"` + Rejected int `json:"rejected"` + Pending int `json:"pending"` + ImmediateAccepted int `json:"immediate_accepted"` + ImmediateRejected int `json:"immediate_rejected"` + Failed int `json:"failed"` + Canceled int `json:"canceled"` +} + type UserFilterType struct { // Filter to search user name - assumes like query unless quoted Name *string `json:"name"` @@ -866,19 +1063,21 @@ type UserUpdateInput struct { Email *string `json:"email"` } +type UserVoteCount struct { + Abstain int `json:"abstain"` + Accept int `json:"accept"` + Reject int `json:"reject"` + ImmediateAccept int `json:"immediate_accept"` + ImmediateReject int `json:"immediate_reject"` +} + type Version struct { Hash string `json:"hash"` BuildTime string `json:"build_time"` + BuildType string `json:"build_type"` Version string `json:"version"` } -type VoteComment struct { - User *User `json:"user"` - Date *string `json:"date"` - Comment *string `json:"comment"` - Type *VoteTypeEnum `json:"type"` -} - type BreastTypeEnum string const ( @@ -1435,6 +1634,7 @@ const ( RoleEnumInvite RoleEnum = "INVITE" // May grant and rescind invite tokens and resind invite keys RoleEnumManageInvites RoleEnum = "MANAGE_INVITES" + RoleEnumBot RoleEnum = "BOT" ) var AllRoleEnum = []RoleEnum{ @@ -1445,11 +1645,12 @@ var AllRoleEnum = []RoleEnum{ RoleEnumAdmin, RoleEnumInvite, RoleEnumManageInvites, + RoleEnumBot, } func (e RoleEnum) IsValid() bool { switch e { - case RoleEnumRead, RoleEnumVote, RoleEnumEdit, RoleEnumModify, RoleEnumAdmin, RoleEnumInvite, RoleEnumManageInvites: + case RoleEnumRead, RoleEnumVote, RoleEnumEdit, RoleEnumModify, RoleEnumAdmin, RoleEnumInvite, RoleEnumManageInvites, RoleEnumBot: return true } return false @@ -1605,6 +1806,49 @@ func (e TargetTypeEnum) MarshalGQL(w io.Writer) { fmt.Fprint(w, strconv.Quote(e.String())) } +type ValidSiteTypeEnum string + +const ( + ValidSiteTypeEnumPerformer ValidSiteTypeEnum = "PERFORMER" + ValidSiteTypeEnumScene ValidSiteTypeEnum = "SCENE" + ValidSiteTypeEnumStudio ValidSiteTypeEnum = "STUDIO" +) + +var AllValidSiteTypeEnum = []ValidSiteTypeEnum{ + ValidSiteTypeEnumPerformer, + ValidSiteTypeEnumScene, + ValidSiteTypeEnumStudio, +} + +func (e ValidSiteTypeEnum) IsValid() bool { + switch e { + case ValidSiteTypeEnumPerformer, ValidSiteTypeEnumScene, ValidSiteTypeEnumStudio: + return true + } + return false +} + +func (e ValidSiteTypeEnum) String() string { + return string(e) +} + +func (e *ValidSiteTypeEnum) UnmarshalGQL(v interface{}) error { + str, ok := v.(string) + if !ok { + return fmt.Errorf("enums must be strings") + } + + *e = ValidSiteTypeEnum(str) + if !e.IsValid() { + return fmt.Errorf("%s is not a valid ValidSiteTypeEnum", str) + } + return nil +} + +func (e ValidSiteTypeEnum) MarshalGQL(w io.Writer) { + fmt.Fprint(w, strconv.Quote(e.String())) +} + type VoteStatusEnum string const ( @@ -1613,6 +1857,8 @@ const ( VoteStatusEnumPending VoteStatusEnum = "PENDING" VoteStatusEnumImmediateAccepted VoteStatusEnum = "IMMEDIATE_ACCEPTED" VoteStatusEnumImmediateRejected VoteStatusEnum = "IMMEDIATE_REJECTED" + VoteStatusEnumFailed VoteStatusEnum = "FAILED" + VoteStatusEnumCanceled VoteStatusEnum = "CANCELED" ) var AllVoteStatusEnum = []VoteStatusEnum{ @@ -1621,11 +1867,13 @@ var AllVoteStatusEnum = []VoteStatusEnum{ VoteStatusEnumPending, VoteStatusEnumImmediateAccepted, VoteStatusEnumImmediateRejected, + VoteStatusEnumFailed, + VoteStatusEnumCanceled, } func (e VoteStatusEnum) IsValid() bool { switch e { - case VoteStatusEnumAccepted, VoteStatusEnumRejected, VoteStatusEnumPending, VoteStatusEnumImmediateAccepted, VoteStatusEnumImmediateRejected: + case VoteStatusEnumAccepted, VoteStatusEnumRejected, VoteStatusEnumPending, VoteStatusEnumImmediateAccepted, VoteStatusEnumImmediateRejected, VoteStatusEnumFailed, VoteStatusEnumCanceled: return true } return false @@ -1655,7 +1903,7 @@ func (e VoteStatusEnum) MarshalGQL(w io.Writer) { type VoteTypeEnum string const ( - VoteTypeEnumComment VoteTypeEnum = "COMMENT" + VoteTypeEnumAbstain VoteTypeEnum = "ABSTAIN" VoteTypeEnumAccept VoteTypeEnum = "ACCEPT" VoteTypeEnumReject VoteTypeEnum = "REJECT" // Immediately accepts the edit - bypassing the vote @@ -1665,7 +1913,7 @@ const ( ) var AllVoteTypeEnum = []VoteTypeEnum{ - VoteTypeEnumComment, + VoteTypeEnumAbstain, VoteTypeEnumAccept, VoteTypeEnumReject, VoteTypeEnumImmediateAccept, @@ -1674,7 +1922,7 @@ var AllVoteTypeEnum = []VoteTypeEnum{ func (e VoteTypeEnum) IsValid() bool { switch e { - case VoteTypeEnumComment, VoteTypeEnumAccept, VoteTypeEnumReject, VoteTypeEnumImmediateAccept, VoteTypeEnumImmediateReject: + case VoteTypeEnumAbstain, VoteTypeEnumAccept, VoteTypeEnumReject, VoteTypeEnumImmediateAccept, VoteTypeEnumImmediateReject: return true } return false diff --git a/pkg/scraper/stashbox/stash_box.go b/pkg/scraper/stashbox/stash_box.go index 2ffefa2ff..ff4d9e101 100644 --- a/pkg/scraper/stashbox/stash_box.go +++ b/pkg/scraper/stashbox/stash_box.go @@ -1,14 +1,19 @@ package stashbox import ( + "bytes" "context" + "encoding/json" "fmt" "io" + "mime/multipart" "net/http" + "os" "strconv" "strings" "github.com/Yamashou/gqlgenc/client" + "github.com/Yamashou/gqlgenc/graphqljson" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/match" @@ -757,3 +762,270 @@ func (c Client) FindStashBoxPerformerByName(ctx context.Context, name string) (* func (c Client) GetUser(ctx context.Context) (*graphql.Me, error) { return c.client.Me(ctx) } + +func (c Client) SubmitSceneDraft(ctx context.Context, sceneID int, endpoint string, imagePath string) (*string, error) { + draft := graphql.SceneDraftInput{} + var image *os.File + if err := c.txnManager.WithReadTxn(ctx, func(r models.ReaderRepository) error { + qb := r.Scene() + pqb := r.Performer() + sqb := r.Studio() + + scene, err := qb.Find(sceneID) + if err != nil { + return err + } + + if scene.Title.Valid { + draft.Title = &scene.Title.String + } + if scene.Details.Valid { + draft.Details = &scene.Details.String + } + if len(strings.TrimSpace(scene.URL.String)) > 0 { + url := strings.TrimSpace(scene.URL.String) + draft.URL = &url + } + if scene.Date.Valid { + draft.Date = &scene.Date.String + } + + if scene.StudioID.Valid { + studio, err := sqb.Find(int(scene.StudioID.Int64)) + if err != nil { + return err + } + studioDraft := graphql.DraftEntityInput{ + Name: studio.Name.String, + } + + stashIDs, err := sqb.GetStashIDs(studio.ID) + if err != nil { + return err + } + for _, stashID := range stashIDs { + if stashID.Endpoint == endpoint { + studioDraft.ID = &stashID.StashID + break + } + } + draft.Studio = &studioDraft + } + + fingerprints := []*graphql.FingerprintInput{} + if scene.OSHash.Valid && scene.Duration.Valid { + fingerprint := graphql.FingerprintInput{ + Hash: scene.OSHash.String, + Algorithm: graphql.FingerprintAlgorithmOshash, + Duration: int(scene.Duration.Float64), + } + fingerprints = append(fingerprints, &fingerprint) + } + + if scene.Checksum.Valid && scene.Duration.Valid { + fingerprint := graphql.FingerprintInput{ + Hash: scene.Checksum.String, + Algorithm: graphql.FingerprintAlgorithmMd5, + Duration: int(scene.Duration.Float64), + } + fingerprints = append(fingerprints, &fingerprint) + } + + if scene.Phash.Valid && scene.Duration.Valid { + fingerprint := graphql.FingerprintInput{ + Hash: utils.PhashToString(scene.Phash.Int64), + Algorithm: graphql.FingerprintAlgorithmPhash, + Duration: int(scene.Duration.Float64), + } + fingerprints = append(fingerprints, &fingerprint) + } + draft.Fingerprints = fingerprints + + scenePerformers, err := pqb.FindBySceneID(sceneID) + if err != nil { + return err + } + + performers := []*graphql.DraftEntityInput{} + for _, p := range scenePerformers { + performerDraft := graphql.DraftEntityInput{ + Name: p.Name.String, + } + + stashIDs, err := pqb.GetStashIDs(p.ID) + if err != nil { + return err + } + + for _, stashID := range stashIDs { + if stashID.Endpoint == endpoint { + performerDraft.ID = &stashID.StashID + break + } + } + + performers = append(performers, &performerDraft) + } + draft.Performers = performers + + var tags []*graphql.DraftEntityInput + sceneTags, err := r.Tag().FindBySceneID(scene.ID) + if err != nil { + return err + } + for _, tag := range sceneTags { + tags = append(tags, &graphql.DraftEntityInput{Name: tag.Name}) + } + draft.Tags = tags + + exists, _ := utils.FileExists(imagePath) + if exists { + file, err := os.Open(imagePath) + if err == nil { + image = file + } + } + + return nil + }); err != nil { + return nil, err + } + + var id *string + var ret graphql.SubmitSceneDraftPayload + err := c.submitDraft(ctx, graphql.SubmitSceneDraftQuery, draft, image, &ret) + id = ret.SubmitSceneDraft.ID + + return id, err +} + +func (c Client) SubmitPerformerDraft(ctx context.Context, performer *models.Performer, endpoint string) (*string, error) { + draft := graphql.PerformerDraftInput{} + var image io.Reader + if err := c.txnManager.WithReadTxn(ctx, func(r models.ReaderRepository) error { + pqb := r.Performer() + img, _ := pqb.GetImage(performer.ID) + if img != nil { + image = bytes.NewReader(img) + } + + if performer.Name.Valid { + draft.Name = performer.Name.String + } + if performer.Birthdate.Valid { + draft.Birthdate = &performer.Birthdate.String + } + if performer.Country.Valid { + draft.Country = &performer.Country.String + } + if performer.Ethnicity.Valid { + draft.Ethnicity = &performer.Ethnicity.String + } + if performer.EyeColor.Valid { + draft.EyeColor = &performer.EyeColor.String + } + if performer.FakeTits.Valid { + draft.BreastType = &performer.FakeTits.String + } + if performer.Gender.Valid { + draft.Gender = &performer.Gender.String + } + if performer.HairColor.Valid { + draft.HairColor = &performer.HairColor.String + } + if performer.Height.Valid { + draft.Height = &performer.Height.String + } + if performer.Measurements.Valid { + draft.Measurements = &performer.Measurements.String + } + if performer.Piercings.Valid { + draft.Piercings = &performer.Piercings.String + } + if performer.Tattoos.Valid { + draft.Tattoos = &performer.Tattoos.String + } + if performer.Aliases.Valid { + draft.Aliases = &performer.Aliases.String + } + + var urls []string + if len(strings.TrimSpace(performer.Twitter.String)) > 0 { + urls = append(urls, "https://twitter.com/"+strings.TrimSpace(performer.Twitter.String)) + } + if len(strings.TrimSpace(performer.Instagram.String)) > 0 { + urls = append(urls, "https://instagram.com/"+strings.TrimSpace(performer.Instagram.String)) + } + if len(strings.TrimSpace(performer.URL.String)) > 0 { + urls = append(urls, strings.TrimSpace(performer.URL.String)) + } + if len(urls) > 0 { + draft.Urls = urls + } + + return nil + }); err != nil { + return nil, err + } + + var id *string + var ret graphql.SubmitPerformerDraftPayload + err := c.submitDraft(ctx, graphql.SubmitPerformerDraftQuery, draft, image, &ret) + id = ret.SubmitPerformerDraft.ID + + return id, err +} + +func (c *Client) submitDraft(ctx context.Context, query string, input interface{}, image io.Reader, ret interface{}) error { + vars := map[string]interface{}{ + "input": input, + } + + r := &client.Request{ + Query: query, + Variables: vars, + OperationName: "", + } + + requestBody, err := json.Marshal(r) + if err != nil { + return fmt.Errorf("encode: %w", err) + } + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + if err := writer.WriteField("operations", string(requestBody)); err != nil { + return err + } + + if image != nil { + if err := writer.WriteField("map", "{ \"0\": [\"variables.input.image\"] }"); err != nil { + return err + } + part, _ := writer.CreateFormFile("0", "draft") + if _, err := io.Copy(part, image); err != nil { + return err + } + } else if err := writer.WriteField("map", "{}"); err != nil { + return err + } + + writer.Close() + + req, _ := http.NewRequestWithContext(ctx, "POST", c.box.Endpoint, body) + req.Header.Add("Content-Type", writer.FormDataContentType()) + req.Header.Set("ApiKey", c.box.APIKey) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if err := graphqljson.Unmarshal(resp.Body, ret); err != nil { + return err + } + + return err +} diff --git a/ui/v2.5/src/components/Changelog/versions/v0130.md b/ui/v2.5/src/components/Changelog/versions/v0130.md index 38bf5ca21..5dc331aaa 100644 --- a/ui/v2.5/src/components/Changelog/versions/v0130.md +++ b/ui/v2.5/src/components/Changelog/versions/v0130.md @@ -1,7 +1,10 @@ +### ✨ New Features +* Added support for submitting performer/scene drafts to stash-box. ([#2234](https://github.com/stashapp/stash/pull/2234)) + ### 🎨 Improvements * Made Performer page consistent with Studio and Tag pages. ([#2200](https://github.com/stashapp/stash/pull/2200)) -* Add gender icons to performers. ([#2179](https://github.com/stashapp/stash/pull/2179)) -* Add button to test credentials when adding/editing stash-box endpoints. ([#2173](https://github.com/stashapp/stash/pull/2173)) +* Added gender icons to performers. ([#2179](https://github.com/stashapp/stash/pull/2179)) +* Added button to test credentials when adding/editing stash-box endpoints. ([#2173](https://github.com/stashapp/stash/pull/2173)) * Show counts on list tabs in Performer, Studio and Tag pages. ([#2169](https://github.com/stashapp/stash/pull/2169)) ### 🐛 Bug fixes diff --git a/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx b/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx new file mode 100644 index 000000000..3b9ecc7d3 --- /dev/null +++ b/ui/v2.5/src/components/Dialogs/SubmitDraft.tsx @@ -0,0 +1,118 @@ +import React, { useState } from "react"; +import { useMutation, DocumentNode } from "@apollo/client"; +import { Button, Form } from "react-bootstrap"; +import * as GQL from "src/core/generated-graphql"; +import { Modal } from "src/components/Shared"; +import { getStashboxBase } from "src/utils"; + +interface IProps { + show: boolean; + entity: { name?: string | null; id: string; title?: string | null }; + boxes: Pick[]; + query: DocumentNode; + onHide: () => void; +} + +type Variables = + | GQL.SubmitStashBoxSceneDraftMutationVariables + | GQL.SubmitStashBoxPerformerDraftMutationVariables; +type Query = + | GQL.SubmitStashBoxSceneDraftMutation + | GQL.SubmitStashBoxPerformerDraftMutation; + +const isSceneDraft = ( + query: Query | null +): query is GQL.SubmitStashBoxSceneDraftMutation => + (query as GQL.SubmitStashBoxSceneDraftMutation).submitStashBoxSceneDraft !== + undefined; + +const getResponseId = (query: Query | null) => + isSceneDraft(query) + ? query.submitStashBoxSceneDraft + : query?.submitStashBoxPerformerDraft; + +export const SubmitStashBoxDraft: React.FC = ({ + show, + boxes, + entity, + query, + onHide, +}) => { + const [submit, { data, error, loading }] = useMutation( + query + ); + const [selectedBox, setSelectedBox] = useState(0); + + const handleSubmit = () => { + submit({ + variables: { + input: { + id: entity.id, + stash_box_index: selectedBox, + }, + }, + }); + }; + + const handleSelectBox = (e: React.ChangeEvent) => + setSelectedBox(Number.parseInt(e.currentTarget.value) ?? 0); + + console.log(data); + + return ( + + {data === undefined ? ( + <> + + + Selected Stash-Box endpoint: + + + {boxes.map((box, i) => ( + + ))} + + + + + ) : ( + <> +
Submission successful
+ + + )} + {error !== undefined && ( + <> +
Submission failed
+
{error.message}
+ + )} +
+ ); +}; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx index 8f231db4a..8f7d16a8f 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx @@ -28,6 +28,7 @@ import { PerformerGalleriesPanel } from "./PerformerGalleriesPanel"; import { PerformerMoviesPanel } from "./PerformerMoviesPanel"; import { PerformerImagesPanel } from "./PerformerImagesPanel"; import { PerformerEditPanel } from "./PerformerEditPanel"; +import { PerformerSubmitButton } from "./PerformerSubmitButton"; import GenderIcon from "../GenderIcon"; interface IProps { @@ -165,8 +166,13 @@ const PerformerPage: React.FC = ({ performer }) => { isEditing={false} onSave={() => {}} onImageChange={() => {}} - classNames="mb-4" - /> + classNames="mb-2" + customButtons={ +
+ +
+ } + > = ({