stash/pkg/stashbox/performer.go
2025-12-03 16:47:17 +11:00

431 lines
10 KiB
Go

package stashbox
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scraper"
"github.com/stashapp/stash/pkg/sliceutil"
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
"github.com/stashapp/stash/pkg/stashbox/graphql"
"github.com/stashapp/stash/pkg/utils"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// QueryPerformer queries stash-box for performers using a query string.
func (c Client) QueryPerformer(ctx context.Context, queryStr string) ([]*models.ScrapedPerformer, error) {
performers, err := c.queryPerformer(ctx, queryStr)
// set the deprecated image field
for _, p := range performers {
if len(p.Images) > 0 {
p.Image = &p.Images[0]
}
}
return performers, err
}
func (c Client) queryPerformer(ctx context.Context, queryStr string) ([]*models.ScrapedPerformer, error) {
performers, err := c.client.SearchPerformer(ctx, queryStr)
if err != nil {
return nil, err
}
performerFragments := performers.SearchPerformer
var ret []*models.ScrapedPerformer
var ignoredTags []string
for _, fragment := range performerFragments {
performer := performerFragmentToScrapedPerformer(*fragment)
// exclude tags that match the excludeTagRE
var thisIgnoredTags []string
performer.Tags, thisIgnoredTags = scraper.FilterTags(c.excludeTagRE, performer.Tags)
ignoredTags = sliceutil.AppendUniques(ignoredTags, thisIgnoredTags)
ret = append(ret, performer)
}
scraper.LogIgnoredTags(ignoredTags)
return ret, nil
}
// QueryPerformers queries stash-box for performers using a list of names.
func (c Client) QueryPerformers(ctx context.Context, names []string) ([][]*models.ScrapedPerformer, error) {
ret := make([][]*models.ScrapedPerformer, len(names))
for i, name := range names {
if name != "" {
continue
}
var err error
ret[i], err = c.queryPerformer(ctx, name)
if err != nil {
return nil, err
}
}
return ret, nil
}
func findURL(urls []*graphql.URLFragment, urlType string) *string {
for _, u := range urls {
if u.Type == urlType {
ret := u.URL
return &ret
}
}
return nil
}
func enumToStringPtr(e fmt.Stringer, titleCase bool) *string {
if e != nil {
ret := strings.ReplaceAll(e.String(), "_", " ")
if titleCase {
c := cases.Title(language.Und)
ret = c.String(strings.ToLower(ret))
}
return &ret
}
return nil
}
func translateGender(gender *graphql.GenderEnum) *string {
var res models.GenderEnum
switch *gender {
case graphql.GenderEnumMale:
res = models.GenderEnumMale
case graphql.GenderEnumFemale:
res = models.GenderEnumFemale
case graphql.GenderEnumIntersex:
res = models.GenderEnumIntersex
case graphql.GenderEnumTransgenderFemale:
res = models.GenderEnumTransgenderFemale
case graphql.GenderEnumTransgenderMale:
res = models.GenderEnumTransgenderMale
case graphql.GenderEnumNonBinary:
res = models.GenderEnumNonBinary
}
if res != "" {
strVal := res.String()
return &strVal
}
return nil
}
func formatMeasurements(m *graphql.MeasurementsFragment) *string {
if m != nil && m.BandSize != nil && m.CupSize != nil && m.Hip != nil && m.Waist != nil {
ret := fmt.Sprintf("%d%s-%d-%d", *m.BandSize, *m.CupSize, *m.Waist, *m.Hip)
return &ret
}
return nil
}
func formatCareerLength(start, end *int) *string {
if start == nil && end == nil {
return nil
}
var ret string
switch {
case end == nil:
ret = fmt.Sprintf("%d -", *start)
case start == nil:
ret = fmt.Sprintf("- %d", *end)
default:
ret = fmt.Sprintf("%d - %d", *start, *end)
}
return &ret
}
func formatBodyModifications(m []*graphql.BodyModificationFragment) *string {
if len(m) == 0 {
return nil
}
var retSlice []string
for _, f := range m {
if f.Description == nil {
retSlice = append(retSlice, f.Location)
} else {
retSlice = append(retSlice, fmt.Sprintf("%s, %s", f.Location, *f.Description))
}
}
ret := strings.Join(retSlice, "; ")
return &ret
}
func fetchImage(ctx context.Context, client *http.Client, url string) (*string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
// determine the image type and set the base64 type
contentType := resp.Header.Get("Content-Type")
if contentType == "" {
contentType = http.DetectContentType(body)
}
img := "data:" + contentType + ";base64," + utils.GetBase64StringFromData(body)
return &img, nil
}
func performerFragmentToScrapedPerformer(p graphql.PerformerFragment) *models.ScrapedPerformer {
images := []string{}
for _, image := range p.Images {
images = append(images, image.URL)
}
sp := &models.ScrapedPerformer{
Name: &p.Name,
Disambiguation: p.Disambiguation,
Country: p.Country,
Measurements: formatMeasurements(p.Measurements),
CareerLength: formatCareerLength(p.CareerStartYear, p.CareerEndYear),
Tattoos: formatBodyModifications(p.Tattoos),
Piercings: formatBodyModifications(p.Piercings),
Twitter: findURL(p.Urls, "TWITTER"),
RemoteSiteID: &p.ID,
RemoteDeleted: p.Deleted,
RemoteMergedIntoId: p.MergedIntoID,
Images: images,
// TODO - tags not currently supported
// graphql schema change to accommodate this. Leave off for now.
}
if len(sp.Images) > 0 {
sp.Image = &sp.Images[0]
}
if p.Height != nil && *p.Height > 0 {
hs := strconv.Itoa(*p.Height)
sp.Height = &hs
}
if p.BirthDate != nil {
sp.Birthdate = padFuzzyDate(p.BirthDate)
}
if p.DeathDate != nil {
sp.DeathDate = padFuzzyDate(p.DeathDate)
}
if p.Gender != nil {
sp.Gender = translateGender(p.Gender)
}
if p.Ethnicity != nil {
sp.Ethnicity = enumToStringPtr(p.Ethnicity, true)
}
if p.EyeColor != nil {
sp.EyeColor = enumToStringPtr(p.EyeColor, true)
}
if p.HairColor != nil {
sp.HairColor = enumToStringPtr(p.HairColor, true)
}
if p.BreastType != nil {
sp.FakeTits = enumToStringPtr(p.BreastType, true)
}
if len(p.Aliases) > 0 {
// #4437 - stash-box may return aliases that are equal to the performer name
// filter these out
p.Aliases = sliceutil.Filter(p.Aliases, func(s string) bool {
return !strings.EqualFold(s, p.Name)
})
// #4596 - stash-box may return duplicate aliases. Filter these out
p.Aliases = stringslice.UniqueFold(p.Aliases)
alias := strings.Join(p.Aliases, ", ")
sp.Aliases = &alias
}
for _, u := range p.Urls {
sp.URLs = append(sp.URLs, u.URL)
}
return sp
}
func padFuzzyDate(date *string) *string {
if date == nil {
return nil
}
var paddedDate string
switch len(*date) {
case 10:
paddedDate = *date
case 7:
paddedDate = fmt.Sprintf("%s-01", *date)
case 4:
paddedDate = fmt.Sprintf("%s-01-01", *date)
}
return &paddedDate
}
// FindPerformerByID queries stash-box for a performer by ID.
func (c Client) FindPerformerByID(ctx context.Context, id string) (*models.ScrapedPerformer, error) {
performer, err := c.client.FindPerformerByID(ctx, id)
if err != nil {
return nil, err
}
if performer.FindPerformer == nil {
return nil, nil
}
ret := performerFragmentToScrapedPerformer(*performer.FindPerformer)
return ret, nil
}
// FindPerformerByName queries stash-box for a performer by name.
// Unlike QueryPerformer, this function will only return a performer if the name matches exactly.
func (c Client) FindPerformerByName(ctx context.Context, name string) (*models.ScrapedPerformer, error) {
performers, err := c.client.SearchPerformer(ctx, name)
if err != nil {
return nil, err
}
var ret *models.ScrapedPerformer
for _, performer := range performers.SearchPerformer {
if strings.EqualFold(performer.Name, name) {
ret = performerFragmentToScrapedPerformer(*performer)
}
}
return ret, nil
}
// SubmitPerformerDraft submits a performer draft to stash-box.
// The performer parameter must have aliases, URLs and stash IDs loaded.
func (c Client) SubmitPerformerDraft(ctx context.Context, performer *models.Performer, img []byte) (*string, error) {
draft := graphql.PerformerDraftInput{}
var image io.Reader
endpoint := c.box.Endpoint
if len(img) > 0 {
image = bytes.NewReader(img)
}
if performer.Name != "" {
draft.Name = performer.Name
}
if performer.Disambiguation != "" {
draft.Disambiguation = &performer.Disambiguation
}
if performer.Birthdate != nil {
d := performer.Birthdate.String()
draft.Birthdate = &d
}
if performer.Country != "" {
draft.Country = &performer.Country
}
if performer.Ethnicity != "" {
draft.Ethnicity = &performer.Ethnicity
}
if performer.EyeColor != "" {
draft.EyeColor = &performer.EyeColor
}
if performer.FakeTits != "" {
draft.BreastType = &performer.FakeTits
}
if performer.Gender != nil && performer.Gender.IsValid() {
v := performer.Gender.String()
draft.Gender = &v
}
if performer.HairColor != "" {
draft.HairColor = &performer.HairColor
}
if performer.Height != nil {
v := strconv.Itoa(*performer.Height)
draft.Height = &v
}
if performer.Measurements != "" {
draft.Measurements = &performer.Measurements
}
if performer.Piercings != "" {
draft.Piercings = &performer.Piercings
}
if performer.Tattoos != "" {
draft.Tattoos = &performer.Tattoos
}
if len(performer.Aliases.List()) > 0 {
aliases := strings.Join(performer.Aliases.List(), ",")
draft.Aliases = &aliases
}
if performer.CareerLength != "" {
var career = strings.Split(performer.CareerLength, "-")
if i, err := strconv.Atoi(strings.TrimSpace(career[0])); err == nil {
draft.CareerStartYear = &i
}
if len(career) == 2 {
if y, err := strconv.Atoi(strings.TrimSpace(career[1])); err == nil {
draft.CareerEndYear = &y
}
}
}
if len(performer.URLs.List()) > 0 {
draft.Urls = performer.URLs.List()
}
var stashID *string
for _, v := range performer.StashIDs.List() {
c := v
if v.Endpoint == endpoint {
stashID = &c.StashID
break
}
}
draft.ID = stashID
var id *string
var ret graphql.SubmitPerformerDraft
err := c.submitDraft(ctx, graphql.SubmitPerformerDraftDocument, draft, image, &ret)
id = ret.SubmitPerformerDraft.ID
return id, err
// ret, err := c.client.SubmitPerformerDraft(ctx, draft, uploadImage(image))
// if err != nil {
// return nil, err
// }
// id := ret.SubmitPerformerDraft.ID
// return id, nil
}