Performer urls (#4958)

* Populate URLs from legacy fields
* Return nil properly in xpath/json scrapers
* Improve migration logging
This commit is contained in:
WithoutPants 2024-06-18 13:41:05 +10:00 committed by GitHub
parent fda4776d30
commit f26766033e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 992 additions and 379 deletions

View file

@ -16,10 +16,11 @@ type Performer {
id: ID! id: ID!
name: String! name: String!
disambiguation: String disambiguation: String
url: String url: String @deprecated(reason: "Use urls")
urls: [String!]
gender: GenderEnum gender: GenderEnum
twitter: String twitter: String @deprecated(reason: "Use urls")
instagram: String instagram: String @deprecated(reason: "Use urls")
birthdate: String birthdate: String
ethnicity: String ethnicity: String
country: String country: String
@ -60,7 +61,8 @@ type Performer {
input PerformerCreateInput { input PerformerCreateInput {
name: String! name: String!
disambiguation: String disambiguation: String
url: String url: String @deprecated(reason: "Use urls")
urls: [String!]
gender: GenderEnum gender: GenderEnum
birthdate: String birthdate: String
ethnicity: String ethnicity: String
@ -75,8 +77,8 @@ input PerformerCreateInput {
tattoos: String tattoos: String
piercings: String piercings: String
alias_list: [String!] alias_list: [String!]
twitter: String twitter: String @deprecated(reason: "Use urls")
instagram: String instagram: String @deprecated(reason: "Use urls")
favorite: Boolean favorite: Boolean
tag_ids: [ID!] tag_ids: [ID!]
"This should be a URL or a base64 encoded data URL" "This should be a URL or a base64 encoded data URL"
@ -95,7 +97,8 @@ input PerformerUpdateInput {
id: ID! id: ID!
name: String name: String
disambiguation: String disambiguation: String
url: String url: String @deprecated(reason: "Use urls")
urls: [String!]
gender: GenderEnum gender: GenderEnum
birthdate: String birthdate: String
ethnicity: String ethnicity: String
@ -110,8 +113,8 @@ input PerformerUpdateInput {
tattoos: String tattoos: String
piercings: String piercings: String
alias_list: [String!] alias_list: [String!]
twitter: String twitter: String @deprecated(reason: "Use urls")
instagram: String instagram: String @deprecated(reason: "Use urls")
favorite: Boolean favorite: Boolean
tag_ids: [ID!] tag_ids: [ID!]
"This should be a URL or a base64 encoded data URL" "This should be a URL or a base64 encoded data URL"
@ -135,7 +138,8 @@ input BulkPerformerUpdateInput {
clientMutationId: String clientMutationId: String
ids: [ID!] ids: [ID!]
disambiguation: String disambiguation: String
url: String url: String @deprecated(reason: "Use urls")
urls: BulkUpdateStrings
gender: GenderEnum gender: GenderEnum
birthdate: String birthdate: String
ethnicity: String ethnicity: String
@ -150,8 +154,8 @@ input BulkPerformerUpdateInput {
tattoos: String tattoos: String
piercings: String piercings: String
alias_list: BulkUpdateStrings alias_list: BulkUpdateStrings
twitter: String twitter: String @deprecated(reason: "Use urls")
instagram: String instagram: String @deprecated(reason: "Use urls")
favorite: Boolean favorite: Boolean
tag_ids: BulkUpdateIds tag_ids: BulkUpdateIds
# rating expressed as 1-100 # rating expressed as 1-100

View file

@ -5,9 +5,10 @@ type ScrapedPerformer {
name: String name: String
disambiguation: String disambiguation: String
gender: String gender: String
url: String url: String @deprecated(reason: "use urls")
twitter: String urls: [String!]
instagram: String twitter: String @deprecated(reason: "use urls")
instagram: String @deprecated(reason: "use urls")
birthdate: String birthdate: String
ethnicity: String ethnicity: String
country: String country: String
@ -40,9 +41,10 @@ input ScrapedPerformerInput {
name: String name: String
disambiguation: String disambiguation: String
gender: String gender: String
url: String url: String @deprecated(reason: "use urls")
twitter: String urls: [String!]
instagram: String twitter: String @deprecated(reason: "use urls")
instagram: String @deprecated(reason: "use urls")
birthdate: String birthdate: String
ethnicity: String ethnicity: String
country: String country: String

View file

@ -24,6 +24,79 @@ func (r *performerResolver) AliasList(ctx context.Context, obj *models.Performer
return obj.Aliases.List(), nil return obj.Aliases.List(), nil
} }
func (r *performerResolver) URL(ctx context.Context, obj *models.Performer) (*string, error) {
if !obj.URLs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadURLs(ctx, r.repository.Performer)
}); err != nil {
return nil, err
}
}
urls := obj.URLs.List()
if len(urls) == 0 {
return nil, nil
}
return &urls[0], nil
}
func (r *performerResolver) Twitter(ctx context.Context, obj *models.Performer) (*string, error) {
if !obj.URLs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadURLs(ctx, r.repository.Performer)
}); err != nil {
return nil, err
}
}
urls := obj.URLs.List()
// find the first twitter url
for _, url := range urls {
if performer.IsTwitterURL(url) {
u := url
return &u, nil
}
}
return nil, nil
}
func (r *performerResolver) Instagram(ctx context.Context, obj *models.Performer) (*string, error) {
if !obj.URLs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadURLs(ctx, r.repository.Performer)
}); err != nil {
return nil, err
}
}
urls := obj.URLs.List()
// find the first instagram url
for _, url := range urls {
if performer.IsInstagramURL(url) {
u := url
return &u, nil
}
}
return nil, nil
}
func (r *performerResolver) Urls(ctx context.Context, obj *models.Performer) ([]string, error) {
if !obj.URLs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadURLs(ctx, r.repository.Performer)
}); err != nil {
return nil, err
}
}
return obj.URLs.List(), nil
}
func (r *performerResolver) Height(ctx context.Context, obj *models.Performer) (*string, error) { func (r *performerResolver) Height(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Height != nil { if obj.Height != nil {
ret := strconv.Itoa(*obj.Height) ret := strconv.Itoa(*obj.Height)

View file

@ -12,6 +12,11 @@ import (
"github.com/stashapp/stash/pkg/utils" "github.com/stashapp/stash/pkg/utils"
) )
const (
twitterURL = "https://twitter.com"
instagramURL = "https://instagram.com"
)
// used to refetch performer after hooks run // used to refetch performer after hooks run
func (r *mutationResolver) getPerformer(ctx context.Context, id int) (ret *models.Performer, err error) { func (r *mutationResolver) getPerformer(ctx context.Context, id int) (ret *models.Performer, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error { if err := r.withTxn(ctx, func(ctx context.Context) error {
@ -35,7 +40,6 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
newPerformer.Name = input.Name newPerformer.Name = input.Name
newPerformer.Disambiguation = translator.string(input.Disambiguation) newPerformer.Disambiguation = translator.string(input.Disambiguation)
newPerformer.Aliases = models.NewRelatedStrings(input.AliasList) newPerformer.Aliases = models.NewRelatedStrings(input.AliasList)
newPerformer.URL = translator.string(input.URL)
newPerformer.Gender = input.Gender newPerformer.Gender = input.Gender
newPerformer.Ethnicity = translator.string(input.Ethnicity) newPerformer.Ethnicity = translator.string(input.Ethnicity)
newPerformer.Country = translator.string(input.Country) newPerformer.Country = translator.string(input.Country)
@ -47,8 +51,6 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
newPerformer.CareerLength = translator.string(input.CareerLength) newPerformer.CareerLength = translator.string(input.CareerLength)
newPerformer.Tattoos = translator.string(input.Tattoos) newPerformer.Tattoos = translator.string(input.Tattoos)
newPerformer.Piercings = translator.string(input.Piercings) newPerformer.Piercings = translator.string(input.Piercings)
newPerformer.Twitter = translator.string(input.Twitter)
newPerformer.Instagram = translator.string(input.Instagram)
newPerformer.Favorite = translator.bool(input.Favorite) newPerformer.Favorite = translator.bool(input.Favorite)
newPerformer.Rating = input.Rating100 newPerformer.Rating = input.Rating100
newPerformer.Details = translator.string(input.Details) newPerformer.Details = translator.string(input.Details)
@ -58,6 +60,21 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
newPerformer.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag) newPerformer.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
newPerformer.StashIDs = models.NewRelatedStashIDs(input.StashIds) newPerformer.StashIDs = models.NewRelatedStashIDs(input.StashIds)
newPerformer.URLs = models.NewRelatedStrings([]string{})
if input.URL != nil {
newPerformer.URLs.Add(*input.URL)
}
if input.Twitter != nil {
newPerformer.URLs.Add(utils.URLFromHandle(*input.Twitter, twitterURL))
}
if input.Instagram != nil {
newPerformer.URLs.Add(utils.URLFromHandle(*input.Instagram, instagramURL))
}
if input.Urls != nil {
newPerformer.URLs.Add(input.Urls...)
}
var err error var err error
newPerformer.Birthdate, err = translator.datePtr(input.Birthdate) newPerformer.Birthdate, err = translator.datePtr(input.Birthdate)
@ -112,6 +129,96 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
return r.getPerformer(ctx, newPerformer.ID) return r.getPerformer(ctx, newPerformer.ID)
} }
func (r *mutationResolver) validateNoLegacyURLs(translator changesetTranslator) error {
// ensure url/twitter/instagram are not included in the input
if translator.hasField("url") {
return fmt.Errorf("url field must not be included if urls is included")
}
if translator.hasField("twitter") {
return fmt.Errorf("twitter field must not be included if urls is included")
}
if translator.hasField("instagram") {
return fmt.Errorf("instagram field must not be included if urls is included")
}
return nil
}
func (r *mutationResolver) handleLegacyURLs(ctx context.Context, performerID int, legacyURL, legacyTwitter, legacyInstagram models.OptionalString, updatedPerformer *models.PerformerPartial) error {
qb := r.repository.Performer
// we need to be careful with URL/Twitter/Instagram
// treat URL as replacing the first non-Twitter/Instagram URL in the list
// twitter should replace any existing twitter URL
// instagram should replace any existing instagram URL
p, err := qb.Find(ctx, performerID)
if err != nil {
return err
}
if err := p.LoadURLs(ctx, qb); err != nil {
return fmt.Errorf("loading performer URLs: %w", err)
}
existingURLs := p.URLs.List()
// performer partial URLs should be empty
if legacyURL.Set {
replaced := false
for i, url := range existingURLs {
if !performer.IsTwitterURL(url) && !performer.IsInstagramURL(url) {
existingURLs[i] = legacyURL.Value
replaced = true
break
}
}
if !replaced {
existingURLs = append(existingURLs, legacyURL.Value)
}
}
if legacyTwitter.Set {
value := utils.URLFromHandle(legacyTwitter.Value, twitterURL)
found := false
// find and replace the first twitter URL
for i, url := range existingURLs {
if performer.IsTwitterURL(url) {
existingURLs[i] = value
found = true
break
}
}
if !found {
existingURLs = append(existingURLs, value)
}
}
if legacyInstagram.Set {
found := false
value := utils.URLFromHandle(legacyInstagram.Value, instagramURL)
// find and replace the first instagram URL
for i, url := range existingURLs {
if performer.IsInstagramURL(url) {
existingURLs[i] = value
found = true
break
}
}
if !found {
existingURLs = append(existingURLs, value)
}
}
updatedPerformer.URLs = &models.UpdateStrings{
Values: existingURLs,
Mode: models.RelationshipUpdateModeSet,
}
return nil
}
func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.PerformerUpdateInput) (*models.Performer, error) { func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.PerformerUpdateInput) (*models.Performer, error) {
performerID, err := strconv.Atoi(input.ID) performerID, err := strconv.Atoi(input.ID)
if err != nil { if err != nil {
@ -127,7 +234,6 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
updatedPerformer.Name = translator.optionalString(input.Name, "name") updatedPerformer.Name = translator.optionalString(input.Name, "name")
updatedPerformer.Disambiguation = translator.optionalString(input.Disambiguation, "disambiguation") updatedPerformer.Disambiguation = translator.optionalString(input.Disambiguation, "disambiguation")
updatedPerformer.URL = translator.optionalString(input.URL, "url")
updatedPerformer.Gender = translator.optionalString((*string)(input.Gender), "gender") updatedPerformer.Gender = translator.optionalString((*string)(input.Gender), "gender")
updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity") updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity")
updatedPerformer.Country = translator.optionalString(input.Country, "country") updatedPerformer.Country = translator.optionalString(input.Country, "country")
@ -139,8 +245,6 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length") updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos") updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings") updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite") updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedPerformer.Rating = translator.optionalInt(input.Rating100, "rating100") updatedPerformer.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedPerformer.Details = translator.optionalString(input.Details, "details") updatedPerformer.Details = translator.optionalString(input.Details, "details")
@ -149,6 +253,19 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag") updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
updatedPerformer.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids") updatedPerformer.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids")
if translator.hasField("urls") {
// ensure url/twitter/instagram are not included in the input
if err := r.validateNoLegacyURLs(translator); err != nil {
return nil, err
}
updatedPerformer.URLs = translator.updateStrings(input.Urls, "urls")
}
legacyURL := translator.optionalString(input.URL, "url")
legacyTwitter := translator.optionalString(input.Twitter, "twitter")
legacyInstagram := translator.optionalString(input.Instagram, "instagram")
updatedPerformer.Birthdate, err = translator.optionalDate(input.Birthdate, "birthdate") updatedPerformer.Birthdate, err = translator.optionalDate(input.Birthdate, "birthdate")
if err != nil { if err != nil {
return nil, fmt.Errorf("converting birthdate: %w", err) return nil, fmt.Errorf("converting birthdate: %w", err)
@ -186,6 +303,12 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
if err := r.withTxn(ctx, func(ctx context.Context) error { if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Performer qb := r.repository.Performer
if legacyURL.Set || legacyTwitter.Set || legacyInstagram.Set {
if err := r.handleLegacyURLs(ctx, performerID, legacyURL, legacyTwitter, legacyInstagram, &updatedPerformer); err != nil {
return err
}
}
if err := performer.ValidateUpdate(ctx, performerID, updatedPerformer, qb); err != nil { if err := performer.ValidateUpdate(ctx, performerID, updatedPerformer, qb); err != nil {
return err return err
} }
@ -225,7 +348,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
updatedPerformer := models.NewPerformerPartial() updatedPerformer := models.NewPerformerPartial()
updatedPerformer.Disambiguation = translator.optionalString(input.Disambiguation, "disambiguation") updatedPerformer.Disambiguation = translator.optionalString(input.Disambiguation, "disambiguation")
updatedPerformer.URL = translator.optionalString(input.URL, "url")
updatedPerformer.Gender = translator.optionalString((*string)(input.Gender), "gender") updatedPerformer.Gender = translator.optionalString((*string)(input.Gender), "gender")
updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity") updatedPerformer.Ethnicity = translator.optionalString(input.Ethnicity, "ethnicity")
updatedPerformer.Country = translator.optionalString(input.Country, "country") updatedPerformer.Country = translator.optionalString(input.Country, "country")
@ -237,8 +360,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length") updatedPerformer.CareerLength = translator.optionalString(input.CareerLength, "career_length")
updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos") updatedPerformer.Tattoos = translator.optionalString(input.Tattoos, "tattoos")
updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings") updatedPerformer.Piercings = translator.optionalString(input.Piercings, "piercings")
updatedPerformer.Twitter = translator.optionalString(input.Twitter, "twitter")
updatedPerformer.Instagram = translator.optionalString(input.Instagram, "instagram")
updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite") updatedPerformer.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedPerformer.Rating = translator.optionalInt(input.Rating100, "rating100") updatedPerformer.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedPerformer.Details = translator.optionalString(input.Details, "details") updatedPerformer.Details = translator.optionalString(input.Details, "details")
@ -246,6 +368,19 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight") updatedPerformer.Weight = translator.optionalInt(input.Weight, "weight")
updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag") updatedPerformer.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
if translator.hasField("urls") {
// ensure url/twitter/instagram are not included in the input
if err := r.validateNoLegacyURLs(translator); err != nil {
return nil, err
}
updatedPerformer.URLs = translator.updateStringsBulk(input.Urls, "urls")
}
legacyURL := translator.optionalString(input.URL, "url")
legacyTwitter := translator.optionalString(input.Twitter, "twitter")
legacyInstagram := translator.optionalString(input.Instagram, "instagram")
updatedPerformer.Birthdate, err = translator.optionalDate(input.Birthdate, "birthdate") updatedPerformer.Birthdate, err = translator.optionalDate(input.Birthdate, "birthdate")
if err != nil { if err != nil {
return nil, fmt.Errorf("converting birthdate: %w", err) return nil, fmt.Errorf("converting birthdate: %w", err)
@ -277,6 +412,12 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input BulkPe
qb := r.repository.Performer qb := r.repository.Performer
for _, performerID := range performerIDs { for _, performerID := range performerIDs {
if legacyURL.Set || legacyTwitter.Set || legacyInstagram.Set {
if err := r.handleLegacyURLs(ctx, performerID, legacyURL, legacyTwitter, legacyInstagram, &updatedPerformer); err != nil {
return err
}
}
if err := performer.ValidateUpdate(ctx, performerID, updatedPerformer, qb); err != nil { if err := performer.ValidateUpdate(ctx, performerID, updatedPerformer, qb); err != nil {
return err return err
} }

View file

@ -23,19 +23,27 @@ type MigrateJob struct {
Database *sqlite.Database Database *sqlite.Database
} }
type databaseSchemaInfo struct {
CurrentSchemaVersion uint
RequiredSchemaVersion uint
StepsRequired uint
}
func (s *MigrateJob) Execute(ctx context.Context, progress *job.Progress) error { func (s *MigrateJob) Execute(ctx context.Context, progress *job.Progress) error {
required, err := s.required() schemaInfo, err := s.required()
if err != nil { if err != nil {
return err return err
} }
if required == 0 { if schemaInfo.StepsRequired == 0 {
logger.Infof("database is already at the latest schema version") logger.Infof("database is already at the latest schema version")
return nil return nil
} }
logger.Infof("Migrating database from %d to %d", schemaInfo.CurrentSchemaVersion, schemaInfo.RequiredSchemaVersion)
// set the number of tasks = required steps + optimise // set the number of tasks = required steps + optimise
progress.SetTotal(int(required + 1)) progress.SetTotal(int(schemaInfo.StepsRequired + 1))
database := s.Database database := s.Database
@ -79,28 +87,31 @@ func (s *MigrateJob) Execute(ctx context.Context, progress *job.Progress) error
} }
} }
logger.Infof("Database migration complete")
return nil return nil
} }
func (s *MigrateJob) required() (uint, error) { func (s *MigrateJob) required() (ret databaseSchemaInfo, err error) {
database := s.Database database := s.Database
m, err := sqlite.NewMigrator(database) m, err := sqlite.NewMigrator(database)
if err != nil { if err != nil {
return 0, err return
} }
defer m.Close() defer m.Close()
currentSchemaVersion := m.CurrentSchemaVersion() ret.CurrentSchemaVersion = m.CurrentSchemaVersion()
targetSchemaVersion := m.RequiredSchemaVersion() ret.RequiredSchemaVersion = m.RequiredSchemaVersion()
if targetSchemaVersion < currentSchemaVersion { if ret.RequiredSchemaVersion < ret.CurrentSchemaVersion {
// shouldn't happen // shouldn't happen
return 0, nil return
} }
return targetSchemaVersion - currentSchemaVersion, nil ret.StepsRequired = ret.RequiredSchemaVersion - ret.CurrentSchemaVersion
return
} }
func (s *MigrateJob) runMigrations(ctx context.Context, progress *job.Progress) error { func (s *MigrateJob) runMigrations(ctx context.Context, progress *job.Progress) error {

View file

@ -37,9 +37,7 @@ type Performer struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Disambiguation string `json:"disambiguation,omitempty"` Disambiguation string `json:"disambiguation,omitempty"`
Gender string `json:"gender,omitempty"` Gender string `json:"gender,omitempty"`
URL string `json:"url,omitempty"` URLs []string `json:"urls,omitempty"`
Twitter string `json:"twitter,omitempty"`
Instagram string `json:"instagram,omitempty"`
Birthdate string `json:"birthdate,omitempty"` Birthdate string `json:"birthdate,omitempty"`
Ethnicity string `json:"ethnicity,omitempty"` Ethnicity string `json:"ethnicity,omitempty"`
Country string `json:"country,omitempty"` Country string `json:"country,omitempty"`
@ -66,6 +64,11 @@ type Performer struct {
Weight int `json:"weight,omitempty"` Weight int `json:"weight,omitempty"`
StashIDs []models.StashID `json:"stash_ids,omitempty"` StashIDs []models.StashID `json:"stash_ids,omitempty"`
IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"` IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"`
// deprecated - for import only
URL string `json:"url,omitempty"`
Twitter string `json:"twitter,omitempty"`
Instagram string `json:"instagram,omitempty"`
} }
func (s Performer) Filename() string { func (s Performer) Filename() string {

View file

@ -383,6 +383,29 @@ func (_m *PerformerReaderWriter) GetTagIDs(ctx context.Context, relatedID int) (
return r0, r1 return r0, r1
} }
// GetURLs provides a mock function with given fields: ctx, relatedID
func (_m *PerformerReaderWriter) GetURLs(ctx context.Context, relatedID int) ([]string, error) {
ret := _m.Called(ctx, relatedID)
var r0 []string
if rf, ok := ret.Get(0).(func(context.Context, int) []string); ok {
r0 = rf(ctx, relatedID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, relatedID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// HasImage provides a mock function with given fields: ctx, performerID // HasImage provides a mock function with given fields: ctx, performerID
func (_m *PerformerReaderWriter) HasImage(ctx context.Context, performerID int) (bool, error) { func (_m *PerformerReaderWriter) HasImage(ctx context.Context, performerID int) (bool, error) {
ret := _m.Called(ctx, performerID) ret := _m.Called(ctx, performerID)

View file

@ -10,9 +10,6 @@ type Performer struct {
Name string `json:"name"` Name string `json:"name"`
Disambiguation string `json:"disambiguation"` Disambiguation string `json:"disambiguation"`
Gender *GenderEnum `json:"gender"` Gender *GenderEnum `json:"gender"`
URL string `json:"url"`
Twitter string `json:"twitter"`
Instagram string `json:"instagram"`
Birthdate *Date `json:"birthdate"` Birthdate *Date `json:"birthdate"`
Ethnicity string `json:"ethnicity"` Ethnicity string `json:"ethnicity"`
Country string `json:"country"` Country string `json:"country"`
@ -37,6 +34,7 @@ type Performer struct {
IgnoreAutoTag bool `json:"ignore_auto_tag"` IgnoreAutoTag bool `json:"ignore_auto_tag"`
Aliases RelatedStrings `json:"aliases"` Aliases RelatedStrings `json:"aliases"`
URLs RelatedStrings `json:"urls"`
TagIDs RelatedIDs `json:"tag_ids"` TagIDs RelatedIDs `json:"tag_ids"`
StashIDs RelatedStashIDs `json:"stash_ids"` StashIDs RelatedStashIDs `json:"stash_ids"`
} }
@ -55,9 +53,7 @@ type PerformerPartial struct {
Name OptionalString Name OptionalString
Disambiguation OptionalString Disambiguation OptionalString
Gender OptionalString Gender OptionalString
URL OptionalString URLs *UpdateStrings
Twitter OptionalString
Instagram OptionalString
Birthdate OptionalDate Birthdate OptionalDate
Ethnicity OptionalString Ethnicity OptionalString
Country OptionalString Country OptionalString
@ -99,6 +95,12 @@ func (s *Performer) LoadAliases(ctx context.Context, l AliasLoader) error {
}) })
} }
func (s *Performer) LoadURLs(ctx context.Context, l URLLoader) error {
return s.URLs.load(func() ([]string, error) {
return l.GetURLs(ctx, s.ID)
})
}
func (s *Performer) LoadTagIDs(ctx context.Context, l TagIDLoader) error { func (s *Performer) LoadTagIDs(ctx context.Context, l TagIDLoader) error {
return s.TagIDs.load(func() ([]int, error) { return s.TagIDs.load(func() ([]int, error) {
return l.GetTagIDs(ctx, s.ID) return l.GetTagIDs(ctx, s.ID)

View file

@ -107,9 +107,10 @@ type ScrapedPerformer struct {
Name *string `json:"name"` Name *string `json:"name"`
Disambiguation *string `json:"disambiguation"` Disambiguation *string `json:"disambiguation"`
Gender *string `json:"gender"` Gender *string `json:"gender"`
URL *string `json:"url"` URLs []string `json:"urls"`
Twitter *string `json:"twitter"` URL *string `json:"url"` // deprecated
Instagram *string `json:"instagram"` Twitter *string `json:"twitter"` // deprecated
Instagram *string `json:"instagram"` // deprecated
Birthdate *string `json:"birthdate"` Birthdate *string `json:"birthdate"`
Ethnicity *string `json:"ethnicity"` Ethnicity *string `json:"ethnicity"`
Country *string `json:"country"` Country *string `json:"country"`
@ -191,9 +192,7 @@ func (p *ScrapedPerformer) ToPerformer(endpoint string, excluded map[string]bool
ret.Weight = &w ret.Weight = &w
} }
} }
if p.Instagram != nil && !excluded["instagram"] {
ret.Instagram = *p.Instagram
}
if p.Measurements != nil && !excluded["measurements"] { if p.Measurements != nil && !excluded["measurements"] {
ret.Measurements = *p.Measurements ret.Measurements = *p.Measurements
} }
@ -221,11 +220,27 @@ func (p *ScrapedPerformer) ToPerformer(endpoint string, excluded map[string]bool
ret.Circumcised = &v ret.Circumcised = &v
} }
} }
if p.Twitter != nil && !excluded["twitter"] {
ret.Twitter = *p.Twitter // if URLs are provided, only use those
if len(p.URLs) > 0 {
if !excluded["urls"] {
ret.URLs = NewRelatedStrings(p.URLs)
} }
} else {
urls := []string{}
if p.URL != nil && !excluded["url"] { if p.URL != nil && !excluded["url"] {
ret.URL = *p.URL urls = append(urls, *p.URL)
}
if p.Twitter != nil && !excluded["twitter"] {
urls = append(urls, *p.Twitter)
}
if p.Instagram != nil && !excluded["instagram"] {
urls = append(urls, *p.Instagram)
}
if len(urls) > 0 {
ret.URLs = NewRelatedStrings(urls)
}
} }
if p.RemoteSiteID != nil && endpoint != "" { if p.RemoteSiteID != nil && endpoint != "" {
@ -309,9 +324,6 @@ func (p *ScrapedPerformer) ToPartial(endpoint string, excluded map[string]bool,
ret.Weight = NewOptionalInt(w) ret.Weight = NewOptionalInt(w)
} }
} }
if p.Instagram != nil && !excluded["instagram"] {
ret.Instagram = NewOptionalString(*p.Instagram)
}
if p.Measurements != nil && !excluded["measurements"] { if p.Measurements != nil && !excluded["measurements"] {
ret.Measurements = NewOptionalString(*p.Measurements) ret.Measurements = NewOptionalString(*p.Measurements)
} }
@ -330,11 +342,33 @@ func (p *ScrapedPerformer) ToPartial(endpoint string, excluded map[string]bool,
if p.Tattoos != nil && !excluded["tattoos"] { if p.Tattoos != nil && !excluded["tattoos"] {
ret.Tattoos = NewOptionalString(*p.Tattoos) ret.Tattoos = NewOptionalString(*p.Tattoos)
} }
if p.Twitter != nil && !excluded["twitter"] {
ret.Twitter = NewOptionalString(*p.Twitter) // if URLs are provided, only use those
if len(p.URLs) > 0 {
if !excluded["urls"] {
ret.URLs = &UpdateStrings{
Values: p.URLs,
Mode: RelationshipUpdateModeSet,
} }
}
} else {
urls := []string{}
if p.URL != nil && !excluded["url"] { if p.URL != nil && !excluded["url"] {
ret.URL = NewOptionalString(*p.URL) urls = append(urls, *p.URL)
}
if p.Twitter != nil && !excluded["twitter"] {
urls = append(urls, *p.Twitter)
}
if p.Instagram != nil && !excluded["instagram"] {
urls = append(urls, *p.Instagram)
}
if len(urls) > 0 {
ret.URLs = &UpdateStrings{
Values: urls,
Mode: RelationshipUpdateModeSet,
}
}
} }
if p.RemoteSiteID != nil && endpoint != "" { if p.RemoteSiteID != nil && endpoint != "" {

View file

@ -161,9 +161,9 @@ func Test_scrapedToPerformerInput(t *testing.T) {
Tattoos: nextVal(), Tattoos: nextVal(),
Piercings: nextVal(), Piercings: nextVal(),
Aliases: nextVal(), Aliases: nextVal(),
URL: nextVal(),
Twitter: nextVal(), Twitter: nextVal(),
Instagram: nextVal(), Instagram: nextVal(),
URL: nextVal(),
Details: nextVal(), Details: nextVal(),
RemoteSiteID: &remoteSiteID, RemoteSiteID: &remoteSiteID,
}, },
@ -186,9 +186,7 @@ func Test_scrapedToPerformerInput(t *testing.T) {
Tattoos: *nextVal(), Tattoos: *nextVal(),
Piercings: *nextVal(), Piercings: *nextVal(),
Aliases: NewRelatedStrings([]string{*nextVal()}), Aliases: NewRelatedStrings([]string{*nextVal()}),
Twitter: *nextVal(), URLs: NewRelatedStrings([]string{*nextVal(), *nextVal(), *nextVal()}),
Instagram: *nextVal(),
URL: *nextVal(),
Details: *nextVal(), Details: *nextVal(),
StashIDs: NewRelatedStashIDs([]StashID{ StashIDs: NewRelatedStashIDs([]StashID{
{ {

View file

@ -203,7 +203,8 @@ type PerformerFilterType struct {
type PerformerCreateInput struct { type PerformerCreateInput struct {
Name string `json:"name"` Name string `json:"name"`
Disambiguation *string `json:"disambiguation"` Disambiguation *string `json:"disambiguation"`
URL *string `json:"url"` URL *string `json:"url"` // deprecated
Urls []string `json:"urls"`
Gender *GenderEnum `json:"gender"` Gender *GenderEnum `json:"gender"`
Birthdate *string `json:"birthdate"` Birthdate *string `json:"birthdate"`
Ethnicity *string `json:"ethnicity"` Ethnicity *string `json:"ethnicity"`
@ -220,8 +221,8 @@ type PerformerCreateInput struct {
Piercings *string `json:"piercings"` Piercings *string `json:"piercings"`
Aliases *string `json:"aliases"` Aliases *string `json:"aliases"`
AliasList []string `json:"alias_list"` AliasList []string `json:"alias_list"`
Twitter *string `json:"twitter"` Twitter *string `json:"twitter"` // deprecated
Instagram *string `json:"instagram"` Instagram *string `json:"instagram"` // deprecated
Favorite *bool `json:"favorite"` Favorite *bool `json:"favorite"`
TagIds []string `json:"tag_ids"` TagIds []string `json:"tag_ids"`
// This should be a URL or a base64 encoded data URL // This should be a URL or a base64 encoded data URL
@ -239,7 +240,8 @@ type PerformerUpdateInput struct {
ID string `json:"id"` ID string `json:"id"`
Name *string `json:"name"` Name *string `json:"name"`
Disambiguation *string `json:"disambiguation"` Disambiguation *string `json:"disambiguation"`
URL *string `json:"url"` URL *string `json:"url"` // deprecated
Urls []string `json:"urls"`
Gender *GenderEnum `json:"gender"` Gender *GenderEnum `json:"gender"`
Birthdate *string `json:"birthdate"` Birthdate *string `json:"birthdate"`
Ethnicity *string `json:"ethnicity"` Ethnicity *string `json:"ethnicity"`
@ -256,8 +258,8 @@ type PerformerUpdateInput struct {
Piercings *string `json:"piercings"` Piercings *string `json:"piercings"`
Aliases *string `json:"aliases"` Aliases *string `json:"aliases"`
AliasList []string `json:"alias_list"` AliasList []string `json:"alias_list"`
Twitter *string `json:"twitter"` Twitter *string `json:"twitter"` // deprecated
Instagram *string `json:"instagram"` Instagram *string `json:"instagram"` // deprecated
Favorite *bool `json:"favorite"` Favorite *bool `json:"favorite"`
TagIds []string `json:"tag_ids"` TagIds []string `json:"tag_ids"`
// This should be a URL or a base64 encoded data URL // This should be a URL or a base64 encoded data URL

View file

@ -78,6 +78,7 @@ type PerformerReader interface {
AliasLoader AliasLoader
StashIDLoader StashIDLoader
TagIDLoader TagIDLoader
URLLoader
All(ctx context.Context) ([]*Performer, error) All(ctx context.Context) ([]*Performer, error)
GetImage(ctx context.Context, performerID int) ([]byte, error) GetImage(ctx context.Context, performerID int) ([]byte, error)

View file

@ -16,6 +16,7 @@ type ImageAliasStashIDGetter interface {
GetImage(ctx context.Context, performerID int) ([]byte, error) GetImage(ctx context.Context, performerID int) ([]byte, error)
models.AliasLoader models.AliasLoader
models.StashIDLoader models.StashIDLoader
models.URLLoader
} }
// ToJSON converts a Performer object into its JSON equivalent. // ToJSON converts a Performer object into its JSON equivalent.
@ -23,7 +24,6 @@ func ToJSON(ctx context.Context, reader ImageAliasStashIDGetter, performer *mode
newPerformerJSON := jsonschema.Performer{ newPerformerJSON := jsonschema.Performer{
Name: performer.Name, Name: performer.Name,
Disambiguation: performer.Disambiguation, Disambiguation: performer.Disambiguation,
URL: performer.URL,
Ethnicity: performer.Ethnicity, Ethnicity: performer.Ethnicity,
Country: performer.Country, Country: performer.Country,
EyeColor: performer.EyeColor, EyeColor: performer.EyeColor,
@ -32,8 +32,6 @@ func ToJSON(ctx context.Context, reader ImageAliasStashIDGetter, performer *mode
CareerLength: performer.CareerLength, CareerLength: performer.CareerLength,
Tattoos: performer.Tattoos, Tattoos: performer.Tattoos,
Piercings: performer.Piercings, Piercings: performer.Piercings,
Twitter: performer.Twitter,
Instagram: performer.Instagram,
Favorite: performer.Favorite, Favorite: performer.Favorite,
Details: performer.Details, Details: performer.Details,
HairColor: performer.HairColor, HairColor: performer.HairColor,
@ -78,6 +76,11 @@ func ToJSON(ctx context.Context, reader ImageAliasStashIDGetter, performer *mode
newPerformerJSON.Aliases = performer.Aliases.List() newPerformerJSON.Aliases = performer.Aliases.List()
if err := performer.LoadURLs(ctx, reader); err != nil {
return nil, fmt.Errorf("loading performer urls: %w", err)
}
newPerformerJSON.URLs = performer.URLs.List()
if err := performer.LoadStashIDs(ctx, reader); err != nil { if err := performer.LoadStashIDs(ctx, reader); err != nil {
return nil, fmt.Errorf("loading performer stash ids: %w", err) return nil, fmt.Errorf("loading performer stash ids: %w", err)
} }

View file

@ -77,7 +77,7 @@ func createFullPerformer(id int, name string) *models.Performer {
ID: id, ID: id,
Name: name, Name: name,
Disambiguation: disambiguation, Disambiguation: disambiguation,
URL: url, URLs: models.NewRelatedStrings([]string{url, twitter, instagram}),
Aliases: models.NewRelatedStrings(aliases), Aliases: models.NewRelatedStrings(aliases),
Birthdate: &birthDate, Birthdate: &birthDate,
CareerLength: careerLength, CareerLength: careerLength,
@ -90,11 +90,9 @@ func createFullPerformer(id int, name string) *models.Performer {
Favorite: true, Favorite: true,
Gender: &genderEnum, Gender: &genderEnum,
Height: &height, Height: &height,
Instagram: instagram,
Measurements: measurements, Measurements: measurements,
Piercings: piercings, Piercings: piercings,
Tattoos: tattoos, Tattoos: tattoos,
Twitter: twitter,
CreatedAt: createTime, CreatedAt: createTime,
UpdatedAt: updateTime, UpdatedAt: updateTime,
Rating: &rating, Rating: &rating,
@ -114,6 +112,7 @@ func createEmptyPerformer(id int) models.Performer {
CreatedAt: createTime, CreatedAt: createTime,
UpdatedAt: updateTime, UpdatedAt: updateTime,
Aliases: models.NewRelatedStrings([]string{}), Aliases: models.NewRelatedStrings([]string{}),
URLs: models.NewRelatedStrings([]string{}),
TagIDs: models.NewRelatedIDs([]int{}), TagIDs: models.NewRelatedIDs([]int{}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{}), StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
} }
@ -123,7 +122,7 @@ func createFullJSONPerformer(name string, image string) *jsonschema.Performer {
return &jsonschema.Performer{ return &jsonschema.Performer{
Name: name, Name: name,
Disambiguation: disambiguation, Disambiguation: disambiguation,
URL: url, URLs: []string{url, twitter, instagram},
Aliases: aliases, Aliases: aliases,
Birthdate: birthDate.String(), Birthdate: birthDate.String(),
CareerLength: careerLength, CareerLength: careerLength,
@ -136,11 +135,9 @@ func createFullJSONPerformer(name string, image string) *jsonschema.Performer {
Favorite: true, Favorite: true,
Gender: gender, Gender: gender,
Height: strconv.Itoa(height), Height: strconv.Itoa(height),
Instagram: instagram,
Measurements: measurements, Measurements: measurements,
Piercings: piercings, Piercings: piercings,
Tattoos: tattoos, Tattoos: tattoos,
Twitter: twitter,
CreatedAt: json.JSONTime{ CreatedAt: json.JSONTime{
Time: createTime, Time: createTime,
}, },
@ -161,6 +158,7 @@ func createFullJSONPerformer(name string, image string) *jsonschema.Performer {
func createEmptyJSONPerformer() *jsonschema.Performer { func createEmptyJSONPerformer() *jsonschema.Performer {
return &jsonschema.Performer{ return &jsonschema.Performer{
Aliases: []string{}, Aliases: []string{},
URLs: []string{},
StashIDs: []models.StashID{}, StashIDs: []models.StashID{},
CreatedAt: json.JSONTime{ CreatedAt: json.JSONTime{
Time: createTime, Time: createTime,

View file

@ -188,7 +188,6 @@ func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Perform
newPerformer := models.Performer{ newPerformer := models.Performer{
Name: performerJSON.Name, Name: performerJSON.Name,
Disambiguation: performerJSON.Disambiguation, Disambiguation: performerJSON.Disambiguation,
URL: performerJSON.URL,
Ethnicity: performerJSON.Ethnicity, Ethnicity: performerJSON.Ethnicity,
Country: performerJSON.Country, Country: performerJSON.Country,
EyeColor: performerJSON.EyeColor, EyeColor: performerJSON.EyeColor,
@ -198,8 +197,6 @@ func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Perform
Tattoos: performerJSON.Tattoos, Tattoos: performerJSON.Tattoos,
Piercings: performerJSON.Piercings, Piercings: performerJSON.Piercings,
Aliases: models.NewRelatedStrings(performerJSON.Aliases), Aliases: models.NewRelatedStrings(performerJSON.Aliases),
Twitter: performerJSON.Twitter,
Instagram: performerJSON.Instagram,
Details: performerJSON.Details, Details: performerJSON.Details,
HairColor: performerJSON.HairColor, HairColor: performerJSON.HairColor,
Favorite: performerJSON.Favorite, Favorite: performerJSON.Favorite,
@ -211,6 +208,25 @@ func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Perform
StashIDs: models.NewRelatedStashIDs(performerJSON.StashIDs), StashIDs: models.NewRelatedStashIDs(performerJSON.StashIDs),
} }
if len(performerJSON.URLs) > 0 {
newPerformer.URLs = models.NewRelatedStrings(performerJSON.URLs)
} else {
urls := []string{}
if performerJSON.URL != "" {
urls = append(urls, performerJSON.URL)
}
if performerJSON.Twitter != "" {
urls = append(urls, performerJSON.Twitter)
}
if performerJSON.Instagram != "" {
urls = append(urls, performerJSON.Instagram)
}
if len(urls) > 0 {
newPerformer.URLs = models.NewRelatedStrings([]string{performerJSON.URL})
}
}
if performerJSON.Gender != "" { if performerJSON.Gender != "" {
v := models.GenderEnum(performerJSON.Gender) v := models.GenderEnum(performerJSON.Gender)
newPerformer.Gender = &v newPerformer.Gender = &v

18
pkg/performer/url.go Normal file
View file

@ -0,0 +1,18 @@
package performer
import (
"regexp"
)
var (
twitterURLRE = regexp.MustCompile(`^https?:\/\/(?:www\.)?twitter\.com\/`)
instagramURLRE = regexp.MustCompile(`^https?:\/\/(?:www\.)?instagram\.com\/`)
)
func IsTwitterURL(url string) bool {
return twitterURLRE.MatchString(url)
}
func IsInstagramURL(url string) bool {
return instagramURLRE.MatchString(url)
}

View file

@ -81,15 +81,33 @@ func (s *jsonScraper) scrapeByURL(ctx context.Context, url string, ty ScrapeCont
} }
q := s.getJsonQuery(doc) q := s.getJsonQuery(doc)
// if these just return the return values from scraper.scrape* functions then
// it ends up returning ScrapedContent(nil) rather than nil
switch ty { switch ty {
case ScrapeContentTypePerformer: case ScrapeContentTypePerformer:
return scraper.scrapePerformer(ctx, q) ret, err := scraper.scrapePerformer(ctx, q)
if err != nil || ret == nil {
return nil, err
}
return ret, nil
case ScrapeContentTypeScene: case ScrapeContentTypeScene:
return scraper.scrapeScene(ctx, q) ret, err := scraper.scrapeScene(ctx, q)
if err != nil || ret == nil {
return nil, err
}
return ret, nil
case ScrapeContentTypeGallery: case ScrapeContentTypeGallery:
return scraper.scrapeGallery(ctx, q) ret, err := scraper.scrapeGallery(ctx, q)
if err != nil || ret == nil {
return nil, err
}
return ret, nil
case ScrapeContentTypeMovie: case ScrapeContentTypeMovie:
return scraper.scrapeMovie(ctx, q) ret, err := scraper.scrapeMovie(ctx, q)
if err != nil || ret == nil {
return nil, err
}
return ret, nil
} }
return nil, ErrNotSupported return nil, ErrNotSupported

View file

@ -6,9 +6,10 @@ type ScrapedPerformerInput struct {
Name *string `json:"name"` Name *string `json:"name"`
Disambiguation *string `json:"disambiguation"` Disambiguation *string `json:"disambiguation"`
Gender *string `json:"gender"` Gender *string `json:"gender"`
URL *string `json:"url"` URLs []string `json:"urls"`
Twitter *string `json:"twitter"` URL *string `json:"url"` // deprecated
Instagram *string `json:"instagram"` Twitter *string `json:"twitter"` // deprecated
Instagram *string `json:"instagram"` // deprecated
Birthdate *string `json:"birthdate"` Birthdate *string `json:"birthdate"`
Ethnicity *string `json:"ethnicity"` Ethnicity *string `json:"ethnicity"`
Country *string `json:"country"` Country *string `json:"country"`

View file

@ -6,6 +6,7 @@ import (
"github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/match" "github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
) )
// postScrape handles post-processing of scraped content. If the content // postScrape handles post-processing of scraped content. If the content
@ -67,6 +68,31 @@ func (c Cache) postScrapePerformer(ctx context.Context, p models.ScrapedPerforme
p.Country = resolveCountryName(p.Country) p.Country = resolveCountryName(p.Country)
// populate URL/URLs
// if URLs are provided, only use those
if len(p.URLs) > 0 {
p.URL = &p.URLs[0]
} else {
urls := []string{}
if p.URL != nil {
urls = append(urls, *p.URL)
}
if p.Twitter != nil && *p.Twitter != "" {
// handle twitter profile names
u := utils.URLFromHandle(*p.Twitter, "https://twitter.com")
urls = append(urls, u)
}
if p.Instagram != nil && *p.Instagram != "" {
// handle instagram profile names
u := utils.URLFromHandle(*p.Instagram, "https://instagram.com")
urls = append(urls, u)
}
if len(urls) > 0 {
p.URLs = urls
}
}
return p, nil return p, nil
} }

View file

@ -163,6 +163,12 @@ func (i *Input) populateURL() {
if i.Scene != nil && i.Scene.URL == nil && len(i.Scene.URLs) > 0 { if i.Scene != nil && i.Scene.URL == nil && len(i.Scene.URLs) > 0 {
i.Scene.URL = &i.Scene.URLs[0] i.Scene.URL = &i.Scene.URLs[0]
} }
if i.Gallery != nil && i.Gallery.URL == nil && len(i.Gallery.URLs) > 0 {
i.Gallery.URL = &i.Gallery.URLs[0]
}
if i.Performer != nil && i.Performer.URL == nil && len(i.Performer.URLs) > 0 {
i.Performer.URL = &i.Performer.URLs[0]
}
} }
// simple type definitions that can help customize // simple type definitions that can help customize

View file

@ -9,7 +9,6 @@ import (
"io" "io"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"regexp"
"strconv" "strconv"
"strings" "strings"
@ -41,6 +40,7 @@ type PerformerReader interface {
match.PerformerFinder match.PerformerFinder
models.AliasLoader models.AliasLoader
models.StashIDLoader models.StashIDLoader
models.URLLoader
FindBySceneID(ctx context.Context, sceneID int) ([]*models.Performer, error) FindBySceneID(ctx context.Context, sceneID int) ([]*models.Performer, error)
GetImage(ctx context.Context, performerID int) ([]byte, error) GetImage(ctx context.Context, performerID int) ([]byte, error)
} }
@ -685,6 +685,10 @@ func performerFragmentToScrapedPerformer(p graphql.PerformerFragment) *models.Sc
sp.Aliases = &alias sp.Aliases = &alias
} }
for _, u := range p.Urls {
sp.URLs = append(sp.URLs, u.URL)
}
return sp return sp
} }
@ -1128,6 +1132,10 @@ func (c Client) SubmitPerformerDraft(ctx context.Context, performer *models.Perf
return nil, err return nil, err
} }
if err := performer.LoadURLs(ctx, pqb); err != nil {
return nil, err
}
if err := performer.LoadStashIDs(ctx, pqb); err != nil { if err := performer.LoadStashIDs(ctx, pqb); err != nil {
return nil, err return nil, err
} }
@ -1195,28 +1203,8 @@ func (c Client) SubmitPerformerDraft(ctx context.Context, performer *models.Perf
} }
} }
var urls []string if len(performer.URLs.List()) > 0 {
if len(strings.TrimSpace(performer.Twitter)) > 0 { draft.Urls = performer.URLs.List()
reg := regexp.MustCompile(`https?:\/\/(?:www\.)?twitter\.com`)
if reg.MatchString(performer.Twitter) {
urls = append(urls, strings.TrimSpace(performer.Twitter))
} else {
urls = append(urls, "https://twitter.com/"+strings.TrimSpace(performer.Twitter))
}
}
if len(strings.TrimSpace(performer.Instagram)) > 0 {
reg := regexp.MustCompile(`https?:\/\/(?:www\.)?instagram\.com`)
if reg.MatchString(performer.Instagram) {
urls = append(urls, strings.TrimSpace(performer.Instagram))
} else {
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) stashIDs, err := pqb.GetStashIDs(ctx, performer.ID)

View file

@ -62,15 +62,33 @@ func (s *xpathScraper) scrapeByURL(ctx context.Context, url string, ty ScrapeCon
} }
q := s.getXPathQuery(doc) q := s.getXPathQuery(doc)
// if these just return the return values from scraper.scrape* functions then
// it ends up returning ScrapedContent(nil) rather than nil
switch ty { switch ty {
case ScrapeContentTypePerformer: case ScrapeContentTypePerformer:
return scraper.scrapePerformer(ctx, q) ret, err := scraper.scrapePerformer(ctx, q)
if err != nil || ret == nil {
return nil, err
}
return ret, nil
case ScrapeContentTypeScene: case ScrapeContentTypeScene:
return scraper.scrapeScene(ctx, q) ret, err := scraper.scrapeScene(ctx, q)
if err != nil || ret == nil {
return nil, err
}
return ret, nil
case ScrapeContentTypeGallery: case ScrapeContentTypeGallery:
return scraper.scrapeGallery(ctx, q) ret, err := scraper.scrapeGallery(ctx, q)
if err != nil || ret == nil {
return nil, err
}
return ret, nil
case ScrapeContentTypeMovie: case ScrapeContentTypeMovie:
return scraper.scrapeMovie(ctx, q) ret, err := scraper.scrapeMovie(ctx, q)
if err != nil || ret == nil {
return nil, err
}
return ret, nil
} }
return nil, ErrNotSupported return nil, ErrNotSupported

View file

@ -495,9 +495,6 @@ func (db *Anonymiser) anonymisePerformers(ctx context.Context) error {
table.Col(idColumn), table.Col(idColumn),
table.Col("name"), table.Col("name"),
table.Col("details"), table.Col("details"),
table.Col("url"),
table.Col("twitter"),
table.Col("instagram"),
table.Col("tattoos"), table.Col("tattoos"),
table.Col("piercings"), table.Col("piercings"),
).Where(table.Col(idColumn).Gt(lastID)).Limit(1000) ).Where(table.Col(idColumn).Gt(lastID)).Limit(1000)
@ -510,9 +507,6 @@ func (db *Anonymiser) anonymisePerformers(ctx context.Context) error {
id int id int
name sql.NullString name sql.NullString
details sql.NullString details sql.NullString
url sql.NullString
twitter sql.NullString
instagram sql.NullString
tattoos sql.NullString tattoos sql.NullString
piercings sql.NullString piercings sql.NullString
) )
@ -521,9 +515,6 @@ func (db *Anonymiser) anonymisePerformers(ctx context.Context) error {
&id, &id,
&name, &name,
&details, &details,
&url,
&twitter,
&instagram,
&tattoos, &tattoos,
&piercings, &piercings,
); err != nil { ); err != nil {
@ -533,9 +524,6 @@ func (db *Anonymiser) anonymisePerformers(ctx context.Context) error {
set := goqu.Record{} set := goqu.Record{}
db.obfuscateNullString(set, "name", name) db.obfuscateNullString(set, "name", name)
db.obfuscateNullString(set, "details", details) db.obfuscateNullString(set, "details", details)
db.obfuscateNullString(set, "url", url)
db.obfuscateNullString(set, "twitter", twitter)
db.obfuscateNullString(set, "instagram", instagram)
db.obfuscateNullString(set, "tattoos", tattoos) db.obfuscateNullString(set, "tattoos", tattoos)
db.obfuscateNullString(set, "piercings", piercings) db.obfuscateNullString(set, "piercings", piercings)
@ -566,6 +554,10 @@ func (db *Anonymiser) anonymisePerformers(ctx context.Context) error {
return err return err
} }
if err := db.anonymiseURLs(ctx, goqu.T(performerURLsTable), "performer_id"); err != nil {
return err
}
return nil return nil
} }

View file

@ -30,7 +30,7 @@ const (
dbConnTimeout = 30 dbConnTimeout = 30
) )
var appSchemaVersion uint = 61 var appSchemaVersion uint = 62
//go:embed migrations/*.sql //go:embed migrations/*.sql
var migrationsBox embed.FS var migrationsBox embed.FS

View file

@ -0,0 +1,155 @@
PRAGMA foreign_keys=OFF;
CREATE TABLE `performer_urls` (
`performer_id` integer NOT NULL,
`position` integer NOT NULL,
`url` varchar(255) NOT NULL,
foreign key(`performer_id`) references `performers`(`id`) on delete CASCADE,
PRIMARY KEY(`performer_id`, `position`, `url`)
);
CREATE INDEX `performers_urls_url` on `performer_urls` (`url`);
-- drop url, twitter and instagram
-- make name not null
CREATE TABLE `performers_new` (
`id` integer not null primary key autoincrement,
`name` varchar(255) not null,
`disambiguation` varchar(255),
`gender` varchar(20),
`birthdate` date,
`ethnicity` varchar(255),
`country` varchar(255),
`eye_color` varchar(255),
`height` int,
`measurements` varchar(255),
`fake_tits` varchar(255),
`career_length` varchar(255),
`tattoos` varchar(255),
`piercings` varchar(255),
`favorite` boolean not null default '0',
`created_at` datetime not null,
`updated_at` datetime not null,
`details` text,
`death_date` date,
`hair_color` varchar(255),
`weight` integer,
`rating` tinyint,
`ignore_auto_tag` boolean not null default '0',
`image_blob` varchar(255) REFERENCES `blobs`(`checksum`),
`penis_length` float,
`circumcised` varchar[10]
);
INSERT INTO `performers_new`
(
`id`,
`name`,
`disambiguation`,
`gender`,
`birthdate`,
`ethnicity`,
`country`,
`eye_color`,
`height`,
`measurements`,
`fake_tits`,
`career_length`,
`tattoos`,
`piercings`,
`favorite`,
`created_at`,
`updated_at`,
`details`,
`death_date`,
`hair_color`,
`weight`,
`rating`,
`ignore_auto_tag`,
`image_blob`,
`penis_length`,
`circumcised`
)
SELECT
`id`,
`name`,
`disambiguation`,
`gender`,
`birthdate`,
`ethnicity`,
`country`,
`eye_color`,
`height`,
`measurements`,
`fake_tits`,
`career_length`,
`tattoos`,
`piercings`,
`favorite`,
`created_at`,
`updated_at`,
`details`,
`death_date`,
`hair_color`,
`weight`,
`rating`,
`ignore_auto_tag`,
`image_blob`,
`penis_length`,
`circumcised`
FROM `performers`;
INSERT INTO `performer_urls`
(
`performer_id`,
`position`,
`url`
)
SELECT
`id`,
'0',
`url`
FROM `performers`
WHERE `performers`.`url` IS NOT NULL AND `performers`.`url` != '';
INSERT INTO `performer_urls`
(
`performer_id`,
`position`,
`url`
)
SELECT
`id`,
(SELECT count(*) FROM `performer_urls` WHERE `performer_id` = `performers`.`id`)+1,
CASE
WHEN `twitter` LIKE 'http%://%' THEN `twitter`
ELSE 'https://www.twitter.com/' || `twitter`
END
FROM `performers`
WHERE `performers`.`twitter` IS NOT NULL AND `performers`.`twitter` != '';
INSERT INTO `performer_urls`
(
`performer_id`,
`position`,
`url`
)
SELECT
`id`,
(SELECT count(*) FROM `performer_urls` WHERE `performer_id` = `performers`.`id`)+1,
CASE
WHEN `instagram` LIKE 'http%://%' THEN `instagram`
ELSE 'https://www.instagram.com/' || `instagram`
END
FROM `performers`
WHERE `performers`.`instagram` IS NOT NULL AND `performers`.`instagram` != '';
DROP INDEX `performers_name_disambiguation_unique`;
DROP INDEX `performers_name_unique`;
DROP TABLE `performers`;
ALTER TABLE `performers_new` rename to `performers`;
CREATE UNIQUE INDEX `performers_name_disambiguation_unique` on `performers` (`name`, `disambiguation`) WHERE `disambiguation` IS NOT NULL;
CREATE UNIQUE INDEX `performers_name_unique` on `performers` (`name`) WHERE `disambiguation` IS NULL;
PRAGMA foreign_keys=ON;

View file

@ -23,6 +23,9 @@ const (
performerAliasColumn = "alias" performerAliasColumn = "alias"
performersTagsTable = "performers_tags" performersTagsTable = "performers_tags"
performerURLsTable = "performer_urls"
performerURLColumn = "url"
performerImageBlobColumn = "image_blob" performerImageBlobColumn = "image_blob"
) )
@ -31,9 +34,6 @@ type performerRow struct {
Name null.String `db:"name"` // TODO: make schema non-nullable Name null.String `db:"name"` // TODO: make schema non-nullable
Disambigation zero.String `db:"disambiguation"` Disambigation zero.String `db:"disambiguation"`
Gender zero.String `db:"gender"` Gender zero.String `db:"gender"`
URL zero.String `db:"url"`
Twitter zero.String `db:"twitter"`
Instagram zero.String `db:"instagram"`
Birthdate NullDate `db:"birthdate"` Birthdate NullDate `db:"birthdate"`
Ethnicity zero.String `db:"ethnicity"` Ethnicity zero.String `db:"ethnicity"`
Country zero.String `db:"country"` Country zero.String `db:"country"`
@ -68,9 +68,6 @@ func (r *performerRow) fromPerformer(o models.Performer) {
if o.Gender != nil && o.Gender.IsValid() { if o.Gender != nil && o.Gender.IsValid() {
r.Gender = zero.StringFrom(o.Gender.String()) r.Gender = zero.StringFrom(o.Gender.String())
} }
r.URL = zero.StringFrom(o.URL)
r.Twitter = zero.StringFrom(o.Twitter)
r.Instagram = zero.StringFrom(o.Instagram)
r.Birthdate = NullDateFromDatePtr(o.Birthdate) r.Birthdate = NullDateFromDatePtr(o.Birthdate)
r.Ethnicity = zero.StringFrom(o.Ethnicity) r.Ethnicity = zero.StringFrom(o.Ethnicity)
r.Country = zero.StringFrom(o.Country) r.Country = zero.StringFrom(o.Country)
@ -101,9 +98,6 @@ func (r *performerRow) resolve() *models.Performer {
ID: r.ID, ID: r.ID,
Name: r.Name.String, Name: r.Name.String,
Disambiguation: r.Disambigation.String, Disambiguation: r.Disambigation.String,
URL: r.URL.String,
Twitter: r.Twitter.String,
Instagram: r.Instagram.String,
Birthdate: r.Birthdate.DatePtr(), Birthdate: r.Birthdate.DatePtr(),
Ethnicity: r.Ethnicity.String, Ethnicity: r.Ethnicity.String,
Country: r.Country.String, Country: r.Country.String,
@ -148,9 +142,6 @@ func (r *performerRowRecord) fromPartial(o models.PerformerPartial) {
r.setString("name", o.Name) r.setString("name", o.Name)
r.setNullString("disambiguation", o.Disambiguation) r.setNullString("disambiguation", o.Disambiguation)
r.setNullString("gender", o.Gender) r.setNullString("gender", o.Gender)
r.setNullString("url", o.URL)
r.setNullString("twitter", o.Twitter)
r.setNullString("instagram", o.Instagram)
r.setNullDate("birthdate", o.Birthdate) r.setNullDate("birthdate", o.Birthdate)
r.setNullString("ethnicity", o.Ethnicity) r.setNullString("ethnicity", o.Ethnicity)
r.setNullString("country", o.Country) r.setNullString("country", o.Country)
@ -272,6 +263,13 @@ func (qb *PerformerStore) Create(ctx context.Context, newObject *models.Performe
} }
} }
if newObject.URLs.Loaded() {
const startPos = 0
if err := performersURLsTableMgr.insertJoins(ctx, id, startPos, newObject.URLs.List()); err != nil {
return err
}
}
if newObject.TagIDs.Loaded() { if newObject.TagIDs.Loaded() {
if err := performersTagsTableMgr.insertJoins(ctx, id, newObject.TagIDs.List()); err != nil { if err := performersTagsTableMgr.insertJoins(ctx, id, newObject.TagIDs.List()); err != nil {
return err return err
@ -315,6 +313,12 @@ func (qb *PerformerStore) UpdatePartial(ctx context.Context, id int, partial mod
} }
} }
if partial.URLs != nil {
if err := performersURLsTableMgr.modifyJoins(ctx, id, partial.URLs.Values, partial.URLs.Mode); err != nil {
return nil, err
}
}
if partial.TagIDs != nil { if partial.TagIDs != nil {
if err := performersTagsTableMgr.modifyJoins(ctx, id, partial.TagIDs.IDs, partial.TagIDs.Mode); err != nil { if err := performersTagsTableMgr.modifyJoins(ctx, id, partial.TagIDs.IDs, partial.TagIDs.Mode); err != nil {
return nil, err return nil, err
@ -343,6 +347,12 @@ func (qb *PerformerStore) Update(ctx context.Context, updatedObject *models.Perf
} }
} }
if updatedObject.URLs.Loaded() {
if err := performersURLsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.URLs.List()); err != nil {
return err
}
}
if updatedObject.TagIDs.Loaded() { if updatedObject.TagIDs.Loaded() {
if err := performersTagsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.TagIDs.List()); err != nil { if err := performersTagsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.TagIDs.List()); err != nil {
return err return err
@ -785,6 +795,10 @@ func (qb *PerformerStore) GetAliases(ctx context.Context, performerID int) ([]st
return performersAliasesTableMgr.get(ctx, performerID) return performersAliasesTableMgr.get(ctx, performerID)
} }
func (qb *PerformerStore) GetURLs(ctx context.Context, performerID int) ([]string, error) {
return performersURLsTableMgr.get(ctx, performerID)
}
func (qb *PerformerStore) GetStashIDs(ctx context.Context, performerID int) ([]models.StashID, error) { func (qb *PerformerStore) GetStashIDs(ctx context.Context, performerID int) ([]models.StashID, error) {
return performersStashIDsTableMgr.get(ctx, performerID) return performersStashIDsTableMgr.get(ctx, performerID)
} }

View file

@ -134,7 +134,7 @@ func (qb *performerFilterHandler) criterionHandler() criterionHandler {
stringCriterionHandler(filter.Piercings, tableName+".piercings"), stringCriterionHandler(filter.Piercings, tableName+".piercings"),
intCriterionHandler(filter.Rating100, tableName+".rating", nil), intCriterionHandler(filter.Rating100, tableName+".rating", nil),
stringCriterionHandler(filter.HairColor, tableName+".hair_color"), stringCriterionHandler(filter.HairColor, tableName+".hair_color"),
stringCriterionHandler(filter.URL, tableName+".url"), qb.urlsCriterionHandler(filter.URL),
intCriterionHandler(filter.Weight, tableName+".weight", nil), intCriterionHandler(filter.Weight, tableName+".weight", nil),
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) { criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
if filter.StashID != nil { if filter.StashID != nil {
@ -211,6 +211,9 @@ func (qb *performerFilterHandler) performerIsMissingCriterionHandler(isMissing *
return func(ctx context.Context, f *filterBuilder) { return func(ctx context.Context, f *filterBuilder) {
if isMissing != nil && *isMissing != "" { if isMissing != nil && *isMissing != "" {
switch *isMissing { switch *isMissing {
case "url":
performersURLsTableMgr.join(f, "", "performers.id")
f.addWhere("performer_urls.url IS NULL")
case "scenes": // Deprecated: use `scene_count == 0` filter instead case "scenes": // Deprecated: use `scene_count == 0` filter instead
f.addLeftJoin(performersScenesTable, "scenes_join", "scenes_join.performer_id = performers.id") f.addLeftJoin(performersScenesTable, "scenes_join", "scenes_join.performer_id = performers.id")
f.addWhere("scenes_join.scene_id IS NULL") f.addWhere("scenes_join.scene_id IS NULL")
@ -241,6 +244,20 @@ func (qb *performerFilterHandler) performerAgeFilterCriterionHandler(age *models
} }
} }
func (qb *performerFilterHandler) urlsCriterionHandler(url *models.StringCriterionInput) criterionHandlerFunc {
h := stringListCriterionHandlerBuilder{
primaryTable: performerTable,
primaryFK: performerIDColumn,
joinTable: performerURLsTable,
stringColumn: performerURLColumn,
addJoinTable: func(f *filterBuilder) {
performersURLsTableMgr.join(f, "", "performers.id")
},
}
return h.handler(url)
}
func (qb *performerFilterHandler) aliasCriterionHandler(alias *models.StringCriterionInput) criterionHandlerFunc { func (qb *performerFilterHandler) aliasCriterionHandler(alias *models.StringCriterionInput) criterionHandlerFunc {
h := stringListCriterionHandlerBuilder{ h := stringListCriterionHandlerBuilder{
primaryTable: performerTable, primaryTable: performerTable,

View file

@ -22,6 +22,11 @@ func loadPerformerRelationships(ctx context.Context, expected models.Performer,
return err return err
} }
} }
if expected.URLs.Loaded() {
if err := actual.LoadURLs(ctx, db.Performer); err != nil {
return err
}
}
if expected.TagIDs.Loaded() { if expected.TagIDs.Loaded() {
if err := actual.LoadTagIDs(ctx, db.Performer); err != nil { if err := actual.LoadTagIDs(ctx, db.Performer); err != nil {
return err return err
@ -45,6 +50,7 @@ func Test_PerformerStore_Create(t *testing.T) {
url = "url" url = "url"
twitter = "twitter" twitter = "twitter"
instagram = "instagram" instagram = "instagram"
urls = []string{url, twitter, instagram}
rating = 3 rating = 3
ethnicity = "ethnicity" ethnicity = "ethnicity"
country = "country" country = "country"
@ -84,9 +90,7 @@ func Test_PerformerStore_Create(t *testing.T) {
Name: name, Name: name,
Disambiguation: disambiguation, Disambiguation: disambiguation,
Gender: &gender, Gender: &gender,
URL: url, URLs: models.NewRelatedStrings(urls),
Twitter: twitter,
Instagram: instagram,
Birthdate: &birthdate, Birthdate: &birthdate,
Ethnicity: ethnicity, Ethnicity: ethnicity,
Country: country, Country: country,
@ -193,6 +197,7 @@ func Test_PerformerStore_Update(t *testing.T) {
url = "url" url = "url"
twitter = "twitter" twitter = "twitter"
instagram = "instagram" instagram = "instagram"
urls = []string{url, twitter, instagram}
rating = 3 rating = 3
ethnicity = "ethnicity" ethnicity = "ethnicity"
country = "country" country = "country"
@ -233,9 +238,7 @@ func Test_PerformerStore_Update(t *testing.T) {
Name: name, Name: name,
Disambiguation: disambiguation, Disambiguation: disambiguation,
Gender: &gender, Gender: &gender,
URL: url, URLs: models.NewRelatedStrings(urls),
Twitter: twitter,
Instagram: instagram,
Birthdate: &birthdate, Birthdate: &birthdate,
Ethnicity: ethnicity, Ethnicity: ethnicity,
Country: country, Country: country,
@ -277,6 +280,7 @@ func Test_PerformerStore_Update(t *testing.T) {
&models.Performer{ &models.Performer{
ID: performerIDs[performerIdxWithGallery], ID: performerIDs[performerIdxWithGallery],
Aliases: models.NewRelatedStrings([]string{}), Aliases: models.NewRelatedStrings([]string{}),
URLs: models.NewRelatedStrings([]string{}),
TagIDs: models.NewRelatedIDs([]int{}), TagIDs: models.NewRelatedIDs([]int{}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{}), StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
}, },
@ -341,9 +345,7 @@ func clearPerformerPartial() models.PerformerPartial {
return models.PerformerPartial{ return models.PerformerPartial{
Disambiguation: nullString, Disambiguation: nullString,
Gender: nullString, Gender: nullString,
URL: nullString, URLs: &models.UpdateStrings{Mode: models.RelationshipUpdateModeSet},
Twitter: nullString,
Instagram: nullString,
Birthdate: nullDate, Birthdate: nullDate,
Ethnicity: nullString, Ethnicity: nullString,
Country: nullString, Country: nullString,
@ -376,6 +378,7 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
url = "url" url = "url"
twitter = "twitter" twitter = "twitter"
instagram = "instagram" instagram = "instagram"
urls = []string{url, twitter, instagram}
rating = 3 rating = 3
ethnicity = "ethnicity" ethnicity = "ethnicity"
country = "country" country = "country"
@ -418,9 +421,10 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
Name: models.NewOptionalString(name), Name: models.NewOptionalString(name),
Disambiguation: models.NewOptionalString(disambiguation), Disambiguation: models.NewOptionalString(disambiguation),
Gender: models.NewOptionalString(gender.String()), Gender: models.NewOptionalString(gender.String()),
URL: models.NewOptionalString(url), URLs: &models.UpdateStrings{
Twitter: models.NewOptionalString(twitter), Values: urls,
Instagram: models.NewOptionalString(instagram), Mode: models.RelationshipUpdateModeSet,
},
Birthdate: models.NewOptionalDate(birthdate), Birthdate: models.NewOptionalDate(birthdate),
Ethnicity: models.NewOptionalString(ethnicity), Ethnicity: models.NewOptionalString(ethnicity),
Country: models.NewOptionalString(country), Country: models.NewOptionalString(country),
@ -469,9 +473,7 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
Name: name, Name: name,
Disambiguation: disambiguation, Disambiguation: disambiguation,
Gender: &gender, Gender: &gender,
URL: url, URLs: models.NewRelatedStrings(urls),
Twitter: twitter,
Instagram: instagram,
Birthdate: &birthdate, Birthdate: &birthdate,
Ethnicity: ethnicity, Ethnicity: ethnicity,
Country: country, Country: country,
@ -516,6 +518,7 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
ID: performerIDs[performerIdxWithTwoTags], ID: performerIDs[performerIdxWithTwoTags],
Name: getPerformerStringValue(performerIdxWithTwoTags, "Name"), Name: getPerformerStringValue(performerIdxWithTwoTags, "Name"),
Favorite: getPerformerBoolValue(performerIdxWithTwoTags), Favorite: getPerformerBoolValue(performerIdxWithTwoTags),
URLs: models.NewRelatedStrings([]string{}),
Aliases: models.NewRelatedStrings([]string{}), Aliases: models.NewRelatedStrings([]string{}),
TagIDs: models.NewRelatedIDs([]int{}), TagIDs: models.NewRelatedIDs([]int{}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{}), StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
@ -1290,7 +1293,14 @@ func TestPerformerQueryURL(t *testing.T) {
verifyFn := func(g *models.Performer) { verifyFn := func(g *models.Performer) {
t.Helper() t.Helper()
verifyString(t, g.URL, urlCriterion)
urls := g.URLs.List()
var url string
if len(urls) > 0 {
url = urls[0]
}
verifyString(t, url, urlCriterion)
} }
verifyPerformerQuery(t, filter, verifyFn) verifyPerformerQuery(t, filter, verifyFn)
@ -1318,6 +1328,12 @@ func verifyPerformerQuery(t *testing.T, filter models.PerformerFilterType, verif
t.Helper() t.Helper()
performers := queryPerformers(ctx, t, &filter, nil) performers := queryPerformers(ctx, t, &filter, nil)
for _, performer := range performers {
if err := performer.LoadURLs(ctx, db.Performer); err != nil {
t.Errorf("Error loading movie relationships: %v", err)
}
}
// assume it should find at least one // assume it should find at least one
assert.Greater(t, len(performers), 0) assert.Greater(t, len(performers), 0)

View file

@ -1374,6 +1374,15 @@ func getPerformerNullStringValue(index int, field string) string {
return ret.String return ret.String
} }
func getPerformerEmptyString(index int, field string) string {
v := getPrefixedNullStringValue("performer", index, field)
if !v.Valid {
return ""
}
return v.String
}
func getPerformerBoolValue(index int) bool { func getPerformerBoolValue(index int) bool {
index = index % 2 index = index % 2
return index == 1 return index == 1
@ -1479,7 +1488,9 @@ func createPerformers(ctx context.Context, n int, o int) error {
Name: getPerformerStringValue(index, name), Name: getPerformerStringValue(index, name),
Disambiguation: getPerformerStringValue(index, "disambiguation"), Disambiguation: getPerformerStringValue(index, "disambiguation"),
Aliases: models.NewRelatedStrings(performerAliases(index)), Aliases: models.NewRelatedStrings(performerAliases(index)),
URL: getPerformerNullStringValue(i, urlField), URLs: models.NewRelatedStrings([]string{
getPerformerEmptyString(i, urlField),
}),
Favorite: getPerformerBoolValue(i), Favorite: getPerformerBoolValue(i),
Birthdate: getPerformerBirthdate(i), Birthdate: getPerformerBirthdate(i),
DeathDate: getPerformerDeathDate(i), DeathDate: getPerformerDeathDate(i),

View file

@ -29,6 +29,7 @@ var (
scenesURLsJoinTable = goqu.T(scenesURLsTable) scenesURLsJoinTable = goqu.T(scenesURLsTable)
performersAliasesJoinTable = goqu.T(performersAliasesTable) performersAliasesJoinTable = goqu.T(performersAliasesTable)
performersURLsJoinTable = goqu.T(performerURLsTable)
performersTagsJoinTable = goqu.T(performersTagsTable) performersTagsJoinTable = goqu.T(performersTagsTable)
performersStashIDsJoinTable = goqu.T("performer_stash_ids") performersStashIDsJoinTable = goqu.T("performer_stash_ids")
@ -255,6 +256,14 @@ var (
stringColumn: performersAliasesJoinTable.Col(performerAliasColumn), stringColumn: performersAliasesJoinTable.Col(performerAliasColumn),
} }
performersURLsTableMgr = &orderedValueTable[string]{
table: table{
table: performersURLsJoinTable,
idColumn: performersURLsJoinTable.Col(performerIDColumn),
},
valueColumn: performersURLsJoinTable.Col(performerURLColumn),
}
performersTagsTableMgr = &joinTable{ performersTagsTableMgr = &joinTable{
table: table{ table: table{
table: performersTagsJoinTable, table: performersTagsJoinTable,

15
pkg/utils/url.go Normal file
View file

@ -0,0 +1,15 @@
package utils
import "regexp"
// URLFromHandle adds the site URL to the input if the input is not already a URL
// siteURL must not end with a slash
func URLFromHandle(input string, siteURL string) string {
// if the input is already a URL, return it
re := regexp.MustCompile(`^https?://`)
if re.MatchString(input) {
return input
}
return siteURL + "/" + input
}

47
pkg/utils/url_test.go Normal file
View file

@ -0,0 +1,47 @@
package utils
import "testing"
func TestURLFromHandle(t *testing.T) {
type args struct {
input string
siteURL string
}
tests := []struct {
name string
args args
want string
}{
{
name: "input is already a URL https",
args: args{
input: "https://foo.com",
siteURL: "https://bar.com",
},
want: "https://foo.com",
},
{
name: "input is already a URL http",
args: args{
input: "http://foo.com",
siteURL: "https://bar.com",
},
want: "http://foo.com",
},
{
name: "input is not a URL",
args: args{
input: "foo",
siteURL: "https://foo.com",
},
want: "https://foo.com/foo",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := URLFromHandle(tt.args.input, tt.args.siteURL); got != tt.want {
t.Errorf("URLFromHandle() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -3,9 +3,7 @@ fragment SlimPerformerData on Performer {
name name
disambiguation disambiguation
gender gender
url urls
twitter
instagram
image_path image_path
favorite favorite
ignore_auto_tag ignore_auto_tag

View file

@ -2,10 +2,8 @@ fragment PerformerData on Performer {
id id
name name
disambiguation disambiguation
url urls
gender gender
twitter
instagram
birthdate birthdate
ethnicity ethnicity
country country

View file

@ -18,9 +18,7 @@ fragment ScrapedPerformerData on ScrapedPerformer {
name name
disambiguation disambiguation
gender gender
url urls
twitter
instagram
birthdate birthdate
ethnicity ethnicity
country country
@ -50,9 +48,7 @@ fragment ScrapedScenePerformerData on ScrapedPerformer {
name name
disambiguation disambiguation
gender gender
url urls
twitter
instagram
birthdate birthdate
ethnicity ethnicity
country country

View file

@ -36,9 +36,6 @@ interface IListOperationProps {
const performerFields = [ const performerFields = [
"favorite", "favorite",
"disambiguation", "disambiguation",
"url",
"instagram",
"twitter",
"rating100", "rating100",
"gender", "gender",
"birthdate", "birthdate",
@ -359,15 +356,6 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
{renderTextField("career_length", updateInput.career_length, (v) => {renderTextField("career_length", updateInput.career_length, (v) =>
setUpdateField({ career_length: v }) setUpdateField({ career_length: v })
)} )}
{renderTextField("url", updateInput.url, (v) =>
setUpdateField({ url: v })
)}
{renderTextField("twitter", updateInput.twitter, (v) =>
setUpdateField({ twitter: v })
)}
{renderTextField("instagram", updateInput.instagram, (v) =>
setUpdateField({ instagram: v })
)}
<Form.Group controlId="tags"> <Form.Group controlId="tags">
<Form.Label> <Form.Label>

View file

@ -20,7 +20,6 @@ import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { useLightbox } from "src/hooks/Lightbox/hooks"; import { useLightbox } from "src/hooks/Lightbox/hooks";
import { useToast } from "src/hooks/Toast"; import { useToast } from "src/hooks/Toast";
import { ConfigurationContext } from "src/hooks/Config"; import { ConfigurationContext } from "src/hooks/Config";
import TextUtils from "src/utils/text";
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
import { import {
CompressedPerformerDetailsPanel, CompressedPerformerDetailsPanel,
@ -44,7 +43,7 @@ import { useRatingKeybinds } from "src/hooks/keybinds";
import { DetailImage } from "src/components/Shared/DetailImage"; import { DetailImage } from "src/components/Shared/DetailImage";
import { useLoadStickyHeader } from "src/hooks/detailsPanel"; import { useLoadStickyHeader } from "src/hooks/detailsPanel";
import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
import { ExternalLink } from "src/components/Shared/ExternalLink"; import { ExternalLinksButton } from "src/components/Shared/ExternalLinksButton";
interface IProps { interface IProps {
performer: GQL.PerformerDataFragment; performer: GQL.PerformerDataFragment;
@ -90,6 +89,29 @@ const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
const [encodingImage, setEncodingImage] = useState<boolean>(false); const [encodingImage, setEncodingImage] = useState<boolean>(false);
const loadStickyHeader = useLoadStickyHeader(); const loadStickyHeader = useLoadStickyHeader();
// a list of urls to display in the performer details
const urls = useMemo(() => {
if (!performer.urls?.length) {
return [];
}
const twitter = performer.urls.filter((u) =>
u.match(/https?:\/\/(?:www\.)?twitter.com\//)
);
const instagram = performer.urls.filter((u) =>
u.match(/https?:\/\/(?:www\.)?instagram.com\//)
);
const others = performer.urls.filter(
(u) => !twitter.includes(u) && !instagram.includes(u)
);
return [
{ icon: faLink, className: "", urls: others },
{ icon: faTwitter, className: "twitter", urls: twitter },
{ icon: faInstagram, className: "instagram", urls: instagram },
];
}, [performer.urls]);
const activeImage = useMemo(() => { const activeImage = useMemo(() => {
const performerImage = performer.image_path; const performerImage = performer.image_path;
if (isEditing) { if (isEditing) {
@ -478,11 +500,6 @@ const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
} }
function renderClickableIcons() { function renderClickableIcons() {
/* Collect urls adding into details */
/* This code can be removed once multple urls are supported for performers */
const detailURLsRegex = /\[((?:http|www\.)[^\n\]]+)\]/gm;
let urls = performer?.details?.match(detailURLsRegex);
return ( return (
<span className="name-icons"> <span className="name-icons">
<Button <Button
@ -494,53 +511,14 @@ const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
> >
<Icon icon={faHeart} /> <Icon icon={faHeart} />
</Button> </Button>
{performer.url && ( {urls.map((url) => (
<Button <ExternalLinksButton
as={ExternalLink} key={url.icon.iconName}
href={TextUtils.sanitiseURL(performer.url)} icon={url.icon}
className="minimal link" className={url.className}
title={performer.url} urls={url.urls}
> />
<Icon icon={faLink} />
</Button>
)}
{(urls ?? []).map((url, index) => (
<Button
key={index}
as={ExternalLink}
href={TextUtils.sanitiseURL(url)}
className={`minimal link detail-link detail-link-${index}`}
title={url}
>
<Icon icon={faLink} />
</Button>
))} ))}
{performer.twitter && (
<Button
as={ExternalLink}
href={TextUtils.sanitiseURL(
performer.twitter,
TextUtils.twitterURL
)}
className="minimal link twitter"
title={performer.twitter}
>
<Icon icon={faTwitter} />
</Button>
)}
{performer.instagram && (
<Button
as={ExternalLink}
href={TextUtils.sanitiseURL(
performer.instagram,
TextUtils.instagramURL
)}
className="minimal link instagram"
title={performer.instagram}
>
<Icon icon={faInstagram} />
</Button>
)}
</span> </span>
); );
} }

View file

@ -14,7 +14,6 @@ import { Icon } from "src/components/Shared/Icon";
import { ImageInput } from "src/components/Shared/ImageInput"; import { ImageInput } from "src/components/Shared/ImageInput";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { CountrySelect } from "src/components/Shared/CountrySelect"; import { CountrySelect } from "src/components/Shared/CountrySelect";
import { URLField } from "src/components/Shared/URLField";
import ImageUtils from "src/utils/image"; import ImageUtils from "src/utils/image";
import { getStashIDs } from "src/utils/stashIds"; import { getStashIDs } from "src/utils/stashIds";
import { stashboxDisplayName } from "src/utils/stashbox"; import { stashboxDisplayName } from "src/utils/stashbox";
@ -45,6 +44,7 @@ import {
yupInputEnum, yupInputEnum,
yupDateString, yupDateString,
yupUniqueAliases, yupUniqueAliases,
yupUniqueStringList,
} from "src/utils/yup"; } from "src/utils/yup";
import { useTagsEdit } from "src/hooks/tagsEdit"; import { useTagsEdit } from "src/hooks/tagsEdit";
@ -109,9 +109,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
tattoos: yup.string().ensure(), tattoos: yup.string().ensure(),
piercings: yup.string().ensure(), piercings: yup.string().ensure(),
career_length: yup.string().ensure(), career_length: yup.string().ensure(),
url: yup.string().ensure(), urls: yupUniqueStringList(intl),
twitter: yup.string().ensure(),
instagram: yup.string().ensure(),
details: yup.string().ensure(), details: yup.string().ensure(),
tag_ids: yup.array(yup.string().required()).defined(), tag_ids: yup.array(yup.string().required()).defined(),
ignore_auto_tag: yup.boolean().defined(), ignore_auto_tag: yup.boolean().defined(),
@ -139,9 +137,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
tattoos: performer.tattoos ?? "", tattoos: performer.tattoos ?? "",
piercings: performer.piercings ?? "", piercings: performer.piercings ?? "",
career_length: performer.career_length ?? "", career_length: performer.career_length ?? "",
url: performer.url ?? "", urls: performer.urls ?? [],
twitter: performer.twitter ?? "",
instagram: performer.instagram ?? "",
details: performer.details ?? "", details: performer.details ?? "",
tag_ids: (performer.tags ?? []).map((t) => t.id), tag_ids: (performer.tags ?? []).map((t) => t.id),
ignore_auto_tag: performer.ignore_auto_tag ?? false, ignore_auto_tag: performer.ignore_auto_tag ?? false,
@ -239,14 +235,8 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
if (state.piercings) { if (state.piercings) {
formik.setFieldValue("piercings", state.piercings); formik.setFieldValue("piercings", state.piercings);
} }
if (state.url) { if (state.urls) {
formik.setFieldValue("url", state.url); formik.setFieldValue("urls", state.urls);
}
if (state.twitter) {
formik.setFieldValue("twitter", state.twitter);
}
if (state.instagram) {
formik.setFieldValue("instagram", state.instagram);
} }
if (state.gender) { if (state.gender) {
// gender is a string in the scraper data // gender is a string in the scraper data
@ -411,8 +401,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
} }
} }
async function onScrapePerformerURL() { async function onScrapePerformerURL(url: string) {
const { url } = formik.values;
if (!url) return; if (!url) return;
setIsLoading(true); setIsLoading(true);
try { try {
@ -613,6 +602,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
renderDateField, renderDateField,
renderStringListField, renderStringListField,
renderStashIDsField, renderStashIDsField,
renderURLListField,
} = formikUtils(intl, formik); } = formikUtils(intl, formik);
function renderCountryField() { function renderCountryField() {
@ -627,18 +617,6 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
return renderField("country", title, control); return renderField("country", title, control);
} }
function renderUrlField() {
const title = intl.formatMessage({ id: "url" });
const control = (
<URLField
{...formik.getFieldProps("url")}
onScrapeClick={onScrapePerformerURL}
urlScrapable={urlScrapable}
/>
);
return renderField("url", title, control);
}
function renderTagsField() { function renderTagsField() {
const title = intl.formatMessage({ id: "tags" }); const title = intl.formatMessage({ id: "tags" });
@ -686,10 +664,8 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
{renderInputField("career_length")} {renderInputField("career_length")}
{renderUrlField()} {renderURLListField("urls", onScrapePerformerURL, urlScrapable)}
{renderInputField("twitter")}
{renderInputField("instagram")}
{renderInputField("details", "textarea")} {renderInputField("details", "textarea")}
{renderTagsField()} {renderTagsField()}

View file

@ -8,6 +8,7 @@ import {
ScrapeDialogRow, ScrapeDialogRow,
ScrapedTextAreaRow, ScrapedTextAreaRow,
ScrapedCountryRow, ScrapedCountryRow,
ScrapedStringListRow,
} from "src/components/Shared/ScrapeDialog/ScrapeDialog"; } from "src/components/Shared/ScrapeDialog/ScrapeDialog";
import { Form } from "react-bootstrap"; import { Form } from "react-bootstrap";
import { import {
@ -23,6 +24,7 @@ import {
import { IStashBox } from "./PerformerStashBoxModal"; import { IStashBox } from "./PerformerStashBoxModal";
import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult"; import { ScrapeResult } from "src/components/Shared/ScrapeDialog/scrapeResult";
import { Tag } from "src/components/Tags/TagSelect"; import { Tag } from "src/components/Tags/TagSelect";
import { uniq } from "lodash-es";
import { useScrapedTags } from "src/components/Shared/ScrapeDialog/scrapedTags"; import { useScrapedTags } from "src/components/Shared/ScrapeDialog/scrapedTags";
function renderScrapedGender( function renderScrapedGender(
@ -268,14 +270,13 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
const [piercings, setPiercings] = useState<ScrapeResult<string>>( const [piercings, setPiercings] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(props.performer.piercings, props.scraped.piercings) new ScrapeResult<string>(props.performer.piercings, props.scraped.piercings)
); );
const [url, setURL] = useState<ScrapeResult<string>>( const [urls, setURLs] = useState<ScrapeResult<string[]>>(
new ScrapeResult<string>(props.performer.url, props.scraped.url) new ScrapeResult<string[]>(
); props.performer.urls,
const [twitter, setTwitter] = useState<ScrapeResult<string>>( props.scraped.urls
new ScrapeResult<string>(props.performer.twitter, props.scraped.twitter) ? uniq((props.performer.urls ?? []).concat(props.scraped.urls ?? []))
); : undefined
const [instagram, setInstagram] = useState<ScrapeResult<string>>( )
new ScrapeResult<string>(props.performer.instagram, props.scraped.instagram)
); );
const [gender, setGender] = useState<ScrapeResult<string>>( const [gender, setGender] = useState<ScrapeResult<string>>(
new ScrapeResult<string>( new ScrapeResult<string>(
@ -334,9 +335,7 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
careerLength, careerLength,
tattoos, tattoos,
piercings, piercings,
url, urls,
twitter,
instagram,
gender, gender,
image, image,
tags, tags,
@ -368,9 +367,7 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
career_length: careerLength.getNewValue(), career_length: careerLength.getNewValue(),
tattoos: tattoos.getNewValue(), tattoos: tattoos.getNewValue(),
piercings: piercings.getNewValue(), piercings: piercings.getNewValue(),
url: url.getNewValue(), urls: urls.getNewValue(),
twitter: twitter.getNewValue(),
instagram: instagram.getNewValue(),
gender: gender.getNewValue(), gender: gender.getNewValue(),
tags: tags.getNewValue(), tags: tags.getNewValue(),
images: newImage ? [newImage] : undefined, images: newImage ? [newImage] : undefined,
@ -482,20 +479,10 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
result={piercings} result={piercings}
onChange={(value) => setPiercings(value)} onChange={(value) => setPiercings(value)}
/> />
<ScrapedInputGroupRow <ScrapedStringListRow
title={intl.formatMessage({ id: "url" })} title={intl.formatMessage({ id: "urls" })}
result={url} result={urls}
onChange={(value) => setURL(value)} onChange={(value) => setURLs(value)}
/>
<ScrapedInputGroupRow
title={intl.formatMessage({ id: "twitter" })}
result={twitter}
onChange={(value) => setTwitter(value)}
/>
<ScrapedInputGroupRow
title={intl.formatMessage({ id: "instagram" })}
result={instagram}
onChange={(value) => setInstagram(value)}
/> />
<ScrapedTextAreaRow <ScrapedTextAreaRow
title={intl.formatMessage({ id: "details" })} title={intl.formatMessage({ id: "details" })}

View file

@ -62,8 +62,8 @@ const PerformerScrapeModal: React.FC<IProps> = ({
</div> </div>
) : ( ) : (
<ul className={CLASSNAME_LIST}> <ul className={CLASSNAME_LIST}>
{performers.map((p) => ( {performers.map((p, i) => (
<li key={p.url}> <li key={i}>
<Button <Button
variant="link" variant="link"
onClick={() => onSelectPerformer(p, scraper)} onClick={() => onSelectPerformer(p, scraper)}

View file

@ -7,7 +7,8 @@ import { IconDefinition, faLink } from "@fortawesome/free-solid-svg-icons";
export const ExternalLinksButton: React.FC<{ export const ExternalLinksButton: React.FC<{
icon?: IconDefinition; icon?: IconDefinition;
urls: string[]; urls: string[];
}> = ({ urls, icon = faLink }) => { className?: string;
}> = ({ urls, icon = faLink, className = "" }) => {
if (!urls.length) { if (!urls.length) {
return null; return null;
} }
@ -17,7 +18,7 @@ export const ExternalLinksButton: React.FC<{
<Button <Button
as={ExternalLink} as={ExternalLink}
href={TextUtils.sanitiseURL(urls[0])} href={TextUtils.sanitiseURL(urls[0])}
className="minimal link external-links-button" className={`minimal link external-links-button ${className}`}
title={urls[0]} title={urls[0]}
> >
<Icon icon={icon} /> <Icon icon={icon} />
@ -27,7 +28,7 @@ export const ExternalLinksButton: React.FC<{
return ( return (
<Dropdown className="external-links-button"> <Dropdown className="external-links-button">
<Dropdown.Toggle as={Button} className="minimal link"> <Dropdown.Toggle as={Button} className={`minimal link ${className}`}>
<Icon icon={icon} /> <Icon icon={icon} />
</Dropdown.Toggle> </Dropdown.Toggle>

View file

@ -107,16 +107,54 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
</strong> </strong>
</div> </div>
{truncate ? ( {truncate ? (
<div className="col-7"> <div className="col-7 performer-create-modal-value">
<TruncatedText text={text} /> <TruncatedText text={text} />
</div> </div>
) : ( ) : (
<span className="col-7">{text}</span> <span className="col-7 performer-create-modal-value">{text}</span>
)} )}
</div> </div>
); );
} }
function maybeRenderURLListField(
name: string,
text: string[] | null | undefined,
truncate: boolean = true
) {
if (!text) return;
return (
<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] ? faTimes : faCheck} />
</Button>
)}
<strong>
<FormattedMessage id={name} />:
</strong>
</div>
<div className="col-7 performer-create-modal-value">
<ul>
{text.map((t, i) => (
<li key={i}>
<ExternalLink href={t}>
{truncate ? <TruncatedText text={t} /> : t}
</ExternalLink>
</li>
))}
</ul>
</div>
</div>
);
}
function maybeRenderImage() { function maybeRenderImage() {
if (!images.length) return; if (!images.length) return;
@ -205,9 +243,7 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
career_length: performer.career_length, career_length: performer.career_length,
tattoos: performer.tattoos, tattoos: performer.tattoos,
piercings: performer.piercings, piercings: performer.piercings,
url: performer.url, urls: performer.urls,
twitter: performer.twitter,
instagram: performer.instagram,
image: images.length > imageIndex ? images[imageIndex] : undefined, image: images.length > imageIndex ? images[imageIndex] : undefined,
details: performer.details, details: performer.details,
death_date: performer.death_date, death_date: performer.death_date,
@ -290,9 +326,7 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
{maybeRenderField("piercings", performer.piercings, false)} {maybeRenderField("piercings", performer.piercings, false)}
{maybeRenderField("weight", performer.weight, false)} {maybeRenderField("weight", performer.weight, false)}
{maybeRenderField("details", performer.details)} {maybeRenderField("details", performer.details)}
{maybeRenderField("url", performer.url)} {maybeRenderURLListField("urls", performer.urls)}
{maybeRenderField("twitter", performer.twitter)}
{maybeRenderField("instagram", performer.instagram)}
{maybeRenderStashBoxLink()} {maybeRenderStashBoxLink()}
</div> </div>
{maybeRenderImage()} {maybeRenderImage()}

View file

@ -78,10 +78,8 @@ export const PERFORMER_FIELDS = [
"tattoos", "tattoos",
"piercings", "piercings",
"career_length", "career_length",
"url", "urls",
"twitter",
"instagram",
"details", "details",
]; ];
export const STUDIO_FIELDS = ["name", "image", "url", "parent_studio"]; export const STUDIO_FIELDS = ["name", "image", "urls", "parent_studio"];

View file

@ -165,6 +165,12 @@
width: 12px; width: 12px;
} }
} }
&-value ul {
font-size: 0.8em;
list-style-type: none;
padding-inline-start: 0;
}
} }
.PerformerTagger { .PerformerTagger {

View file

@ -90,7 +90,6 @@ export const scrapedPerformerToCreateInput = (
const input: GQL.PerformerCreateInput = { const input: GQL.PerformerCreateInput = {
name: toCreate.name ?? "", name: toCreate.name ?? "",
url: toCreate.url,
gender: stringToGender(toCreate.gender), gender: stringToGender(toCreate.gender),
birthdate: toCreate.birthdate, birthdate: toCreate.birthdate,
ethnicity: toCreate.ethnicity, ethnicity: toCreate.ethnicity,
@ -103,8 +102,7 @@ export const scrapedPerformerToCreateInput = (
tattoos: toCreate.tattoos, tattoos: toCreate.tattoos,
piercings: toCreate.piercings, piercings: toCreate.piercings,
alias_list: aliases, alias_list: aliases,
twitter: toCreate.twitter, urls: toCreate.urls,
instagram: toCreate.instagram,
tag_ids: filterData((toCreate.tags ?? []).map((t) => t.stored_id)), tag_ids: filterData((toCreate.tags ?? []).map((t) => t.stored_id)),
image: image:
(toCreate.images ?? []).length > 0 (toCreate.images ?? []).length > 0

View file

@ -50,8 +50,6 @@ export const PerformerIsMissingCriterionOption = new IsMissingCriterionOption(
"is_missing", "is_missing",
[ [
"url", "url",
"twitter",
"instagram",
"ethnicity", "ethnicity",
"country", "country",
"hair_color", "hair_color",

View file

@ -369,9 +369,6 @@ const resolution = (width: number, height: number) => {
} }
}; };
const twitterURL = new URL("https://www.twitter.com");
const instagramURL = new URL("https://www.instagram.com");
const sanitiseURL = (url?: string, siteURL?: URL) => { const sanitiseURL = (url?: string, siteURL?: URL) => {
if (!url) { if (!url) {
return url; return url;
@ -485,8 +482,6 @@ const TextUtils = {
resolution, resolution,
sanitiseURL, sanitiseURL,
domainFromURL, domainFromURL,
twitterURL,
instagramURL,
formatDate, formatDate,
formatDateTime, formatDateTime,
secondsAsTimeString, secondsAsTimeString,