Add support for submitting performer/scene drafts to stash-box (#2234)

* Add support for submitting performer/scene drafts to stash-box

Co-authored-by: Kermie <kermie@isinthe.house>
This commit is contained in:
InfiniteTF 2022-02-01 05:06:51 +01:00 committed by GitHub
parent c5cd0e1c9c
commit a3c20ce8da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1235 additions and 348 deletions

View file

@ -5,3 +5,11 @@ mutation SubmitStashBoxFingerprints($input: StashBoxFingerprintSubmissionInput!)
mutation StashBoxBatchPerformerTag($input: StashBoxBatchPerformerTagInput!) { mutation StashBoxBatchPerformerTag($input: StashBoxBatchPerformerTagInput!) {
stashBoxBatchPerformerTag(input: $input) stashBoxBatchPerformerTag(input: $input)
} }
mutation SubmitStashBoxSceneDraft($input: StashBoxDraftSubmissionInput!) {
submitStashBoxSceneDraft(input: $input)
}
mutation SubmitStashBoxPerformerDraft($input: StashBoxDraftSubmissionInput!) {
submitStashBoxPerformerDraft(input: $input)
}

View file

@ -282,6 +282,11 @@ type Mutation {
"""Submit fingerprints to stash-box instance""" """Submit fingerprints to stash-box instance"""
submitStashBoxFingerprints(input: StashBoxFingerprintSubmissionInput!): Boolean! 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""" """Backup the database. Optionally returns a link to download the database file"""
backupDatabase(input: BackupDatabaseInput!): String backupDatabase(input: BackupDatabaseInput!): String

View file

@ -24,3 +24,8 @@ input StashBoxFingerprintSubmissionInput {
scene_ids: [String!]! scene_ids: [String!]!
stash_box_index: Int! stash_box_index: Int!
} }
input StashBoxDraftSubmissionInput {
id: String!
stash_box_index: Int!
}

View file

@ -162,3 +162,15 @@ query Me {
name name
} }
} }
mutation SubmitSceneDraft($input: SceneDraftInput!) {
submitSceneDraft(input: $input) {
id
}
}
mutation SubmitPerformerDraft($input: PerformerDraftInput!) {
submitPerformerDraft(input: $input) {
id
}
}

View file

@ -27,3 +27,62 @@ func (r *mutationResolver) StashBoxBatchPerformerTag(ctx context.Context, input
jobID := manager.GetInstance().StashBoxBatchPerformerTag(ctx, input) jobID := manager.GetInstance().StashBoxBatchPerformerTag(ctx, input)
return strconv.Itoa(jobID), nil 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
}

View file

@ -31,6 +31,8 @@ type Query struct {
FindScenesByFingerprints []*Scene "json:\"findScenesByFingerprints\" graphql:\"findScenesByFingerprints\"" FindScenesByFingerprints []*Scene "json:\"findScenesByFingerprints\" graphql:\"findScenesByFingerprints\""
FindScenesByFullFingerprints []*Scene "json:\"findScenesByFullFingerprints\" graphql:\"findScenesByFullFingerprints\"" FindScenesByFullFingerprints []*Scene "json:\"findScenesByFullFingerprints\" graphql:\"findScenesByFullFingerprints\""
QueryScenes QueryScenesResultType "json:\"queryScenes\" graphql:\"queryScenes\"" QueryScenes QueryScenesResultType "json:\"queryScenes\" graphql:\"queryScenes\""
FindSite *Site "json:\"findSite\" graphql:\"findSite\""
QuerySites QuerySitesResultType "json:\"querySites\" graphql:\"querySites\""
FindEdit *Edit "json:\"findEdit\" graphql:\"findEdit\"" FindEdit *Edit "json:\"findEdit\" graphql:\"findEdit\""
QueryEdits QueryEditsResultType "json:\"queryEdits\" graphql:\"queryEdits\"" QueryEdits QueryEditsResultType "json:\"queryEdits\" graphql:\"queryEdits\""
FindUser *User "json:\"findUser\" graphql:\"findUser\"" FindUser *User "json:\"findUser\" graphql:\"findUser\""
@ -38,48 +40,57 @@ type Query struct {
Me *User "json:\"me\" graphql:\"me\"" Me *User "json:\"me\" graphql:\"me\""
SearchPerformer []*Performer "json:\"searchPerformer\" graphql:\"searchPerformer\"" SearchPerformer []*Performer "json:\"searchPerformer\" graphql:\"searchPerformer\""
SearchScene []*Scene "json:\"searchScene\" graphql:\"searchScene\"" SearchScene []*Scene "json:\"searchScene\" graphql:\"searchScene\""
FindDraft *Draft "json:\"findDraft\" graphql:\"findDraft\""
FindDrafts []*Draft "json:\"findDrafts\" graphql:\"findDrafts\""
Version Version "json:\"version\" graphql:\"version\"" Version Version "json:\"version\" graphql:\"version\""
GetConfig StashBoxConfig "json:\"getConfig\" graphql:\"getConfig\""
} }
type Mutation struct { type Mutation struct {
SceneCreate *Scene "json:\"sceneCreate\" graphql:\"sceneCreate\"" SceneCreate *Scene "json:\"sceneCreate\" graphql:\"sceneCreate\""
SceneUpdate *Scene "json:\"sceneUpdate\" graphql:\"sceneUpdate\"" SceneUpdate *Scene "json:\"sceneUpdate\" graphql:\"sceneUpdate\""
SceneDestroy bool "json:\"sceneDestroy\" graphql:\"sceneDestroy\"" SceneDestroy bool "json:\"sceneDestroy\" graphql:\"sceneDestroy\""
PerformerCreate *Performer "json:\"performerCreate\" graphql:\"performerCreate\"" PerformerCreate *Performer "json:\"performerCreate\" graphql:\"performerCreate\""
PerformerUpdate *Performer "json:\"performerUpdate\" graphql:\"performerUpdate\"" PerformerUpdate *Performer "json:\"performerUpdate\" graphql:\"performerUpdate\""
PerformerDestroy bool "json:\"performerDestroy\" graphql:\"performerDestroy\"" PerformerDestroy bool "json:\"performerDestroy\" graphql:\"performerDestroy\""
StudioCreate *Studio "json:\"studioCreate\" graphql:\"studioCreate\"" StudioCreate *Studio "json:\"studioCreate\" graphql:\"studioCreate\""
StudioUpdate *Studio "json:\"studioUpdate\" graphql:\"studioUpdate\"" StudioUpdate *Studio "json:\"studioUpdate\" graphql:\"studioUpdate\""
StudioDestroy bool "json:\"studioDestroy\" graphql:\"studioDestroy\"" StudioDestroy bool "json:\"studioDestroy\" graphql:\"studioDestroy\""
TagCreate *Tag "json:\"tagCreate\" graphql:\"tagCreate\"" TagCreate *Tag "json:\"tagCreate\" graphql:\"tagCreate\""
TagUpdate *Tag "json:\"tagUpdate\" graphql:\"tagUpdate\"" TagUpdate *Tag "json:\"tagUpdate\" graphql:\"tagUpdate\""
TagDestroy bool "json:\"tagDestroy\" graphql:\"tagDestroy\"" TagDestroy bool "json:\"tagDestroy\" graphql:\"tagDestroy\""
UserCreate *User "json:\"userCreate\" graphql:\"userCreate\"" UserCreate *User "json:\"userCreate\" graphql:\"userCreate\""
UserUpdate *User "json:\"userUpdate\" graphql:\"userUpdate\"" UserUpdate *User "json:\"userUpdate\" graphql:\"userUpdate\""
UserDestroy bool "json:\"userDestroy\" graphql:\"userDestroy\"" UserDestroy bool "json:\"userDestroy\" graphql:\"userDestroy\""
ImageCreate *Image "json:\"imageCreate\" graphql:\"imageCreate\"" ImageCreate *Image "json:\"imageCreate\" graphql:\"imageCreate\""
ImageDestroy bool "json:\"imageDestroy\" graphql:\"imageDestroy\"" ImageDestroy bool "json:\"imageDestroy\" graphql:\"imageDestroy\""
NewUser *string "json:\"newUser\" graphql:\"newUser\"" NewUser *string "json:\"newUser\" graphql:\"newUser\""
ActivateNewUser *User "json:\"activateNewUser\" graphql:\"activateNewUser\"" ActivateNewUser *User "json:\"activateNewUser\" graphql:\"activateNewUser\""
GenerateInviteCode string "json:\"generateInviteCode\" graphql:\"generateInviteCode\"" GenerateInviteCode *string "json:\"generateInviteCode\" graphql:\"generateInviteCode\""
RescindInviteCode bool "json:\"rescindInviteCode\" graphql:\"rescindInviteCode\"" RescindInviteCode bool "json:\"rescindInviteCode\" graphql:\"rescindInviteCode\""
GrantInvite int "json:\"grantInvite\" graphql:\"grantInvite\"" GrantInvite int "json:\"grantInvite\" graphql:\"grantInvite\""
RevokeInvite int "json:\"revokeInvite\" graphql:\"revokeInvite\"" RevokeInvite int "json:\"revokeInvite\" graphql:\"revokeInvite\""
TagCategoryCreate *TagCategory "json:\"tagCategoryCreate\" graphql:\"tagCategoryCreate\"" TagCategoryCreate *TagCategory "json:\"tagCategoryCreate\" graphql:\"tagCategoryCreate\""
TagCategoryUpdate *TagCategory "json:\"tagCategoryUpdate\" graphql:\"tagCategoryUpdate\"" TagCategoryUpdate *TagCategory "json:\"tagCategoryUpdate\" graphql:\"tagCategoryUpdate\""
TagCategoryDestroy bool "json:\"tagCategoryDestroy\" graphql:\"tagCategoryDestroy\"" TagCategoryDestroy bool "json:\"tagCategoryDestroy\" graphql:\"tagCategoryDestroy\""
RegenerateAPIKey string "json:\"regenerateAPIKey\" graphql:\"regenerateAPIKey\"" SiteCreate *Site "json:\"siteCreate\" graphql:\"siteCreate\""
ResetPassword bool "json:\"resetPassword\" graphql:\"resetPassword\"" SiteUpdate *Site "json:\"siteUpdate\" graphql:\"siteUpdate\""
ChangePassword bool "json:\"changePassword\" graphql:\"changePassword\"" SiteDestroy bool "json:\"siteDestroy\" graphql:\"siteDestroy\""
SceneEdit Edit "json:\"sceneEdit\" graphql:\"sceneEdit\"" RegenerateAPIKey string "json:\"regenerateAPIKey\" graphql:\"regenerateAPIKey\""
PerformerEdit Edit "json:\"performerEdit\" graphql:\"performerEdit\"" ResetPassword bool "json:\"resetPassword\" graphql:\"resetPassword\""
StudioEdit Edit "json:\"studioEdit\" graphql:\"studioEdit\"" ChangePassword bool "json:\"changePassword\" graphql:\"changePassword\""
TagEdit Edit "json:\"tagEdit\" graphql:\"tagEdit\"" SceneEdit Edit "json:\"sceneEdit\" graphql:\"sceneEdit\""
EditVote Edit "json:\"editVote\" graphql:\"editVote\"" PerformerEdit Edit "json:\"performerEdit\" graphql:\"performerEdit\""
EditComment Edit "json:\"editComment\" graphql:\"editComment\"" StudioEdit Edit "json:\"studioEdit\" graphql:\"studioEdit\""
ApplyEdit Edit "json:\"applyEdit\" graphql:\"applyEdit\"" TagEdit Edit "json:\"tagEdit\" graphql:\"tagEdit\""
CancelEdit Edit "json:\"cancelEdit\" graphql:\"cancelEdit\"" EditVote Edit "json:\"editVote\" graphql:\"editVote\""
SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\"" 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 { type URLFragment struct {
URL string "json:\"url\" graphql:\"url\"" URL string "json:\"url\" graphql:\"url\""
@ -185,12 +196,26 @@ type Me struct {
Name string "json:\"name\" graphql:\"name\"" Name string "json:\"name\" graphql:\"name\""
} "json:\"me\" graphql:\"me\"" } "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!) { const FindSceneByFingerprintQuery = `query FindSceneByFingerprint ($fingerprint: FingerprintQueryInput!) {
findSceneByFingerprint(fingerprint: $fingerprint) { findSceneByFingerprint(fingerprint: $fingerprint) {
... SceneFragment ... SceneFragment
} }
} }
fragment BodyModificationFragment on BodyModification {
location
description
}
fragment FingerprintFragment on Fingerprint { fragment FingerprintFragment on Fingerprint {
algorithm algorithm
hash hash
@ -200,27 +225,9 @@ fragment URLFragment on URL {
url url
type type
} }
fragment ImageFragment on Image { fragment TagFragment on Tag {
id
url
width
height
}
fragment StudioFragment on Studio {
name name
id id
urls {
... URLFragment
}
images {
... ImageFragment
}
}
fragment PerformerAppearanceFragment on PerformerAppearance {
as
performer {
... PerformerFragment
}
} }
fragment PerformerFragment on Performer { fragment PerformerFragment on Performer {
id id
@ -256,9 +263,15 @@ fragment PerformerFragment on Performer {
... BodyModificationFragment ... BodyModificationFragment
} }
} }
fragment BodyModificationFragment on BodyModification { fragment FuzzyDateFragment on FuzzyDate {
location date
description accuracy
}
fragment MeasurementsFragment on Measurements {
band_size
cup_size
waist
hip
} }
fragment SceneFragment on Scene { fragment SceneFragment on Scene {
id id
@ -285,19 +298,27 @@ fragment SceneFragment on Scene {
... FingerprintFragment ... FingerprintFragment
} }
} }
fragment TagFragment on Tag { fragment ImageFragment on Image {
id
url
width
height
}
fragment StudioFragment on Studio {
name name
id id
urls {
... URLFragment
}
images {
... ImageFragment
}
} }
fragment FuzzyDateFragment on FuzzyDate { fragment PerformerAppearanceFragment on PerformerAppearance {
date as
accuracy performer {
} ... PerformerFragment
fragment MeasurementsFragment on Measurements { }
band_size
cup_size
waist
hip
} }
` `
@ -367,10 +388,36 @@ fragment FuzzyDateFragment on FuzzyDate {
date date
accuracy accuracy
} }
fragment MeasurementsFragment on Measurements {
band_size
cup_size
waist
hip
}
fragment BodyModificationFragment on BodyModification { fragment BodyModificationFragment on BodyModification {
location location
description 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 { fragment FingerprintFragment on Fingerprint {
algorithm algorithm
hash hash
@ -401,32 +448,6 @@ fragment SceneFragment on Scene {
... FingerprintFragment ... 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) { 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 ... SceneFragment
} }
} }
fragment FingerprintFragment on Fingerprint {
algorithm
hash
duration
}
fragment URLFragment on URL { fragment URLFragment on URL {
url url
type type
@ -457,9 +483,15 @@ fragment ImageFragment on Image {
width width
height height
} }
fragment FuzzyDateFragment on FuzzyDate { fragment TagFragment on Tag {
date name
accuracy id
}
fragment PerformerAppearanceFragment on PerformerAppearance {
as
performer {
... PerformerFragment
}
} }
fragment MeasurementsFragment on Measurements { fragment MeasurementsFragment on Measurements {
band_size band_size
@ -506,16 +538,6 @@ fragment StudioFragment on Studio {
... ImageFragment ... ImageFragment
} }
} }
fragment TagFragment on Tag {
name
id
}
fragment PerformerAppearanceFragment on PerformerAppearance {
as
performer {
... PerformerFragment
}
}
fragment PerformerFragment on Performer { fragment PerformerFragment on Performer {
id id
name name
@ -550,10 +572,9 @@ fragment PerformerFragment on Performer {
... BodyModificationFragment ... BodyModificationFragment
} }
} }
fragment FingerprintFragment on Fingerprint { fragment FuzzyDateFragment on FuzzyDate {
algorithm date
hash accuracy
duration
} }
` `
@ -653,6 +674,30 @@ const FindPerformerByIDQuery = `query FindPerformerByID ($id: ID!) {
... PerformerFragment ... 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 { fragment PerformerFragment on Performer {
id id
name name
@ -687,30 +732,6 @@ fragment PerformerFragment on Performer {
... BodyModificationFragment ... 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) { 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 ... SceneFragment
} }
} }
fragment ImageFragment on Image {
id
url
width
height
}
fragment TagFragment on Tag { fragment TagFragment on Tag {
name name
id id
} }
fragment PerformerAppearanceFragment on PerformerAppearance {
as
performer {
... PerformerFragment
}
}
fragment FuzzyDateFragment on FuzzyDate { fragment FuzzyDateFragment on FuzzyDate {
date date
accuracy accuracy
@ -762,58 +771,6 @@ fragment FingerprintFragment on Fingerprint {
hash hash
duration 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 { fragment SceneFragment on Scene {
id id
title title
@ -839,6 +796,70 @@ fragment SceneFragment on Scene {
... FingerprintFragment ... 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) { 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 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
}

View file

@ -11,6 +11,10 @@ import (
"github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql"
) )
type DraftData interface {
IsDraftData()
}
type EditDetails interface { type EditDetails interface {
IsEditDetails() IsEditDetails()
} }
@ -19,6 +23,18 @@ type EditTarget interface {
IsEditTarget() IsEditTarget()
} }
type SceneDraftPerformer interface {
IsSceneDraftPerformer()
}
type SceneDraftStudio interface {
IsSceneDraftStudio()
}
type SceneDraftTag interface {
IsSceneDraftTag()
}
type ActivateNewUserInput struct { type ActivateNewUserInput struct {
Name string `json:"name"` Name string `json:"name"`
Email string `json:"email"` Email string `json:"email"`
@ -60,6 +76,37 @@ type DateCriterionInput struct {
Modifier CriterionModifier `json:"modifier"` 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 { type Edit struct {
ID string `json:"id"` ID string `json:"id"`
User *User `json:"user"` User *User `json:"user"`
@ -75,13 +122,15 @@ type Edit struct {
// Entity specific options // Entity specific options
Options *PerformerEditOptions `json:"options"` Options *PerformerEditOptions `json:"options"`
Comments []*EditComment `json:"comments"` Comments []*EditComment `json:"comments"`
Votes []*VoteComment `json:"votes"` Votes []*EditVote `json:"votes"`
// = Accepted - Rejected // = Accepted - Rejected
VoteCount int `json:"vote_count"` VoteCount int `json:"vote_count"`
Status VoteStatusEnum `json:"status"` // Is the edit considered destructive.
Applied bool `json:"applied"` Destructive bool `json:"destructive"`
Created time.Time `json:"created"` Status VoteStatusEnum `json:"status"`
Updated time.Time `json:"updated"` Applied bool `json:"applied"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
} }
type EditComment struct { type EditComment struct {
@ -123,10 +172,15 @@ type EditInput struct {
Comment *string `json:"comment"` Comment *string `json:"comment"`
} }
type EditVote struct {
User *User `json:"user"`
Date time.Time `json:"date"`
Vote VoteTypeEnum `json:"vote"`
}
type EditVoteInput struct { type EditVoteInput struct {
ID string `json:"id"` ID string `json:"id"`
Comment *string `json:"comment"` Vote VoteTypeEnum `json:"vote"`
Type VoteTypeEnum `json:"type"`
} }
type EyeColorCriterionInput struct { type EyeColorCriterionInput struct {
@ -135,24 +189,30 @@ 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"` Submissions int `json:"submissions"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
Updated time.Time `json:"updated"` Updated time.Time `json:"updated"`
UserSubmitted bool `json:"user_submitted"`
} }
type FingerprintEditInput struct { type FingerprintEditInput struct {
Hash string `json:"hash"` UserIds []string `json:"user_ids"`
Algorithm FingerprintAlgorithm `json:"algorithm"` Hash string `json:"hash"`
Duration int `json:"duration"` Algorithm FingerprintAlgorithm `json:"algorithm"`
Submissions int `json:"submissions"` Duration int `json:"duration"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
Updated time.Time `json:"updated"` // @deprecated(reason: "unused")
Submissions *int `json:"submissions"`
// @deprecated(reason: "unused")
Updated *time.Time `json:"updated"`
} }
type FingerprintInput struct { type FingerprintInput struct {
// assumes current user if omitted. Ignored for non-modify Users
UserIds []string `json:"user_ids"`
Hash string `json:"hash"` Hash string `json:"hash"`
Algorithm FingerprintAlgorithm `json:"algorithm"` Algorithm FingerprintAlgorithm `json:"algorithm"`
Duration int `json:"duration"` Duration int `json:"duration"`
@ -166,6 +226,7 @@ type FingerprintQueryInput struct {
type FingerprintSubmission struct { type FingerprintSubmission struct {
SceneID string `json:"scene_id"` SceneID string `json:"scene_id"`
Fingerprint *FingerprintInput `json:"fingerprint"` Fingerprint *FingerprintInput `json:"fingerprint"`
Unmatch *bool `json:"unmatch"`
} }
type FuzzyDate struct { type FuzzyDate struct {
@ -238,6 +299,11 @@ type MultiIDCriterionInput struct {
Modifier CriterionModifier `json:"modifier"` Modifier CriterionModifier `json:"modifier"`
} }
type MultiStringCriterionInput struct {
Value []string `json:"value"`
Modifier CriterionModifier `json:"modifier"`
}
type NewUserInput struct { type NewUserInput struct {
Email string `json:"email"` Email string `json:"email"`
InviteKey *string `json:"invite_key"` InviteKey *string `json:"invite_key"`
@ -272,7 +338,8 @@ type Performer struct {
Studios []*PerformerStudio `json:"studios"` Studios []*PerformerStudio `json:"studios"`
} }
func (Performer) IsEditTarget() {} func (Performer) IsEditTarget() {}
func (Performer) IsSceneDraftPerformer() {}
type PerformerAppearance struct { type PerformerAppearance struct {
Performer *Performer `json:"performer"` Performer *Performer `json:"performer"`
@ -305,12 +372,55 @@ type PerformerCreateInput struct {
Tattoos []*BodyModificationInput `json:"tattoos"` Tattoos []*BodyModificationInput `json:"tattoos"`
Piercings []*BodyModificationInput `json:"piercings"` Piercings []*BodyModificationInput `json:"piercings"`
ImageIds []string `json:"image_ids"` ImageIds []string `json:"image_ids"`
DraftID *string `json:"draft_id"`
} }
type PerformerDestroyInput struct { type PerformerDestroyInput struct {
ID string `json:"id"` 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 { type PerformerEdit struct {
Name *string `json:"name"` Name *string `json:"name"`
Disambiguation *string `json:"disambiguation"` Disambiguation *string `json:"disambiguation"`
@ -340,6 +450,7 @@ type PerformerEdit struct {
RemovedPiercings []*BodyModification `json:"removed_piercings"` RemovedPiercings []*BodyModification `json:"removed_piercings"`
AddedImages []*Image `json:"added_images"` AddedImages []*Image `json:"added_images"`
RemovedImages []*Image `json:"removed_images"` RemovedImages []*Image `json:"removed_images"`
DraftID *string `json:"draft_id"`
} }
func (PerformerEdit) IsEditDetails() {} func (PerformerEdit) IsEditDetails() {}
@ -363,6 +474,7 @@ type PerformerEditDetailsInput struct {
Tattoos []*BodyModificationInput `json:"tattoos"` Tattoos []*BodyModificationInput `json:"tattoos"`
Piercings []*BodyModificationInput `json:"piercings"` Piercings []*BodyModificationInput `json:"piercings"`
ImageIds []string `json:"image_ids"` ImageIds []string `json:"image_ids"`
DraftID *string `json:"draft_id"`
} }
type PerformerEditInput struct { type PerformerEditInput struct {
@ -459,6 +571,11 @@ type QueryScenesResultType struct {
Scenes []*Scene `json:"scenes"` Scenes []*Scene `json:"scenes"`
} }
type QuerySitesResultType struct {
Count int `json:"count"`
Sites []*Site `json:"sites"`
}
type QuerySpec struct { type QuerySpec struct {
Page *int `json:"page"` Page *int `json:"page"`
PerPage *int `json:"per_page"` PerPage *int `json:"per_page"`
@ -514,6 +631,7 @@ type Scene struct {
Duration *int `json:"duration"` Duration *int `json:"duration"`
Director *string `json:"director"` Director *string `json:"director"`
Deleted bool `json:"deleted"` Deleted bool `json:"deleted"`
Edits []*Edit `json:"edits"`
} }
func (Scene) IsEditTarget() {} func (Scene) IsEditTarget() {}
@ -536,13 +654,39 @@ type SceneDestroyInput struct {
ID string `json:"id"` 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 { type SceneEdit struct {
Title *string `json:"title"` Title *string `json:"title"`
Details *string `json:"details"` Details *string `json:"details"`
AddedUrls []*URL `json:"added_urls"` AddedUrls []*URL `json:"added_urls"`
RemovedUrls []*URL `json:"removed_urls"` RemovedUrls []*URL `json:"removed_urls"`
Date *string `json:"date"` Date *string `json:"date"`
StudioID *string `json:"studio_id"` Studio *Studio `json:"studio"`
// Added or modified performer appearance entries // Added or modified performer appearance entries
AddedPerformers []*PerformerAppearance `json:"added_performers"` AddedPerformers []*PerformerAppearance `json:"added_performers"`
RemovedPerformers []*PerformerAppearance `json:"removed_performers"` RemovedPerformers []*PerformerAppearance `json:"removed_performers"`
@ -554,6 +698,7 @@ type SceneEdit struct {
RemovedFingerprints []*Fingerprint `json:"removed_fingerprints"` RemovedFingerprints []*Fingerprint `json:"removed_fingerprints"`
Duration *int `json:"duration"` Duration *int `json:"duration"`
Director *string `json:"director"` Director *string `json:"director"`
DraftID *string `json:"draft_id"`
} }
func (SceneEdit) IsEditDetails() {} func (SceneEdit) IsEditDetails() {}
@ -567,9 +712,10 @@ 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 []*FingerprintEditInput `json:"fingerprints"`
Duration *int `json:"duration"` Duration *int `json:"duration"`
Director *string `json:"director"` Director *string `json:"director"`
Fingerprints []*FingerprintInput `json:"fingerprints"`
DraftID *string `json:"draft_id"`
} }
type SceneEditInput struct { type SceneEditInput struct {
@ -599,7 +745,7 @@ type SceneFilterType struct {
// 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 // Filter to only include scenes with these fingerprints
Fingerprints *MultiIDCriterionInput `json:"fingerprints"` Fingerprints *MultiStringCriterionInput `json:"fingerprints"`
} }
type SceneUpdateInput struct { type SceneUpdateInput struct {
@ -617,6 +763,50 @@ type SceneUpdateInput struct {
Director *string `json:"director"` 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 { type StringCriterionInput struct {
Value string `json:"value"` Value string `json:"value"`
Modifier CriterionModifier `json:"modifier"` Modifier CriterionModifier `json:"modifier"`
@ -632,14 +822,14 @@ type Studio struct {
Deleted bool `json:"deleted"` Deleted bool `json:"deleted"`
} }
func (Studio) IsEditTarget() {} func (Studio) IsEditTarget() {}
func (Studio) IsSceneDraftStudio() {}
type StudioCreateInput struct { type StudioCreateInput struct {
Name string `json:"name"` Name string `json:"name"`
Urls []*URLInput `json:"urls"` Urls []*URLInput `json:"urls"`
ParentID *string `json:"parent_id"` ParentID *string `json:"parent_id"`
ChildStudioIds []string `json:"child_studio_ids"` ImageIds []string `json:"image_ids"`
ImageIds []string `json:"image_ids"`
} }
type StudioDestroyInput struct { type StudioDestroyInput struct {
@ -649,23 +839,20 @@ type StudioDestroyInput struct {
type StudioEdit struct { type StudioEdit struct {
Name *string `json:"name"` Name *string `json:"name"`
// Added and modified URLs // Added and modified URLs
AddedUrls []*URL `json:"added_urls"` AddedUrls []*URL `json:"added_urls"`
RemovedUrls []*URL `json:"removed_urls"` RemovedUrls []*URL `json:"removed_urls"`
Parent *Studio `json:"parent"` Parent *Studio `json:"parent"`
AddedChildStudios []*Studio `json:"added_child_studios"` AddedImages []*Image `json:"added_images"`
RemovedChildStudios []*Studio `json:"removed_child_studios"` RemovedImages []*Image `json:"removed_images"`
AddedImages []*Image `json:"added_images"`
RemovedImages []*Image `json:"removed_images"`
} }
func (StudioEdit) IsEditDetails() {} func (StudioEdit) IsEditDetails() {}
type StudioEditDetailsInput struct { type StudioEditDetailsInput struct {
Name *string `json:"name"` Name *string `json:"name"`
Urls []*URLInput `json:"urls"` Urls []*URLInput `json:"urls"`
ParentID *string `json:"parent_id"` ParentID *string `json:"parent_id"`
ChildStudioIds []string `json:"child_studio_ids"` ImageIds []string `json:"image_ids"`
ImageIds []string `json:"image_ids"`
} }
type StudioEditInput struct { type StudioEditInput struct {
@ -686,12 +873,11 @@ type StudioFilterType struct {
} }
type StudioUpdateInput struct { type StudioUpdateInput struct {
ID string `json:"id"` ID string `json:"id"`
Name *string `json:"name"` Name *string `json:"name"`
Urls []*URLInput `json:"urls"` Urls []*URLInput `json:"urls"`
ParentID *string `json:"parent_id"` ParentID *string `json:"parent_id"`
ChildStudioIds []string `json:"child_studio_ids"` ImageIds []string `json:"image_ids"`
ImageIds []string `json:"image_ids"`
} }
type Tag struct { type Tag struct {
@ -704,7 +890,8 @@ type Tag struct {
Category *TagCategory `json:"category"` Category *TagCategory `json:"category"`
} }
func (Tag) IsEditTarget() {} func (Tag) IsEditTarget() {}
func (Tag) IsSceneDraftTag() {}
type TagCategory struct { type TagCategory struct {
ID string `json:"id"` ID string `json:"id"`
@ -742,11 +929,11 @@ type TagDestroyInput struct {
} }
type TagEdit struct { type TagEdit struct {
Name *string `json:"name"` Name *string `json:"name"`
Description *string `json:"description"` Description *string `json:"description"`
AddedAliases []string `json:"added_aliases"` AddedAliases []string `json:"added_aliases"`
RemovedAliases []string `json:"removed_aliases"` RemovedAliases []string `json:"removed_aliases"`
CategoryID *string `json:"category_id"` Category *TagCategory `json:"category"`
} }
func (TagEdit) IsEditDetails() {} func (TagEdit) IsEditDetails() {}
@ -786,11 +973,12 @@ type TagUpdateInput struct {
type URL struct { type URL struct {
URL string `json:"url"` URL string `json:"url"`
Type string `json:"type"` Type string `json:"type"`
Site *Site `json:"site"`
} }
type URLInput struct { type URLInput struct {
URL string `json:"url"` URL string `json:"url"`
Type string `json:"type"` SiteID string `json:"site_id"`
} }
type User struct { type User struct {
@ -801,12 +989,11 @@ type User struct {
// Should not be visible to other users // Should not be visible to other users
Email *string `json:"email"` Email *string `json:"email"`
// Should not be visible to other users // Should not be visible to other users
APIKey *string `json:"api_key"` APIKey *string `json:"api_key"`
SuccessfulEdits int `json:"successful_edits"` // Vote counts by type
UnsuccessfulEdits int `json:"unsuccessful_edits"` VoteCount *UserVoteCount `json:"vote_count"`
SuccessfulVotes int `json:"successful_votes"` // Edit counts by status
// Votes on unsuccessful edits EditCount *UserEditCount `json:"edit_count"`
UnsuccessfulVotes int `json:"unsuccessful_votes"`
// Calls to the API from this user over a configurable time period // Calls to the API from this user over a configurable time period
APICalls int `json:"api_calls"` APICalls int `json:"api_calls"`
InvitedBy *User `json:"invited_by"` InvitedBy *User `json:"invited_by"`
@ -834,6 +1021,16 @@ type UserDestroyInput struct {
ID string `json:"id"` 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 { type UserFilterType struct {
// Filter to search user name - assumes like query unless quoted // Filter to search user name - assumes like query unless quoted
Name *string `json:"name"` Name *string `json:"name"`
@ -866,19 +1063,21 @@ type UserUpdateInput struct {
Email *string `json:"email"` 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 { type Version struct {
Hash string `json:"hash"` Hash string `json:"hash"`
BuildTime string `json:"build_time"` BuildTime string `json:"build_time"`
BuildType string `json:"build_type"`
Version string `json:"version"` 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 type BreastTypeEnum string
const ( const (
@ -1435,6 +1634,7 @@ const (
RoleEnumInvite RoleEnum = "INVITE" RoleEnumInvite RoleEnum = "INVITE"
// May grant and rescind invite tokens and resind invite keys // May grant and rescind invite tokens and resind invite keys
RoleEnumManageInvites RoleEnum = "MANAGE_INVITES" RoleEnumManageInvites RoleEnum = "MANAGE_INVITES"
RoleEnumBot RoleEnum = "BOT"
) )
var AllRoleEnum = []RoleEnum{ var AllRoleEnum = []RoleEnum{
@ -1445,11 +1645,12 @@ var AllRoleEnum = []RoleEnum{
RoleEnumAdmin, RoleEnumAdmin,
RoleEnumInvite, RoleEnumInvite,
RoleEnumManageInvites, RoleEnumManageInvites,
RoleEnumBot,
} }
func (e RoleEnum) IsValid() bool { func (e RoleEnum) IsValid() bool {
switch e { switch e {
case RoleEnumRead, RoleEnumVote, RoleEnumEdit, RoleEnumModify, RoleEnumAdmin, RoleEnumInvite, RoleEnumManageInvites: case RoleEnumRead, RoleEnumVote, RoleEnumEdit, RoleEnumModify, RoleEnumAdmin, RoleEnumInvite, RoleEnumManageInvites, RoleEnumBot:
return true return true
} }
return false return false
@ -1605,6 +1806,49 @@ func (e TargetTypeEnum) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String())) 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 type VoteStatusEnum string
const ( const (
@ -1613,6 +1857,8 @@ const (
VoteStatusEnumPending VoteStatusEnum = "PENDING" VoteStatusEnumPending VoteStatusEnum = "PENDING"
VoteStatusEnumImmediateAccepted VoteStatusEnum = "IMMEDIATE_ACCEPTED" VoteStatusEnumImmediateAccepted VoteStatusEnum = "IMMEDIATE_ACCEPTED"
VoteStatusEnumImmediateRejected VoteStatusEnum = "IMMEDIATE_REJECTED" VoteStatusEnumImmediateRejected VoteStatusEnum = "IMMEDIATE_REJECTED"
VoteStatusEnumFailed VoteStatusEnum = "FAILED"
VoteStatusEnumCanceled VoteStatusEnum = "CANCELED"
) )
var AllVoteStatusEnum = []VoteStatusEnum{ var AllVoteStatusEnum = []VoteStatusEnum{
@ -1621,11 +1867,13 @@ var AllVoteStatusEnum = []VoteStatusEnum{
VoteStatusEnumPending, VoteStatusEnumPending,
VoteStatusEnumImmediateAccepted, VoteStatusEnumImmediateAccepted,
VoteStatusEnumImmediateRejected, VoteStatusEnumImmediateRejected,
VoteStatusEnumFailed,
VoteStatusEnumCanceled,
} }
func (e VoteStatusEnum) IsValid() bool { func (e VoteStatusEnum) IsValid() bool {
switch e { switch e {
case VoteStatusEnumAccepted, VoteStatusEnumRejected, VoteStatusEnumPending, VoteStatusEnumImmediateAccepted, VoteStatusEnumImmediateRejected: case VoteStatusEnumAccepted, VoteStatusEnumRejected, VoteStatusEnumPending, VoteStatusEnumImmediateAccepted, VoteStatusEnumImmediateRejected, VoteStatusEnumFailed, VoteStatusEnumCanceled:
return true return true
} }
return false return false
@ -1655,7 +1903,7 @@ func (e VoteStatusEnum) MarshalGQL(w io.Writer) {
type VoteTypeEnum string type VoteTypeEnum string
const ( const (
VoteTypeEnumComment VoteTypeEnum = "COMMENT" VoteTypeEnumAbstain VoteTypeEnum = "ABSTAIN"
VoteTypeEnumAccept VoteTypeEnum = "ACCEPT" VoteTypeEnumAccept VoteTypeEnum = "ACCEPT"
VoteTypeEnumReject VoteTypeEnum = "REJECT" VoteTypeEnumReject VoteTypeEnum = "REJECT"
// Immediately accepts the edit - bypassing the vote // Immediately accepts the edit - bypassing the vote
@ -1665,7 +1913,7 @@ const (
) )
var AllVoteTypeEnum = []VoteTypeEnum{ var AllVoteTypeEnum = []VoteTypeEnum{
VoteTypeEnumComment, VoteTypeEnumAbstain,
VoteTypeEnumAccept, VoteTypeEnumAccept,
VoteTypeEnumReject, VoteTypeEnumReject,
VoteTypeEnumImmediateAccept, VoteTypeEnumImmediateAccept,
@ -1674,7 +1922,7 @@ var AllVoteTypeEnum = []VoteTypeEnum{
func (e VoteTypeEnum) IsValid() bool { func (e VoteTypeEnum) IsValid() bool {
switch e { switch e {
case VoteTypeEnumComment, VoteTypeEnumAccept, VoteTypeEnumReject, VoteTypeEnumImmediateAccept, VoteTypeEnumImmediateReject: case VoteTypeEnumAbstain, VoteTypeEnumAccept, VoteTypeEnumReject, VoteTypeEnumImmediateAccept, VoteTypeEnumImmediateReject:
return true return true
} }
return false return false

View file

@ -1,14 +1,19 @@
package stashbox package stashbox
import ( import (
"bytes"
"context" "context"
"encoding/json"
"fmt" "fmt"
"io" "io"
"mime/multipart"
"net/http" "net/http"
"os"
"strconv" "strconv"
"strings" "strings"
"github.com/Yamashou/gqlgenc/client" "github.com/Yamashou/gqlgenc/client"
"github.com/Yamashou/gqlgenc/graphqljson"
"github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/match" "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) { func (c Client) GetUser(ctx context.Context) (*graphql.Me, error) {
return c.client.Me(ctx) 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
}

View file

@ -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 ### 🎨 Improvements
* Made Performer page consistent with Studio and Tag pages. ([#2200](https://github.com/stashapp/stash/pull/2200)) * 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)) * Added 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 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)) * Show counts on list tabs in Performer, Studio and Tag pages. ([#2169](https://github.com/stashapp/stash/pull/2169))
### 🐛 Bug fixes ### 🐛 Bug fixes

View file

@ -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<GQL.StashBox, "name" | "endpoint">[];
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<IProps> = ({
show,
boxes,
entity,
query,
onHide,
}) => {
const [submit, { data, error, loading }] = useMutation<Query, Variables>(
query
);
const [selectedBox, setSelectedBox] = useState(0);
const handleSubmit = () => {
submit({
variables: {
input: {
id: entity.id,
stash_box_index: selectedBox,
},
},
});
};
const handleSelectBox = (e: React.ChangeEvent<HTMLSelectElement>) =>
setSelectedBox(Number.parseInt(e.currentTarget.value) ?? 0);
console.log(data);
return (
<Modal
icon="paper-plane"
header="Submit to Stash-Box"
isRunning={loading}
show={show}
accept={{
onClick: onHide,
}}
>
{data === undefined ? (
<>
<Form.Group className="form-row align-items-end">
<Form.Label className="col-6">
Selected Stash-Box endpoint:
</Form.Label>
<Form.Control
as="select"
onChange={handleSelectBox}
className="col-6"
>
{boxes.map((box, i) => (
<option value={i} key={`${box.endpoint}-${i}`}>
{box.name}
</option>
))}
</Form.Control>
</Form.Group>
<Button onClick={handleSubmit}>
Submit {`"${entity.name ?? entity.title}"`}
</Button>
</>
) : (
<>
<h6>Submission successful</h6>
<div>
<a
target="_blank"
rel="noreferrer noopener"
href={`${getStashboxBase(
boxes[selectedBox].endpoint
)}drafts/${getResponseId(data)}`}
>
Go to {boxes[selectedBox].name} to review draft.
</a>
</div>
</>
)}
{error !== undefined && (
<>
<h6 className="mt-2">Submission failed</h6>
<div>{error.message}</div>
</>
)}
</Modal>
);
};

View file

@ -28,6 +28,7 @@ import { PerformerGalleriesPanel } from "./PerformerGalleriesPanel";
import { PerformerMoviesPanel } from "./PerformerMoviesPanel"; import { PerformerMoviesPanel } from "./PerformerMoviesPanel";
import { PerformerImagesPanel } from "./PerformerImagesPanel"; import { PerformerImagesPanel } from "./PerformerImagesPanel";
import { PerformerEditPanel } from "./PerformerEditPanel"; import { PerformerEditPanel } from "./PerformerEditPanel";
import { PerformerSubmitButton } from "./PerformerSubmitButton";
import GenderIcon from "../GenderIcon"; import GenderIcon from "../GenderIcon";
interface IProps { interface IProps {
@ -165,8 +166,13 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
isEditing={false} isEditing={false}
onSave={() => {}} onSave={() => {}}
onImageChange={() => {}} onImageChange={() => {}}
classNames="mb-4" classNames="mb-2"
/> customButtons={
<div>
<PerformerSubmitButton performer={performer} />
</div>
}
></DetailsEditNavbar>
</Row> </Row>
</Col> </Col>
<Tabs <Tabs

View file

@ -2,7 +2,7 @@ import React from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { TagLink } from "src/components/Shared"; import { TagLink } from "src/components/Shared";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { TextUtils } from "src/utils"; import { TextUtils, getStashboxBase } from "src/utils";
import { TextField, URLField } from "src/utils/field"; import { TextField, URLField } from "src/utils/field";
interface IPerformerDetails { interface IPerformerDetails {
@ -47,7 +47,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
<dd> <dd>
<ul className="pl-0"> <ul className="pl-0">
{performer.stash_ids.map((stashID) => { {performer.stash_ids.map((stashID) => {
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0]; const base = getStashboxBase(stashID.endpoint);
const link = base ? ( const link = base ? (
<a <a
href={`${base}performers/${stashID.stash_id}`} href={`${base}performers/${stashID.stash_id}`}

View file

@ -36,6 +36,7 @@ import { stashboxDisplayName } from "src/utils/stashbox";
import { PerformerScrapeDialog } from "./PerformerScrapeDialog"; import { PerformerScrapeDialog } from "./PerformerScrapeDialog";
import PerformerScrapeModal from "./PerformerScrapeModal"; import PerformerScrapeModal from "./PerformerScrapeModal";
import PerformerStashBoxModal, { IStashBox } from "./PerformerStashBoxModal"; import PerformerStashBoxModal, { IStashBox } from "./PerformerStashBoxModal";
import cx from "classnames";
const isScraper = ( const isScraper = (
scraper: GQL.Scraper | GQL.StashBox scraper: GQL.Scraper | GQL.StashBox
@ -652,25 +653,25 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
function renderButtons(classNames: string) { function renderButtons(classNames: string) {
return ( return (
<Row> <div className={cx("details-edit", classNames)}>
<Col className={classNames} xs={12}> {!isNew && onCancelEditing ? (
{!isNew && onCancelEditing ? ( <Button
<Button className="mr-2"
className="mr-2" variant="primary"
variant="primary" onClick={() => onCancelEditing()}
onClick={() => onCancelEditing()} >
> <FormattedMessage id="actions.cancel" />
<FormattedMessage id="actions.cancel" /> </Button>
</Button> ) : (
) : ( ""
"" )}
)} {renderScraperMenu()}
{renderScraperMenu()} <ImageInput
<ImageInput isEditing
isEditing onImageChange={onImageChangeHandler}
onImageChange={onImageChangeHandler} onImageURL={onImageChangeURL}
onImageURL={onImageChangeURL} />
/> <div>
<Button <Button
className="mr-2" className="mr-2"
variant="danger" variant="danger"
@ -678,15 +679,15 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
> >
<FormattedMessage id="actions.clear_image" /> <FormattedMessage id="actions.clear_image" />
</Button> </Button>
<Button </div>
variant="success" <Button
disabled={!formik.dirty} variant="success"
onClick={() => formik.submitForm()} disabled={!formik.dirty}
> onClick={() => formik.submitForm()}
<FormattedMessage id="actions.save" /> >
</Button> <FormattedMessage id="actions.save" />
</Col> </Button>
</Row> </div>
); );
} }

View file

@ -0,0 +1,35 @@
import { Button } from "react-bootstrap";
import React, { useState } from "react";
import { FormattedMessage } from "react-intl";
import * as GQL from "src/core/generated-graphql";
import { SubmitStashBoxDraft } from "src/components/Dialogs/SubmitDraft";
interface IPerformerOperationsProps {
performer: GQL.PerformerDataFragment;
}
export const PerformerSubmitButton: React.FC<IPerformerOperationsProps> = ({
performer,
}) => {
const [showDraftModal, setShowDraftModal] = useState(false);
const { data } = GQL.useConfigurationQuery();
const boxes = data?.configuration?.general?.stashBoxes ?? [];
if (boxes.length === 0) return null;
return (
<>
<Button onClick={() => setShowDraftModal(true)}>
<FormattedMessage id="actions.submit_stash_box" />
</Button>
<SubmitStashBoxDraft
boxes={boxes}
entity={performer}
query={GQL.SubmitStashBoxPerformerDraftDocument}
show={showDraftModal}
onHide={() => setShowDraftModal(false)}
/>
</>
);
};

View file

@ -4,6 +4,7 @@ import React, { useEffect, useState, useMemo } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { useParams, useLocation, useHistory, Link } from "react-router-dom"; import { useParams, useLocation, useHistory, Link } from "react-router-dom";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { import {
mutateMetadataScan, mutateMetadataScan,
@ -22,7 +23,7 @@ import { ErrorMessage, LoadingIndicator, Icon } from "src/components/Shared";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import { ScenePlayer } from "src/components/ScenePlayer"; import { ScenePlayer } from "src/components/ScenePlayer";
import { TextUtils, JWUtils } from "src/utils"; import { TextUtils, JWUtils } from "src/utils";
import Mousetrap from "mousetrap"; import { SubmitStashBoxDraft } from "src/components/Dialogs/SubmitDraft";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import { SceneQueue } from "src/models/sceneQueue"; import { SceneQueue } from "src/models/sceneQueue";
import { QueueViewer } from "./QueueViewer"; import { QueueViewer } from "./QueueViewer";
@ -55,6 +56,10 @@ const ScenePage: React.FC<IProps> = ({ scene, refetch }) => {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [showScrubber, setShowScrubber] = useState(true); const [showScrubber, setShowScrubber] = useState(true);
const { data } = GQL.useConfigurationQuery();
const [showDraftModal, setShowDraftModal] = useState(false);
const boxes = data?.configuration?.general?.stashBoxes ?? [];
const { const {
data: sceneStreams, data: sceneStreams,
error: streamableError, error: streamableError,
@ -384,6 +389,15 @@ const ScenePage: React.FC<IProps> = ({ scene, refetch }) => {
> >
<FormattedMessage id="actions.generate_thumb_default" /> <FormattedMessage id="actions.generate_thumb_default" />
</Dropdown.Item> </Dropdown.Item>
{boxes.length > 0 && (
<Dropdown.Item
key="submit"
className="bg-secondary text-white"
onClick={() => setShowDraftModal(true)}
>
<FormattedMessage id="actions.submit_stash_box" />
</Dropdown.Item>
)}
<Dropdown.Item <Dropdown.Item
key="delete-scene" key="delete-scene"
className="bg-secondary text-white" className="bg-secondary text-white"
@ -633,6 +647,13 @@ const ScenePage: React.FC<IProps> = ({ scene, refetch }) => {
/> />
) : undefined} ) : undefined}
</div> </div>
<SubmitStashBoxDraft
boxes={boxes}
entity={scene}
query={GQL.SubmitStashBoxSceneDraftDocument}
show={showDraftModal}
onHide={() => setShowDraftModal(false)}
/>
</div> </div>
); );
}; };

View file

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl"; import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { NavUtils, TextUtils } from "src/utils"; import { NavUtils, TextUtils, getStashboxBase } from "src/utils";
import { TextField, URLField } from "src/utils/field"; import { TextField, URLField } from "src/utils/field";
interface ISceneFileInfoPanelProps { interface ISceneFileInfoPanelProps {
@ -49,7 +49,7 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
<dd> <dd>
<ul> <ul>
{props.scene.stash_ids.map((stashID) => { {props.scene.stash_ids.map((stashID) => {
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0]; const base = getStashboxBase(stashID.endpoint);
const link = base ? ( const link = base ? (
<a <a
href={`${base}scenes/${stashID.stash_id}`} href={`${base}scenes/${stashID.stash_id}`}

View file

@ -22,6 +22,7 @@ interface IProps {
acceptSVG?: boolean; acceptSVG?: boolean;
customButtons?: JSX.Element; customButtons?: JSX.Element;
classNames?: string; classNames?: string;
children?: JSX.Element | JSX.Element[];
} }
export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => { export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
@ -90,16 +91,18 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
if (props.onAutoTag) { if (props.onAutoTag) {
return ( return (
<Button <div>
variant="secondary" <Button
onClick={() => { variant="secondary"
if (props.onAutoTag) { onClick={() => {
props.onAutoTag(); if (props.onAutoTag) {
} props.onAutoTag();
}} }
> }}
<FormattedMessage id="actions.auto_tag" /> >
</Button> <FormattedMessage id="actions.auto_tag" />
</Button>
</div>
); );
} }
} }
@ -143,30 +146,30 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
acceptSVG={props.acceptSVG ?? false} acceptSVG={props.acceptSVG ?? false}
/> />
{props.isEditing && props.onClearImage ? ( {props.isEditing && props.onClearImage ? (
<Button <div>
className="mr-2" <Button
variant="danger" className="mr-2"
onClick={() => props.onClearImage!()} variant="danger"
> onClick={() => props.onClearImage!()}
{props.onClearBackImage >
? intl.formatMessage({ id: "actions.clear_front_image" }) {props.onClearBackImage
: intl.formatMessage({ id: "actions.clear_image" })} ? intl.formatMessage({ id: "actions.clear_front_image" })
</Button> : intl.formatMessage({ id: "actions.clear_image" })}
) : ( </Button>
"" </div>
)} ) : null}
{renderBackImageInput()} {renderBackImageInput()}
{props.isEditing && props.onClearBackImage ? ( {props.isEditing && props.onClearBackImage ? (
<Button <div>
className="mr-2" <Button
variant="danger" className="mr-2"
onClick={() => props.onClearBackImage!()} variant="danger"
> onClick={() => props.onClearBackImage!()}
{intl.formatMessage({ id: "actions.clear_back_image" })} >
</Button> {intl.formatMessage({ id: "actions.clear_back_image" })}
) : ( </Button>
"" </div>
)} ) : null}
{renderAutoTagButton()} {renderAutoTagButton()}
{props.customButtons} {props.customButtons}
{renderSaveButton()} {renderSaveButton()}

View file

@ -31,16 +31,24 @@
} }
.details-edit { .details-edit {
/*
The penultimate button should be wrapped in an unstyled div.
This allows the div to expand, to right-justify the last (save / delete) button.
*/
display: flex; display: flex;
flex-wrap: wrap;
justify-content: left; justify-content: left;
row-gap: 0.5rem;
.btn { .btn {
margin-right: 0.5rem; margin-right: 0.5rem;
white-space: nowrap;
} }
.delete, div:nth-last-child(2) {
.save { flex: 1;
margin-left: auto; max-width: 100%;
} }
} }

View file

@ -277,6 +277,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
onClearImage={() => {}} onClearImage={() => {}}
onAutoTag={onAutoTag} onAutoTag={onAutoTag}
onDelete={onDelete} onDelete={onDelete}
classNames="mb-2"
customButtons={renderMergeButton()} customButtons={renderMergeButton()}
/> />
</> </>

View file

@ -1149,3 +1149,19 @@ export const stashBoxPerformerBatchQuery = (
}, },
}, },
}); });
export const stashBoxSubmitSceneDraft = (
input: GQL.StashBoxDraftSubmissionInput
) =>
client.mutate<GQL.SubmitStashBoxSceneDraftMutation>({
mutation: GQL.SubmitStashBoxSceneDraftDocument,
variables: { input },
});
export const stashBoxSubmitPerformerDraft = (
input: GQL.StashBoxDraftSubmissionInput
) =>
client.mutate<GQL.SubmitStashBoxPerformerDraftMutation>({
mutation: GQL.SubmitStashBoxPerformerDraftDocument,
variables: { input },
});

View file

@ -88,6 +88,7 @@
"show": "Show", "show": "Show",
"skip": "Skip", "skip": "Skip",
"stop": "Stop", "stop": "Stop",
"submit_stash_box": "Submit to Stash-Box",
"tasks": { "tasks": {
"clean_confirm_message": "Are you sure you want to Clean? This will delete database information and generated content for all scenes and galleries that are no longer found in the filesystem.", "clean_confirm_message": "Are you sure you want to Clean? This will delete database information and generated content for all scenes and galleries that are no longer found in the filesystem.",
"dry_mode_selected": "Dry Mode selected. No actual deleting will take place, only logging.", "dry_mode_selected": "Dry Mode selected. No actual deleting will take place, only logging.",

View file

@ -14,3 +14,4 @@ export { default as useFocus } from "./focus";
export { default as downloadFile } from "./download"; export { default as downloadFile } from "./download";
export * from "./data"; export * from "./data";
export { getStashIDs } from "./stashIds"; export { getStashIDs } from "./stashIds";
export * from "./stashbox";

View file

@ -1,3 +1,6 @@
export function stashboxDisplayName(name: string, index: number) { export function stashboxDisplayName(name: string, index: number) {
return name || `Stash-Box #${index + 1}`; return name || `Stash-Box #${index + 1}`;
} }
export const getStashboxBase = (endpoint: string) =>
endpoint.match(/(https?:\/\/.*?\/)graphql/)?.[1];