mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
1202 lines
29 KiB
Go
1202 lines
29 KiB
Go
package stashbox
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/Yamashou/gqlgenc/client"
|
|
"golang.org/x/text/cases"
|
|
"golang.org/x/text/language"
|
|
|
|
"github.com/Yamashou/gqlgenc/graphqljson"
|
|
"github.com/stashapp/stash/pkg/file"
|
|
"github.com/stashapp/stash/pkg/fsutil"
|
|
"github.com/stashapp/stash/pkg/logger"
|
|
"github.com/stashapp/stash/pkg/match"
|
|
"github.com/stashapp/stash/pkg/models"
|
|
"github.com/stashapp/stash/pkg/scraper"
|
|
"github.com/stashapp/stash/pkg/scraper/stashbox/graphql"
|
|
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
|
"github.com/stashapp/stash/pkg/studio"
|
|
"github.com/stashapp/stash/pkg/tag"
|
|
"github.com/stashapp/stash/pkg/txn"
|
|
"github.com/stashapp/stash/pkg/utils"
|
|
)
|
|
|
|
type SceneReader interface {
|
|
Find(ctx context.Context, id int) (*models.Scene, error)
|
|
models.StashIDLoader
|
|
models.VideoFileLoader
|
|
}
|
|
|
|
type PerformerReader interface {
|
|
match.PerformerFinder
|
|
Find(ctx context.Context, id int) (*models.Performer, error)
|
|
FindBySceneID(ctx context.Context, sceneID int) ([]*models.Performer, error)
|
|
models.AliasLoader
|
|
models.StashIDLoader
|
|
GetImage(ctx context.Context, performerID int) ([]byte, error)
|
|
}
|
|
|
|
type StudioReader interface {
|
|
match.StudioFinder
|
|
studio.Finder
|
|
models.StashIDLoader
|
|
}
|
|
type TagFinder interface {
|
|
tag.Queryer
|
|
FindBySceneID(ctx context.Context, sceneID int) ([]*models.Tag, error)
|
|
}
|
|
|
|
type Repository struct {
|
|
Scene SceneReader
|
|
Performer PerformerReader
|
|
Tag TagFinder
|
|
Studio StudioReader
|
|
}
|
|
|
|
// Client represents the client interface to a stash-box server instance.
|
|
type Client struct {
|
|
client *graphql.Client
|
|
txnManager txn.Manager
|
|
repository Repository
|
|
box models.StashBox
|
|
}
|
|
|
|
// NewClient returns a new instance of a stash-box client.
|
|
func NewClient(box models.StashBox, txnManager txn.Manager, repo Repository) *Client {
|
|
authHeader := func(req *http.Request) {
|
|
req.Header.Set("ApiKey", box.APIKey)
|
|
}
|
|
|
|
client := &graphql.Client{
|
|
Client: client.NewClient(http.DefaultClient, box.Endpoint, authHeader),
|
|
}
|
|
|
|
return &Client{
|
|
client: client,
|
|
txnManager: txnManager,
|
|
repository: repo,
|
|
box: box,
|
|
}
|
|
}
|
|
|
|
func (c Client) getHTTPClient() *http.Client {
|
|
return c.client.Client.Client
|
|
}
|
|
|
|
// QueryStashBoxScene queries stash-box for scenes using a query string.
|
|
func (c Client) QueryStashBoxScene(ctx context.Context, queryStr string) ([]*scraper.ScrapedScene, error) {
|
|
scenes, err := c.client.SearchScene(ctx, queryStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sceneFragments := scenes.SearchScene
|
|
|
|
var ret []*scraper.ScrapedScene
|
|
for _, s := range sceneFragments {
|
|
ss, err := c.sceneFragmentToScrapedScene(ctx, s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret = append(ret, ss)
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
// FindStashBoxScenesByFingerprints queries stash-box for a scene using the
|
|
// scene's MD5/OSHASH checksum, or PHash.
|
|
func (c Client) FindStashBoxSceneByFingerprints(ctx context.Context, sceneID int) ([]*scraper.ScrapedScene, error) {
|
|
res, err := c.FindStashBoxScenesByFingerprints(ctx, []int{sceneID})
|
|
if len(res) > 0 {
|
|
return res[0], err
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// FindStashBoxScenesByFingerprints queries stash-box for scenes using every
|
|
// scene's MD5/OSHASH checksum, or PHash, and returns results in the same order
|
|
// as the input slice.
|
|
func (c Client) FindStashBoxScenesByFingerprints(ctx context.Context, ids []int) ([][]*scraper.ScrapedScene, error) {
|
|
var fingerprints [][]*graphql.FingerprintQueryInput
|
|
|
|
if err := txn.WithReadTxn(ctx, c.txnManager, func(ctx context.Context) error {
|
|
qb := c.repository.Scene
|
|
|
|
for _, sceneID := range ids {
|
|
scene, err := qb.Find(ctx, sceneID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if scene == nil {
|
|
return fmt.Errorf("scene with id %d not found", sceneID)
|
|
}
|
|
|
|
if err := scene.LoadFiles(ctx, c.repository.Scene); err != nil {
|
|
return err
|
|
}
|
|
|
|
var sceneFPs []*graphql.FingerprintQueryInput
|
|
|
|
for _, f := range scene.Files.List() {
|
|
checksum := f.Fingerprints.GetString(file.FingerprintTypeMD5)
|
|
if checksum != "" {
|
|
sceneFPs = append(sceneFPs, &graphql.FingerprintQueryInput{
|
|
Hash: checksum,
|
|
Algorithm: graphql.FingerprintAlgorithmMd5,
|
|
})
|
|
}
|
|
|
|
oshash := f.Fingerprints.GetString(file.FingerprintTypeOshash)
|
|
if oshash != "" {
|
|
sceneFPs = append(sceneFPs, &graphql.FingerprintQueryInput{
|
|
Hash: oshash,
|
|
Algorithm: graphql.FingerprintAlgorithmOshash,
|
|
})
|
|
}
|
|
|
|
phash := f.Fingerprints.GetInt64(file.FingerprintTypePhash)
|
|
if phash != 0 {
|
|
phashStr := utils.PhashToString(phash)
|
|
sceneFPs = append(sceneFPs, &graphql.FingerprintQueryInput{
|
|
Hash: phashStr,
|
|
Algorithm: graphql.FingerprintAlgorithmPhash,
|
|
})
|
|
}
|
|
}
|
|
|
|
fingerprints = append(fingerprints, sceneFPs)
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return c.findStashBoxScenesByFingerprints(ctx, fingerprints)
|
|
}
|
|
|
|
func (c Client) findStashBoxScenesByFingerprints(ctx context.Context, scenes [][]*graphql.FingerprintQueryInput) ([][]*scraper.ScrapedScene, error) {
|
|
var results [][]*scraper.ScrapedScene
|
|
|
|
// filter out nils
|
|
var validScenes [][]*graphql.FingerprintQueryInput
|
|
for _, s := range scenes {
|
|
if len(s) > 0 {
|
|
validScenes = append(validScenes, s)
|
|
}
|
|
}
|
|
|
|
for i := 0; i < len(validScenes); i += 40 {
|
|
end := i + 40
|
|
if end > len(validScenes) {
|
|
end = len(validScenes)
|
|
}
|
|
scenes, err := c.client.FindScenesBySceneFingerprints(ctx, validScenes[i:end])
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, sceneFragments := range scenes.FindScenesBySceneFingerprints {
|
|
var sceneResults []*scraper.ScrapedScene
|
|
for _, scene := range sceneFragments {
|
|
ss, err := c.sceneFragmentToScrapedScene(ctx, scene)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sceneResults = append(sceneResults, ss)
|
|
}
|
|
results = append(results, sceneResults)
|
|
}
|
|
}
|
|
|
|
// repopulate the results to be the same order as the input
|
|
ret := make([][]*scraper.ScrapedScene, len(scenes))
|
|
upTo := 0
|
|
|
|
for i, v := range scenes {
|
|
if len(v) > 0 {
|
|
ret[i] = results[upTo]
|
|
upTo++
|
|
}
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func (c Client) SubmitStashBoxFingerprints(ctx context.Context, sceneIDs []string, endpoint string) (bool, error) {
|
|
ids, err := stringslice.StringSliceToIntSlice(sceneIDs)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
var fingerprints []graphql.FingerprintSubmission
|
|
|
|
if err := txn.WithReadTxn(ctx, c.txnManager, func(ctx context.Context) error {
|
|
qb := c.repository.Scene
|
|
|
|
for _, sceneID := range ids {
|
|
// TODO - Find should return an appropriate not found error
|
|
scene, err := qb.Find(ctx, sceneID)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return err
|
|
}
|
|
|
|
if scene == nil {
|
|
continue
|
|
}
|
|
|
|
if err := scene.LoadStashIDs(ctx, qb); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := scene.LoadFiles(ctx, qb); err != nil {
|
|
return err
|
|
}
|
|
|
|
stashIDs := scene.StashIDs.List()
|
|
sceneStashID := ""
|
|
for _, stashID := range stashIDs {
|
|
if stashID.Endpoint == endpoint {
|
|
sceneStashID = stashID.StashID
|
|
}
|
|
}
|
|
|
|
if sceneStashID != "" {
|
|
for _, f := range scene.Files.List() {
|
|
duration := f.Duration
|
|
|
|
if duration != 0 {
|
|
if checksum := f.Fingerprints.GetString(file.FingerprintTypeMD5); checksum != "" {
|
|
fingerprint := graphql.FingerprintInput{
|
|
Hash: checksum,
|
|
Algorithm: graphql.FingerprintAlgorithmMd5,
|
|
Duration: int(duration),
|
|
}
|
|
fingerprints = append(fingerprints, graphql.FingerprintSubmission{
|
|
SceneID: sceneStashID,
|
|
Fingerprint: &fingerprint,
|
|
})
|
|
}
|
|
|
|
if oshash := f.Fingerprints.GetString(file.FingerprintTypeOshash); oshash != "" {
|
|
fingerprint := graphql.FingerprintInput{
|
|
Hash: oshash,
|
|
Algorithm: graphql.FingerprintAlgorithmOshash,
|
|
Duration: int(duration),
|
|
}
|
|
fingerprints = append(fingerprints, graphql.FingerprintSubmission{
|
|
SceneID: sceneStashID,
|
|
Fingerprint: &fingerprint,
|
|
})
|
|
}
|
|
|
|
if phash := f.Fingerprints.GetInt64(file.FingerprintTypePhash); phash != 0 {
|
|
fingerprint := graphql.FingerprintInput{
|
|
Hash: utils.PhashToString(phash),
|
|
Algorithm: graphql.FingerprintAlgorithmPhash,
|
|
Duration: int(duration),
|
|
}
|
|
fingerprints = append(fingerprints, graphql.FingerprintSubmission{
|
|
SceneID: sceneStashID,
|
|
Fingerprint: &fingerprint,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return c.submitStashBoxFingerprints(ctx, fingerprints)
|
|
}
|
|
|
|
func (c Client) submitStashBoxFingerprints(ctx context.Context, fingerprints []graphql.FingerprintSubmission) (bool, error) {
|
|
for _, fingerprint := range fingerprints {
|
|
_, err := c.client.SubmitFingerprint(ctx, fingerprint)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// QueryStashBoxPerformer queries stash-box for performers using a query string.
|
|
func (c Client) QueryStashBoxPerformer(ctx context.Context, queryStr string) ([]*StashBoxPerformerQueryResult, error) {
|
|
performers, err := c.queryStashBoxPerformer(ctx, queryStr)
|
|
|
|
res := []*StashBoxPerformerQueryResult{
|
|
{
|
|
Query: queryStr,
|
|
Results: performers,
|
|
},
|
|
}
|
|
|
|
// set the deprecated image field
|
|
for _, p := range res[0].Results {
|
|
if len(p.Images) > 0 {
|
|
p.Image = &p.Images[0]
|
|
}
|
|
}
|
|
|
|
return res, err
|
|
}
|
|
|
|
func (c Client) queryStashBoxPerformer(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
|
|
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(ctx context.Context, performerIDs []string) ([]*StashBoxPerformerQueryResult, error) {
|
|
ids, err := stringslice.StringSliceToIntSlice(performerIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var performers []*models.Performer
|
|
|
|
if err := txn.WithReadTxn(ctx, c.txnManager, func(ctx context.Context) error {
|
|
qb := c.repository.Performer
|
|
|
|
for _, performerID := range ids {
|
|
performer, err := qb.Find(ctx, performerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if performer == nil {
|
|
return fmt.Errorf("performer with id %d not found", performerID)
|
|
}
|
|
|
|
if performer.Name != "" {
|
|
performers = append(performers, performer)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return c.findStashBoxPerformersByNames(ctx, performers)
|
|
}
|
|
|
|
func (c Client) FindStashBoxPerformersByPerformerNames(ctx context.Context, performerIDs []string) ([][]*models.ScrapedPerformer, error) {
|
|
ids, err := stringslice.StringSliceToIntSlice(performerIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var performers []*models.Performer
|
|
|
|
if err := txn.WithReadTxn(ctx, c.txnManager, func(ctx context.Context) error {
|
|
qb := c.repository.Performer
|
|
|
|
for _, performerID := range ids {
|
|
performer, err := qb.Find(ctx, performerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if performer == nil {
|
|
return fmt.Errorf("performer with id %d not found", performerID)
|
|
}
|
|
|
|
if performer.Name != "" {
|
|
performers = append(performers, performer)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
results, err := c.findStashBoxPerformersByNames(ctx, performers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var ret [][]*models.ScrapedPerformer
|
|
for _, r := range results {
|
|
ret = append(ret, r.Results)
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (c Client) findStashBoxPerformersByNames(ctx context.Context, performers []*models.Performer) ([]*StashBoxPerformerQueryResult, error) {
|
|
var ret []*StashBoxPerformerQueryResult
|
|
for _, performer := range performers {
|
|
if performer.Name != "" {
|
|
performerResults, err := c.queryStashBoxPerformer(ctx, performer.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := StashBoxPerformerQueryResult{
|
|
Query: strconv.Itoa(performer.ID),
|
|
Results: performerResults,
|
|
}
|
|
|
|
ret = append(ret, &result)
|
|
}
|
|
}
|
|
|
|
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.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 performerFragmentToScrapedScenePerformer(p graphql.PerformerFragment) *models.ScrapedPerformer {
|
|
id := p.ID
|
|
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: &id,
|
|
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 {
|
|
b := p.Birthdate.Date
|
|
sp.Birthdate = &b
|
|
}
|
|
|
|
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 {
|
|
alias := strings.Join(p.Aliases, ", ")
|
|
sp.Aliases = &alias
|
|
}
|
|
|
|
return sp
|
|
}
|
|
|
|
func getFirstImage(ctx context.Context, client *http.Client, images []*graphql.ImageFragment) *string {
|
|
ret, err := fetchImage(ctx, client, images[0].URL)
|
|
if err != nil {
|
|
logger.Warnf("Error fetching image %s: %s", images[0].URL, err.Error())
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func getFingerprints(scene *graphql.SceneFragment) []*models.StashBoxFingerprint {
|
|
fingerprints := []*models.StashBoxFingerprint{}
|
|
for _, fp := range scene.Fingerprints {
|
|
fingerprint := models.StashBoxFingerprint{
|
|
Algorithm: fp.Algorithm.String(),
|
|
Hash: fp.Hash,
|
|
Duration: fp.Duration,
|
|
}
|
|
fingerprints = append(fingerprints, &fingerprint)
|
|
}
|
|
return fingerprints
|
|
}
|
|
|
|
func (c Client) sceneFragmentToScrapedScene(ctx context.Context, s *graphql.SceneFragment) (*scraper.ScrapedScene, error) {
|
|
stashID := s.ID
|
|
ss := &scraper.ScrapedScene{
|
|
Title: s.Title,
|
|
Code: s.Code,
|
|
Date: s.Date,
|
|
Details: s.Details,
|
|
Director: s.Director,
|
|
URL: findURL(s.Urls, "STUDIO"),
|
|
Duration: s.Duration,
|
|
RemoteSiteID: &stashID,
|
|
Fingerprints: getFingerprints(s),
|
|
// Image
|
|
// stash_id
|
|
}
|
|
|
|
if len(s.Images) > 0 {
|
|
// TODO - #454 code sorts images by aspect ratio according to a wanted
|
|
// orientation. I'm just grabbing the first for now
|
|
ss.Image = getFirstImage(ctx, c.getHTTPClient(), s.Images)
|
|
}
|
|
|
|
if err := txn.WithReadTxn(ctx, c.txnManager, func(ctx context.Context) error {
|
|
pqb := c.repository.Performer
|
|
tqb := c.repository.Tag
|
|
|
|
if s.Studio != nil {
|
|
studioID := s.Studio.ID
|
|
ss.Studio = &models.ScrapedStudio{
|
|
Name: s.Studio.Name,
|
|
URL: findURL(s.Studio.Urls, "HOME"),
|
|
RemoteSiteID: &studioID,
|
|
}
|
|
|
|
err := match.ScrapedStudio(ctx, c.repository.Studio, ss.Studio, &c.box.Endpoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, p := range s.Performers {
|
|
sp := performerFragmentToScrapedScenePerformer(p.Performer)
|
|
|
|
err := match.ScrapedPerformer(ctx, pqb, sp, &c.box.Endpoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ss.Performers = append(ss.Performers, sp)
|
|
}
|
|
|
|
for _, t := range s.Tags {
|
|
st := &models.ScrapedTag{
|
|
Name: t.Name,
|
|
}
|
|
|
|
err := match.ScrapedTag(ctx, tqb, st)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ss.Tags = append(ss.Tags, st)
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ss, nil
|
|
}
|
|
|
|
func (c Client) FindStashBoxPerformerByID(ctx context.Context, id string) (*models.ScrapedPerformer, error) {
|
|
performer, err := c.client.FindPerformerByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ret := performerFragmentToScrapedScenePerformer(*performer.FindPerformer)
|
|
return ret, nil
|
|
}
|
|
|
|
func (c Client) FindStashBoxPerformerByName(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 = performerFragmentToScrapedScenePerformer(*performer)
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (c Client) GetUser(ctx context.Context) (*graphql.Me, error) {
|
|
return c.client.Me(ctx)
|
|
}
|
|
|
|
func appendFingerprintUnique(v []*graphql.FingerprintInput, toAdd *graphql.FingerprintInput) []*graphql.FingerprintInput {
|
|
for _, vv := range v {
|
|
if vv.Algorithm == toAdd.Algorithm && vv.Hash == toAdd.Hash {
|
|
return v
|
|
}
|
|
}
|
|
|
|
return append(v, toAdd)
|
|
}
|
|
|
|
func (c Client) SubmitSceneDraft(ctx context.Context, scene *models.Scene, endpoint string, imagePath string) (*string, error) {
|
|
draft := graphql.SceneDraftInput{}
|
|
var image io.Reader
|
|
r := c.repository
|
|
pqb := r.Performer
|
|
sqb := r.Studio
|
|
|
|
if scene.Title != "" {
|
|
draft.Title = &scene.Title
|
|
}
|
|
if scene.Details != "" {
|
|
draft.Details = &scene.Details
|
|
}
|
|
if scene.URL != "" && len(strings.TrimSpace(scene.URL)) > 0 {
|
|
url := strings.TrimSpace(scene.URL)
|
|
draft.URL = &url
|
|
}
|
|
if scene.Date != nil {
|
|
v := scene.Date.String()
|
|
draft.Date = &v
|
|
}
|
|
|
|
if scene.StudioID != nil {
|
|
studio, err := sqb.Find(ctx, int(*scene.StudioID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
studioDraft := graphql.DraftEntityInput{
|
|
Name: studio.Name.String,
|
|
}
|
|
|
|
stashIDs, err := sqb.GetStashIDs(ctx, studio.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, stashID := range stashIDs {
|
|
c := stashID
|
|
if stashID.Endpoint == endpoint {
|
|
studioDraft.ID = &c.StashID
|
|
break
|
|
}
|
|
}
|
|
draft.Studio = &studioDraft
|
|
}
|
|
|
|
fingerprints := []*graphql.FingerprintInput{}
|
|
|
|
// submit all file fingerprints
|
|
if err := scene.LoadFiles(ctx, r.Scene); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, f := range scene.Files.List() {
|
|
duration := f.Duration
|
|
|
|
if duration != 0 {
|
|
if oshash := f.Fingerprints.GetString(file.FingerprintTypeOshash); oshash != "" {
|
|
fingerprint := graphql.FingerprintInput{
|
|
Hash: oshash,
|
|
Algorithm: graphql.FingerprintAlgorithmOshash,
|
|
Duration: int(duration),
|
|
}
|
|
fingerprints = appendFingerprintUnique(fingerprints, &fingerprint)
|
|
}
|
|
|
|
if checksum := f.Fingerprints.GetString(file.FingerprintTypeMD5); checksum != "" {
|
|
fingerprint := graphql.FingerprintInput{
|
|
Hash: checksum,
|
|
Algorithm: graphql.FingerprintAlgorithmMd5,
|
|
Duration: int(duration),
|
|
}
|
|
fingerprints = appendFingerprintUnique(fingerprints, &fingerprint)
|
|
}
|
|
|
|
if phash := f.Fingerprints.GetInt64(file.FingerprintTypePhash); phash != 0 {
|
|
fingerprint := graphql.FingerprintInput{
|
|
Hash: utils.PhashToString(phash),
|
|
Algorithm: graphql.FingerprintAlgorithmPhash,
|
|
Duration: int(duration),
|
|
}
|
|
fingerprints = appendFingerprintUnique(fingerprints, &fingerprint)
|
|
}
|
|
}
|
|
}
|
|
draft.Fingerprints = fingerprints
|
|
|
|
scenePerformers, err := pqb.FindBySceneID(ctx, scene.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
performers := []*graphql.DraftEntityInput{}
|
|
for _, p := range scenePerformers {
|
|
performerDraft := graphql.DraftEntityInput{
|
|
Name: p.Name,
|
|
}
|
|
|
|
stashIDs, err := pqb.GetStashIDs(ctx, p.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, stashID := range stashIDs {
|
|
c := stashID
|
|
if stashID.Endpoint == endpoint {
|
|
performerDraft.ID = &c.StashID
|
|
break
|
|
}
|
|
}
|
|
|
|
performers = append(performers, &performerDraft)
|
|
}
|
|
draft.Performers = performers
|
|
|
|
var tags []*graphql.DraftEntityInput
|
|
sceneTags, err := r.Tag.FindBySceneID(ctx, scene.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, tag := range sceneTags {
|
|
tags = append(tags, &graphql.DraftEntityInput{Name: tag.Name})
|
|
}
|
|
draft.Tags = tags
|
|
|
|
if imagePath != "" {
|
|
exists, _ := fsutil.FileExists(imagePath)
|
|
if exists {
|
|
file, err := os.Open(imagePath)
|
|
if err == nil {
|
|
image = file
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := scene.LoadStashIDs(ctx, r.Scene); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
stashIDs := scene.StashIDs.List()
|
|
var stashID *string
|
|
for _, v := range stashIDs {
|
|
if v.Endpoint == endpoint {
|
|
vv := v.StashID
|
|
stashID = &vv
|
|
break
|
|
}
|
|
}
|
|
draft.ID = stashID
|
|
|
|
var id *string
|
|
var ret graphql.SubmitSceneDraft
|
|
err = c.submitDraft(ctx, graphql.SubmitSceneDraftDocument, draft, image, &ret)
|
|
id = ret.SubmitSceneDraft.ID
|
|
|
|
return id, err
|
|
|
|
// ret, err := c.client.SubmitSceneDraft(ctx, draft, uploadImage(image))
|
|
// if err != nil {
|
|
// return nil, err
|
|
// }
|
|
|
|
// id := ret.SubmitSceneDraft.ID
|
|
// return id, nil
|
|
}
|
|
|
|
func (c Client) SubmitPerformerDraft(ctx context.Context, performer *models.Performer, endpoint string) (*string, error) {
|
|
draft := graphql.PerformerDraftInput{}
|
|
var image io.Reader
|
|
pqb := c.repository.Performer
|
|
|
|
if err := performer.LoadAliases(ctx, pqb); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := performer.LoadStashIDs(ctx, pqb); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
img, _ := pqb.GetImage(ctx, performer.ID)
|
|
if img != nil {
|
|
image = bytes.NewReader(img)
|
|
}
|
|
|
|
if performer.Name != "" {
|
|
draft.Name = performer.Name
|
|
}
|
|
// stash-box does not support Disambiguation currently
|
|
// 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.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
|
|
}
|
|
}
|
|
}
|
|
|
|
var urls []string
|
|
if len(strings.TrimSpace(performer.Twitter)) > 0 {
|
|
urls = append(urls, "https://twitter.com/"+strings.TrimSpace(performer.Twitter))
|
|
}
|
|
if len(strings.TrimSpace(performer.Instagram)) > 0 {
|
|
urls = append(urls, "https://instagram.com/"+strings.TrimSpace(performer.Instagram))
|
|
}
|
|
if len(strings.TrimSpace(performer.URL)) > 0 {
|
|
urls = append(urls, strings.TrimSpace(performer.URL))
|
|
}
|
|
if len(urls) > 0 {
|
|
draft.Urls = urls
|
|
}
|
|
|
|
stashIDs, err := pqb.GetStashIDs(ctx, performer.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var stashID *string
|
|
for _, v := range stashIDs {
|
|
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
|
|
}
|
|
|
|
// we can't currently use this due to https://github.com/Yamashou/gqlgenc/issues/109
|
|
// func uploadImage(image io.Reader) client.HTTPRequestOption {
|
|
// return func(req *http.Request) {
|
|
// if image == nil {
|
|
// // return without changing anything
|
|
// return
|
|
// }
|
|
|
|
// // we can't handle errors in here, so if one happens, just return
|
|
// // without changing anything.
|
|
|
|
// // repackage the request to include the image
|
|
// bodyBytes, err := ioutil.ReadAll(req.Body)
|
|
// if err != nil {
|
|
// return
|
|
// }
|
|
|
|
// newBody := &bytes.Buffer{}
|
|
// writer := multipart.NewWriter(newBody)
|
|
// _ = writer.WriteField("operations", string(bodyBytes))
|
|
|
|
// if err := writer.WriteField("map", "{ \"0\": [\"variables.input.image\"] }"); err != nil {
|
|
// return
|
|
// }
|
|
// part, _ := writer.CreateFormFile("0", "draft")
|
|
// if _, err := io.Copy(part, image); err != nil {
|
|
// return
|
|
// }
|
|
|
|
// writer.Close()
|
|
|
|
// // now set the request body to this new body
|
|
// req.Body = io.NopCloser(newBody)
|
|
// req.ContentLength = int64(newBody.Len())
|
|
// req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
// }
|
|
// }
|
|
|
|
func (c *Client) submitDraft(ctx context.Context, query string, input interface{}, image io.Reader, ret interface{}) error {
|
|
vars := map[string]interface{}{
|
|
"input": input,
|
|
}
|
|
|
|
r := &client.Request{
|
|
Query: query,
|
|
Variables: vars,
|
|
OperationName: "",
|
|
}
|
|
|
|
requestBody, err := json.Marshal(r)
|
|
if err != nil {
|
|
return fmt.Errorf("encode: %w", err)
|
|
}
|
|
|
|
body := &bytes.Buffer{}
|
|
writer := multipart.NewWriter(body)
|
|
if err := writer.WriteField("operations", string(requestBody)); err != nil {
|
|
return err
|
|
}
|
|
|
|
if image != nil {
|
|
if err := writer.WriteField("map", "{ \"0\": [\"variables.input.image\"] }"); err != nil {
|
|
return err
|
|
}
|
|
part, _ := writer.CreateFormFile("0", "draft")
|
|
if _, err := io.Copy(part, image); err != nil {
|
|
return err
|
|
}
|
|
} else if err := writer.WriteField("map", "{}"); err != nil {
|
|
return err
|
|
}
|
|
|
|
writer.Close()
|
|
|
|
req, _ := http.NewRequestWithContext(ctx, "POST", c.box.Endpoint, body)
|
|
req.Header.Add("Content-Type", writer.FormDataContentType())
|
|
req.Header.Set("ApiKey", c.box.APIKey)
|
|
|
|
httpClient := c.client.Client.Client
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
responseBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
type response struct {
|
|
Data json.RawMessage `json:"data"`
|
|
Errors json.RawMessage `json:"errors"`
|
|
}
|
|
|
|
var respGQL response
|
|
|
|
if err := json.Unmarshal(responseBytes, &respGQL); err != nil {
|
|
return fmt.Errorf("failed to decode data %s: %w", string(responseBytes), err)
|
|
}
|
|
|
|
if respGQL.Errors != nil && len(respGQL.Errors) > 0 {
|
|
// try to parse standard graphql error
|
|
errors := &client.GqlErrorList{}
|
|
if e := json.Unmarshal(responseBytes, errors); e != nil {
|
|
return fmt.Errorf("failed to parse graphql errors. Response content %s - %w ", string(responseBytes), e)
|
|
}
|
|
|
|
return errors
|
|
}
|
|
|
|
if err := graphqljson.UnmarshalData(respGQL.Data, ret); err != nil {
|
|
return err
|
|
}
|
|
|
|
return err
|
|
}
|