mirror of
https://github.com/stashapp/stash.git
synced 2025-12-16 20:07:05 +03:00
Performer urls (#4958)
* Populate URLs from legacy fields * Return nil properly in xpath/json scrapers * Improve migration logging
This commit is contained in:
@@ -34,16 +34,14 @@ func (s *StringOrStringList) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
|
||||
type Performer struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Disambiguation string `json:"disambiguation,omitempty"`
|
||||
Gender string `json:"gender,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Twitter string `json:"twitter,omitempty"`
|
||||
Instagram string `json:"instagram,omitempty"`
|
||||
Birthdate string `json:"birthdate,omitempty"`
|
||||
Ethnicity string `json:"ethnicity,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
EyeColor string `json:"eye_color,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Disambiguation string `json:"disambiguation,omitempty"`
|
||||
Gender string `json:"gender,omitempty"`
|
||||
URLs []string `json:"urls,omitempty"`
|
||||
Birthdate string `json:"birthdate,omitempty"`
|
||||
Ethnicity string `json:"ethnicity,omitempty"`
|
||||
Country string `json:"country,omitempty"`
|
||||
EyeColor string `json:"eye_color,omitempty"`
|
||||
// this should be int, but keeping string for backwards compatibility
|
||||
Height string `json:"height,omitempty"`
|
||||
Measurements string `json:"measurements,omitempty"`
|
||||
@@ -66,6 +64,11 @@ type Performer struct {
|
||||
Weight int `json:"weight,omitempty"`
|
||||
StashIDs []models.StashID `json:"stash_ids,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 {
|
||||
|
||||
@@ -383,6 +383,29 @@ func (_m *PerformerReaderWriter) GetTagIDs(ctx context.Context, relatedID int) (
|
||||
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
|
||||
func (_m *PerformerReaderWriter) HasImage(ctx context.Context, performerID int) (bool, error) {
|
||||
ret := _m.Called(ctx, performerID)
|
||||
|
||||
@@ -10,9 +10,6 @@ type Performer struct {
|
||||
Name string `json:"name"`
|
||||
Disambiguation string `json:"disambiguation"`
|
||||
Gender *GenderEnum `json:"gender"`
|
||||
URL string `json:"url"`
|
||||
Twitter string `json:"twitter"`
|
||||
Instagram string `json:"instagram"`
|
||||
Birthdate *Date `json:"birthdate"`
|
||||
Ethnicity string `json:"ethnicity"`
|
||||
Country string `json:"country"`
|
||||
@@ -37,6 +34,7 @@ type Performer struct {
|
||||
IgnoreAutoTag bool `json:"ignore_auto_tag"`
|
||||
|
||||
Aliases RelatedStrings `json:"aliases"`
|
||||
URLs RelatedStrings `json:"urls"`
|
||||
TagIDs RelatedIDs `json:"tag_ids"`
|
||||
StashIDs RelatedStashIDs `json:"stash_ids"`
|
||||
}
|
||||
@@ -55,9 +53,7 @@ type PerformerPartial struct {
|
||||
Name OptionalString
|
||||
Disambiguation OptionalString
|
||||
Gender OptionalString
|
||||
URL OptionalString
|
||||
Twitter OptionalString
|
||||
Instagram OptionalString
|
||||
URLs *UpdateStrings
|
||||
Birthdate OptionalDate
|
||||
Ethnicity 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 {
|
||||
return s.TagIDs.load(func() ([]int, error) {
|
||||
return l.GetTagIDs(ctx, s.ID)
|
||||
|
||||
@@ -107,9 +107,10 @@ type ScrapedPerformer struct {
|
||||
Name *string `json:"name"`
|
||||
Disambiguation *string `json:"disambiguation"`
|
||||
Gender *string `json:"gender"`
|
||||
URL *string `json:"url"`
|
||||
Twitter *string `json:"twitter"`
|
||||
Instagram *string `json:"instagram"`
|
||||
URLs []string `json:"urls"`
|
||||
URL *string `json:"url"` // deprecated
|
||||
Twitter *string `json:"twitter"` // deprecated
|
||||
Instagram *string `json:"instagram"` // deprecated
|
||||
Birthdate *string `json:"birthdate"`
|
||||
Ethnicity *string `json:"ethnicity"`
|
||||
Country *string `json:"country"`
|
||||
@@ -191,9 +192,7 @@ func (p *ScrapedPerformer) ToPerformer(endpoint string, excluded map[string]bool
|
||||
ret.Weight = &w
|
||||
}
|
||||
}
|
||||
if p.Instagram != nil && !excluded["instagram"] {
|
||||
ret.Instagram = *p.Instagram
|
||||
}
|
||||
|
||||
if p.Measurements != nil && !excluded["measurements"] {
|
||||
ret.Measurements = *p.Measurements
|
||||
}
|
||||
@@ -221,11 +220,27 @@ func (p *ScrapedPerformer) ToPerformer(endpoint string, excluded map[string]bool
|
||||
ret.Circumcised = &v
|
||||
}
|
||||
}
|
||||
if p.Twitter != nil && !excluded["twitter"] {
|
||||
ret.Twitter = *p.Twitter
|
||||
}
|
||||
if p.URL != nil && !excluded["url"] {
|
||||
ret.URL = *p.URL
|
||||
|
||||
// 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"] {
|
||||
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 != "" {
|
||||
@@ -309,9 +324,6 @@ func (p *ScrapedPerformer) ToPartial(endpoint string, excluded map[string]bool,
|
||||
ret.Weight = NewOptionalInt(w)
|
||||
}
|
||||
}
|
||||
if p.Instagram != nil && !excluded["instagram"] {
|
||||
ret.Instagram = NewOptionalString(*p.Instagram)
|
||||
}
|
||||
if p.Measurements != nil && !excluded["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"] {
|
||||
ret.Tattoos = NewOptionalString(*p.Tattoos)
|
||||
}
|
||||
if p.Twitter != nil && !excluded["twitter"] {
|
||||
ret.Twitter = NewOptionalString(*p.Twitter)
|
||||
}
|
||||
if p.URL != nil && !excluded["url"] {
|
||||
ret.URL = NewOptionalString(*p.URL)
|
||||
|
||||
// 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"] {
|
||||
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 != "" {
|
||||
|
||||
@@ -161,9 +161,9 @@ func Test_scrapedToPerformerInput(t *testing.T) {
|
||||
Tattoos: nextVal(),
|
||||
Piercings: nextVal(),
|
||||
Aliases: nextVal(),
|
||||
URL: nextVal(),
|
||||
Twitter: nextVal(),
|
||||
Instagram: nextVal(),
|
||||
URL: nextVal(),
|
||||
Details: nextVal(),
|
||||
RemoteSiteID: &remoteSiteID,
|
||||
},
|
||||
@@ -186,9 +186,7 @@ func Test_scrapedToPerformerInput(t *testing.T) {
|
||||
Tattoos: *nextVal(),
|
||||
Piercings: *nextVal(),
|
||||
Aliases: NewRelatedStrings([]string{*nextVal()}),
|
||||
Twitter: *nextVal(),
|
||||
Instagram: *nextVal(),
|
||||
URL: *nextVal(),
|
||||
URLs: NewRelatedStrings([]string{*nextVal(), *nextVal(), *nextVal()}),
|
||||
Details: *nextVal(),
|
||||
StashIDs: NewRelatedStashIDs([]StashID{
|
||||
{
|
||||
|
||||
@@ -203,7 +203,8 @@ type PerformerFilterType struct {
|
||||
type PerformerCreateInput struct {
|
||||
Name string `json:"name"`
|
||||
Disambiguation *string `json:"disambiguation"`
|
||||
URL *string `json:"url"`
|
||||
URL *string `json:"url"` // deprecated
|
||||
Urls []string `json:"urls"`
|
||||
Gender *GenderEnum `json:"gender"`
|
||||
Birthdate *string `json:"birthdate"`
|
||||
Ethnicity *string `json:"ethnicity"`
|
||||
@@ -220,8 +221,8 @@ type PerformerCreateInput struct {
|
||||
Piercings *string `json:"piercings"`
|
||||
Aliases *string `json:"aliases"`
|
||||
AliasList []string `json:"alias_list"`
|
||||
Twitter *string `json:"twitter"`
|
||||
Instagram *string `json:"instagram"`
|
||||
Twitter *string `json:"twitter"` // deprecated
|
||||
Instagram *string `json:"instagram"` // deprecated
|
||||
Favorite *bool `json:"favorite"`
|
||||
TagIds []string `json:"tag_ids"`
|
||||
// This should be a URL or a base64 encoded data URL
|
||||
@@ -239,7 +240,8 @@ type PerformerUpdateInput struct {
|
||||
ID string `json:"id"`
|
||||
Name *string `json:"name"`
|
||||
Disambiguation *string `json:"disambiguation"`
|
||||
URL *string `json:"url"`
|
||||
URL *string `json:"url"` // deprecated
|
||||
Urls []string `json:"urls"`
|
||||
Gender *GenderEnum `json:"gender"`
|
||||
Birthdate *string `json:"birthdate"`
|
||||
Ethnicity *string `json:"ethnicity"`
|
||||
@@ -256,8 +258,8 @@ type PerformerUpdateInput struct {
|
||||
Piercings *string `json:"piercings"`
|
||||
Aliases *string `json:"aliases"`
|
||||
AliasList []string `json:"alias_list"`
|
||||
Twitter *string `json:"twitter"`
|
||||
Instagram *string `json:"instagram"`
|
||||
Twitter *string `json:"twitter"` // deprecated
|
||||
Instagram *string `json:"instagram"` // deprecated
|
||||
Favorite *bool `json:"favorite"`
|
||||
TagIds []string `json:"tag_ids"`
|
||||
// This should be a URL or a base64 encoded data URL
|
||||
|
||||
@@ -78,6 +78,7 @@ type PerformerReader interface {
|
||||
AliasLoader
|
||||
StashIDLoader
|
||||
TagIDLoader
|
||||
URLLoader
|
||||
|
||||
All(ctx context.Context) ([]*Performer, error)
|
||||
GetImage(ctx context.Context, performerID int) ([]byte, error)
|
||||
|
||||
@@ -16,6 +16,7 @@ type ImageAliasStashIDGetter interface {
|
||||
GetImage(ctx context.Context, performerID int) ([]byte, error)
|
||||
models.AliasLoader
|
||||
models.StashIDLoader
|
||||
models.URLLoader
|
||||
}
|
||||
|
||||
// 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{
|
||||
Name: performer.Name,
|
||||
Disambiguation: performer.Disambiguation,
|
||||
URL: performer.URL,
|
||||
Ethnicity: performer.Ethnicity,
|
||||
Country: performer.Country,
|
||||
EyeColor: performer.EyeColor,
|
||||
@@ -32,8 +32,6 @@ func ToJSON(ctx context.Context, reader ImageAliasStashIDGetter, performer *mode
|
||||
CareerLength: performer.CareerLength,
|
||||
Tattoos: performer.Tattoos,
|
||||
Piercings: performer.Piercings,
|
||||
Twitter: performer.Twitter,
|
||||
Instagram: performer.Instagram,
|
||||
Favorite: performer.Favorite,
|
||||
Details: performer.Details,
|
||||
HairColor: performer.HairColor,
|
||||
@@ -78,6 +76,11 @@ func ToJSON(ctx context.Context, reader ImageAliasStashIDGetter, performer *mode
|
||||
|
||||
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 {
|
||||
return nil, fmt.Errorf("loading performer stash ids: %w", err)
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func createFullPerformer(id int, name string) *models.Performer {
|
||||
ID: id,
|
||||
Name: name,
|
||||
Disambiguation: disambiguation,
|
||||
URL: url,
|
||||
URLs: models.NewRelatedStrings([]string{url, twitter, instagram}),
|
||||
Aliases: models.NewRelatedStrings(aliases),
|
||||
Birthdate: &birthDate,
|
||||
CareerLength: careerLength,
|
||||
@@ -90,11 +90,9 @@ func createFullPerformer(id int, name string) *models.Performer {
|
||||
Favorite: true,
|
||||
Gender: &genderEnum,
|
||||
Height: &height,
|
||||
Instagram: instagram,
|
||||
Measurements: measurements,
|
||||
Piercings: piercings,
|
||||
Tattoos: tattoos,
|
||||
Twitter: twitter,
|
||||
CreatedAt: createTime,
|
||||
UpdatedAt: updateTime,
|
||||
Rating: &rating,
|
||||
@@ -114,6 +112,7 @@ func createEmptyPerformer(id int) models.Performer {
|
||||
CreatedAt: createTime,
|
||||
UpdatedAt: updateTime,
|
||||
Aliases: models.NewRelatedStrings([]string{}),
|
||||
URLs: models.NewRelatedStrings([]string{}),
|
||||
TagIDs: models.NewRelatedIDs([]int{}),
|
||||
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
|
||||
}
|
||||
@@ -123,7 +122,7 @@ func createFullJSONPerformer(name string, image string) *jsonschema.Performer {
|
||||
return &jsonschema.Performer{
|
||||
Name: name,
|
||||
Disambiguation: disambiguation,
|
||||
URL: url,
|
||||
URLs: []string{url, twitter, instagram},
|
||||
Aliases: aliases,
|
||||
Birthdate: birthDate.String(),
|
||||
CareerLength: careerLength,
|
||||
@@ -136,11 +135,9 @@ func createFullJSONPerformer(name string, image string) *jsonschema.Performer {
|
||||
Favorite: true,
|
||||
Gender: gender,
|
||||
Height: strconv.Itoa(height),
|
||||
Instagram: instagram,
|
||||
Measurements: measurements,
|
||||
Piercings: piercings,
|
||||
Tattoos: tattoos,
|
||||
Twitter: twitter,
|
||||
CreatedAt: json.JSONTime{
|
||||
Time: createTime,
|
||||
},
|
||||
@@ -161,6 +158,7 @@ func createFullJSONPerformer(name string, image string) *jsonschema.Performer {
|
||||
func createEmptyJSONPerformer() *jsonschema.Performer {
|
||||
return &jsonschema.Performer{
|
||||
Aliases: []string{},
|
||||
URLs: []string{},
|
||||
StashIDs: []models.StashID{},
|
||||
CreatedAt: json.JSONTime{
|
||||
Time: createTime,
|
||||
|
||||
@@ -188,7 +188,6 @@ func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Perform
|
||||
newPerformer := models.Performer{
|
||||
Name: performerJSON.Name,
|
||||
Disambiguation: performerJSON.Disambiguation,
|
||||
URL: performerJSON.URL,
|
||||
Ethnicity: performerJSON.Ethnicity,
|
||||
Country: performerJSON.Country,
|
||||
EyeColor: performerJSON.EyeColor,
|
||||
@@ -198,8 +197,6 @@ func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Perform
|
||||
Tattoos: performerJSON.Tattoos,
|
||||
Piercings: performerJSON.Piercings,
|
||||
Aliases: models.NewRelatedStrings(performerJSON.Aliases),
|
||||
Twitter: performerJSON.Twitter,
|
||||
Instagram: performerJSON.Instagram,
|
||||
Details: performerJSON.Details,
|
||||
HairColor: performerJSON.HairColor,
|
||||
Favorite: performerJSON.Favorite,
|
||||
@@ -211,6 +208,25 @@ func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Perform
|
||||
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 != "" {
|
||||
v := models.GenderEnum(performerJSON.Gender)
|
||||
newPerformer.Gender = &v
|
||||
|
||||
18
pkg/performer/url.go
Normal file
18
pkg/performer/url.go
Normal 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)
|
||||
}
|
||||
@@ -81,15 +81,33 @@ func (s *jsonScraper) scrapeByURL(ctx context.Context, url string, ty ScrapeCont
|
||||
}
|
||||
|
||||
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 {
|
||||
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:
|
||||
return scraper.scrapeScene(ctx, q)
|
||||
ret, err := scraper.scrapeScene(ctx, q)
|
||||
if err != nil || ret == nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
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:
|
||||
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
|
||||
|
||||
@@ -2,29 +2,30 @@ package scraper
|
||||
|
||||
type ScrapedPerformerInput struct {
|
||||
// Set if performer matched
|
||||
StoredID *string `json:"stored_id"`
|
||||
Name *string `json:"name"`
|
||||
Disambiguation *string `json:"disambiguation"`
|
||||
Gender *string `json:"gender"`
|
||||
URL *string `json:"url"`
|
||||
Twitter *string `json:"twitter"`
|
||||
Instagram *string `json:"instagram"`
|
||||
Birthdate *string `json:"birthdate"`
|
||||
Ethnicity *string `json:"ethnicity"`
|
||||
Country *string `json:"country"`
|
||||
EyeColor *string `json:"eye_color"`
|
||||
Height *string `json:"height"`
|
||||
Measurements *string `json:"measurements"`
|
||||
FakeTits *string `json:"fake_tits"`
|
||||
PenisLength *string `json:"penis_length"`
|
||||
Circumcised *string `json:"circumcised"`
|
||||
CareerLength *string `json:"career_length"`
|
||||
Tattoos *string `json:"tattoos"`
|
||||
Piercings *string `json:"piercings"`
|
||||
Aliases *string `json:"aliases"`
|
||||
Details *string `json:"details"`
|
||||
DeathDate *string `json:"death_date"`
|
||||
HairColor *string `json:"hair_color"`
|
||||
Weight *string `json:"weight"`
|
||||
RemoteSiteID *string `json:"remote_site_id"`
|
||||
StoredID *string `json:"stored_id"`
|
||||
Name *string `json:"name"`
|
||||
Disambiguation *string `json:"disambiguation"`
|
||||
Gender *string `json:"gender"`
|
||||
URLs []string `json:"urls"`
|
||||
URL *string `json:"url"` // deprecated
|
||||
Twitter *string `json:"twitter"` // deprecated
|
||||
Instagram *string `json:"instagram"` // deprecated
|
||||
Birthdate *string `json:"birthdate"`
|
||||
Ethnicity *string `json:"ethnicity"`
|
||||
Country *string `json:"country"`
|
||||
EyeColor *string `json:"eye_color"`
|
||||
Height *string `json:"height"`
|
||||
Measurements *string `json:"measurements"`
|
||||
FakeTits *string `json:"fake_tits"`
|
||||
PenisLength *string `json:"penis_length"`
|
||||
Circumcised *string `json:"circumcised"`
|
||||
CareerLength *string `json:"career_length"`
|
||||
Tattoos *string `json:"tattoos"`
|
||||
Piercings *string `json:"piercings"`
|
||||
Aliases *string `json:"aliases"`
|
||||
Details *string `json:"details"`
|
||||
DeathDate *string `json:"death_date"`
|
||||
HairColor *string `json:"hair_color"`
|
||||
Weight *string `json:"weight"`
|
||||
RemoteSiteID *string `json:"remote_site_id"`
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/match"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
// 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)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
@@ -163,6 +163,12 @@ func (i *Input) populateURL() {
|
||||
if i.Scene != nil && i.Scene.URL == nil && len(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
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -41,6 +40,7 @@ type PerformerReader interface {
|
||||
match.PerformerFinder
|
||||
models.AliasLoader
|
||||
models.StashIDLoader
|
||||
models.URLLoader
|
||||
FindBySceneID(ctx context.Context, sceneID int) ([]*models.Performer, error)
|
||||
GetImage(ctx context.Context, performerID int) ([]byte, error)
|
||||
}
|
||||
@@ -685,6 +685,10 @@ func performerFragmentToScrapedPerformer(p graphql.PerformerFragment) *models.Sc
|
||||
sp.Aliases = &alias
|
||||
}
|
||||
|
||||
for _, u := range p.Urls {
|
||||
sp.URLs = append(sp.URLs, u.URL)
|
||||
}
|
||||
|
||||
return sp
|
||||
}
|
||||
|
||||
@@ -1128,6 +1132,10 @@ func (c Client) SubmitPerformerDraft(ctx context.Context, performer *models.Perf
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := performer.LoadURLs(ctx, pqb); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := performer.LoadStashIDs(ctx, pqb); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1195,28 +1203,8 @@ func (c Client) SubmitPerformerDraft(ctx context.Context, performer *models.Perf
|
||||
}
|
||||
}
|
||||
|
||||
var urls []string
|
||||
if len(strings.TrimSpace(performer.Twitter)) > 0 {
|
||||
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
|
||||
if len(performer.URLs.List()) > 0 {
|
||||
draft.Urls = performer.URLs.List()
|
||||
}
|
||||
|
||||
stashIDs, err := pqb.GetStashIDs(ctx, performer.ID)
|
||||
|
||||
@@ -62,15 +62,33 @@ func (s *xpathScraper) scrapeByURL(ctx context.Context, url string, ty ScrapeCon
|
||||
}
|
||||
|
||||
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 {
|
||||
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:
|
||||
return scraper.scrapeScene(ctx, q)
|
||||
ret, err := scraper.scrapeScene(ctx, q)
|
||||
if err != nil || ret == nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
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:
|
||||
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
|
||||
|
||||
@@ -495,9 +495,6 @@ func (db *Anonymiser) anonymisePerformers(ctx context.Context) error {
|
||||
table.Col(idColumn),
|
||||
table.Col("name"),
|
||||
table.Col("details"),
|
||||
table.Col("url"),
|
||||
table.Col("twitter"),
|
||||
table.Col("instagram"),
|
||||
table.Col("tattoos"),
|
||||
table.Col("piercings"),
|
||||
).Where(table.Col(idColumn).Gt(lastID)).Limit(1000)
|
||||
@@ -510,9 +507,6 @@ func (db *Anonymiser) anonymisePerformers(ctx context.Context) error {
|
||||
id int
|
||||
name sql.NullString
|
||||
details sql.NullString
|
||||
url sql.NullString
|
||||
twitter sql.NullString
|
||||
instagram sql.NullString
|
||||
tattoos sql.NullString
|
||||
piercings sql.NullString
|
||||
)
|
||||
@@ -521,9 +515,6 @@ func (db *Anonymiser) anonymisePerformers(ctx context.Context) error {
|
||||
&id,
|
||||
&name,
|
||||
&details,
|
||||
&url,
|
||||
&twitter,
|
||||
&instagram,
|
||||
&tattoos,
|
||||
&piercings,
|
||||
); err != nil {
|
||||
@@ -533,9 +524,6 @@ func (db *Anonymiser) anonymisePerformers(ctx context.Context) error {
|
||||
set := goqu.Record{}
|
||||
db.obfuscateNullString(set, "name", name)
|
||||
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, "piercings", piercings)
|
||||
|
||||
@@ -566,6 +554,10 @@ func (db *Anonymiser) anonymisePerformers(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := db.anonymiseURLs(ctx, goqu.T(performerURLsTable), "performer_id"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ const (
|
||||
dbConnTimeout = 30
|
||||
)
|
||||
|
||||
var appSchemaVersion uint = 61
|
||||
var appSchemaVersion uint = 62
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsBox embed.FS
|
||||
|
||||
155
pkg/sqlite/migrations/62_performer_urls.up.sql
Normal file
155
pkg/sqlite/migrations/62_performer_urls.up.sql
Normal 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;
|
||||
@@ -23,6 +23,9 @@ const (
|
||||
performerAliasColumn = "alias"
|
||||
performersTagsTable = "performers_tags"
|
||||
|
||||
performerURLsTable = "performer_urls"
|
||||
performerURLColumn = "url"
|
||||
|
||||
performerImageBlobColumn = "image_blob"
|
||||
)
|
||||
|
||||
@@ -31,9 +34,6 @@ type performerRow struct {
|
||||
Name null.String `db:"name"` // TODO: make schema non-nullable
|
||||
Disambigation zero.String `db:"disambiguation"`
|
||||
Gender zero.String `db:"gender"`
|
||||
URL zero.String `db:"url"`
|
||||
Twitter zero.String `db:"twitter"`
|
||||
Instagram zero.String `db:"instagram"`
|
||||
Birthdate NullDate `db:"birthdate"`
|
||||
Ethnicity zero.String `db:"ethnicity"`
|
||||
Country zero.String `db:"country"`
|
||||
@@ -68,9 +68,6 @@ func (r *performerRow) fromPerformer(o models.Performer) {
|
||||
if o.Gender != nil && o.Gender.IsValid() {
|
||||
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.Ethnicity = zero.StringFrom(o.Ethnicity)
|
||||
r.Country = zero.StringFrom(o.Country)
|
||||
@@ -101,9 +98,6 @@ func (r *performerRow) resolve() *models.Performer {
|
||||
ID: r.ID,
|
||||
Name: r.Name.String,
|
||||
Disambiguation: r.Disambigation.String,
|
||||
URL: r.URL.String,
|
||||
Twitter: r.Twitter.String,
|
||||
Instagram: r.Instagram.String,
|
||||
Birthdate: r.Birthdate.DatePtr(),
|
||||
Ethnicity: r.Ethnicity.String,
|
||||
Country: r.Country.String,
|
||||
@@ -148,9 +142,6 @@ func (r *performerRowRecord) fromPartial(o models.PerformerPartial) {
|
||||
r.setString("name", o.Name)
|
||||
r.setNullString("disambiguation", o.Disambiguation)
|
||||
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.setNullString("ethnicity", o.Ethnicity)
|
||||
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 err := performersTagsTableMgr.insertJoins(ctx, id, newObject.TagIDs.List()); err != nil {
|
||||
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 err := performersTagsTableMgr.modifyJoins(ctx, id, partial.TagIDs.IDs, partial.TagIDs.Mode); err != nil {
|
||||
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 err := performersTagsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.TagIDs.List()); err != nil {
|
||||
return err
|
||||
@@ -785,6 +795,10 @@ func (qb *PerformerStore) GetAliases(ctx context.Context, performerID int) ([]st
|
||||
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) {
|
||||
return performersStashIDsTableMgr.get(ctx, performerID)
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ func (qb *performerFilterHandler) criterionHandler() criterionHandler {
|
||||
stringCriterionHandler(filter.Piercings, tableName+".piercings"),
|
||||
intCriterionHandler(filter.Rating100, tableName+".rating", nil),
|
||||
stringCriterionHandler(filter.HairColor, tableName+".hair_color"),
|
||||
stringCriterionHandler(filter.URL, tableName+".url"),
|
||||
qb.urlsCriterionHandler(filter.URL),
|
||||
intCriterionHandler(filter.Weight, tableName+".weight", nil),
|
||||
criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
|
||||
if filter.StashID != nil {
|
||||
@@ -211,6 +211,9 @@ func (qb *performerFilterHandler) performerIsMissingCriterionHandler(isMissing *
|
||||
return func(ctx context.Context, f *filterBuilder) {
|
||||
if isMissing != nil && *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
|
||||
f.addLeftJoin(performersScenesTable, "scenes_join", "scenes_join.performer_id = performers.id")
|
||||
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 {
|
||||
h := stringListCriterionHandlerBuilder{
|
||||
primaryTable: performerTable,
|
||||
|
||||
@@ -22,6 +22,11 @@ func loadPerformerRelationships(ctx context.Context, expected models.Performer,
|
||||
return err
|
||||
}
|
||||
}
|
||||
if expected.URLs.Loaded() {
|
||||
if err := actual.LoadURLs(ctx, db.Performer); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if expected.TagIDs.Loaded() {
|
||||
if err := actual.LoadTagIDs(ctx, db.Performer); err != nil {
|
||||
return err
|
||||
@@ -45,6 +50,7 @@ func Test_PerformerStore_Create(t *testing.T) {
|
||||
url = "url"
|
||||
twitter = "twitter"
|
||||
instagram = "instagram"
|
||||
urls = []string{url, twitter, instagram}
|
||||
rating = 3
|
||||
ethnicity = "ethnicity"
|
||||
country = "country"
|
||||
@@ -84,9 +90,7 @@ func Test_PerformerStore_Create(t *testing.T) {
|
||||
Name: name,
|
||||
Disambiguation: disambiguation,
|
||||
Gender: &gender,
|
||||
URL: url,
|
||||
Twitter: twitter,
|
||||
Instagram: instagram,
|
||||
URLs: models.NewRelatedStrings(urls),
|
||||
Birthdate: &birthdate,
|
||||
Ethnicity: ethnicity,
|
||||
Country: country,
|
||||
@@ -193,6 +197,7 @@ func Test_PerformerStore_Update(t *testing.T) {
|
||||
url = "url"
|
||||
twitter = "twitter"
|
||||
instagram = "instagram"
|
||||
urls = []string{url, twitter, instagram}
|
||||
rating = 3
|
||||
ethnicity = "ethnicity"
|
||||
country = "country"
|
||||
@@ -233,9 +238,7 @@ func Test_PerformerStore_Update(t *testing.T) {
|
||||
Name: name,
|
||||
Disambiguation: disambiguation,
|
||||
Gender: &gender,
|
||||
URL: url,
|
||||
Twitter: twitter,
|
||||
Instagram: instagram,
|
||||
URLs: models.NewRelatedStrings(urls),
|
||||
Birthdate: &birthdate,
|
||||
Ethnicity: ethnicity,
|
||||
Country: country,
|
||||
@@ -277,6 +280,7 @@ func Test_PerformerStore_Update(t *testing.T) {
|
||||
&models.Performer{
|
||||
ID: performerIDs[performerIdxWithGallery],
|
||||
Aliases: models.NewRelatedStrings([]string{}),
|
||||
URLs: models.NewRelatedStrings([]string{}),
|
||||
TagIDs: models.NewRelatedIDs([]int{}),
|
||||
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
|
||||
},
|
||||
@@ -341,9 +345,7 @@ func clearPerformerPartial() models.PerformerPartial {
|
||||
return models.PerformerPartial{
|
||||
Disambiguation: nullString,
|
||||
Gender: nullString,
|
||||
URL: nullString,
|
||||
Twitter: nullString,
|
||||
Instagram: nullString,
|
||||
URLs: &models.UpdateStrings{Mode: models.RelationshipUpdateModeSet},
|
||||
Birthdate: nullDate,
|
||||
Ethnicity: nullString,
|
||||
Country: nullString,
|
||||
@@ -376,6 +378,7 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
|
||||
url = "url"
|
||||
twitter = "twitter"
|
||||
instagram = "instagram"
|
||||
urls = []string{url, twitter, instagram}
|
||||
rating = 3
|
||||
ethnicity = "ethnicity"
|
||||
country = "country"
|
||||
@@ -418,21 +421,22 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
|
||||
Name: models.NewOptionalString(name),
|
||||
Disambiguation: models.NewOptionalString(disambiguation),
|
||||
Gender: models.NewOptionalString(gender.String()),
|
||||
URL: models.NewOptionalString(url),
|
||||
Twitter: models.NewOptionalString(twitter),
|
||||
Instagram: models.NewOptionalString(instagram),
|
||||
Birthdate: models.NewOptionalDate(birthdate),
|
||||
Ethnicity: models.NewOptionalString(ethnicity),
|
||||
Country: models.NewOptionalString(country),
|
||||
EyeColor: models.NewOptionalString(eyeColor),
|
||||
Height: models.NewOptionalInt(height),
|
||||
Measurements: models.NewOptionalString(measurements),
|
||||
FakeTits: models.NewOptionalString(fakeTits),
|
||||
PenisLength: models.NewOptionalFloat64(penisLength),
|
||||
Circumcised: models.NewOptionalString(circumcised.String()),
|
||||
CareerLength: models.NewOptionalString(careerLength),
|
||||
Tattoos: models.NewOptionalString(tattoos),
|
||||
Piercings: models.NewOptionalString(piercings),
|
||||
URLs: &models.UpdateStrings{
|
||||
Values: urls,
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
},
|
||||
Birthdate: models.NewOptionalDate(birthdate),
|
||||
Ethnicity: models.NewOptionalString(ethnicity),
|
||||
Country: models.NewOptionalString(country),
|
||||
EyeColor: models.NewOptionalString(eyeColor),
|
||||
Height: models.NewOptionalInt(height),
|
||||
Measurements: models.NewOptionalString(measurements),
|
||||
FakeTits: models.NewOptionalString(fakeTits),
|
||||
PenisLength: models.NewOptionalFloat64(penisLength),
|
||||
Circumcised: models.NewOptionalString(circumcised.String()),
|
||||
CareerLength: models.NewOptionalString(careerLength),
|
||||
Tattoos: models.NewOptionalString(tattoos),
|
||||
Piercings: models.NewOptionalString(piercings),
|
||||
Aliases: &models.UpdateStrings{
|
||||
Values: aliases,
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
@@ -469,9 +473,7 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
|
||||
Name: name,
|
||||
Disambiguation: disambiguation,
|
||||
Gender: &gender,
|
||||
URL: url,
|
||||
Twitter: twitter,
|
||||
Instagram: instagram,
|
||||
URLs: models.NewRelatedStrings(urls),
|
||||
Birthdate: &birthdate,
|
||||
Ethnicity: ethnicity,
|
||||
Country: country,
|
||||
@@ -516,6 +518,7 @@ func Test_PerformerStore_UpdatePartial(t *testing.T) {
|
||||
ID: performerIDs[performerIdxWithTwoTags],
|
||||
Name: getPerformerStringValue(performerIdxWithTwoTags, "Name"),
|
||||
Favorite: getPerformerBoolValue(performerIdxWithTwoTags),
|
||||
URLs: models.NewRelatedStrings([]string{}),
|
||||
Aliases: models.NewRelatedStrings([]string{}),
|
||||
TagIDs: models.NewRelatedIDs([]int{}),
|
||||
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
|
||||
@@ -1290,7 +1293,14 @@ func TestPerformerQueryURL(t *testing.T) {
|
||||
|
||||
verifyFn := func(g *models.Performer) {
|
||||
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)
|
||||
@@ -1318,6 +1328,12 @@ func verifyPerformerQuery(t *testing.T, filter models.PerformerFilterType, verif
|
||||
t.Helper()
|
||||
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
|
||||
assert.Greater(t, len(performers), 0)
|
||||
|
||||
|
||||
@@ -1374,6 +1374,15 @@ func getPerformerNullStringValue(index int, field string) 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 {
|
||||
index = index % 2
|
||||
return index == 1
|
||||
@@ -1479,17 +1488,19 @@ func createPerformers(ctx context.Context, n int, o int) error {
|
||||
Name: getPerformerStringValue(index, name),
|
||||
Disambiguation: getPerformerStringValue(index, "disambiguation"),
|
||||
Aliases: models.NewRelatedStrings(performerAliases(index)),
|
||||
URL: getPerformerNullStringValue(i, urlField),
|
||||
Favorite: getPerformerBoolValue(i),
|
||||
Birthdate: getPerformerBirthdate(i),
|
||||
DeathDate: getPerformerDeathDate(i),
|
||||
Details: getPerformerStringValue(i, "Details"),
|
||||
Ethnicity: getPerformerStringValue(i, "Ethnicity"),
|
||||
PenisLength: getPerformerPenisLength(i),
|
||||
Circumcised: getPerformerCircumcised(i),
|
||||
Rating: getIntPtr(getRating(i)),
|
||||
IgnoreAutoTag: getIgnoreAutoTag(i),
|
||||
TagIDs: models.NewRelatedIDs(tids),
|
||||
URLs: models.NewRelatedStrings([]string{
|
||||
getPerformerEmptyString(i, urlField),
|
||||
}),
|
||||
Favorite: getPerformerBoolValue(i),
|
||||
Birthdate: getPerformerBirthdate(i),
|
||||
DeathDate: getPerformerDeathDate(i),
|
||||
Details: getPerformerStringValue(i, "Details"),
|
||||
Ethnicity: getPerformerStringValue(i, "Ethnicity"),
|
||||
PenisLength: getPerformerPenisLength(i),
|
||||
Circumcised: getPerformerCircumcised(i),
|
||||
Rating: getIntPtr(getRating(i)),
|
||||
IgnoreAutoTag: getIgnoreAutoTag(i),
|
||||
TagIDs: models.NewRelatedIDs(tids),
|
||||
}
|
||||
|
||||
careerLength := getPerformerCareerLength(i)
|
||||
|
||||
@@ -29,6 +29,7 @@ var (
|
||||
scenesURLsJoinTable = goqu.T(scenesURLsTable)
|
||||
|
||||
performersAliasesJoinTable = goqu.T(performersAliasesTable)
|
||||
performersURLsJoinTable = goqu.T(performerURLsTable)
|
||||
performersTagsJoinTable = goqu.T(performersTagsTable)
|
||||
performersStashIDsJoinTable = goqu.T("performer_stash_ids")
|
||||
|
||||
@@ -255,6 +256,14 @@ var (
|
||||
stringColumn: performersAliasesJoinTable.Col(performerAliasColumn),
|
||||
}
|
||||
|
||||
performersURLsTableMgr = &orderedValueTable[string]{
|
||||
table: table{
|
||||
table: performersURLsJoinTable,
|
||||
idColumn: performersURLsJoinTable.Col(performerIDColumn),
|
||||
},
|
||||
valueColumn: performersURLsJoinTable.Col(performerURLColumn),
|
||||
}
|
||||
|
||||
performersTagsTableMgr = &joinTable{
|
||||
table: table{
|
||||
table: performersTagsJoinTable,
|
||||
|
||||
15
pkg/utils/url.go
Normal file
15
pkg/utils/url.go
Normal 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
47
pkg/utils/url_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user