mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Stash-Box Performer Tagger (#1277)
* Add bulk stash-box performer task * Add stash-box performer scraper to scrape with menu
This commit is contained in:
@@ -197,3 +197,10 @@ fragment ScrapedStashBoxSceneData on ScrapedScene {
|
|||||||
...ScrapedSceneMovieData
|
...ScrapedSceneMovieData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fragment ScrapedStashBoxPerformerData on StashBoxPerformerQueryResult {
|
||||||
|
query
|
||||||
|
results {
|
||||||
|
...ScrapedScenePerformerData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
mutation SubmitStashBoxFingerprints($input: StashBoxFingerprintSubmissionInput!) {
|
mutation SubmitStashBoxFingerprints($input: StashBoxFingerprintSubmissionInput!) {
|
||||||
submitStashBoxFingerprints(input: $input)
|
submitStashBoxFingerprints(input: $input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutation StashBoxBatchPerformerTag($input: StashBoxBatchPerformerTagInput!) {
|
||||||
|
stashBoxBatchPerformerTag(input: $input)
|
||||||
|
}
|
||||||
|
|||||||
@@ -90,8 +90,14 @@ query ScrapeMovieURL($url: String!) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query QueryStashBoxScene($input: StashBoxQueryInput!) {
|
query QueryStashBoxScene($input: StashBoxSceneQueryInput!) {
|
||||||
queryStashBoxScene(input: $input) {
|
queryStashBoxScene(input: $input) {
|
||||||
...ScrapedStashBoxSceneData
|
...ScrapedStashBoxSceneData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query QueryStashBoxPerformer($input: StashBoxPerformerQueryInput!) {
|
||||||
|
queryStashBoxPerformer(input: $input) {
|
||||||
|
...ScrapedStashBoxPerformerData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -91,7 +91,8 @@ type Query {
|
|||||||
scrapeFreeonesPerformerList(query: String!): [String!]!
|
scrapeFreeonesPerformerList(query: String!): [String!]!
|
||||||
|
|
||||||
"""Query StashBox for scenes"""
|
"""Query StashBox for scenes"""
|
||||||
queryStashBoxScene(input: StashBoxQueryInput!): [ScrapedScene!]!
|
queryStashBoxScene(input: StashBoxSceneQueryInput!): [ScrapedScene!]!
|
||||||
|
queryStashBoxPerformer(input: StashBoxPerformerQueryInput!): [StashBoxPerformerQueryResult!]!
|
||||||
|
|
||||||
# Plugins
|
# Plugins
|
||||||
"""List loaded plugins"""
|
"""List loaded plugins"""
|
||||||
@@ -234,6 +235,9 @@ type Mutation {
|
|||||||
|
|
||||||
"""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
|
||||||
|
|
||||||
|
"""Run batch performer tag task. Returns the job ID."""
|
||||||
|
stashBoxBatchPerformerTag(input: StashBoxBatchPerformerTagInput!): String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Subscription {
|
type Subscription {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ input PerformerFilterType {
|
|||||||
"""Filter by gallery count"""
|
"""Filter by gallery count"""
|
||||||
gallery_count: IntCriterionInput
|
gallery_count: IntCriterionInput
|
||||||
"""Filter by StashID"""
|
"""Filter by StashID"""
|
||||||
stash_id: String
|
stash_id: StringCriterionInput
|
||||||
"""Filter by rating"""
|
"""Filter by rating"""
|
||||||
rating: IntCriterionInput
|
rating: IntCriterionInput
|
||||||
"""Filter by url"""
|
"""Filter by url"""
|
||||||
@@ -130,7 +130,7 @@ input SceneFilterType {
|
|||||||
"""Filter by performer count"""
|
"""Filter by performer count"""
|
||||||
performer_count: IntCriterionInput
|
performer_count: IntCriterionInput
|
||||||
"""Filter by StashID"""
|
"""Filter by StashID"""
|
||||||
stash_id: String
|
stash_id: StringCriterionInput
|
||||||
"""Filter by url"""
|
"""Filter by url"""
|
||||||
url: StringCriterionInput
|
url: StringCriterionInput
|
||||||
}
|
}
|
||||||
@@ -148,7 +148,7 @@ input StudioFilterType {
|
|||||||
"""Filter to only include studios with this parent studio"""
|
"""Filter to only include studios with this parent studio"""
|
||||||
parents: MultiCriterionInput
|
parents: MultiCriterionInput
|
||||||
"""Filter by StashID"""
|
"""Filter by StashID"""
|
||||||
stash_id: String
|
stash_id: StringCriterionInput
|
||||||
"""Filter to only include studios missing this property"""
|
"""Filter to only include studios missing this property"""
|
||||||
is_missing: String
|
is_missing: String
|
||||||
"""Filter by rating"""
|
"""Filter by rating"""
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ type ScrapedGallery {
|
|||||||
performers: [ScrapedScenePerformer!]
|
performers: [ScrapedScenePerformer!]
|
||||||
}
|
}
|
||||||
|
|
||||||
input StashBoxQueryInput {
|
input StashBoxSceneQueryInput {
|
||||||
"""Index of the configured stash-box instance to use"""
|
"""Index of the configured stash-box instance to use"""
|
||||||
stash_box_index: Int!
|
stash_box_index: Int!
|
||||||
"""Instructs query by scene fingerprints"""
|
"""Instructs query by scene fingerprints"""
|
||||||
@@ -124,8 +124,30 @@ input StashBoxQueryInput {
|
|||||||
q: String
|
q: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input StashBoxPerformerQueryInput {
|
||||||
|
"""Index of the configured stash-box instance to use"""
|
||||||
|
stash_box_index: Int!
|
||||||
|
"""Instructs query by scene fingerprints"""
|
||||||
|
performer_ids: [ID!]
|
||||||
|
"""Query by query string"""
|
||||||
|
q: String
|
||||||
|
}
|
||||||
|
|
||||||
|
type StashBoxPerformerQueryResult {
|
||||||
|
query: String!
|
||||||
|
results: [ScrapedScenePerformer!]!
|
||||||
|
}
|
||||||
|
|
||||||
type StashBoxFingerprint {
|
type StashBoxFingerprint {
|
||||||
algorithm: String!
|
algorithm: String!
|
||||||
hash: String!
|
hash: String!
|
||||||
duration: Int!
|
duration: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input StashBoxBatchPerformerTagInput {
|
||||||
|
endpoint: Int!
|
||||||
|
exclude_fields: [String!]
|
||||||
|
refresh: Boolean!
|
||||||
|
performer_ids: [ID!]
|
||||||
|
performer_names: [String!]
|
||||||
|
}
|
||||||
|
|||||||
@@ -139,6 +139,18 @@ query SearchScene($term: String!) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query SearchPerformer($term: String!) {
|
||||||
|
searchPerformer(term: $term) {
|
||||||
|
...PerformerFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query FindPerformerByID($id: ID!) {
|
||||||
|
findPerformer(id: $id) {
|
||||||
|
...PerformerFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mutation SubmitFingerprint($input: FingerprintSubmission!) {
|
mutation SubmitFingerprint($input: FingerprintSubmission!) {
|
||||||
submitFingerprint(input: $input)
|
submitFingerprint(input: $input)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/manager"
|
||||||
"github.com/stashapp/stash/pkg/manager/config"
|
"github.com/stashapp/stash/pkg/manager/config"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/scraper/stashbox"
|
"github.com/stashapp/stash/pkg/scraper/stashbox"
|
||||||
@@ -20,3 +21,8 @@ func (r *mutationResolver) SubmitStashBoxFingerprints(ctx context.Context, input
|
|||||||
|
|
||||||
return client.SubmitStashBoxFingerprints(input.SceneIds, boxes[input.StashBoxIndex].Endpoint)
|
return client.SubmitStashBoxFingerprints(input.SceneIds, boxes[input.StashBoxIndex].Endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) StashBoxBatchPerformerTag(ctx context.Context, input models.StashBoxBatchPerformerTagInput) (string, error) {
|
||||||
|
manager.GetInstance().StashBoxBatchPerformerTag(input)
|
||||||
|
return "todo", nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ func (r *queryResolver) ScrapeMovieURL(ctx context.Context, url string) (*models
|
|||||||
return manager.GetInstance().ScraperCache.ScrapeMovieURL(url)
|
return manager.GetInstance().ScraperCache.ScrapeMovieURL(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) QueryStashBoxScene(ctx context.Context, input models.StashBoxQueryInput) ([]*models.ScrapedScene, error) {
|
func (r *queryResolver) QueryStashBoxScene(ctx context.Context, input models.StashBoxSceneQueryInput) ([]*models.ScrapedScene, error) {
|
||||||
boxes := config.GetInstance().GetStashBoxes()
|
boxes := config.GetInstance().GetStashBoxes()
|
||||||
|
|
||||||
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
|
if input.StashBoxIndex < 0 || input.StashBoxIndex >= len(boxes) {
|
||||||
@@ -107,3 +107,23 @@ func (r *queryResolver) QueryStashBoxScene(ctx context.Context, input models.Sta
|
|||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *queryResolver) QueryStashBoxPerformer(ctx context.Context, input models.StashBoxPerformerQueryInput) ([]*models.StashBoxPerformerQueryResult, 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)
|
||||||
|
|
||||||
|
if len(input.PerformerIds) > 0 {
|
||||||
|
return client.FindStashBoxPerformersByNames(input.PerformerIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.Q != nil {
|
||||||
|
return client.QueryStashBoxPerformer(*input.Q)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,16 +3,17 @@ package manager
|
|||||||
type JobStatus int
|
type JobStatus int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Idle JobStatus = 0
|
Idle JobStatus = 0
|
||||||
Import JobStatus = 1
|
Import JobStatus = 1
|
||||||
Export JobStatus = 2
|
Export JobStatus = 2
|
||||||
Scan JobStatus = 3
|
Scan JobStatus = 3
|
||||||
Generate JobStatus = 4
|
Generate JobStatus = 4
|
||||||
Clean JobStatus = 5
|
Clean JobStatus = 5
|
||||||
Scrape JobStatus = 6
|
Scrape JobStatus = 6
|
||||||
AutoTag JobStatus = 7
|
AutoTag JobStatus = 7
|
||||||
Migrate JobStatus = 8
|
Migrate JobStatus = 8
|
||||||
PluginOperation JobStatus = 9
|
PluginOperation JobStatus = 9
|
||||||
|
StashBoxBatchPerformer JobStatus = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s JobStatus) String() string {
|
func (s JobStatus) String() string {
|
||||||
@@ -37,6 +38,8 @@ func (s JobStatus) String() string {
|
|||||||
statusMessage = "Clean"
|
statusMessage = "Clean"
|
||||||
case PluginOperation:
|
case PluginOperation:
|
||||||
statusMessage = "Plugin Operation"
|
statusMessage = "Plugin Operation"
|
||||||
|
case StashBoxBatchPerformer:
|
||||||
|
statusMessage = "Stash-Box Performer Batch Operation"
|
||||||
}
|
}
|
||||||
|
|
||||||
return statusMessage
|
return statusMessage
|
||||||
|
|||||||
@@ -1209,3 +1209,109 @@ func (s *singleton) neededGenerate(scenes []*models.Scene, input models.Generate
|
|||||||
}
|
}
|
||||||
return &totals
|
return &totals
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *singleton) StashBoxBatchPerformerTag(input models.StashBoxBatchPerformerTagInput) {
|
||||||
|
if s.Status.Status != Idle {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.Status.SetStatus(StashBoxBatchPerformer)
|
||||||
|
s.Status.indefiniteProgress()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer s.returnToIdleState()
|
||||||
|
logger.Infof("Initiating stash-box batch performer tag")
|
||||||
|
|
||||||
|
boxes := config.GetInstance().GetStashBoxes()
|
||||||
|
if input.Endpoint < 0 || input.Endpoint >= len(boxes) {
|
||||||
|
logger.Error(fmt.Errorf("invalid stash_box_index %d", input.Endpoint))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
box := boxes[input.Endpoint]
|
||||||
|
|
||||||
|
var tasks []StashBoxPerformerTagTask
|
||||||
|
|
||||||
|
if len(input.PerformerIds) > 0 {
|
||||||
|
if err := s.TxnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error {
|
||||||
|
performerQuery := r.Performer()
|
||||||
|
|
||||||
|
for _, performerID := range input.PerformerIds {
|
||||||
|
if id, err := strconv.Atoi(performerID); err == nil {
|
||||||
|
performer, err := performerQuery.Find(id)
|
||||||
|
if err == nil {
|
||||||
|
tasks = append(tasks, StashBoxPerformerTagTask{
|
||||||
|
txnManager: s.TxnManager,
|
||||||
|
performer: performer,
|
||||||
|
refresh: input.Refresh,
|
||||||
|
box: box,
|
||||||
|
excluded_fields: input.ExcludeFields,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
logger.Error(err.Error())
|
||||||
|
}
|
||||||
|
} else if len(input.PerformerNames) > 0 {
|
||||||
|
for i := range input.PerformerNames {
|
||||||
|
if len(input.PerformerNames[i]) > 0 {
|
||||||
|
tasks = append(tasks, StashBoxPerformerTagTask{
|
||||||
|
txnManager: s.TxnManager,
|
||||||
|
name: &input.PerformerNames[i],
|
||||||
|
refresh: input.Refresh,
|
||||||
|
box: box,
|
||||||
|
excluded_fields: input.ExcludeFields,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := s.TxnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error {
|
||||||
|
performerQuery := r.Performer()
|
||||||
|
var performers []*models.Performer
|
||||||
|
var err error
|
||||||
|
if input.Refresh {
|
||||||
|
performers, err = performerQuery.FindByStashIDStatus(true, box.Endpoint)
|
||||||
|
} else {
|
||||||
|
performers, err = performerQuery.FindByStashIDStatus(false, box.Endpoint)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error querying performers: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, performer := range performers {
|
||||||
|
tasks = append(tasks, StashBoxPerformerTagTask{
|
||||||
|
txnManager: s.TxnManager,
|
||||||
|
performer: performer,
|
||||||
|
refresh: input.Refresh,
|
||||||
|
box: box,
|
||||||
|
excluded_fields: input.ExcludeFields,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
logger.Error(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tasks) == 0 {
|
||||||
|
s.returnToIdleState()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Status.setProgress(0, len(tasks))
|
||||||
|
|
||||||
|
logger.Infof("Starting stash-box batch operation for %d performers", len(tasks))
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, task := range tasks {
|
||||||
|
wg.Add(1)
|
||||||
|
go task.Start(&wg)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
s.Status.incrementProgress()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|||||||
252
pkg/manager/task_stash_box_tag.go
Normal file
252
pkg/manager/task_stash_box_tag.go
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/scraper/stashbox"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StashBoxPerformerTagTask struct {
|
||||||
|
txnManager models.TransactionManager
|
||||||
|
box *models.StashBox
|
||||||
|
name *string
|
||||||
|
performer *models.Performer
|
||||||
|
refresh bool
|
||||||
|
excluded_fields []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *StashBoxPerformerTagTask) Start(wg *sync.WaitGroup) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
t.stashBoxPerformerTag()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *StashBoxPerformerTagTask) stashBoxPerformerTag() {
|
||||||
|
var performer *models.ScrapedScenePerformer
|
||||||
|
var err error
|
||||||
|
|
||||||
|
client := stashbox.NewClient(*t.box, t.txnManager)
|
||||||
|
|
||||||
|
if t.refresh {
|
||||||
|
var performerID string
|
||||||
|
t.txnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error {
|
||||||
|
stashids, _ := r.Performer().GetStashIDs(t.performer.ID)
|
||||||
|
for _, id := range stashids {
|
||||||
|
if id.Endpoint == t.box.Endpoint {
|
||||||
|
performerID = id.StashID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if performerID != "" {
|
||||||
|
performer, err = client.FindStashBoxPerformerByID(performerID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var name string
|
||||||
|
if t.name != nil {
|
||||||
|
name = *t.name
|
||||||
|
} else {
|
||||||
|
name = t.performer.Name.String
|
||||||
|
}
|
||||||
|
performer, err = client.FindStashBoxPerformerByName(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Error fetching performer data from stash-box: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
excluded := map[string]bool{}
|
||||||
|
for _, field := range t.excluded_fields {
|
||||||
|
excluded[field] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if performer != nil {
|
||||||
|
updatedTime := time.Now()
|
||||||
|
|
||||||
|
if t.performer != nil {
|
||||||
|
partial := models.PerformerPartial{
|
||||||
|
ID: t.performer.ID,
|
||||||
|
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||||
|
}
|
||||||
|
|
||||||
|
if performer.Aliases != nil && !excluded["aliases"] {
|
||||||
|
value := getNullString(performer.Aliases)
|
||||||
|
partial.Aliases = &value
|
||||||
|
}
|
||||||
|
if performer.Birthdate != nil && *performer.Birthdate != "" && !excluded["birthdate"] {
|
||||||
|
value := getDate(performer.Birthdate)
|
||||||
|
partial.Birthdate = &value
|
||||||
|
}
|
||||||
|
if performer.CareerLength != nil && !excluded["career_length"] {
|
||||||
|
value := getNullString(performer.CareerLength)
|
||||||
|
partial.CareerLength = &value
|
||||||
|
}
|
||||||
|
if performer.Country != nil && !excluded["country"] {
|
||||||
|
value := getNullString(performer.Country)
|
||||||
|
partial.Country = &value
|
||||||
|
}
|
||||||
|
if performer.Ethnicity != nil && !excluded["ethnicity"] {
|
||||||
|
value := getNullString(performer.Ethnicity)
|
||||||
|
partial.Ethnicity = &value
|
||||||
|
}
|
||||||
|
if performer.EyeColor != nil && !excluded["eye_color"] {
|
||||||
|
value := getNullString(performer.EyeColor)
|
||||||
|
partial.EyeColor = &value
|
||||||
|
}
|
||||||
|
if performer.FakeTits != nil && !excluded["fake_tits"] {
|
||||||
|
value := getNullString(performer.FakeTits)
|
||||||
|
partial.FakeTits = &value
|
||||||
|
}
|
||||||
|
if performer.Gender != nil && !excluded["gender"] {
|
||||||
|
value := getNullString(performer.Gender)
|
||||||
|
partial.Gender = &value
|
||||||
|
}
|
||||||
|
if performer.Height != nil && !excluded["height"] {
|
||||||
|
value := getNullString(performer.Height)
|
||||||
|
partial.Height = &value
|
||||||
|
}
|
||||||
|
if performer.Instagram != nil && !excluded["instagram"] {
|
||||||
|
value := getNullString(performer.Instagram)
|
||||||
|
partial.Instagram = &value
|
||||||
|
}
|
||||||
|
if performer.Measurements != nil && !excluded["measurements"] {
|
||||||
|
value := getNullString(performer.Measurements)
|
||||||
|
partial.Measurements = &value
|
||||||
|
}
|
||||||
|
if excluded["name"] {
|
||||||
|
value := sql.NullString{String: performer.Name, Valid: true}
|
||||||
|
partial.Name = &value
|
||||||
|
}
|
||||||
|
if performer.Piercings != nil && !excluded["piercings"] {
|
||||||
|
value := getNullString(performer.Piercings)
|
||||||
|
partial.Piercings = &value
|
||||||
|
}
|
||||||
|
if performer.Tattoos != nil && !excluded["tattoos"] {
|
||||||
|
value := getNullString(performer.Tattoos)
|
||||||
|
partial.Tattoos = &value
|
||||||
|
}
|
||||||
|
if performer.Twitter != nil && !excluded["twitter"] {
|
||||||
|
value := getNullString(performer.Tattoos)
|
||||||
|
partial.Twitter = &value
|
||||||
|
}
|
||||||
|
if performer.URL != nil && !excluded["url"] {
|
||||||
|
value := getNullString(performer.URL)
|
||||||
|
partial.URL = &value
|
||||||
|
}
|
||||||
|
|
||||||
|
t.txnManager.WithTxn(context.TODO(), func(r models.Repository) error {
|
||||||
|
_, err := r.Performer().Update(partial)
|
||||||
|
|
||||||
|
if !t.refresh {
|
||||||
|
err = r.Performer().UpdateStashIDs(t.performer.ID, []models.StashID{
|
||||||
|
{
|
||||||
|
Endpoint: t.box.Endpoint,
|
||||||
|
StashID: *performer.RemoteSiteID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(performer.Images) > 0 && !excluded["image"] {
|
||||||
|
image, err := utils.ReadImageFromURL(performer.Images[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = r.Performer().UpdateImage(t.performer.ID, image)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
logger.Infof("Updated performer %s", performer.Name)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
} else if t.name != nil {
|
||||||
|
currentTime := time.Now()
|
||||||
|
newPerformer := models.Performer{
|
||||||
|
Aliases: getNullString(performer.Aliases),
|
||||||
|
Birthdate: getDate(performer.Birthdate),
|
||||||
|
CareerLength: getNullString(performer.CareerLength),
|
||||||
|
Checksum: utils.MD5FromString(performer.Name),
|
||||||
|
Country: getNullString(performer.Country),
|
||||||
|
CreatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
|
||||||
|
Ethnicity: getNullString(performer.Ethnicity),
|
||||||
|
EyeColor: getNullString(performer.EyeColor),
|
||||||
|
FakeTits: getNullString(performer.FakeTits),
|
||||||
|
Favorite: sql.NullBool{Bool: false, Valid: true},
|
||||||
|
Gender: getNullString(performer.Gender),
|
||||||
|
Height: getNullString(performer.Height),
|
||||||
|
Instagram: getNullString(performer.Instagram),
|
||||||
|
Measurements: getNullString(performer.Measurements),
|
||||||
|
Name: sql.NullString{String: performer.Name, Valid: true},
|
||||||
|
Piercings: getNullString(performer.Piercings),
|
||||||
|
Tattoos: getNullString(performer.Tattoos),
|
||||||
|
Twitter: getNullString(performer.Twitter),
|
||||||
|
URL: getNullString(performer.URL),
|
||||||
|
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
|
||||||
|
}
|
||||||
|
err := t.txnManager.WithTxn(context.TODO(), func(r models.Repository) error {
|
||||||
|
createdPerformer, err := r.Performer().Create(newPerformer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.Performer().UpdateStashIDs(createdPerformer.ID, []models.StashID{
|
||||||
|
{
|
||||||
|
Endpoint: t.box.Endpoint,
|
||||||
|
StashID: *performer.RemoteSiteID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(performer.Images) > 0 {
|
||||||
|
image, err := utils.ReadImageFromURL(performer.Images[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = r.Performer().UpdateImage(createdPerformer.ID, image)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Failed to save performer %s: %s", *t.name, err.Error())
|
||||||
|
} else {
|
||||||
|
logger.Infof("Saved performer %s", *t.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var name string
|
||||||
|
if t.name != nil {
|
||||||
|
name = *t.name
|
||||||
|
} else if t.performer != nil {
|
||||||
|
name = t.performer.Name.String
|
||||||
|
}
|
||||||
|
logger.Infof("No match found for %s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDate(val *string) models.SQLiteDate {
|
||||||
|
if val == nil {
|
||||||
|
return models.SQLiteDate{Valid: false}
|
||||||
|
} else {
|
||||||
|
return models.SQLiteDate{String: *val, Valid: false}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNullString(val *string) sql.NullString {
|
||||||
|
if val == nil {
|
||||||
|
return sql.NullString{Valid: false}
|
||||||
|
} else {
|
||||||
|
return sql.NullString{String: *val, Valid: true}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -498,3 +498,26 @@ func (_m *PerformerReaderWriter) UpdateTags(sceneID int, tagIDs []int) error {
|
|||||||
|
|
||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindByStashIDStatus provides a mock function with given fields: hasStashID, stashboxEndpoint
|
||||||
|
func (_m *PerformerReaderWriter) FindByStashIDStatus(hasStashID bool, stashboxEndpoint string) ([]*models.Performer, error) {
|
||||||
|
ret := _m.Called(hasStashID, stashboxEndpoint)
|
||||||
|
|
||||||
|
var r0 []*models.Performer
|
||||||
|
if rf, ok := ret.Get(0).(func(bool, string) []*models.Performer); ok {
|
||||||
|
r0 = rf(hasStashID, stashboxEndpoint)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*models.Performer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(bool, string) error); ok {
|
||||||
|
r1 = rf(hasStashID, stashboxEndpoint)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ type PerformerReader interface {
|
|||||||
FindByImageID(imageID int) ([]*Performer, error)
|
FindByImageID(imageID int) ([]*Performer, error)
|
||||||
FindByGalleryID(galleryID int) ([]*Performer, error)
|
FindByGalleryID(galleryID int) ([]*Performer, error)
|
||||||
FindByNames(names []string, nocase bool) ([]*Performer, error)
|
FindByNames(names []string, nocase bool) ([]*Performer, error)
|
||||||
|
FindByStashIDStatus(hasStashID bool, stashboxEndpoint string) ([]*Performer, error)
|
||||||
CountByTagID(tagID int) (int, error)
|
CountByTagID(tagID int) (int, error)
|
||||||
Count() (int, error)
|
Count() (int, error)
|
||||||
All() ([]*Performer, error)
|
All() ([]*Performer, error)
|
||||||
|
|||||||
@@ -166,6 +166,12 @@ type FindScenesByFingerprints struct {
|
|||||||
type SearchScene struct {
|
type SearchScene struct {
|
||||||
SearchScene []*SceneFragment "json:\"searchScene\" graphql:\"searchScene\""
|
SearchScene []*SceneFragment "json:\"searchScene\" graphql:\"searchScene\""
|
||||||
}
|
}
|
||||||
|
type SearchPerformer struct {
|
||||||
|
SearchPerformer []*PerformerFragment "json:\"searchPerformer\" graphql:\"searchPerformer\""
|
||||||
|
}
|
||||||
|
type FindPerformerByID struct {
|
||||||
|
FindPerformer *PerformerFragment "json:\"findPerformer\" graphql:\"findPerformer\""
|
||||||
|
}
|
||||||
type SubmitFingerprintPayload struct {
|
type SubmitFingerprintPayload struct {
|
||||||
SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\""
|
SubmitFingerprint bool "json:\"submitFingerprint\" graphql:\"submitFingerprint\""
|
||||||
}
|
}
|
||||||
@@ -175,67 +181,6 @@ const FindSceneByFingerprintQuery = `query FindSceneByFingerprint ($fingerprint:
|
|||||||
... SceneFragment
|
... SceneFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fragment SceneFragment on Scene {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
details
|
|
||||||
duration
|
|
||||||
date
|
|
||||||
urls {
|
|
||||||
... URLFragment
|
|
||||||
}
|
|
||||||
images {
|
|
||||||
... ImageFragment
|
|
||||||
}
|
|
||||||
studio {
|
|
||||||
... StudioFragment
|
|
||||||
}
|
|
||||||
tags {
|
|
||||||
... TagFragment
|
|
||||||
}
|
|
||||||
performers {
|
|
||||||
... PerformerAppearanceFragment
|
|
||||||
}
|
|
||||||
fingerprints {
|
|
||||||
... FingerprintFragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 FuzzyDateFragment on FuzzyDate {
|
|
||||||
date
|
|
||||||
accuracy
|
|
||||||
}
|
|
||||||
fragment MeasurementsFragment on Measurements {
|
|
||||||
band_size
|
|
||||||
cup_size
|
|
||||||
waist
|
|
||||||
hip
|
|
||||||
}
|
|
||||||
fragment URLFragment on URL {
|
|
||||||
url
|
|
||||||
type
|
|
||||||
}
|
|
||||||
fragment TagFragment on Tag {
|
fragment TagFragment on Tag {
|
||||||
name
|
name
|
||||||
id
|
id
|
||||||
@@ -273,6 +218,10 @@ fragment PerformerFragment on Performer {
|
|||||||
... BodyModificationFragment
|
... BodyModificationFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fragment FuzzyDateFragment on FuzzyDate {
|
||||||
|
date
|
||||||
|
accuracy
|
||||||
|
}
|
||||||
fragment BodyModificationFragment on BodyModification {
|
fragment BodyModificationFragment on BodyModification {
|
||||||
location
|
location
|
||||||
description
|
description
|
||||||
@@ -282,6 +231,63 @@ fragment FingerprintFragment on Fingerprint {
|
|||||||
hash
|
hash
|
||||||
duration
|
duration
|
||||||
}
|
}
|
||||||
|
fragment SceneFragment on Scene {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
details
|
||||||
|
duration
|
||||||
|
date
|
||||||
|
urls {
|
||||||
|
... URLFragment
|
||||||
|
}
|
||||||
|
images {
|
||||||
|
... ImageFragment
|
||||||
|
}
|
||||||
|
studio {
|
||||||
|
... StudioFragment
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
... TagFragment
|
||||||
|
}
|
||||||
|
performers {
|
||||||
|
... PerformerAppearanceFragment
|
||||||
|
}
|
||||||
|
fingerprints {
|
||||||
|
... FingerprintFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fragment URLFragment on URL {
|
||||||
|
url
|
||||||
|
type
|
||||||
|
}
|
||||||
|
fragment StudioFragment on Studio {
|
||||||
|
name
|
||||||
|
id
|
||||||
|
urls {
|
||||||
|
... URLFragment
|
||||||
|
}
|
||||||
|
images {
|
||||||
|
... ImageFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fragment ImageFragment on Image {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
width
|
||||||
|
height
|
||||||
|
}
|
||||||
|
fragment PerformerAppearanceFragment on PerformerAppearance {
|
||||||
|
as
|
||||||
|
performer {
|
||||||
|
... PerformerFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fragment MeasurementsFragment on Measurements {
|
||||||
|
band_size
|
||||||
|
cup_size
|
||||||
|
waist
|
||||||
|
hip
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
func (c *Client) FindSceneByFingerprint(ctx context.Context, fingerprint FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindSceneByFingerprint, error) {
|
func (c *Client) FindSceneByFingerprint(ctx context.Context, fingerprint FingerprintQueryInput, httpRequestOptions ...client.HTTPRequestOption) (*FindSceneByFingerprint, error) {
|
||||||
@@ -302,30 +308,20 @@ const FindScenesByFingerprintsQuery = `query FindScenesByFingerprints ($fingerpr
|
|||||||
... SceneFragment
|
... SceneFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fragment BodyModificationFragment on BodyModification {
|
|
||||||
location
|
|
||||||
description
|
|
||||||
}
|
|
||||||
fragment ImageFragment on Image {
|
|
||||||
id
|
|
||||||
url
|
|
||||||
width
|
|
||||||
height
|
|
||||||
}
|
|
||||||
fragment StudioFragment on Studio {
|
|
||||||
name
|
|
||||||
id
|
|
||||||
urls {
|
|
||||||
... URLFragment
|
|
||||||
}
|
|
||||||
images {
|
|
||||||
... ImageFragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fragment TagFragment on Tag {
|
fragment TagFragment on Tag {
|
||||||
name
|
name
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
fragment FuzzyDateFragment on FuzzyDate {
|
||||||
|
date
|
||||||
|
accuracy
|
||||||
|
}
|
||||||
|
fragment PerformerAppearanceFragment on PerformerAppearance {
|
||||||
|
as
|
||||||
|
performer {
|
||||||
|
... PerformerFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
fragment PerformerFragment on Performer {
|
fragment PerformerFragment on Performer {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
@@ -365,6 +361,10 @@ fragment MeasurementsFragment on Measurements {
|
|||||||
waist
|
waist
|
||||||
hip
|
hip
|
||||||
}
|
}
|
||||||
|
fragment BodyModificationFragment on BodyModification {
|
||||||
|
location
|
||||||
|
description
|
||||||
|
}
|
||||||
fragment SceneFragment on Scene {
|
fragment SceneFragment on Scene {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
@@ -394,15 +394,21 @@ fragment URLFragment on URL {
|
|||||||
url
|
url
|
||||||
type
|
type
|
||||||
}
|
}
|
||||||
fragment PerformerAppearanceFragment on PerformerAppearance {
|
fragment ImageFragment on Image {
|
||||||
as
|
id
|
||||||
performer {
|
url
|
||||||
... PerformerFragment
|
width
|
||||||
}
|
height
|
||||||
}
|
}
|
||||||
fragment FuzzyDateFragment on FuzzyDate {
|
fragment StudioFragment on Studio {
|
||||||
date
|
name
|
||||||
accuracy
|
id
|
||||||
|
urls {
|
||||||
|
... URLFragment
|
||||||
|
}
|
||||||
|
images {
|
||||||
|
... ImageFragment
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fragment FingerprintFragment on Fingerprint {
|
fragment FingerprintFragment on Fingerprint {
|
||||||
algorithm
|
algorithm
|
||||||
@@ -429,11 +435,11 @@ const SearchSceneQuery = `query SearchScene ($term: String!) {
|
|||||||
... SceneFragment
|
... SceneFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fragment PerformerAppearanceFragment on PerformerAppearance {
|
fragment MeasurementsFragment on Measurements {
|
||||||
as
|
band_size
|
||||||
performer {
|
cup_size
|
||||||
... PerformerFragment
|
waist
|
||||||
}
|
hip
|
||||||
}
|
}
|
||||||
fragment BodyModificationFragment on BodyModification {
|
fragment BodyModificationFragment on BodyModification {
|
||||||
location
|
location
|
||||||
@@ -444,6 +450,24 @@ fragment FingerprintFragment on Fingerprint {
|
|||||||
hash
|
hash
|
||||||
duration
|
duration
|
||||||
}
|
}
|
||||||
|
fragment URLFragment on URL {
|
||||||
|
url
|
||||||
|
type
|
||||||
|
}
|
||||||
|
fragment ImageFragment on Image {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
width
|
||||||
|
height
|
||||||
|
}
|
||||||
|
fragment TagFragment on Tag {
|
||||||
|
name
|
||||||
|
id
|
||||||
|
}
|
||||||
|
fragment FuzzyDateFragment on FuzzyDate {
|
||||||
|
date
|
||||||
|
accuracy
|
||||||
|
}
|
||||||
fragment SceneFragment on Scene {
|
fragment SceneFragment on Scene {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
@@ -469,13 +493,21 @@ fragment SceneFragment on Scene {
|
|||||||
... FingerprintFragment
|
... FingerprintFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fragment URLFragment on URL {
|
fragment StudioFragment on Studio {
|
||||||
url
|
|
||||||
type
|
|
||||||
}
|
|
||||||
fragment TagFragment on Tag {
|
|
||||||
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
|
||||||
@@ -510,6 +542,26 @@ fragment PerformerFragment on Performer {
|
|||||||
... BodyModificationFragment
|
... BodyModificationFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
func (c *Client) SearchScene(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchScene, error) {
|
||||||
|
vars := map[string]interface{}{
|
||||||
|
"term": term,
|
||||||
|
}
|
||||||
|
|
||||||
|
var res SearchScene
|
||||||
|
if err := c.Client.Post(ctx, SearchSceneQuery, &res, vars, httpRequestOptions...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchPerformerQuery = `query SearchPerformer ($term: String!) {
|
||||||
|
searchPerformer(term: $term) {
|
||||||
|
... PerformerFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
fragment FuzzyDateFragment on FuzzyDate {
|
fragment FuzzyDateFragment on FuzzyDate {
|
||||||
date
|
date
|
||||||
accuracy
|
accuracy
|
||||||
@@ -520,31 +572,139 @@ fragment MeasurementsFragment on Measurements {
|
|||||||
waist
|
waist
|
||||||
hip
|
hip
|
||||||
}
|
}
|
||||||
fragment ImageFragment on Image {
|
fragment BodyModificationFragment on BodyModification {
|
||||||
id
|
location
|
||||||
url
|
description
|
||||||
width
|
|
||||||
height
|
|
||||||
}
|
}
|
||||||
fragment StudioFragment on Studio {
|
fragment PerformerFragment on Performer {
|
||||||
name
|
|
||||||
id
|
id
|
||||||
|
name
|
||||||
|
disambiguation
|
||||||
|
aliases
|
||||||
|
gender
|
||||||
urls {
|
urls {
|
||||||
... URLFragment
|
... URLFragment
|
||||||
}
|
}
|
||||||
images {
|
images {
|
||||||
... ImageFragment
|
... 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 URLFragment on URL {
|
||||||
|
url
|
||||||
|
type
|
||||||
|
}
|
||||||
|
fragment ImageFragment on Image {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
width
|
||||||
|
height
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
func (c *Client) SearchScene(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchScene, error) {
|
func (c *Client) SearchPerformer(ctx context.Context, term string, httpRequestOptions ...client.HTTPRequestOption) (*SearchPerformer, error) {
|
||||||
vars := map[string]interface{}{
|
vars := map[string]interface{}{
|
||||||
"term": term,
|
"term": term,
|
||||||
}
|
}
|
||||||
|
|
||||||
var res SearchScene
|
var res SearchPerformer
|
||||||
if err := c.Client.Post(ctx, SearchSceneQuery, &res, vars, httpRequestOptions...); err != nil {
|
if err := c.Client.Post(ctx, SearchPerformerQuery, &res, vars, httpRequestOptions...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const FindPerformerByIDQuery = `query FindPerformerByID ($id: ID!) {
|
||||||
|
findPerformer(id: $id) {
|
||||||
|
... PerformerFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fragment FuzzyDateFragment on FuzzyDate {
|
||||||
|
date
|
||||||
|
accuracy
|
||||||
|
}
|
||||||
|
fragment MeasurementsFragment on Measurements {
|
||||||
|
band_size
|
||||||
|
cup_size
|
||||||
|
waist
|
||||||
|
hip
|
||||||
|
}
|
||||||
|
fragment BodyModificationFragment on BodyModification {
|
||||||
|
location
|
||||||
|
description
|
||||||
|
}
|
||||||
|
fragment PerformerFragment on Performer {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
disambiguation
|
||||||
|
aliases
|
||||||
|
gender
|
||||||
|
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 URLFragment on URL {
|
||||||
|
url
|
||||||
|
type
|
||||||
|
}
|
||||||
|
fragment ImageFragment on Image {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
width
|
||||||
|
height
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
func (c *Client) FindPerformerByID(ctx context.Context, id string, httpRequestOptions ...client.HTTPRequestOption) (*FindPerformerByID, error) {
|
||||||
|
vars := map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
}
|
||||||
|
|
||||||
|
var res FindPerformerByID
|
||||||
|
if err := c.Client.Post(ctx, FindPerformerByIDQuery, &res, vars, httpRequestOptions...); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -227,6 +227,92 @@ func (c Client) submitStashBoxFingerprints(fingerprints []graphql.FingerprintSub
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QueryStashBoxPerformer queries stash-box for performers using a query string.
|
||||||
|
func (c Client) QueryStashBoxPerformer(queryStr string) ([]*models.StashBoxPerformerQueryResult, error) {
|
||||||
|
performers, err := c.queryStashBoxPerformer(queryStr)
|
||||||
|
|
||||||
|
res := []*models.StashBoxPerformerQueryResult{
|
||||||
|
{
|
||||||
|
Query: queryStr,
|
||||||
|
Results: performers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) queryStashBoxPerformer(queryStr string) ([]*models.ScrapedScenePerformer, error) {
|
||||||
|
performers, err := c.client.SearchPerformer(context.TODO(), queryStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
performerFragments := performers.SearchPerformer
|
||||||
|
|
||||||
|
var ret []*models.ScrapedScenePerformer
|
||||||
|
for _, fragment := range performerFragments {
|
||||||
|
performer := performerFragmentToScrapedScenePerformer(*fragment)
|
||||||
|
ret = append(ret, performer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindStashBoxPerformersByNames queries stash-box for performers by name
|
||||||
|
func (c Client) FindStashBoxPerformersByNames(performerIDs []string) ([]*models.StashBoxPerformerQueryResult, error) {
|
||||||
|
ids, err := utils.StringSliceToIntSlice(performerIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var performers []*models.Performer
|
||||||
|
|
||||||
|
if err := c.txnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error {
|
||||||
|
qb := r.Performer()
|
||||||
|
|
||||||
|
for _, performerID := range ids {
|
||||||
|
performer, err := qb.Find(performerID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if performer == nil {
|
||||||
|
return fmt.Errorf("performer with id %d not found", performerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if performer.Name.Valid {
|
||||||
|
performers = append(performers, performer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.findStashBoxPerformersByNames(performers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) findStashBoxPerformersByNames(performers []*models.Performer) ([]*models.StashBoxPerformerQueryResult, error) {
|
||||||
|
var ret []*models.StashBoxPerformerQueryResult
|
||||||
|
for _, performer := range performers {
|
||||||
|
if performer.Name.Valid {
|
||||||
|
performerResults, err := c.queryStashBoxPerformer(performer.Name.String)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := models.StashBoxPerformerQueryResult{
|
||||||
|
Query: strconv.Itoa(performer.ID),
|
||||||
|
Results: performerResults,
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = append(ret, &result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
func findURL(urls []*graphql.URLFragment, urlType string) *string {
|
func findURL(urls []*graphql.URLFragment, urlType string) *string {
|
||||||
for _, u := range urls {
|
for _, u := range urls {
|
||||||
if u.Type == urlType {
|
if u.Type == urlType {
|
||||||
@@ -238,9 +324,12 @@ func findURL(urls []*graphql.URLFragment, urlType string) *string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func enumToStringPtr(e fmt.Stringer) *string {
|
func enumToStringPtr(e fmt.Stringer, titleCase bool) *string {
|
||||||
if e != nil {
|
if e != nil {
|
||||||
ret := e.String()
|
ret := e.String()
|
||||||
|
if titleCase {
|
||||||
|
ret = strings.Title(strings.ToLower(ret))
|
||||||
|
}
|
||||||
return &ret
|
return &ret
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,6 +353,8 @@ func formatCareerLength(start, end *int) *string {
|
|||||||
var ret string
|
var ret string
|
||||||
if end == nil {
|
if end == nil {
|
||||||
ret = fmt.Sprintf("%d -", *start)
|
ret = fmt.Sprintf("%d -", *start)
|
||||||
|
} else if start == nil {
|
||||||
|
ret = fmt.Sprintf("- %d", *end)
|
||||||
} else {
|
} else {
|
||||||
ret = fmt.Sprintf("%d - %d", *start, *end)
|
ret = fmt.Sprintf("%d - %d", *start, *end)
|
||||||
}
|
}
|
||||||
@@ -354,19 +445,19 @@ func performerFragmentToScrapedScenePerformer(p graphql.PerformerFragment) *mode
|
|||||||
}
|
}
|
||||||
|
|
||||||
if p.Gender != nil {
|
if p.Gender != nil {
|
||||||
sp.Gender = enumToStringPtr(p.Gender)
|
sp.Gender = enumToStringPtr(p.Gender, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.Ethnicity != nil {
|
if p.Ethnicity != nil {
|
||||||
sp.Ethnicity = enumToStringPtr(p.Ethnicity)
|
sp.Ethnicity = enumToStringPtr(p.Ethnicity, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.EyeColor != nil {
|
if p.EyeColor != nil {
|
||||||
sp.EyeColor = enumToStringPtr(p.EyeColor)
|
sp.EyeColor = enumToStringPtr(p.EyeColor, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.BreastType != nil {
|
if p.BreastType != nil {
|
||||||
sp.FakeTits = enumToStringPtr(p.BreastType)
|
sp.FakeTits = enumToStringPtr(p.BreastType, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sp
|
return sp
|
||||||
@@ -463,3 +554,29 @@ func sceneFragmentToScrapedScene(txnManager models.TransactionManager, s *graphq
|
|||||||
|
|
||||||
return ss, nil
|
return ss, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c Client) FindStashBoxPerformerByID(id string) (*models.ScrapedScenePerformer, error) {
|
||||||
|
performer, err := c.client.FindPerformerByID(context.TODO(), id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := performerFragmentToScrapedScenePerformer(*performer.FindPerformer)
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Client) FindStashBoxPerformerByName(name string) (*models.ScrapedScenePerformer, error) {
|
||||||
|
performers, err := c.client.SearchPerformer(context.TODO(), name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret *models.ScrapedScenePerformer
|
||||||
|
for _, performer := range performers.SearchPerformer {
|
||||||
|
if strings.ToLower(performer.Name) == strings.ToLower(name) {
|
||||||
|
ret = performerFragmentToScrapedScenePerformer(*performer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -258,18 +258,11 @@ func (qb *performerQueryBuilder) Query(performerFilter *models.PerformerFilterTy
|
|||||||
query.body += `left join performers_image on performers_image.performer_id = performers.id
|
query.body += `left join performers_image on performers_image.performer_id = performers.id
|
||||||
`
|
`
|
||||||
query.addWhere("performers_image.performer_id IS NULL")
|
query.addWhere("performers_image.performer_id IS NULL")
|
||||||
case "stash_id":
|
|
||||||
query.addWhere("performer_stash_ids.performer_id IS NULL")
|
|
||||||
default:
|
default:
|
||||||
query.addWhere("(performers." + *isMissingFilter + " IS NULL OR TRIM(performers." + *isMissingFilter + ") = '')")
|
query.addWhere("(performers." + *isMissingFilter + " IS NULL OR TRIM(performers." + *isMissingFilter + ") = '')")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if stashIDFilter := performerFilter.StashID; stashIDFilter != nil {
|
|
||||||
query.addWhere("performer_stash_ids.stash_id = ?")
|
|
||||||
query.addArg(stashIDFilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
query.handleStringCriterionInput(performerFilter.Ethnicity, tableName+".ethnicity")
|
query.handleStringCriterionInput(performerFilter.Ethnicity, tableName+".ethnicity")
|
||||||
query.handleStringCriterionInput(performerFilter.Country, tableName+".country")
|
query.handleStringCriterionInput(performerFilter.Country, tableName+".country")
|
||||||
query.handleStringCriterionInput(performerFilter.EyeColor, tableName+".eye_color")
|
query.handleStringCriterionInput(performerFilter.EyeColor, tableName+".eye_color")
|
||||||
@@ -283,6 +276,7 @@ func (qb *performerQueryBuilder) Query(performerFilter *models.PerformerFilterTy
|
|||||||
query.handleStringCriterionInput(performerFilter.HairColor, tableName+".hair_color")
|
query.handleStringCriterionInput(performerFilter.HairColor, tableName+".hair_color")
|
||||||
query.handleStringCriterionInput(performerFilter.URL, tableName+".url")
|
query.handleStringCriterionInput(performerFilter.URL, tableName+".url")
|
||||||
query.handleIntCriterionInput(performerFilter.Weight, tableName+".weight")
|
query.handleIntCriterionInput(performerFilter.Weight, tableName+".weight")
|
||||||
|
query.handleStringCriterionInput(performerFilter.StashID, "performer_stash_ids.stash_id")
|
||||||
|
|
||||||
// TODO - need better handling of aliases
|
// TODO - need better handling of aliases
|
||||||
query.handleStringCriterionInput(performerFilter.Aliases, tableName+".aliases")
|
query.handleStringCriterionInput(performerFilter.Aliases, tableName+".aliases")
|
||||||
@@ -470,3 +464,23 @@ func (qb *performerQueryBuilder) GetStashIDs(performerID int) ([]*models.StashID
|
|||||||
func (qb *performerQueryBuilder) UpdateStashIDs(performerID int, stashIDs []models.StashID) error {
|
func (qb *performerQueryBuilder) UpdateStashIDs(performerID int, stashIDs []models.StashID) error {
|
||||||
return qb.stashIDRepository().replace(performerID, stashIDs)
|
return qb.stashIDRepository().replace(performerID, stashIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qb *performerQueryBuilder) FindByStashIDStatus(hasStashID bool, stashboxEndpoint string) ([]*models.Performer, error) {
|
||||||
|
query := selectAll("performers") + `
|
||||||
|
LEFT JOIN performer_stash_ids on performer_stash_ids.performer_id = performers.id
|
||||||
|
`
|
||||||
|
|
||||||
|
if hasStashID {
|
||||||
|
query += `
|
||||||
|
WHERE performer_stash_ids.stash_id IS NOT NULL
|
||||||
|
AND performer_stash_ids.endpoint = ?
|
||||||
|
`
|
||||||
|
} else {
|
||||||
|
query += `
|
||||||
|
WHERE performer_stash_ids.stash_id IS NULL
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []interface{}{stashboxEndpoint}
|
||||||
|
return qb.queryPerformers(query, args)
|
||||||
|
}
|
||||||
|
|||||||
@@ -362,6 +362,7 @@ func (qb *sceneQueryBuilder) makeFilter(sceneFilter *models.SceneFilterType) *fi
|
|||||||
query.handleCriterionFunc(hasMarkersCriterionHandler(sceneFilter.HasMarkers))
|
query.handleCriterionFunc(hasMarkersCriterionHandler(sceneFilter.HasMarkers))
|
||||||
query.handleCriterionFunc(sceneIsMissingCriterionHandler(qb, sceneFilter.IsMissing))
|
query.handleCriterionFunc(sceneIsMissingCriterionHandler(qb, sceneFilter.IsMissing))
|
||||||
query.handleCriterionFunc(stringCriterionHandler(sceneFilter.URL, "scenes.url"))
|
query.handleCriterionFunc(stringCriterionHandler(sceneFilter.URL, "scenes.url"))
|
||||||
|
query.handleCriterionFunc(stringCriterionHandler(sceneFilter.StashID, "scene_stash_ids.stash_id"))
|
||||||
|
|
||||||
query.handleCriterionFunc(sceneTagsCriterionHandler(qb, sceneFilter.Tags))
|
query.handleCriterionFunc(sceneTagsCriterionHandler(qb, sceneFilter.Tags))
|
||||||
query.handleCriterionFunc(sceneTagCountCriterionHandler(qb, sceneFilter.TagCount))
|
query.handleCriterionFunc(sceneTagCountCriterionHandler(qb, sceneFilter.TagCount))
|
||||||
@@ -369,7 +370,6 @@ func (qb *sceneQueryBuilder) makeFilter(sceneFilter *models.SceneFilterType) *fi
|
|||||||
query.handleCriterionFunc(scenePerformerCountCriterionHandler(qb, sceneFilter.PerformerCount))
|
query.handleCriterionFunc(scenePerformerCountCriterionHandler(qb, sceneFilter.PerformerCount))
|
||||||
query.handleCriterionFunc(sceneStudioCriterionHandler(qb, sceneFilter.Studios))
|
query.handleCriterionFunc(sceneStudioCriterionHandler(qb, sceneFilter.Studios))
|
||||||
query.handleCriterionFunc(sceneMoviesCriterionHandler(qb, sceneFilter.Movies))
|
query.handleCriterionFunc(sceneMoviesCriterionHandler(qb, sceneFilter.Movies))
|
||||||
query.handleCriterionFunc(sceneStashIDsHandler(qb, sceneFilter.StashID))
|
|
||||||
query.handleCriterionFunc(scenePerformerTagsCriterionHandler(qb, sceneFilter.PerformerTags))
|
query.handleCriterionFunc(scenePerformerTagsCriterionHandler(qb, sceneFilter.PerformerTags))
|
||||||
|
|
||||||
return query
|
return query
|
||||||
@@ -400,6 +400,10 @@ func (qb *sceneQueryBuilder) Query(sceneFilter *models.SceneFilterType, findFilt
|
|||||||
}
|
}
|
||||||
filter := qb.makeFilter(sceneFilter)
|
filter := qb.makeFilter(sceneFilter)
|
||||||
|
|
||||||
|
if sceneFilter.StashID != nil {
|
||||||
|
qb.stashIDRepository().join(filter, "scene_stash_ids", "scenes.id")
|
||||||
|
}
|
||||||
|
|
||||||
query.addFilter(filter)
|
query.addFilter(filter)
|
||||||
|
|
||||||
qb.setSceneSort(&query, findFilter)
|
qb.setSceneSort(&query, findFilter)
|
||||||
@@ -519,9 +523,6 @@ func sceneIsMissingCriterionHandler(qb *sceneQueryBuilder, isMissing *string) cr
|
|||||||
case "tags":
|
case "tags":
|
||||||
qb.tagsRepository().join(f, "tags_join", "scenes.id")
|
qb.tagsRepository().join(f, "tags_join", "scenes.id")
|
||||||
f.addWhere("tags_join.scene_id IS NULL")
|
f.addWhere("tags_join.scene_id IS NULL")
|
||||||
case "stash_id":
|
|
||||||
qb.stashIDRepository().join(f, "scene_stash_ids", "scenes.id")
|
|
||||||
f.addWhere("scene_stash_ids.scene_id IS NULL")
|
|
||||||
default:
|
default:
|
||||||
f.addWhere("(scenes." + *isMissing + " IS NULL OR TRIM(scenes." + *isMissing + ") = '')")
|
f.addWhere("(scenes." + *isMissing + " IS NULL OR TRIM(scenes." + *isMissing + ") = '')")
|
||||||
}
|
}
|
||||||
@@ -598,15 +599,6 @@ func sceneMoviesCriterionHandler(qb *sceneQueryBuilder, movies *models.MultiCrit
|
|||||||
return h.handler(movies)
|
return h.handler(movies)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sceneStashIDsHandler(qb *sceneQueryBuilder, stashID *string) criterionHandlerFunc {
|
|
||||||
return func(f *filterBuilder) {
|
|
||||||
if stashID != nil && *stashID != "" {
|
|
||||||
qb.stashIDRepository().join(f, "scene_stash_ids", "scenes.id")
|
|
||||||
stringLiteralCriterionHandler(stashID, "scene_stash_ids.stash_id")(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func scenePerformerTagsCriterionHandler(qb *sceneQueryBuilder, performerTagsFilter *models.MultiCriterionInput) criterionHandlerFunc {
|
func scenePerformerTagsCriterionHandler(qb *sceneQueryBuilder, performerTagsFilter *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
return func(f *filterBuilder) {
|
return func(f *filterBuilder) {
|
||||||
if performerTagsFilter != nil && len(performerTagsFilter.Value) > 0 {
|
if performerTagsFilter != nil && len(performerTagsFilter.Value) > 0 {
|
||||||
|
|||||||
@@ -178,11 +178,6 @@ func (qb *studioQueryBuilder) Query(studioFilter *models.StudioFilterType, findF
|
|||||||
query.addHaving(havingClause)
|
query.addHaving(havingClause)
|
||||||
}
|
}
|
||||||
|
|
||||||
if stashIDFilter := studioFilter.StashID; stashIDFilter != nil {
|
|
||||||
query.addWhere("studio_stash_ids.stash_id = ?")
|
|
||||||
query.addArg(stashIDFilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
if rating := studioFilter.Rating; rating != nil {
|
if rating := studioFilter.Rating; rating != nil {
|
||||||
query.handleIntCriterionInput(studioFilter.Rating, "studios.rating")
|
query.handleIntCriterionInput(studioFilter.Rating, "studios.rating")
|
||||||
}
|
}
|
||||||
@@ -190,6 +185,7 @@ func (qb *studioQueryBuilder) Query(studioFilter *models.StudioFilterType, findF
|
|||||||
query.handleCountCriterion(studioFilter.ImageCount, studioTable, imageTable, studioIDColumn)
|
query.handleCountCriterion(studioFilter.ImageCount, studioTable, imageTable, studioIDColumn)
|
||||||
query.handleCountCriterion(studioFilter.GalleryCount, studioTable, galleryTable, studioIDColumn)
|
query.handleCountCriterion(studioFilter.GalleryCount, studioTable, galleryTable, studioIDColumn)
|
||||||
query.handleStringCriterionInput(studioFilter.URL, "studios.url")
|
query.handleStringCriterionInput(studioFilter.URL, "studios.url")
|
||||||
|
query.handleStringCriterionInput(studioFilter.StashID, "studio_stash_ids.stash_id")
|
||||||
|
|
||||||
if isMissingFilter := studioFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
|
if isMissingFilter := studioFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
|
||||||
switch *isMissingFilter {
|
switch *isMissingFilter {
|
||||||
|
|||||||
@@ -34,8 +34,7 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "^5.15.2",
|
"@fortawesome/free-regular-svg-icons": "^5.15.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.2",
|
"@fortawesome/free-solid-svg-icons": "^5.15.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||||
"@types/react-select": "^3.1.2",
|
"@types/react-select": "^4.0.8",
|
||||||
"@types/yup": "^0.29.11",
|
|
||||||
"apollo-upload-client": "^14.1.3",
|
"apollo-upload-client": "^14.1.3",
|
||||||
"axios": "0.21.1",
|
"axios": "0.21.1",
|
||||||
"base64-blob": "^1.4.1",
|
"base64-blob": "^1.4.1",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
### ✨ New Features
|
### ✨ New Features
|
||||||
|
* Added stash-box performer tagger.
|
||||||
* Auto-tagger now tags images and galleries.
|
* Auto-tagger now tags images and galleries.
|
||||||
* Added rating field to performers and studios.
|
* Added rating field to performers and studios.
|
||||||
* Support serving UI from specific directory location.
|
* Support serving UI from specific directory location.
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import {
|
|||||||
usePerformerCreate,
|
usePerformerCreate,
|
||||||
useTagCreate,
|
useTagCreate,
|
||||||
queryScrapePerformerURL,
|
queryScrapePerformerURL,
|
||||||
|
useConfiguration,
|
||||||
|
queryStashBoxPerformer,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
@@ -33,6 +35,7 @@ import {
|
|||||||
TagSelect,
|
TagSelect,
|
||||||
} from "src/components/Shared";
|
} from "src/components/Shared";
|
||||||
import { ImageUtils } from "src/utils";
|
import { ImageUtils } from "src/utils";
|
||||||
|
import { getCountryByISO } from "src/utils/country";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { Prompt, useHistory } from "react-router-dom";
|
import { Prompt, useHistory } from "react-router-dom";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
@@ -77,6 +80,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
const [scrapedPerformer, setScrapedPerformer] = useState<
|
const [scrapedPerformer, setScrapedPerformer] = useState<
|
||||||
GQL.ScrapedPerformer | undefined
|
GQL.ScrapedPerformer | undefined
|
||||||
>();
|
>();
|
||||||
|
const stashConfig = useConfiguration();
|
||||||
|
|
||||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
||||||
|
|
||||||
@@ -544,15 +548,57 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onScrapeStashBoxClicked(stashBoxIndex: number) {
|
||||||
|
if (!performer.id) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await queryStashBoxPerformer(stashBoxIndex, performer.id);
|
||||||
|
if (!result.data || !result.data.queryStashBoxPerformer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.data.queryStashBoxPerformer.length > 0) {
|
||||||
|
const performerResult =
|
||||||
|
result.data.queryStashBoxPerformer[0].results[0];
|
||||||
|
setScrapedPerformer({
|
||||||
|
...performerResult,
|
||||||
|
image: performerResult.images?.[0] ?? undefined,
|
||||||
|
country: getCountryByISO(performerResult.country),
|
||||||
|
__typename: "ScrapedPerformer",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Toast.success({
|
||||||
|
content: "No performers found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderScraperMenu() {
|
function renderScraperMenu() {
|
||||||
if (!performer) {
|
if (!performer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const stashBoxes = stashConfig.data?.configuration.general.stashBoxes ?? [];
|
||||||
|
|
||||||
const popover = (
|
const popover = (
|
||||||
<Popover id="performer-scraper-popover">
|
<Popover id="performer-scraper-popover">
|
||||||
<Popover.Content>
|
<Popover.Content>
|
||||||
<>
|
<>
|
||||||
|
{stashBoxes.map((s, index) => (
|
||||||
|
<div key={s.endpoint}>
|
||||||
|
<Button
|
||||||
|
className="minimal"
|
||||||
|
onClick={() => onScrapeStashBoxClicked(index)}
|
||||||
|
>
|
||||||
|
{s.name ?? "Stash-Box"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
{queryableScrapers
|
{queryableScrapers
|
||||||
? queryableScrapers.map((s) => (
|
? queryableScrapers.map((s) => (
|
||||||
<div key={s.name}>
|
<div key={s.name}>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { usePerformersList } from "src/hooks";
|
|||||||
import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook";
|
import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { DisplayMode } from "src/models/list-filter/types";
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
|
import { PerformerTagger } from "src/components/Tagger";
|
||||||
import { ExportDialog, DeleteEntityDialog } from "src/components/Shared";
|
import { ExportDialog, DeleteEntityDialog } from "src/components/Shared";
|
||||||
import { PerformerCard } from "./PerformerCard";
|
import { PerformerCard } from "./PerformerCard";
|
||||||
import { PerformerListTable } from "./PerformerListTable";
|
import { PerformerListTable } from "./PerformerListTable";
|
||||||
@@ -184,6 +185,11 @@ export const PerformerList: React.FC<IPerformerList> = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (filter.displayMode === DisplayMode.Tagger) {
|
||||||
|
return (
|
||||||
|
<PerformerTagger performers={result.data.findPerformers.performers} />
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return listData.template;
|
return listData.template;
|
||||||
|
|||||||
@@ -284,10 +284,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// function onStashBoxQueryClicked(/* stashBoxIndex: number */) {
|
|
||||||
// TODO
|
|
||||||
// }
|
|
||||||
|
|
||||||
async function onScrapeClicked(scraper: GQL.Scraper) {
|
async function onScrapeClicked(scraper: GQL.Scraper) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -361,7 +357,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
key={s.endpoint}
|
key={s.endpoint}
|
||||||
onClick={() => onScrapeStashBoxClicked(index)}
|
onClick={() => onScrapeStashBoxClicked(index)}
|
||||||
>
|
>
|
||||||
stash-box
|
{s.name ?? "Stash-Box"}
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
))}
|
))}
|
||||||
{queryableScrapers.map((s) => (
|
{queryableScrapers.map((s) => (
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
return "Running Plugin Operation";
|
return "Running Plugin Operation";
|
||||||
case "Migrate":
|
case "Migrate":
|
||||||
return "Migrating";
|
return "Migrating";
|
||||||
|
case "Stash-Box Performer Batch Operation":
|
||||||
|
return "Tagging performers from Stash-Box instance";
|
||||||
default:
|
default:
|
||||||
return "Idle";
|
return "Idle";
|
||||||
}
|
}
|
||||||
|
|||||||
63
ui/v2.5/src/components/Tagger/PerformerFieldSelector.tsx
Normal file
63
ui/v2.5/src/components/Tagger/PerformerFieldSelector.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Button } from "react-bootstrap";
|
||||||
|
|
||||||
|
import { Modal, Icon } from "src/components/Shared";
|
||||||
|
import { TextUtils } from "src/utils";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
fields: string[];
|
||||||
|
show: boolean;
|
||||||
|
excludedFields: string[];
|
||||||
|
onSelect: (fields: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PerformerFieldSelect: React.FC<IProps> = ({
|
||||||
|
fields,
|
||||||
|
show,
|
||||||
|
excludedFields,
|
||||||
|
onSelect,
|
||||||
|
}) => {
|
||||||
|
const [excluded, setExcluded] = useState<Record<string, boolean>>(
|
||||||
|
excludedFields.reduce((dict, field) => ({ ...dict, [field]: true }), {})
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleField = (name: string) =>
|
||||||
|
setExcluded({
|
||||||
|
...excluded,
|
||||||
|
[name]: !excluded[name],
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderField = (name: string) => (
|
||||||
|
<div className="mb-1" key={name}>
|
||||||
|
<Button
|
||||||
|
onClick={() => toggleField(name)}
|
||||||
|
variant="secondary"
|
||||||
|
className={excluded[name] ? "text-muted" : "text-success"}
|
||||||
|
>
|
||||||
|
<Icon icon={excluded[name] ? "times" : "check"} />
|
||||||
|
</Button>
|
||||||
|
<span className="ml-3">{TextUtils.capitalize(name)}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
show={show}
|
||||||
|
icon="list"
|
||||||
|
dialogClassName="FieldSelect"
|
||||||
|
accept={{
|
||||||
|
text: "Save",
|
||||||
|
onClick: () =>
|
||||||
|
onSelect(Object.keys(excluded).filter((f) => excluded[f])),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h4>Select tagged fields</h4>
|
||||||
|
<div className="mb-2">
|
||||||
|
These fields will be tagged by default. Click the button to toggle.
|
||||||
|
</div>
|
||||||
|
{fields.map((f) => renderField(f))}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PerformerFieldSelect;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Button } from "react-bootstrap";
|
import { Button } from "react-bootstrap";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
|
import { IconName } from "@fortawesome/fontawesome-svg-core";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
LoadingIndicator,
|
LoadingIndicator,
|
||||||
@@ -10,26 +11,43 @@ import {
|
|||||||
} from "src/components/Shared";
|
} from "src/components/Shared";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { genderToString } from "src/core/StashService";
|
import { genderToString } from "src/core/StashService";
|
||||||
|
import { TextUtils } from "src/utils";
|
||||||
import { IStashBoxPerformer } from "./utils";
|
import { IStashBoxPerformer } from "./utils";
|
||||||
|
|
||||||
interface IPerformerModalProps {
|
interface IPerformerModalProps {
|
||||||
performer: IStashBoxPerformer;
|
performer: IStashBoxPerformer;
|
||||||
modalVisible: boolean;
|
modalVisible: boolean;
|
||||||
showModal: (show: boolean) => void;
|
closeModal: () => void;
|
||||||
handlePerformerCreate: (imageIndex: number) => void;
|
handlePerformerCreate: (imageIndex: number, excludedFields: string[]) => void;
|
||||||
|
excludedPerformerFields?: string[];
|
||||||
|
header: string;
|
||||||
|
icon: IconName;
|
||||||
|
create?: boolean;
|
||||||
|
endpoint: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PerformerModal: React.FC<IPerformerModalProps> = ({
|
const PerformerModal: React.FC<IPerformerModalProps> = ({
|
||||||
modalVisible,
|
modalVisible,
|
||||||
performer,
|
performer,
|
||||||
handlePerformerCreate,
|
handlePerformerCreate,
|
||||||
showModal,
|
closeModal,
|
||||||
|
excludedPerformerFields = [],
|
||||||
|
header,
|
||||||
|
icon,
|
||||||
|
create = false,
|
||||||
|
endpoint,
|
||||||
}) => {
|
}) => {
|
||||||
const [imageIndex, setImageIndex] = useState(0);
|
const [imageIndex, setImageIndex] = useState(0);
|
||||||
const [imageState, setImageState] = useState<
|
const [imageState, setImageState] = useState<
|
||||||
"loading" | "error" | "loaded" | "empty"
|
"loading" | "error" | "loaded" | "empty"
|
||||||
>("empty");
|
>("empty");
|
||||||
const [loadDict, setLoadDict] = useState<Record<number, boolean>>({});
|
const [loadDict, setLoadDict] = useState<Record<number, boolean>>({});
|
||||||
|
const [excluded, setExcluded] = useState<Record<string, boolean>>(
|
||||||
|
excludedPerformerFields.reduce(
|
||||||
|
(dict, field) => ({ ...dict, [field]: true }),
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const { images } = performer;
|
const { images } = performer;
|
||||||
|
|
||||||
@@ -51,106 +69,101 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
|
|||||||
};
|
};
|
||||||
const handleError = () => setImageState("error");
|
const handleError = () => setImageState("error");
|
||||||
|
|
||||||
|
const toggleField = (name: string) =>
|
||||||
|
setExcluded({
|
||||||
|
...excluded,
|
||||||
|
[name]: !excluded[name],
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderField = (
|
||||||
|
name: string,
|
||||||
|
text: string | null | undefined,
|
||||||
|
truncate: boolean = true
|
||||||
|
) =>
|
||||||
|
text && (
|
||||||
|
<div className="row no-gutters">
|
||||||
|
<div className="col-5 performer-create-modal-field" key={name}>
|
||||||
|
{!create && (
|
||||||
|
<Button
|
||||||
|
onClick={() => toggleField(name)}
|
||||||
|
variant="secondary"
|
||||||
|
className={excluded[name] ? "text-muted" : "text-success"}
|
||||||
|
>
|
||||||
|
<Icon icon={excluded[name] ? "times" : "check"} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<strong>{TextUtils.capitalize(name)}:</strong>
|
||||||
|
</div>
|
||||||
|
{truncate ? (
|
||||||
|
<TruncatedText className="col-7" text={text} />
|
||||||
|
) : (
|
||||||
|
<span className="col-7">{text}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const base = endpoint.match(/https?:\/\/.*?\//)?.[0];
|
||||||
|
const link = base ? `${base}performers/${performer.stash_id}` : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
show={modalVisible}
|
show={modalVisible}
|
||||||
accept={{
|
accept={{
|
||||||
text: "Save",
|
text: "Save",
|
||||||
onClick: () => handlePerformerCreate(imageIndex),
|
onClick: () =>
|
||||||
|
handlePerformerCreate(
|
||||||
|
imageIndex,
|
||||||
|
create ? [] : Object.keys(excluded).filter((key) => excluded[key])
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
cancel={{ onClick: () => showModal(false), variant: "secondary" }}
|
cancel={{ onClick: () => closeModal(), variant: "secondary" }}
|
||||||
onHide={() => showModal(false)}
|
onHide={() => closeModal()}
|
||||||
dialogClassName="performer-create-modal"
|
dialogClassName="performer-create-modal"
|
||||||
|
icon={icon}
|
||||||
|
header={header}
|
||||||
>
|
>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-6">
|
<div className="col-7">
|
||||||
<div className="row no-gutters mb-4">
|
{renderField("name", performer.name)}
|
||||||
<strong>Performer information</strong>
|
{renderField("gender", genderToString(performer.gender))}
|
||||||
</div>
|
{renderField("birthdate", performer.birthdate)}
|
||||||
<div className="row no-gutters">
|
{renderField("death_date", performer.death_date)}
|
||||||
<strong className="col-6">Name:</strong>
|
{renderField("ethnicity", performer.ethnicity)}
|
||||||
<TruncatedText className="col-6" text={performer.name} />
|
{renderField("country", performer.country)}
|
||||||
</div>
|
{renderField("hair_color", performer.hair_color)}
|
||||||
<div className="row no-gutters">
|
{renderField("eye_color", performer.eye_color)}
|
||||||
<strong className="col-6">Gender:</strong>
|
{renderField("height", performer.height)}
|
||||||
<TruncatedText
|
{renderField("weight", performer.weight)}
|
||||||
className="col-6 text-capitalize"
|
{renderField("measurements", performer.measurements)}
|
||||||
text={performer.gender && genderToString(performer.gender)}
|
{performer?.gender !== GQL.GenderEnum.Male &&
|
||||||
/>
|
renderField("fake_tits", performer.fake_tits)}
|
||||||
</div>
|
{renderField("career_length", performer.career_length)}
|
||||||
<div className="row no-gutters">
|
{renderField("tattoos", performer.tattoos, false)}
|
||||||
<strong className="col-6">Birthdate:</strong>
|
{renderField("piercings", performer.piercings, false)}
|
||||||
<TruncatedText
|
{link && (
|
||||||
className="col-6"
|
<h6 className="mt-2">
|
||||||
text={performer.birthdate ?? "Unknown"}
|
<a href={link} target="_blank" rel="noopener noreferrer">
|
||||||
/>
|
Stash-Box Source
|
||||||
</div>
|
<Icon icon="external-link-alt" className="ml-2" />
|
||||||
<div className="row no-gutters">
|
</a>
|
||||||
<strong className="col-6">Death Date:</strong>
|
</h6>
|
||||||
<TruncatedText
|
|
||||||
className="col-6"
|
|
||||||
text={performer.death_date ?? "Unknown"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="row no-gutters">
|
|
||||||
<strong className="col-6">Ethnicity:</strong>
|
|
||||||
<TruncatedText
|
|
||||||
className="col-6 text-capitalize"
|
|
||||||
text={performer.ethnicity}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="row no-gutters">
|
|
||||||
<strong className="col-6">Country:</strong>
|
|
||||||
<TruncatedText className="col-6" text={performer.country ?? ""} />
|
|
||||||
</div>
|
|
||||||
<div className="row no-gutters">
|
|
||||||
<strong className="col-6">Hair Color:</strong>
|
|
||||||
<TruncatedText
|
|
||||||
className="col-6 text-capitalize"
|
|
||||||
text={performer.hair_color}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="row no-gutters">
|
|
||||||
<strong className="col-6">Eye Color:</strong>
|
|
||||||
<TruncatedText
|
|
||||||
className="col-6 text-capitalize"
|
|
||||||
text={performer.eye_color}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="row no-gutters">
|
|
||||||
<strong className="col-6">Height:</strong>
|
|
||||||
<TruncatedText className="col-6" text={performer.height} />
|
|
||||||
</div>
|
|
||||||
<div className="row no-gutters">
|
|
||||||
<strong className="col-6">Weight:</strong>
|
|
||||||
<TruncatedText className="col-6" text={performer.weight} />
|
|
||||||
</div>
|
|
||||||
<div className="row no-gutters">
|
|
||||||
<strong className="col-6">Measurements:</strong>
|
|
||||||
<TruncatedText className="col-6" text={performer.measurements} />
|
|
||||||
</div>
|
|
||||||
{performer?.gender !== GQL.GenderEnum.Male && (
|
|
||||||
<div className="row no-gutters">
|
|
||||||
<strong className="col-6">Fake Tits:</strong>
|
|
||||||
<TruncatedText className="col-6" text={performer.fake_tits} />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<div className="row no-gutters">
|
|
||||||
<strong className="col-6">Career Length:</strong>
|
|
||||||
<TruncatedText className="col-6" text={performer.career_length} />
|
|
||||||
</div>
|
|
||||||
<div className="row no-gutters">
|
|
||||||
<strong className="col-6">Tattoos:</strong>
|
|
||||||
<TruncatedText className="col-6" text={performer.tattoos} />
|
|
||||||
</div>
|
|
||||||
<div className="row no-gutters ">
|
|
||||||
<strong className="col-6">Piercings:</strong>
|
|
||||||
<TruncatedText className="col-6" text={performer.piercings} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{images.length > 0 && (
|
{images.length > 0 && (
|
||||||
<div className="col-6 image-selection">
|
<div className="col-5 image-selection">
|
||||||
<div className="performer-image">
|
<div className="performer-image">
|
||||||
|
{!create && (
|
||||||
|
<Button
|
||||||
|
onClick={() => toggleField("image")}
|
||||||
|
variant="secondary"
|
||||||
|
className={cx(
|
||||||
|
"performer-image-exclude",
|
||||||
|
excluded.image ? "text-muted" : "text-success"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon icon={excluded.image ? "times" : "check"} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<img
|
<img
|
||||||
src={images[imageIndex]}
|
src={images[imageIndex]}
|
||||||
className={cx({ "d-none": imageState !== "loaded" })}
|
className={cx({ "d-none": imageState !== "loaded" })}
|
||||||
@@ -167,24 +180,16 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="d-flex mt-2">
|
<div className="d-flex mt-3">
|
||||||
<Button
|
<Button onClick={setPrev} disabled={images.length === 1}>
|
||||||
className="mr-auto"
|
|
||||||
onClick={setPrev}
|
|
||||||
disabled={images.length === 1}
|
|
||||||
>
|
|
||||||
<Icon icon="arrow-left" />
|
<Icon icon="arrow-left" />
|
||||||
</Button>
|
</Button>
|
||||||
<h5>
|
<h5 className="flex-grow-1">
|
||||||
Select performer image
|
Select performer image
|
||||||
<br />
|
<br />
|
||||||
{imageIndex + 1} of {images.length}
|
{imageIndex + 1} of {images.length}
|
||||||
</h5>
|
</h5>
|
||||||
<Button
|
<Button onClick={setNext} disabled={images.length === 1}>
|
||||||
className="ml-auto"
|
|
||||||
onClick={setNext}
|
|
||||||
disabled={images.length === 1}
|
|
||||||
>
|
|
||||||
<Icon icon="arrow-right" />
|
<Icon icon="arrow-right" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import cx from "classnames";
|
|||||||
import { SuccessIcon, PerformerSelect } from "src/components/Shared";
|
import { SuccessIcon, PerformerSelect } from "src/components/Shared";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { ValidTypes } from "src/components/Shared/Select";
|
import { ValidTypes } from "src/components/Shared/Select";
|
||||||
import { IStashBoxPerformer } from "./utils";
|
import { IStashBoxPerformer, filterPerformer } from "./utils";
|
||||||
|
|
||||||
import PerformerModal from "./PerformerModal";
|
import PerformerModal from "./PerformerModal";
|
||||||
|
|
||||||
@@ -18,11 +18,13 @@ export type PerformerOperation =
|
|||||||
interface IPerformerResultProps {
|
interface IPerformerResultProps {
|
||||||
performer: IStashBoxPerformer;
|
performer: IStashBoxPerformer;
|
||||||
setPerformer: (data: PerformerOperation) => void;
|
setPerformer: (data: PerformerOperation) => void;
|
||||||
|
endpoint: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PerformerResult: React.FC<IPerformerResultProps> = ({
|
const PerformerResult: React.FC<IPerformerResultProps> = ({
|
||||||
performer,
|
performer,
|
||||||
setPerformer,
|
setPerformer,
|
||||||
|
endpoint,
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedPerformer, setSelectedPerformer] = useState<string | null>();
|
const [selectedPerformer, setSelectedPerformer] = useState<string | null>();
|
||||||
const [selectedSource, setSelectedSource] = useState<
|
const [selectedSource, setSelectedSource] = useState<
|
||||||
@@ -37,7 +39,10 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
|
|||||||
{
|
{
|
||||||
variables: {
|
variables: {
|
||||||
performer_filter: {
|
performer_filter: {
|
||||||
stash_id: performer.stash_id,
|
stash_id: {
|
||||||
|
value: performer.stash_id,
|
||||||
|
modifier: GQL.CriterionModifier.Equals,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -74,14 +79,20 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePerformerCreate = (imageIndex: number) => {
|
const handlePerformerCreate = (
|
||||||
|
imageIndex: number,
|
||||||
|
excludedFields: string[]
|
||||||
|
) => {
|
||||||
const selectedImage = performer.images[imageIndex];
|
const selectedImage = performer.images[imageIndex];
|
||||||
const images = selectedImage ? [selectedImage] : [];
|
const images = selectedImage ? [selectedImage] : [];
|
||||||
|
|
||||||
setSelectedSource("create");
|
setSelectedSource("create");
|
||||||
setPerformer({
|
setPerformer({
|
||||||
type: "create",
|
type: "create",
|
||||||
data: {
|
data: {
|
||||||
...performer,
|
...filterPerformer(performer, excludedFields),
|
||||||
|
name: performer.name,
|
||||||
|
stash_id: performer.stash_id,
|
||||||
images,
|
images,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -117,10 +128,14 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="row no-gutters align-items-center mt-2">
|
<div className="row no-gutters align-items-center mt-2">
|
||||||
<PerformerModal
|
<PerformerModal
|
||||||
showModal={showModal}
|
closeModal={() => showModal(false)}
|
||||||
modalVisible={modalVisible}
|
modalVisible={modalVisible}
|
||||||
performer={performer}
|
performer={performer}
|
||||||
handlePerformerCreate={handlePerformerCreate}
|
handlePerformerCreate={handlePerformerCreate}
|
||||||
|
icon="star"
|
||||||
|
header="Create Performer"
|
||||||
|
create
|
||||||
|
endpoint={endpoint}
|
||||||
/>
|
/>
|
||||||
<div className="entity-name">
|
<div className="entity-name">
|
||||||
Performer:
|
Performer:
|
||||||
|
|||||||
@@ -416,6 +416,7 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
|||||||
setPerformer(data, performer.stash_id)
|
setPerformer(data, performer.stash_id)
|
||||||
}
|
}
|
||||||
key={`${scene.stash_id}${performer.stash_id}`}
|
key={`${scene.stash_id}${performer.stash_id}`}
|
||||||
|
endpoint={endpoint}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<div className="row no-gutters mt-2 align-items-center justify-content-end">
|
<div className="row no-gutters mt-2 align-items-center justify-content-end">
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ const StudioResult: React.FC<IStudioResultProps> = ({ studio, setStudio }) => {
|
|||||||
} = GQL.useFindStudiosQuery({
|
} = GQL.useFindStudiosQuery({
|
||||||
variables: {
|
variables: {
|
||||||
studio_filter: {
|
studio_filter: {
|
||||||
stash_id: studio?.stash_id,
|
stash_id: {
|
||||||
|
value: studio?.stash_id ?? "no-stashid",
|
||||||
|
modifier: GQL.CriterionModifier.Equals,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import { useLocalForage } from "src/hooks";
|
|||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { LoadingIndicator, TruncatedText } from "src/components/Shared";
|
import { LoadingIndicator, TruncatedText } from "src/components/Shared";
|
||||||
import {
|
import {
|
||||||
stashBoxQuery,
|
stashBoxSceneQuery,
|
||||||
stashBoxBatchQuery,
|
stashBoxSceneBatchQuery,
|
||||||
useConfiguration,
|
useConfiguration,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { Manual } from "src/components/Help/Manual";
|
import { Manual } from "src/components/Help/Manual";
|
||||||
@@ -190,7 +190,7 @@ const TaggerList: React.FC<ITaggerListProps> = ({
|
|||||||
}, [config.mode, config.blacklist]);
|
}, [config.mode, config.blacklist]);
|
||||||
|
|
||||||
const doBoxSearch = (sceneID: string, searchVal: string) => {
|
const doBoxSearch = (sceneID: string, searchVal: string) => {
|
||||||
stashBoxQuery(searchVal, selectedEndpoint.index)
|
stashBoxSceneQuery(searchVal, selectedEndpoint.index)
|
||||||
.then((queryData) => {
|
.then((queryData) => {
|
||||||
const s = selectScenes(queryData.data?.queryStashBoxScene);
|
const s = selectScenes(queryData.data?.queryStashBoxScene);
|
||||||
setSearchResults({
|
setSearchResults({
|
||||||
@@ -257,7 +257,7 @@ const TaggerList: React.FC<ITaggerListProps> = ({
|
|||||||
.filter((s) => s.stash_ids.length === 0)
|
.filter((s) => s.stash_ids.length === 0)
|
||||||
.map((s) => s.id);
|
.map((s) => s.id);
|
||||||
|
|
||||||
const results = await stashBoxBatchQuery(
|
const results = await stashBoxSceneBatchQuery(
|
||||||
sceneIDs,
|
sceneIDs,
|
||||||
selectedEndpoint.index
|
selectedEndpoint.index
|
||||||
).catch(() => {
|
).catch(() => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const DEFAULT_BLACKLIST = [
|
|||||||
"\\[",
|
"\\[",
|
||||||
"\\]",
|
"\\]",
|
||||||
];
|
];
|
||||||
|
export const DEFAULT_EXCLUDED_PERFORMER_FIELDS = ["name"];
|
||||||
|
|
||||||
export const initialConfig: ITaggerConfig = {
|
export const initialConfig: ITaggerConfig = {
|
||||||
blacklist: DEFAULT_BLACKLIST,
|
blacklist: DEFAULT_BLACKLIST,
|
||||||
@@ -19,6 +20,7 @@ export const initialConfig: ITaggerConfig = {
|
|||||||
setTags: false,
|
setTags: false,
|
||||||
tagOperation: "merge",
|
tagOperation: "merge",
|
||||||
fingerprintQueue: {},
|
fingerprintQueue: {},
|
||||||
|
excludedPerformerFields: DEFAULT_EXCLUDED_PERFORMER_FIELDS,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ParseMode = "auto" | "filename" | "dir" | "path" | "metadata";
|
export type ParseMode = "auto" | "filename" | "dir" | "path" | "metadata";
|
||||||
@@ -39,4 +41,22 @@ export interface ITaggerConfig {
|
|||||||
tagOperation: string;
|
tagOperation: string;
|
||||||
selectedEndpoint?: string;
|
selectedEndpoint?: string;
|
||||||
fingerprintQueue: Record<string, string[]>;
|
fingerprintQueue: Record<string, string[]>;
|
||||||
|
excludedPerformerFields?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PERFORMER_FIELDS = [
|
||||||
|
"name",
|
||||||
|
"aliases",
|
||||||
|
"image",
|
||||||
|
"gender",
|
||||||
|
"birthdate",
|
||||||
|
"ethnicity",
|
||||||
|
"country",
|
||||||
|
"eye_color",
|
||||||
|
"height",
|
||||||
|
"measurements",
|
||||||
|
"fake_tits",
|
||||||
|
"career_length",
|
||||||
|
"tattoos",
|
||||||
|
"piercings",
|
||||||
|
];
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export { Tagger as default } from "./Tagger";
|
export { Tagger as default } from "./Tagger";
|
||||||
|
export { PerformerTagger } from "./performers/PerformerTagger";
|
||||||
|
|||||||
103
ui/v2.5/src/components/Tagger/performers/Config.tsx
Normal file
103
ui/v2.5/src/components/Tagger/performers/Config.tsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import React, { Dispatch, useState } from "react";
|
||||||
|
import { Badge, Button, Card, Collapse, Form } from "react-bootstrap";
|
||||||
|
import { useConfiguration } from "src/core/StashService";
|
||||||
|
|
||||||
|
import { TextUtils } from "src/utils";
|
||||||
|
import { ITaggerConfig, PERFORMER_FIELDS } from "../constants";
|
||||||
|
import PerformerFieldSelector from "../PerformerFieldSelector";
|
||||||
|
|
||||||
|
interface IConfigProps {
|
||||||
|
show: boolean;
|
||||||
|
config: ITaggerConfig;
|
||||||
|
setConfig: Dispatch<ITaggerConfig>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
|
||||||
|
const stashConfig = useConfiguration();
|
||||||
|
const [showExclusionModal, setShowExclusionModal] = useState(false);
|
||||||
|
|
||||||
|
const excludedFields = config.excludedPerformerFields ?? [];
|
||||||
|
|
||||||
|
const handleInstanceSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const selectedEndpoint = e.currentTarget.value;
|
||||||
|
setConfig({
|
||||||
|
...config,
|
||||||
|
selectedEndpoint,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const stashBoxes = stashConfig.data?.configuration.general.stashBoxes ?? [];
|
||||||
|
|
||||||
|
const handleFieldSelect = (fields: string[]) => {
|
||||||
|
setConfig({ ...config, excludedPerformerFields: fields });
|
||||||
|
setShowExclusionModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Collapse in={show}>
|
||||||
|
<Card>
|
||||||
|
<div className="row">
|
||||||
|
<h4 className="col-12">Configuration</h4>
|
||||||
|
<hr className="w-100" />
|
||||||
|
<div className="col-md-6">
|
||||||
|
<Form.Group controlId="excluded-performer-fields">
|
||||||
|
<h6>Excluded fields:</h6>
|
||||||
|
<span>
|
||||||
|
{excludedFields.length > 0
|
||||||
|
? excludedFields.map((f) => (
|
||||||
|
<Badge variant="secondary" className="tag-item">
|
||||||
|
{TextUtils.capitalize(f)}
|
||||||
|
</Badge>
|
||||||
|
))
|
||||||
|
: "No fields are excluded"}
|
||||||
|
</span>
|
||||||
|
<Form.Text>
|
||||||
|
These fields will not be changed when updating performers.
|
||||||
|
</Form.Text>
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowExclusionModal(true)}
|
||||||
|
className="mt-2"
|
||||||
|
>
|
||||||
|
Edit Excluded Fields
|
||||||
|
</Button>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group
|
||||||
|
controlId="stash-box-endpoint"
|
||||||
|
className="align-items-center row no-gutters mt-4"
|
||||||
|
>
|
||||||
|
<Form.Label className="mr-4">
|
||||||
|
Active stash-box instance:
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
as="select"
|
||||||
|
value={config.selectedEndpoint}
|
||||||
|
className="col-md-4 col-6 input-control"
|
||||||
|
disabled={!stashBoxes.length}
|
||||||
|
onChange={handleInstanceSelect}
|
||||||
|
>
|
||||||
|
{!stashBoxes.length && <option>No instances found</option>}
|
||||||
|
{stashConfig.data?.configuration.general.stashBoxes.map(
|
||||||
|
(i) => (
|
||||||
|
<option value={i.endpoint} key={i.endpoint}>
|
||||||
|
{i.endpoint}
|
||||||
|
</option>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Form.Control>
|
||||||
|
</Form.Group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Collapse>
|
||||||
|
<PerformerFieldSelector
|
||||||
|
fields={PERFORMER_FIELDS}
|
||||||
|
show={showExclusionModal}
|
||||||
|
onSelect={handleFieldSelect}
|
||||||
|
excludedFields={excludedFields}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Config;
|
||||||
611
ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx
Executable file
611
ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx
Executable file
@@ -0,0 +1,611 @@
|
|||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
import { Button, Card, Form, InputGroup, ProgressBar } from "react-bootstrap";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { HashLink } from "react-router-hash-link";
|
||||||
|
import { useLocalForage } from "src/hooks";
|
||||||
|
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { LoadingIndicator, Modal } from "src/components/Shared";
|
||||||
|
import {
|
||||||
|
stashBoxPerformerQuery,
|
||||||
|
useConfiguration,
|
||||||
|
useMetadataUpdate,
|
||||||
|
} from "src/core/StashService";
|
||||||
|
import { Manual } from "src/components/Help/Manual";
|
||||||
|
|
||||||
|
import StashSearchResult from "./StashSearchResult";
|
||||||
|
import PerformerConfig from "./Config";
|
||||||
|
import { LOCAL_FORAGE_KEY, ITaggerConfig, initialConfig } from "../constants";
|
||||||
|
import {
|
||||||
|
IStashBoxPerformer,
|
||||||
|
selectPerformers,
|
||||||
|
filterPerformer,
|
||||||
|
} from "../utils";
|
||||||
|
import PerformerModal from "../PerformerModal";
|
||||||
|
import { useUpdatePerformer } from "../queries";
|
||||||
|
|
||||||
|
const CLASSNAME = "PerformerTagger";
|
||||||
|
|
||||||
|
interface IPerformerTaggerListProps {
|
||||||
|
performers: GQL.PerformerDataFragment[];
|
||||||
|
selectedEndpoint: { endpoint: string; index: number };
|
||||||
|
isIdle: boolean;
|
||||||
|
config: ITaggerConfig;
|
||||||
|
stashBoxes?: GQL.StashBox[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const PerformerTaggerList: React.FC<IPerformerTaggerListProps> = ({
|
||||||
|
performers,
|
||||||
|
selectedEndpoint,
|
||||||
|
isIdle,
|
||||||
|
config,
|
||||||
|
stashBoxes,
|
||||||
|
}) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [searchResults, setSearchResults] = useState<
|
||||||
|
Record<string, IStashBoxPerformer[]>
|
||||||
|
>({});
|
||||||
|
const [searchErrors, setSearchErrors] = useState<
|
||||||
|
Record<string, string | undefined>
|
||||||
|
>({});
|
||||||
|
const [taggedPerformers, setTaggedPerformers] = useState<
|
||||||
|
Record<string, Partial<GQL.SlimPerformerDataFragment>>
|
||||||
|
>({});
|
||||||
|
const [queries, setQueries] = useState<Record<string, string>>({});
|
||||||
|
const [queryAll, setQueryAll] = useState(false);
|
||||||
|
|
||||||
|
const [refresh, setRefresh] = useState(false);
|
||||||
|
const { data: allPerformers } = GQL.useFindPerformersQuery({
|
||||||
|
variables: {
|
||||||
|
performer_filter: {
|
||||||
|
stash_id: {
|
||||||
|
value: "",
|
||||||
|
modifier: refresh
|
||||||
|
? GQL.CriterionModifier.NotNull
|
||||||
|
: GQL.CriterionModifier.IsNull,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filter: {
|
||||||
|
per_page: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [showBatchAdd, setShowBatchAdd] = useState(false);
|
||||||
|
const [showBatchUpdate, setShowBatchUpdate] = useState(false);
|
||||||
|
const performerInput = useRef<HTMLTextAreaElement | null>(null);
|
||||||
|
const [doBatchQuery] = GQL.useStashBoxBatchPerformerTagMutation();
|
||||||
|
|
||||||
|
const [error, setError] = useState<
|
||||||
|
Record<string, { message?: string; details?: string } | undefined>
|
||||||
|
>({});
|
||||||
|
const [loadingUpdate, setLoadingUpdate] = useState<string | undefined>();
|
||||||
|
const [modalPerformer, setModalPerformer] = useState<
|
||||||
|
IStashBoxPerformer | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
|
const doBoxSearch = (performerID: string, searchVal: string) => {
|
||||||
|
stashBoxPerformerQuery(searchVal, selectedEndpoint.index)
|
||||||
|
.then((queryData) => {
|
||||||
|
const s = selectPerformers(
|
||||||
|
queryData.data?.queryStashBoxPerformer?.[0].results ?? []
|
||||||
|
);
|
||||||
|
setSearchResults({
|
||||||
|
...searchResults,
|
||||||
|
[performerID]: s,
|
||||||
|
});
|
||||||
|
setSearchErrors({
|
||||||
|
...searchErrors,
|
||||||
|
[performerID]: undefined,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setLoading(false);
|
||||||
|
// Destructure to remove existing result
|
||||||
|
const { [performerID]: unassign, ...results } = searchResults;
|
||||||
|
setSearchResults(results);
|
||||||
|
setSearchErrors({
|
||||||
|
...searchErrors,
|
||||||
|
[performerID]: "Network Error",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const doBoxUpdate = (
|
||||||
|
performerID: string,
|
||||||
|
stashID: string,
|
||||||
|
endpointIndex: number
|
||||||
|
) => {
|
||||||
|
setLoadingUpdate(stashID);
|
||||||
|
setError({
|
||||||
|
...error,
|
||||||
|
[performerID]: undefined,
|
||||||
|
});
|
||||||
|
stashBoxPerformerQuery(stashID, endpointIndex)
|
||||||
|
.then((queryData) => {
|
||||||
|
const data = selectPerformers(
|
||||||
|
queryData.data?.queryStashBoxPerformer?.[0].results ?? []
|
||||||
|
);
|
||||||
|
if (data.length > 0) {
|
||||||
|
setModalPerformer({
|
||||||
|
...data[0],
|
||||||
|
id: performerID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => setLoadingUpdate(undefined));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBatchAdd = () => {
|
||||||
|
if (performerInput.current) {
|
||||||
|
const names = performerInput.current.value
|
||||||
|
.split(",")
|
||||||
|
.map((n) => n.trim())
|
||||||
|
.filter((n) => n.length > 0);
|
||||||
|
|
||||||
|
if (names.length > 0) {
|
||||||
|
doBatchQuery({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
performer_names: names,
|
||||||
|
endpoint: selectedEndpoint.index,
|
||||||
|
refresh: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setShowBatchAdd(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBatchUpdate = () => {
|
||||||
|
const ids = !queryAll ? performers.map((p) => p.id) : undefined;
|
||||||
|
doBatchQuery({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
performer_ids: ids,
|
||||||
|
endpoint: selectedEndpoint.index,
|
||||||
|
refresh,
|
||||||
|
exclude_fields: config.excludedPerformerFields ?? [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setShowBatchUpdate(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTaggedPerformer = (
|
||||||
|
performer: Pick<GQL.SlimPerformerDataFragment, "id"> &
|
||||||
|
Partial<Omit<GQL.SlimPerformerDataFragment, "id">>
|
||||||
|
) => {
|
||||||
|
setTaggedPerformers({
|
||||||
|
...taggedPerformers,
|
||||||
|
[performer.id]: performer,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePerformer = useUpdatePerformer();
|
||||||
|
|
||||||
|
const handlePerformerUpdate = async (
|
||||||
|
imageIndex: number,
|
||||||
|
excludedFields: string[]
|
||||||
|
) => {
|
||||||
|
const performerData = modalPerformer;
|
||||||
|
setModalPerformer(undefined);
|
||||||
|
if (performerData?.id) {
|
||||||
|
const filteredData = filterPerformer(performerData, excludedFields);
|
||||||
|
|
||||||
|
const res = await updatePerformer({
|
||||||
|
...filteredData,
|
||||||
|
image: excludedFields.includes("image")
|
||||||
|
? undefined
|
||||||
|
: performerData.images[imageIndex],
|
||||||
|
id: performerData.id,
|
||||||
|
});
|
||||||
|
if (!res.data?.performerUpdate)
|
||||||
|
setError({
|
||||||
|
...error,
|
||||||
|
[performerData.id]: {
|
||||||
|
message: `Failed to save performer "${performerData.name}"`,
|
||||||
|
details:
|
||||||
|
res?.errors?.[0].message ===
|
||||||
|
"UNIQUE constraint failed: performers.checksum"
|
||||||
|
? "Name already exists"
|
||||||
|
: res?.errors?.[0].message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setModalPerformer(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderPerformers = () =>
|
||||||
|
performers.map((performer) => {
|
||||||
|
const isTagged = taggedPerformers[performer.id];
|
||||||
|
const hasStashIDs = performer.stash_ids.length > 0;
|
||||||
|
|
||||||
|
let mainContent;
|
||||||
|
if (!isTagged && hasStashIDs) {
|
||||||
|
mainContent = (
|
||||||
|
<div className="text-left">
|
||||||
|
<h5 className="text-bold">Performer already tagged</h5>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (!isTagged && !hasStashIDs) {
|
||||||
|
mainContent = (
|
||||||
|
<InputGroup>
|
||||||
|
<Form.Control
|
||||||
|
className="text-input"
|
||||||
|
defaultValue={performer.name ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setQueries({
|
||||||
|
...queries,
|
||||||
|
[performer.id]: e.currentTarget.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onKeyPress={(e: React.KeyboardEvent<HTMLInputElement>) =>
|
||||||
|
e.key === "Enter" &&
|
||||||
|
doBoxSearch(
|
||||||
|
performer.id,
|
||||||
|
queries[performer.id] ?? performer.name ?? ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputGroup.Append>
|
||||||
|
<Button
|
||||||
|
disabled={loading}
|
||||||
|
onClick={() =>
|
||||||
|
doBoxSearch(
|
||||||
|
performer.id,
|
||||||
|
queries[performer.id] ?? performer.name ?? ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
</InputGroup.Append>
|
||||||
|
</InputGroup>
|
||||||
|
);
|
||||||
|
} else if (isTagged) {
|
||||||
|
mainContent = (
|
||||||
|
<div className="d-flex flex-column text-left">
|
||||||
|
<h5>Performer successfully tagged:</h5>
|
||||||
|
<h6>
|
||||||
|
<Link className="bold" to={`/performers/${performer.id}`}>
|
||||||
|
{taggedPerformers[performer.id].name}
|
||||||
|
</Link>
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let subContent;
|
||||||
|
if (performer.stash_ids.length > 0) {
|
||||||
|
const stashLinks = performer.stash_ids.map((stashID) => {
|
||||||
|
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
||||||
|
const link = base ? (
|
||||||
|
<a
|
||||||
|
className="small d-block"
|
||||||
|
href={`${base}performers/${stashID.stash_id}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{stashID.stash_id}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<div className="small">{stashID.stash_id}</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const endpoint =
|
||||||
|
stashBoxes?.findIndex((box) => box.endpoint === stashID.endpoint) ??
|
||||||
|
-1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<InputGroup className="PerformerTagger-box-link">
|
||||||
|
<InputGroup.Text>{link}</InputGroup.Text>
|
||||||
|
<InputGroup.Append>
|
||||||
|
{endpoint !== -1 && (
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
doBoxUpdate(performer.id, stashID.stash_id, endpoint)
|
||||||
|
}
|
||||||
|
disabled={!!loadingUpdate}
|
||||||
|
>
|
||||||
|
{loadingUpdate === stashID.stash_id ? (
|
||||||
|
<LoadingIndicator inline small message="" />
|
||||||
|
) : (
|
||||||
|
"Refresh"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</InputGroup.Append>
|
||||||
|
</InputGroup>
|
||||||
|
{error[performer.id] && (
|
||||||
|
<div className="text-danger mt-1">
|
||||||
|
<strong>
|
||||||
|
<span className="mr-2">Error:</span>
|
||||||
|
{error[performer.id]?.message}
|
||||||
|
</strong>
|
||||||
|
<div>{error[performer.id]?.details}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
subContent = <>{stashLinks}</>;
|
||||||
|
} else if (searchErrors[performer.id]) {
|
||||||
|
subContent = (
|
||||||
|
<div className="text-danger font-weight-bold">
|
||||||
|
{searchErrors[performer.id]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (searchResults[performer.id]?.length === 0) {
|
||||||
|
subContent = (
|
||||||
|
<div className="text-danger font-weight-bold">No results found.</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchResult;
|
||||||
|
if (searchResults[performer.id]?.length > 0 && !isTagged) {
|
||||||
|
searchResult = (
|
||||||
|
<StashSearchResult
|
||||||
|
key={performer.id}
|
||||||
|
stashboxPerformers={searchResults[performer.id]}
|
||||||
|
performer={performer}
|
||||||
|
endpoint={selectedEndpoint.endpoint}
|
||||||
|
onPerformerTagged={handleTaggedPerformer}
|
||||||
|
excludedPerformerFields={config.excludedPerformerFields ?? []}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={performer.id} className={`${CLASSNAME}-performer`}>
|
||||||
|
{modalPerformer && (
|
||||||
|
<PerformerModal
|
||||||
|
closeModal={() => setModalPerformer(undefined)}
|
||||||
|
modalVisible={modalPerformer !== undefined}
|
||||||
|
performer={modalPerformer}
|
||||||
|
handlePerformerCreate={handlePerformerUpdate}
|
||||||
|
excludedPerformerFields={config.excludedPerformerFields}
|
||||||
|
icon="tags"
|
||||||
|
header="Update Performer"
|
||||||
|
endpoint={selectedEndpoint.endpoint}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Card className="performer-card p-0 m-0">
|
||||||
|
<img src={performer.image_path ?? ""} alt="" />
|
||||||
|
</Card>
|
||||||
|
<div className={`${CLASSNAME}-details`}>
|
||||||
|
<Link
|
||||||
|
to={`/performers/${performer.id}`}
|
||||||
|
className={`${CLASSNAME}-header`}
|
||||||
|
>
|
||||||
|
<h2>{performer.name}</h2>
|
||||||
|
</Link>
|
||||||
|
{mainContent}
|
||||||
|
<div className="sub-content text-left">{subContent}</div>
|
||||||
|
{searchResult}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Modal
|
||||||
|
show={showBatchUpdate}
|
||||||
|
icon="tags"
|
||||||
|
header="Update Performers"
|
||||||
|
accept={{ text: "Update Performers", onClick: handleBatchUpdate }}
|
||||||
|
cancel={{
|
||||||
|
text: "Cancel",
|
||||||
|
variant: "danger",
|
||||||
|
onClick: () => setShowBatchUpdate(false),
|
||||||
|
}}
|
||||||
|
disabled={!isIdle}
|
||||||
|
>
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Label>
|
||||||
|
<h6>Performer selection</h6>
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Check
|
||||||
|
id="query-page"
|
||||||
|
type="radio"
|
||||||
|
name="performer-query"
|
||||||
|
label="Current page"
|
||||||
|
defaultChecked
|
||||||
|
onChange={() => setQueryAll(false)}
|
||||||
|
/>
|
||||||
|
<Form.Check
|
||||||
|
id="query-all"
|
||||||
|
type="radio"
|
||||||
|
name="performer-query"
|
||||||
|
label="All performers in the database"
|
||||||
|
defaultChecked={false}
|
||||||
|
onChange={() => setQueryAll(true)}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Label>
|
||||||
|
<h6>Tag Status</h6>
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Check
|
||||||
|
id="untagged-performers"
|
||||||
|
type="radio"
|
||||||
|
name="performer-refresh"
|
||||||
|
label="Untagged performers"
|
||||||
|
defaultChecked
|
||||||
|
onChange={() => setRefresh(false)}
|
||||||
|
/>
|
||||||
|
<Form.Text>
|
||||||
|
Updating untagged performers will try to match any performers that
|
||||||
|
lack a stashid and update the metadata.
|
||||||
|
</Form.Text>
|
||||||
|
<Form.Check
|
||||||
|
id="tagged-performers"
|
||||||
|
type="radio"
|
||||||
|
name="performer-refresh"
|
||||||
|
label="Refresh tagged performers"
|
||||||
|
defaultChecked={false}
|
||||||
|
onChange={() => setRefresh(true)}
|
||||||
|
/>
|
||||||
|
<Form.Text>
|
||||||
|
Refreshing will update the data of any tagged performers from the
|
||||||
|
stash-box instance.
|
||||||
|
</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
<b>{`${
|
||||||
|
queryAll
|
||||||
|
? allPerformers?.findPerformers.count
|
||||||
|
: performers.filter((p) =>
|
||||||
|
refresh ? p.stash_ids.length > 0 : p.stash_ids.length === 0
|
||||||
|
).length
|
||||||
|
} performers will be processed`}</b>
|
||||||
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
show={showBatchAdd}
|
||||||
|
icon="star"
|
||||||
|
header="Add New Performers"
|
||||||
|
accept={{ text: "Add Performers", onClick: handleBatchAdd }}
|
||||||
|
cancel={{
|
||||||
|
text: "Cancel",
|
||||||
|
variant: "danger",
|
||||||
|
onClick: () => setShowBatchAdd(false),
|
||||||
|
}}
|
||||||
|
disabled={!isIdle}
|
||||||
|
>
|
||||||
|
<Form.Control
|
||||||
|
className="text-input"
|
||||||
|
as="textarea"
|
||||||
|
ref={performerInput}
|
||||||
|
placeholder="Performer names separated by comma"
|
||||||
|
rows={6}
|
||||||
|
/>
|
||||||
|
<Form.Text>
|
||||||
|
Any names entered will be queried from the remote Stash-Box instance
|
||||||
|
and added if found. Only exact matches will be considered a match.
|
||||||
|
</Form.Text>
|
||||||
|
</Modal>
|
||||||
|
<div className="ml-auto mb-3">
|
||||||
|
<Button onClick={() => setShowBatchAdd(true)}>
|
||||||
|
Batch Add Performers
|
||||||
|
</Button>
|
||||||
|
<Button className="ml-3" onClick={() => setShowBatchUpdate(true)}>
|
||||||
|
Batch Update Performers
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className={CLASSNAME}>{renderPerformers()}</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ITaggerProps {
|
||||||
|
performers: GQL.PerformerDataFragment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PerformerTagger: React.FC<ITaggerProps> = ({ performers }) => {
|
||||||
|
const jobStatus = useMetadataUpdate();
|
||||||
|
const stashConfig = useConfiguration();
|
||||||
|
const [{ data: config }, setConfig] = useLocalForage<ITaggerConfig>(
|
||||||
|
LOCAL_FORAGE_KEY,
|
||||||
|
initialConfig
|
||||||
|
);
|
||||||
|
const [showConfig, setShowConfig] = useState(false);
|
||||||
|
const [showManual, setShowManual] = useState(false);
|
||||||
|
|
||||||
|
if (!config) return <LoadingIndicator />;
|
||||||
|
|
||||||
|
const savedEndpointIndex =
|
||||||
|
stashConfig.data?.configuration.general.stashBoxes.findIndex(
|
||||||
|
(s) => s.endpoint === config.selectedEndpoint
|
||||||
|
) ?? -1;
|
||||||
|
const selectedEndpointIndex =
|
||||||
|
savedEndpointIndex === -1 &&
|
||||||
|
stashConfig.data?.configuration.general.stashBoxes.length
|
||||||
|
? 0
|
||||||
|
: savedEndpointIndex;
|
||||||
|
const selectedEndpoint =
|
||||||
|
stashConfig.data?.configuration.general.stashBoxes[selectedEndpointIndex];
|
||||||
|
|
||||||
|
const progress =
|
||||||
|
jobStatus.data?.metadataUpdate.status ===
|
||||||
|
"Stash-Box Performer Batch Operation" &&
|
||||||
|
jobStatus.data.metadataUpdate.progress >= 0
|
||||||
|
? jobStatus.data.metadataUpdate.progress * 100
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Manual
|
||||||
|
show={showManual}
|
||||||
|
onClose={() => setShowManual(false)}
|
||||||
|
defaultActiveTab="Tagger.md"
|
||||||
|
/>
|
||||||
|
{progress !== null && (
|
||||||
|
<Form.Group className="px-4">
|
||||||
|
<h5>Status: Tagging performers</h5>
|
||||||
|
<ProgressBar
|
||||||
|
animated
|
||||||
|
now={progress}
|
||||||
|
label={`${progress.toFixed(0)}%`}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
)}
|
||||||
|
<div className="tagger-container mx-md-auto">
|
||||||
|
{selectedEndpointIndex !== -1 && selectedEndpoint ? (
|
||||||
|
<>
|
||||||
|
<div className="row mb-2 no-gutters">
|
||||||
|
<Button onClick={() => setShowConfig(!showConfig)} variant="link">
|
||||||
|
{showConfig ? "Hide" : "Show"} Configuration
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="ml-auto"
|
||||||
|
onClick={() => setShowManual(true)}
|
||||||
|
title="Help"
|
||||||
|
variant="link"
|
||||||
|
>
|
||||||
|
Help
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PerformerConfig
|
||||||
|
config={config}
|
||||||
|
setConfig={setConfig}
|
||||||
|
show={showConfig}
|
||||||
|
/>
|
||||||
|
<PerformerTaggerList
|
||||||
|
performers={performers}
|
||||||
|
selectedEndpoint={{
|
||||||
|
endpoint: selectedEndpoint.endpoint,
|
||||||
|
index: selectedEndpointIndex,
|
||||||
|
}}
|
||||||
|
isIdle={progress === null}
|
||||||
|
config={config}
|
||||||
|
stashBoxes={stashConfig.data?.configuration.general.stashBoxes}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="my-4">
|
||||||
|
<h3 className="text-center mt-4">
|
||||||
|
To use the performer tagger a stash-box instance needs to be
|
||||||
|
configured.
|
||||||
|
</h3>
|
||||||
|
<h5 className="text-center">
|
||||||
|
Please see{" "}
|
||||||
|
<HashLink
|
||||||
|
to="/settings?tab=configuration#stashbox"
|
||||||
|
scroll={(el) =>
|
||||||
|
el.scrollIntoView({ behavior: "smooth", block: "center" })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Settings.
|
||||||
|
</HashLink>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
112
ui/v2.5/src/components/Tagger/performers/StashSearchResult.tsx
Executable file
112
ui/v2.5/src/components/Tagger/performers/StashSearchResult.tsx
Executable file
@@ -0,0 +1,112 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { Button } from "react-bootstrap";
|
||||||
|
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { IStashBoxPerformer, filterPerformer } from "../utils";
|
||||||
|
import { useUpdatePerformer } from "../queries";
|
||||||
|
import PerformerModal from "../PerformerModal";
|
||||||
|
|
||||||
|
interface IStashSearchResultProps {
|
||||||
|
performer: GQL.SlimPerformerDataFragment;
|
||||||
|
stashboxPerformers: IStashBoxPerformer[];
|
||||||
|
endpoint: string;
|
||||||
|
onPerformerTagged: (
|
||||||
|
performer: Pick<GQL.SlimPerformerDataFragment, "id"> &
|
||||||
|
Partial<Omit<GQL.SlimPerformerDataFragment, "id">>
|
||||||
|
) => void;
|
||||||
|
excludedPerformerFields: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
||||||
|
performer,
|
||||||
|
stashboxPerformers,
|
||||||
|
onPerformerTagged,
|
||||||
|
excludedPerformerFields,
|
||||||
|
endpoint,
|
||||||
|
}) => {
|
||||||
|
const [modalPerformer, setModalPerformer] = useState<
|
||||||
|
IStashBoxPerformer | undefined
|
||||||
|
>();
|
||||||
|
const [saveState, setSaveState] = useState<string>("");
|
||||||
|
const [error, setError] = useState<{ message?: string; details?: string }>(
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatePerformer = useUpdatePerformer();
|
||||||
|
|
||||||
|
const handleSave = async (image: number, excludedFields: string[]) => {
|
||||||
|
if (modalPerformer) {
|
||||||
|
const performerData = filterPerformer(modalPerformer, excludedFields);
|
||||||
|
setError({});
|
||||||
|
setSaveState("Saving performer");
|
||||||
|
setModalPerformer(undefined);
|
||||||
|
|
||||||
|
const res = await updatePerformer({
|
||||||
|
...performerData,
|
||||||
|
image: excludedFields.includes("image")
|
||||||
|
? undefined
|
||||||
|
: modalPerformer.images[image],
|
||||||
|
stash_ids: [{ stash_id: modalPerformer.stash_id, endpoint }],
|
||||||
|
id: performer.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res?.data?.performerUpdate)
|
||||||
|
setError({
|
||||||
|
message: `Failed to save performer "${performer.name}"`,
|
||||||
|
details:
|
||||||
|
res?.errors?.[0].message ===
|
||||||
|
"UNIQUE constraint failed: performers.checksum"
|
||||||
|
? "Name already exists"
|
||||||
|
: res?.errors?.[0].message,
|
||||||
|
});
|
||||||
|
else onPerformerTagged(performer);
|
||||||
|
setSaveState("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const performers = stashboxPerformers.map((p) => (
|
||||||
|
<Button
|
||||||
|
className="PerformerTagger-performer-search-item minimal col-6"
|
||||||
|
variant="link"
|
||||||
|
key={p.stash_id}
|
||||||
|
onClick={() => setModalPerformer(p)}
|
||||||
|
>
|
||||||
|
<img src={p.images[0]} alt="" className="PerformerTagger-thumb" />
|
||||||
|
<span>{p.name}</span>
|
||||||
|
</Button>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{modalPerformer && (
|
||||||
|
<PerformerModal
|
||||||
|
closeModal={() => setModalPerformer(undefined)}
|
||||||
|
modalVisible={modalPerformer !== undefined}
|
||||||
|
performer={modalPerformer}
|
||||||
|
handlePerformerCreate={handleSave}
|
||||||
|
icon="tags"
|
||||||
|
header="Update Performer"
|
||||||
|
excludedPerformerFields={excludedPerformerFields}
|
||||||
|
endpoint={endpoint}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="PerformerTagger-performer-search">{performers}</div>
|
||||||
|
<div className="row no-gutters mt-2 align-items-center justify-content-end">
|
||||||
|
{error.message && (
|
||||||
|
<div className="text-right text-danger mt-1">
|
||||||
|
<strong>
|
||||||
|
<span className="mr-2">Error:</span>
|
||||||
|
{error.message}
|
||||||
|
</strong>
|
||||||
|
<div>{error.details}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{saveState && (
|
||||||
|
<strong className="col-4 mt-1 mr-2 text-right">{saveState}</strong>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StashSearchResult;
|
||||||
@@ -31,7 +31,10 @@ export const useUpdatePerformerStashID = () => {
|
|||||||
query: GQL.FindPerformersDocument,
|
query: GQL.FindPerformersDocument,
|
||||||
variables: {
|
variables: {
|
||||||
performer_filter: {
|
performer_filter: {
|
||||||
stash_id: newStashID,
|
stash_id: {
|
||||||
|
value: newStashID,
|
||||||
|
modifier: GQL.CriterionModifier.Equals,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
@@ -48,6 +51,49 @@ export const useUpdatePerformerStashID = () => {
|
|||||||
return updatePerformerHandler;
|
return updatePerformerHandler;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useUpdatePerformer = () => {
|
||||||
|
const [updatePerformer] = GQL.usePerformerUpdateMutation({
|
||||||
|
onError: (errors) => errors,
|
||||||
|
errorPolicy: "all",
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatePerformerHandler = (input: GQL.PerformerUpdateInput) =>
|
||||||
|
updatePerformer({
|
||||||
|
variables: {
|
||||||
|
input,
|
||||||
|
},
|
||||||
|
update: (store, updatedPerformer) => {
|
||||||
|
if (!updatedPerformer.data?.performerUpdate) return;
|
||||||
|
|
||||||
|
updatedPerformer.data.performerUpdate.stash_ids.forEach((id) => {
|
||||||
|
store.writeQuery<
|
||||||
|
GQL.FindPerformersQuery,
|
||||||
|
GQL.FindPerformersQueryVariables
|
||||||
|
>({
|
||||||
|
query: GQL.FindPerformersDocument,
|
||||||
|
variables: {
|
||||||
|
performer_filter: {
|
||||||
|
stash_id: {
|
||||||
|
value: id.stash_id,
|
||||||
|
modifier: GQL.CriterionModifier.Equals,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
findPerformers: {
|
||||||
|
count: 1,
|
||||||
|
performers: [updatedPerformer.data!.performerUpdate!],
|
||||||
|
__typename: "FindPerformersResultType",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatePerformerHandler;
|
||||||
|
};
|
||||||
|
|
||||||
export const useCreatePerformer = () => {
|
export const useCreatePerformer = () => {
|
||||||
const [createPerformer] = GQL.usePerformerCreateMutation({
|
const [createPerformer] = GQL.usePerformerCreateMutation({
|
||||||
onError: (errors) => errors,
|
onError: (errors) => errors,
|
||||||
@@ -91,7 +137,10 @@ export const useCreatePerformer = () => {
|
|||||||
query: GQL.FindPerformersDocument,
|
query: GQL.FindPerformersDocument,
|
||||||
variables: {
|
variables: {
|
||||||
performer_filter: {
|
performer_filter: {
|
||||||
stash_id: stashID,
|
stash_id: {
|
||||||
|
value: stashID,
|
||||||
|
modifier: GQL.CriterionModifier.Equals,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
@@ -135,7 +184,10 @@ export const useUpdateStudioStashID = () => {
|
|||||||
query: GQL.FindStudiosDocument,
|
query: GQL.FindStudiosDocument,
|
||||||
variables: {
|
variables: {
|
||||||
studio_filter: {
|
studio_filter: {
|
||||||
stash_id: newStashID,
|
stash_id: {
|
||||||
|
value: newStashID,
|
||||||
|
modifier: GQL.CriterionModifier.Equals,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
@@ -189,7 +241,10 @@ export const useCreateStudio = () => {
|
|||||||
query: GQL.FindStudiosDocument,
|
query: GQL.FindStudiosDocument,
|
||||||
variables: {
|
variables: {
|
||||||
studio_filter: {
|
studio_filter: {
|
||||||
stash_id: stashID,
|
stash_id: {
|
||||||
|
value: stashID,
|
||||||
|
modifier: GQL.CriterionModifier.Equals,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
|
|
||||||
.performer-create-modal {
|
.performer-create-modal {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
max-width: 768px;
|
max-width: 800px;
|
||||||
|
|
||||||
.image-selection {
|
.image-selection {
|
||||||
height: 450px;
|
height: 450px;
|
||||||
@@ -100,6 +100,13 @@
|
|||||||
|
|
||||||
.performer-image {
|
.performer-image {
|
||||||
height: 85%;
|
height: 85%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&-exclude {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
@@ -111,4 +118,89 @@
|
|||||||
.LoadingIndicator {
|
.LoadingIndicator {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-field {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-icon {
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.PerformerTagger {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
max-width: 1600px;
|
||||||
|
|
||||||
|
&-header {
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-performer {
|
||||||
|
background-color: #495b68;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: flex;
|
||||||
|
margin: 1rem;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
.performer-card {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 12rem;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
|
max-height: 18rem;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-details {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-left: 1rem;
|
||||||
|
width: 24rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-performer-search {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
align-items: center;
|
||||||
|
align-text: left;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-thumb {
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-box-link {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
.input-group-text {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.FieldSelect {
|
||||||
|
.fa-icon {
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ const selectTags = (tags: GQL.ScrapedSceneTag[]): IStashBoxTag[] =>
|
|||||||
name: t.name ?? "",
|
name: t.name ?? "",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const selectPerformers = (
|
export const selectPerformers = (
|
||||||
performers: GQL.ScrapedScenePerformer[]
|
performers: GQL.ScrapedScenePerformer[]
|
||||||
): IStashBoxPerformer[] =>
|
): IStashBoxPerformer[] =>
|
||||||
performers.map((p) => ({
|
performers.map((p) => ({
|
||||||
@@ -186,3 +186,63 @@ export const sortScenesByDuration = (
|
|||||||
if (aDiff > bDiff) return 1;
|
if (aDiff > bDiff) return 1;
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const filterPerformer = (
|
||||||
|
performer: IStashBoxPerformer,
|
||||||
|
excludedFields: string[]
|
||||||
|
) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
aliases,
|
||||||
|
gender,
|
||||||
|
birthdate,
|
||||||
|
ethnicity,
|
||||||
|
country,
|
||||||
|
eye_color,
|
||||||
|
height,
|
||||||
|
measurements,
|
||||||
|
fake_tits,
|
||||||
|
career_length,
|
||||||
|
tattoos,
|
||||||
|
piercings,
|
||||||
|
} = performer;
|
||||||
|
return {
|
||||||
|
name: !excludedFields.includes("name") && name ? name : undefined,
|
||||||
|
aliases:
|
||||||
|
!excludedFields.includes("aliases") && aliases ? aliases : undefined,
|
||||||
|
gender: !excludedFields.includes("gender") && gender ? gender : undefined,
|
||||||
|
birthdate:
|
||||||
|
!excludedFields.includes("birthdate") && birthdate
|
||||||
|
? birthdate
|
||||||
|
: undefined,
|
||||||
|
ethnicity:
|
||||||
|
!excludedFields.includes("ethnicity") && ethnicity
|
||||||
|
? ethnicity
|
||||||
|
: undefined,
|
||||||
|
country:
|
||||||
|
!excludedFields.includes("country") && country ? country : undefined,
|
||||||
|
eye_color:
|
||||||
|
!excludedFields.includes("eye_color") && eye_color
|
||||||
|
? eye_color
|
||||||
|
: undefined,
|
||||||
|
height: !excludedFields.includes("height") && height ? height : undefined,
|
||||||
|
measurements:
|
||||||
|
!excludedFields.includes("measurements") && measurements
|
||||||
|
? measurements
|
||||||
|
: undefined,
|
||||||
|
fake_tits:
|
||||||
|
!excludedFields.includes("fake_tits") && fake_tits
|
||||||
|
? fake_tits
|
||||||
|
: undefined,
|
||||||
|
career_length:
|
||||||
|
!excludedFields.includes("career_length") && career_length
|
||||||
|
? career_length
|
||||||
|
: undefined,
|
||||||
|
tattoos:
|
||||||
|
!excludedFields.includes("tattoos") && tattoos ? tattoos : undefined,
|
||||||
|
piercings:
|
||||||
|
!excludedFields.includes("piercings") && piercings
|
||||||
|
? piercings
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -813,6 +813,20 @@ export const queryStashBoxScene = (stashBoxIndex: number, sceneID: string) =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const queryStashBoxPerformer = (
|
||||||
|
stashBoxIndex: number,
|
||||||
|
performerID: string
|
||||||
|
) =>
|
||||||
|
client.query<GQL.QueryStashBoxPerformerQuery>({
|
||||||
|
query: GQL.QueryStashBoxPerformerDocument,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
stash_box_index: stashBoxIndex,
|
||||||
|
performer_ids: [performerID],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const queryScrapeGallery = (
|
export const queryScrapeGallery = (
|
||||||
scraperId: string,
|
scraperId: string,
|
||||||
gallery: GQL.GalleryUpdateInput
|
gallery: GQL.GalleryUpdateInput
|
||||||
@@ -1006,7 +1020,7 @@ export const makePerformerCreateInput = (
|
|||||||
return input;
|
return input;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const stashBoxQuery = (searchVal: string, stashBoxIndex: number) =>
|
export const stashBoxSceneQuery = (searchVal: string, stashBoxIndex: number) =>
|
||||||
client?.query<
|
client?.query<
|
||||||
GQL.QueryStashBoxSceneQuery,
|
GQL.QueryStashBoxSceneQuery,
|
||||||
GQL.QueryStashBoxSceneQueryVariables
|
GQL.QueryStashBoxSceneQueryVariables
|
||||||
@@ -1015,7 +1029,22 @@ export const stashBoxQuery = (searchVal: string, stashBoxIndex: number) =>
|
|||||||
variables: { input: { q: searchVal, stash_box_index: stashBoxIndex } },
|
variables: { input: { q: searchVal, stash_box_index: stashBoxIndex } },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const stashBoxBatchQuery = (sceneIds: string[], stashBoxIndex: number) =>
|
export const stashBoxPerformerQuery = (
|
||||||
|
searchVal: string,
|
||||||
|
stashBoxIndex: number
|
||||||
|
) =>
|
||||||
|
client?.query<
|
||||||
|
GQL.QueryStashBoxPerformerQuery,
|
||||||
|
GQL.QueryStashBoxPerformerQueryVariables
|
||||||
|
>({
|
||||||
|
query: GQL.QueryStashBoxPerformerDocument,
|
||||||
|
variables: { input: { q: searchVal, stash_box_index: stashBoxIndex } },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const stashBoxSceneBatchQuery = (
|
||||||
|
sceneIds: string[],
|
||||||
|
stashBoxIndex: number
|
||||||
|
) =>
|
||||||
client?.query<
|
client?.query<
|
||||||
GQL.QueryStashBoxSceneQuery,
|
GQL.QueryStashBoxSceneQuery,
|
||||||
GQL.QueryStashBoxSceneQueryVariables
|
GQL.QueryStashBoxSceneQueryVariables
|
||||||
@@ -1025,3 +1054,17 @@ export const stashBoxBatchQuery = (sceneIds: string[], stashBoxIndex: number) =>
|
|||||||
input: { scene_ids: sceneIds, stash_box_index: stashBoxIndex },
|
input: { scene_ids: sceneIds, stash_box_index: stashBoxIndex },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const stashBoxPerformerBatchQuery = (
|
||||||
|
performerIds: string[],
|
||||||
|
stashBoxIndex: number
|
||||||
|
) =>
|
||||||
|
client?.query<
|
||||||
|
GQL.QueryStashBoxPerformerQuery,
|
||||||
|
GQL.QueryStashBoxPerformerQueryVariables
|
||||||
|
>({
|
||||||
|
query: GQL.QueryStashBoxPerformerDocument,
|
||||||
|
variables: {
|
||||||
|
input: { performer_ids: performerIds, stash_box_index: stashBoxIndex },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ export type CriterionType =
|
|||||||
| "gallery_count"
|
| "gallery_count"
|
||||||
| "performer_count"
|
| "performer_count"
|
||||||
| "death_year"
|
| "death_year"
|
||||||
| "url";
|
| "url"
|
||||||
|
| "stash_id";
|
||||||
|
|
||||||
type Option = string | number | IOptionType;
|
type Option = string | number | IOptionType;
|
||||||
export type CriterionValue = string | number | ILabeledId[];
|
export type CriterionValue = string | number | ILabeledId[];
|
||||||
@@ -150,6 +151,8 @@ export abstract class Criterion {
|
|||||||
return "Performer Count";
|
return "Performer Count";
|
||||||
case "url":
|
case "url":
|
||||||
return "URL";
|
return "URL";
|
||||||
|
case "stash_id":
|
||||||
|
return "StashID";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ export class SceneIsMissingCriterion extends IsMissingCriterion {
|
|||||||
"movie",
|
"movie",
|
||||||
"performers",
|
"performers",
|
||||||
"tags",
|
"tags",
|
||||||
"stash_id",
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +65,6 @@ export class PerformerIsMissingCriterion extends IsMissingCriterion {
|
|||||||
"gender",
|
"gender",
|
||||||
"scenes",
|
"scenes",
|
||||||
"image",
|
"image",
|
||||||
"stash_id",
|
|
||||||
"details",
|
"details",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -107,7 +105,7 @@ export class TagIsMissingCriterionOption implements ICriterionOption {
|
|||||||
|
|
||||||
export class StudioIsMissingCriterion extends IsMissingCriterion {
|
export class StudioIsMissingCriterion extends IsMissingCriterion {
|
||||||
public type: CriterionType = "studioIsMissing";
|
public type: CriterionType = "studioIsMissing";
|
||||||
public options: string[] = ["image", "stash_id", "details"];
|
public options: string[] = ["image", "details"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StudioIsMissingCriterionOption implements ICriterionOption {
|
export class StudioIsMissingCriterionOption implements ICriterionOption {
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ export function makeCriteria(type: CriterionType = "none") {
|
|||||||
case "piercings":
|
case "piercings":
|
||||||
case "aliases":
|
case "aliases":
|
||||||
case "url":
|
case "url":
|
||||||
|
case "stash_id":
|
||||||
return new StringCriterion(type, type);
|
return new StringCriterion(type, type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ export class ListFilterModel {
|
|||||||
new StudiosCriterionOption(),
|
new StudiosCriterionOption(),
|
||||||
new MoviesCriterionOption(),
|
new MoviesCriterionOption(),
|
||||||
ListFilterModel.createCriterionOption("url"),
|
ListFilterModel.createCriterionOption("url"),
|
||||||
|
ListFilterModel.createCriterionOption("stash_id"),
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
case FilterMode.Images:
|
case FilterMode.Images:
|
||||||
@@ -204,7 +205,11 @@ export class ListFilterModel {
|
|||||||
"random",
|
"random",
|
||||||
"rating",
|
"rating",
|
||||||
];
|
];
|
||||||
this.displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
|
this.displayModeOptions = [
|
||||||
|
DisplayMode.Grid,
|
||||||
|
DisplayMode.List,
|
||||||
|
DisplayMode.Tagger,
|
||||||
|
];
|
||||||
|
|
||||||
const numberCriteria: CriterionType[] = [
|
const numberCriteria: CriterionType[] = [
|
||||||
"birth_year",
|
"birth_year",
|
||||||
@@ -224,6 +229,7 @@ export class ListFilterModel {
|
|||||||
"tattoos",
|
"tattoos",
|
||||||
"piercings",
|
"piercings",
|
||||||
"aliases",
|
"aliases",
|
||||||
|
"stash_id",
|
||||||
];
|
];
|
||||||
|
|
||||||
this.criterionOptions = [
|
this.criterionOptions = [
|
||||||
@@ -265,6 +271,7 @@ export class ListFilterModel {
|
|||||||
ListFilterModel.createCriterionOption("image_count"),
|
ListFilterModel.createCriterionOption("image_count"),
|
||||||
ListFilterModel.createCriterionOption("gallery_count"),
|
ListFilterModel.createCriterionOption("gallery_count"),
|
||||||
ListFilterModel.createCriterionOption("url"),
|
ListFilterModel.createCriterionOption("url"),
|
||||||
|
ListFilterModel.createCriterionOption("stash_id"),
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
case FilterMode.Movies:
|
case FilterMode.Movies:
|
||||||
@@ -655,6 +662,14 @@ export class ListFilterModel {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "stash_id": {
|
||||||
|
const stashIdCrit = criterion as StringCriterion;
|
||||||
|
result.stash_id = {
|
||||||
|
value: stashIdCrit.value,
|
||||||
|
modifier: stashIdCrit.modifier,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
// no default
|
// no default
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -832,6 +847,14 @@ export class ListFilterModel {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "stash_id": {
|
||||||
|
const stashIdCrit = criterion as StringCriterion;
|
||||||
|
result.stash_id = {
|
||||||
|
value: stashIdCrit.value,
|
||||||
|
modifier: stashIdCrit.modifier,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
// no default
|
// no default
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1099,6 +1122,14 @@ export class ListFilterModel {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "stash_id": {
|
||||||
|
const stashIdCrit = criterion as StringCriterion;
|
||||||
|
result.stash_id = {
|
||||||
|
value: stashIdCrit.value,
|
||||||
|
modifier: stashIdCrit.modifier,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
// no default
|
// no default
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -186,6 +186,11 @@ const formatDate = (intl: IntlShape, date?: string) => {
|
|||||||
return intl.formatDate(date, { format: "long", timeZone: "utc" });
|
return intl.formatDate(date, { format: "long", timeZone: "utc" });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const capitalize = (val: string) =>
|
||||||
|
val
|
||||||
|
.replace(/^[-_]*(.)/, (_, c) => c.toUpperCase())
|
||||||
|
.replace(/[-_]+(.)/g, (_, c) => ` ${c.toUpperCase()}`);
|
||||||
|
|
||||||
const TextUtils = {
|
const TextUtils = {
|
||||||
fileSize,
|
fileSize,
|
||||||
formatFileSizeUnit,
|
formatFileSizeUnit,
|
||||||
@@ -200,6 +205,7 @@ const TextUtils = {
|
|||||||
twitterURL,
|
twitterURL,
|
||||||
instagramURL,
|
instagramURL,
|
||||||
formatDate,
|
formatDate,
|
||||||
|
capitalize,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TextUtils;
|
export default TextUtils;
|
||||||
|
|||||||
@@ -2881,11 +2881,12 @@
|
|||||||
"@types/history" "*"
|
"@types/history" "*"
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react-select@^3.1.2":
|
"@types/react-select@^4.0.8":
|
||||||
version "3.1.2"
|
version "4.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-select/-/react-select-3.1.2.tgz#38627df4b49be9b28f800ed72b35d830369a624b"
|
resolved "https://registry.yarnpkg.com/@types/react-select/-/react-select-4.0.8.tgz#109e8223cf3d9a50c20b386b3bc7fbc46c701916"
|
||||||
integrity sha512-ygvR/2FL87R2OLObEWFootYzkvm67LRA+URYEAcBuvKk7IXmdsnIwSGm60cVXGaqkJQHozb2Cy1t94tCYb6rJA==
|
integrity sha512-dOfoJxPq4s4shWmI9mDjhs6w7tXlH4bgQarqp5HulL3jgwzEPGK/DaGah4pdCNrY70mnIvMAN7cAzZbUWomESQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@emotion/serialize" "^1.0.0"
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
"@types/react-dom" "*"
|
"@types/react-dom" "*"
|
||||||
"@types/react-transition-group" "*"
|
"@types/react-transition-group" "*"
|
||||||
|
|||||||
Reference in New Issue
Block a user