Gallery URLs (#4114)

* Initial backend changes
* Fix unit tests
* UI changes
* Fix missing URL filters
This commit is contained in:
WithoutPants 2023-09-25 12:27:20 +10:00 committed by GitHub
parent a369e395e7
commit 9577600804
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 361 additions and 117 deletions

View file

@ -2,7 +2,7 @@ fragment SlimGalleryData on Gallery {
id id
title title
date date
url urls
details details
rating100 rating100
organized organized

View file

@ -4,7 +4,7 @@ fragment GalleryData on Gallery {
updated_at updated_at
title title
date date
url urls
details details
rating100 rating100
organized organized

View file

@ -185,7 +185,7 @@ fragment ScrapedSceneData on ScrapedScene {
fragment ScrapedGalleryData on ScrapedGallery { fragment ScrapedGalleryData on ScrapedGallery {
title title
details details
url urls
date date
studio { studio {

View file

@ -4,7 +4,8 @@ type Gallery {
checksum: String! @deprecated(reason: "Use files.fingerprints") checksum: String! @deprecated(reason: "Use files.fingerprints")
path: String @deprecated(reason: "Use files.path") path: String @deprecated(reason: "Use files.path")
title: String title: String
url: String url: String @deprecated(reason: "Use urls")
urls: [String!]!
date: String date: String
details: String details: String
# rating expressed as 1-5 # rating expressed as 1-5
@ -33,7 +34,8 @@ type Gallery {
input GalleryCreateInput { input GalleryCreateInput {
title: String! title: String!
url: String url: String @deprecated(reason: "Use urls")
urls: [String!]
date: String date: String
details: String details: String
# rating expressed as 1-5 # rating expressed as 1-5
@ -51,7 +53,8 @@ input GalleryUpdateInput {
clientMutationId: String clientMutationId: String
id: ID! id: ID!
title: String title: String
url: String url: String @deprecated(reason: "Use urls")
urls: [String!]
date: String date: String
details: String details: String
# rating expressed as 1-5 # rating expressed as 1-5
@ -70,7 +73,8 @@ input GalleryUpdateInput {
input BulkGalleryUpdateInput { input BulkGalleryUpdateInput {
clientMutationId: String clientMutationId: String
ids: [ID!] ids: [ID!]
url: String url: String @deprecated(reason: "Use urls")
urls: BulkUpdateStrings
date: String date: String
details: String details: String
# rating expressed as 1-5 # rating expressed as 1-5

View file

@ -100,7 +100,8 @@ input ScrapedSceneInput {
type ScrapedGallery { type ScrapedGallery {
title: String title: String
details: String details: String
url: String url: String @deprecated(reason: "use urls")
urls: [String!]
date: String date: String
studio: ScrapedStudio studio: ScrapedStudio
@ -111,7 +112,8 @@ type ScrapedGallery {
input ScrapedGalleryInput { input ScrapedGalleryInput {
title: String title: String
details: String details: String
url: String url: String @deprecated(reason: "use urls")
urls: [String!]
date: String date: String
# no studio, tags or performers # no studio, tags or performers

View file

@ -226,3 +226,32 @@ func (r *galleryResolver) Chapters(ctx context.Context, obj *models.Gallery) (re
return ret, nil return ret, nil
} }
func (r *galleryResolver) URL(ctx context.Context, obj *models.Gallery) (*string, error) {
if !obj.URLs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadURLs(ctx, r.repository.Gallery)
}); err != nil {
return nil, err
}
}
urls := obj.URLs.List()
if len(urls) == 0 {
return nil, nil
}
return &urls[0], nil
}
func (r *galleryResolver) Urls(ctx context.Context, obj *models.Gallery) ([]string, error) {
if !obj.URLs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadURLs(ctx, r.repository.Gallery)
}); err != nil {
return nil, err
}
}
return obj.URLs.List(), nil
}

View file

@ -43,7 +43,6 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
newGallery := models.NewGallery() newGallery := models.NewGallery()
newGallery.Title = input.Title newGallery.Title = input.Title
newGallery.URL = translator.string(input.URL)
newGallery.Details = translator.string(input.Details) newGallery.Details = translator.string(input.Details)
newGallery.Rating = translator.ratingConversion(input.Rating, input.Rating100) newGallery.Rating = translator.ratingConversion(input.Rating, input.Rating100)
@ -71,6 +70,12 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
return nil, fmt.Errorf("converting scene ids: %w", err) return nil, fmt.Errorf("converting scene ids: %w", err)
} }
if input.Urls != nil {
newGallery.URLs = models.NewRelatedStrings(input.Urls)
} else if input.URL != nil {
newGallery.URLs = models.NewRelatedStrings([]string{*input.URL})
}
// Start the transaction and save the gallery // Start the transaction and save the gallery
if err := r.withTxn(ctx, func(ctx context.Context) error { if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Gallery qb := r.repository.Gallery
@ -178,7 +183,6 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle
} }
updatedGallery.Details = translator.optionalString(input.Details, "details") updatedGallery.Details = translator.optionalString(input.Details, "details")
updatedGallery.URL = translator.optionalString(input.URL, "url")
updatedGallery.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100) updatedGallery.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
updatedGallery.Organized = translator.optionalBool(input.Organized, "organized") updatedGallery.Organized = translator.optionalBool(input.Organized, "organized")
@ -191,6 +195,8 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle
return nil, fmt.Errorf("converting studio id: %w", err) return nil, fmt.Errorf("converting studio id: %w", err)
} }
updatedGallery.URLs = translator.optionalURLs(input.Urls, input.URL)
updatedGallery.PrimaryFileID, err = translator.fileIDPtrFromString(input.PrimaryFileID) updatedGallery.PrimaryFileID, err = translator.fileIDPtrFromString(input.PrimaryFileID)
if err != nil { if err != nil {
return nil, fmt.Errorf("converting primary file id: %w", err) return nil, fmt.Errorf("converting primary file id: %w", err)
@ -252,9 +258,9 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGall
updatedGallery := models.NewGalleryPartial() updatedGallery := models.NewGalleryPartial()
updatedGallery.Details = translator.optionalString(input.Details, "details") updatedGallery.Details = translator.optionalString(input.Details, "details")
updatedGallery.URL = translator.optionalString(input.URL, "url")
updatedGallery.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100) updatedGallery.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
updatedGallery.Organized = translator.optionalBool(input.Organized, "organized") updatedGallery.Organized = translator.optionalBool(input.Organized, "organized")
updatedGallery.URLs = translator.optionalURLsBulk(input.Urls, input.URL)
updatedGallery.Date, err = translator.optionalDate(input.Date, "date") updatedGallery.Date, err = translator.optionalDate(input.Date, "date")
if err != nil { if err != nil {

View file

@ -14,7 +14,7 @@ import (
func ToBasicJSON(gallery *models.Gallery) (*jsonschema.Gallery, error) { func ToBasicJSON(gallery *models.Gallery) (*jsonschema.Gallery, error) {
newGalleryJSON := jsonschema.Gallery{ newGalleryJSON := jsonschema.Gallery{
Title: gallery.Title, Title: gallery.Title,
URL: gallery.URL, URLs: gallery.URLs.List(),
Details: gallery.Details, Details: gallery.Details,
CreatedAt: json.JSONTime{Time: gallery.CreatedAt}, CreatedAt: json.JSONTime{Time: gallery.CreatedAt},
UpdatedAt: json.JSONTime{Time: gallery.UpdatedAt}, UpdatedAt: json.JSONTime{Time: gallery.UpdatedAt},

View file

@ -59,7 +59,7 @@ func createFullGallery(id int) models.Gallery {
Details: details, Details: details,
Rating: &rating, Rating: &rating,
Organized: organized, Organized: organized,
URL: url, URLs: models.NewRelatedStrings([]string{url}),
CreatedAt: createTime, CreatedAt: createTime,
UpdatedAt: updateTime, UpdatedAt: updateTime,
} }
@ -85,7 +85,7 @@ func createFullJSONGallery() *jsonschema.Gallery {
Details: details, Details: details,
Rating: rating, Rating: rating,
Organized: organized, Organized: organized,
URL: url, URLs: []string{url},
ZipFiles: []string{path}, ZipFiles: []string{path},
CreatedAt: json.JSONTime{ CreatedAt: json.JSONTime{
Time: createTime, Time: createTime,

View file

@ -65,8 +65,10 @@ func (i *Importer) galleryJSONToGallery(galleryJSON jsonschema.Gallery) models.G
if galleryJSON.Details != "" { if galleryJSON.Details != "" {
newGallery.Details = galleryJSON.Details newGallery.Details = galleryJSON.Details
} }
if galleryJSON.URL != "" { if len(galleryJSON.URLs) > 0 {
newGallery.URL = galleryJSON.URL newGallery.URLs = models.NewRelatedStrings(galleryJSON.URLs)
} else if galleryJSON.URL != "" {
newGallery.URLs = models.NewRelatedStrings([]string{galleryJSON.URL})
} }
if galleryJSON.Date != "" { if galleryJSON.Date != "" {
d, err := models.ParseDate(galleryJSON.Date) d, err := models.ParseDate(galleryJSON.Date)

View file

@ -66,7 +66,7 @@ func TestImporterPreImport(t *testing.T) {
Details: details, Details: details,
Rating: &rating, Rating: &rating,
Organized: organized, Organized: organized,
URL: url, URLs: models.NewRelatedStrings([]string{url}),
Files: models.NewRelatedFiles([]models.File{}), Files: models.NewRelatedFiles([]models.File{}),
TagIDs: models.NewRelatedIDs([]int{}), TagIDs: models.NewRelatedIDs([]int{}),
PerformerIDs: models.NewRelatedIDs([]int{}), PerformerIDs: models.NewRelatedIDs([]int{}),

View file

@ -59,7 +59,7 @@ type GalleryUpdateInput struct {
ClientMutationID *string `json:"clientMutationId"` ClientMutationID *string `json:"clientMutationId"`
ID string `json:"id"` ID string `json:"id"`
Title *string `json:"title"` Title *string `json:"title"`
URL *string `json:"url"` Urls []string `json:"urls"`
Date *string `json:"date"` Date *string `json:"date"`
Details *string `json:"details"` Details *string `json:"details"`
Rating *int `json:"rating"` Rating *int `json:"rating"`
@ -70,6 +70,9 @@ type GalleryUpdateInput struct {
TagIds []string `json:"tag_ids"` TagIds []string `json:"tag_ids"`
PerformerIds []string `json:"performer_ids"` PerformerIds []string `json:"performer_ids"`
PrimaryFileID *string `json:"primary_file_id"` PrimaryFileID *string `json:"primary_file_id"`
// deprecated
URL *string `json:"url"`
} }
type GalleryDestroyInput struct { type GalleryDestroyInput struct {

View file

@ -21,7 +21,7 @@ type Gallery struct {
ZipFiles []string `json:"zip_files,omitempty"` ZipFiles []string `json:"zip_files,omitempty"`
FolderPath string `json:"folder_path,omitempty"` FolderPath string `json:"folder_path,omitempty"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
URL string `json:"url,omitempty"` URLs []string `json:"urls,omitempty"`
Date string `json:"date,omitempty"` Date string `json:"date,omitempty"`
Details string `json:"details,omitempty"` Details string `json:"details,omitempty"`
Rating int `json:"rating,omitempty"` Rating int `json:"rating,omitempty"`
@ -32,6 +32,9 @@ type Gallery struct {
Tags []string `json:"tags,omitempty"` Tags []string `json:"tags,omitempty"`
CreatedAt json.JSONTime `json:"created_at,omitempty"` CreatedAt json.JSONTime `json:"created_at,omitempty"`
UpdatedAt json.JSONTime `json:"updated_at,omitempty"` UpdatedAt json.JSONTime `json:"updated_at,omitempty"`
// deprecated - for import only
URL string `json:"url,omitempty"`
} }
func (s Gallery) Filename(basename string, hash string) string { func (s Gallery) Filename(basename string, hash string) string {

View file

@ -533,6 +533,29 @@ func (_m *GalleryReaderWriter) GetTagIDs(ctx context.Context, relatedID int) ([]
return r0, r1 return r0, r1
} }
// GetURLs provides a mock function with given fields: ctx, relatedID
func (_m *GalleryReaderWriter) 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
}
// Query provides a mock function with given fields: ctx, galleryFilter, findFilter // Query provides a mock function with given fields: ctx, galleryFilter, findFilter
func (_m *GalleryReaderWriter) Query(ctx context.Context, galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) ([]*models.Gallery, int, error) { func (_m *GalleryReaderWriter) Query(ctx context.Context, galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) ([]*models.Gallery, int, error) {
ret := _m.Called(ctx, galleryFilter, findFilter) ret := _m.Called(ctx, galleryFilter, findFilter)

View file

@ -11,7 +11,6 @@ type Gallery struct {
ID int `json:"id"` ID int `json:"id"`
Title string `json:"title"` Title string `json:"title"`
URL string `json:"url"`
Date *Date `json:"date"` Date *Date `json:"date"`
Details string `json:"details"` Details string `json:"details"`
// Rating expressed in 1-100 scale // Rating expressed in 1-100 scale
@ -31,6 +30,7 @@ type Gallery struct {
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
URLs RelatedStrings `json:"urls"`
SceneIDs RelatedIDs `json:"scene_ids"` SceneIDs RelatedIDs `json:"scene_ids"`
TagIDs RelatedIDs `json:"tag_ids"` TagIDs RelatedIDs `json:"tag_ids"`
PerformerIDs RelatedIDs `json:"performer_ids"` PerformerIDs RelatedIDs `json:"performer_ids"`
@ -51,7 +51,7 @@ type GalleryPartial struct {
// Checksum OptionalString // Checksum OptionalString
// Zip OptionalBool // Zip OptionalBool
Title OptionalString Title OptionalString
URL OptionalString URLs *UpdateStrings
Date OptionalDate Date OptionalDate
Details OptionalString Details OptionalString
// Rating expressed in 1-100 scale // Rating expressed in 1-100 scale
@ -81,6 +81,12 @@ func (g *Gallery) IsUserCreated() bool {
return g.PrimaryFileID == nil && g.FolderID == nil return g.PrimaryFileID == nil && g.FolderID == nil
} }
func (g *Gallery) LoadURLs(ctx context.Context, l URLLoader) error {
return g.URLs.load(func() ([]string, error) {
return l.GetURLs(ctx, g.ID)
})
}
func (g *Gallery) LoadFiles(ctx context.Context, l FileLoader) error { func (g *Gallery) LoadFiles(ctx context.Context, l FileLoader) error {
return g.Files.load(func() ([]File, error) { return g.Files.load(func() ([]File, error) {
return l.GetFiles(ctx, g.ID) return l.GetFiles(ctx, g.ID)

View file

@ -63,6 +63,7 @@ type GalleryReader interface {
GalleryQueryer GalleryQueryer
GalleryCounter GalleryCounter
URLLoader
FileIDLoader FileIDLoader
ImageIDLoader ImageIDLoader
SceneIDLoader SceneIDLoader

View file

@ -5,11 +5,14 @@ import "github.com/stashapp/stash/pkg/models"
type ScrapedGallery struct { type ScrapedGallery struct {
Title *string `json:"title"` Title *string `json:"title"`
Details *string `json:"details"` Details *string `json:"details"`
URL *string `json:"url"` URLs []string `json:"urls"`
Date *string `json:"date"` Date *string `json:"date"`
Studio *models.ScrapedStudio `json:"studio"` Studio *models.ScrapedStudio `json:"studio"`
Tags []*models.ScrapedTag `json:"tags"` Tags []*models.ScrapedTag `json:"tags"`
Performers []*models.ScrapedPerformer `json:"performers"` Performers []*models.ScrapedPerformer `json:"performers"`
// deprecated
URL *string `json:"url"`
} }
func (ScrapedGallery) IsScrapedContent() {} func (ScrapedGallery) IsScrapedContent() {}
@ -17,6 +20,9 @@ func (ScrapedGallery) IsScrapedContent() {}
type ScrapedGalleryInput struct { type ScrapedGalleryInput struct {
Title *string `json:"title"` Title *string `json:"title"`
Details *string `json:"details"` Details *string `json:"details"`
URL *string `json:"url"` URLs []string `json:"urls"`
Date *string `json:"date"` Date *string `json:"date"`
// deprecated
URL *string `json:"url"`
} }

View file

@ -66,8 +66,8 @@ func queryURLParametersFromGallery(gallery *models.Gallery) queryURLParameters {
ret["title"] = gallery.Title ret["title"] = gallery.Title
} }
if gallery.URL != "" { if len(gallery.URLs.List()) > 0 {
ret["url"] = gallery.URL ret["url"] = gallery.URLs.List()[0]
} }
return ret return ret

View file

@ -354,11 +354,18 @@ func galleryToUpdateInput(gallery *models.Gallery) models.GalleryUpdateInput {
// fallback to file basename if title is empty // fallback to file basename if title is empty
title := gallery.GetTitle() title := gallery.GetTitle()
var url *string
urls := gallery.URLs.List()
if len(urls) > 0 {
url = &urls[0]
}
return models.GalleryUpdateInput{ return models.GalleryUpdateInput{
ID: strconv.Itoa(gallery.ID), ID: strconv.Itoa(gallery.ID),
Title: &title, Title: &title,
Details: &gallery.Details, Details: &gallery.Details,
URL: &gallery.URL, URL: url,
Urls: urls,
Date: dateToStringPtr(gallery.Date), Date: dateToStringPtr(gallery.Date),
} }
} }

View file

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

View file

@ -26,12 +26,13 @@ const (
galleriesImagesTable = "galleries_images" galleriesImagesTable = "galleries_images"
galleriesScenesTable = "scenes_galleries" galleriesScenesTable = "scenes_galleries"
galleryIDColumn = "gallery_id" galleryIDColumn = "gallery_id"
galleriesURLsTable = "gallery_urls"
galleriesURLColumn = "url"
) )
type galleryRow struct { type galleryRow struct {
ID int `db:"id" goqu:"skipinsert"` ID int `db:"id" goqu:"skipinsert"`
Title zero.String `db:"title"` Title zero.String `db:"title"`
URL zero.String `db:"url"`
Date NullDate `db:"date"` Date NullDate `db:"date"`
Details zero.String `db:"details"` Details zero.String `db:"details"`
// expressed as 1-100 // expressed as 1-100
@ -46,7 +47,6 @@ type galleryRow struct {
func (r *galleryRow) fromGallery(o models.Gallery) { func (r *galleryRow) fromGallery(o models.Gallery) {
r.ID = o.ID r.ID = o.ID
r.Title = zero.StringFrom(o.Title) r.Title = zero.StringFrom(o.Title)
r.URL = zero.StringFrom(o.URL)
r.Date = NullDateFromDatePtr(o.Date) r.Date = NullDateFromDatePtr(o.Date)
r.Details = zero.StringFrom(o.Details) r.Details = zero.StringFrom(o.Details)
r.Rating = intFromPtr(o.Rating) r.Rating = intFromPtr(o.Rating)
@ -70,7 +70,6 @@ func (r *galleryQueryRow) resolve() *models.Gallery {
ret := &models.Gallery{ ret := &models.Gallery{
ID: r.ID, ID: r.ID,
Title: r.Title.String, Title: r.Title.String,
URL: r.URL.String,
Date: r.Date.DatePtr(), Date: r.Date.DatePtr(),
Details: r.Details.String, Details: r.Details.String,
Rating: nullIntPtr(r.Rating), Rating: nullIntPtr(r.Rating),
@ -97,7 +96,6 @@ type galleryRowRecord struct {
func (r *galleryRowRecord) fromPartial(o models.GalleryPartial) { func (r *galleryRowRecord) fromPartial(o models.GalleryPartial) {
r.setNullString("title", o.Title) r.setNullString("title", o.Title)
r.setNullString("url", o.URL)
r.setNullDate("date", o.Date) r.setNullDate("date", o.Date)
r.setNullString("details", o.Details) r.setNullString("details", o.Details)
r.setNullInt("rating", o.Rating) r.setNullInt("rating", o.Rating)
@ -178,6 +176,12 @@ func (qb *GalleryStore) Create(ctx context.Context, newObject *models.Gallery, f
} }
} }
if newObject.URLs.Loaded() {
const startPos = 0
if err := galleriesURLsTableMgr.insertJoins(ctx, id, startPos, newObject.URLs.List()); err != nil {
return err
}
}
if newObject.PerformerIDs.Loaded() { if newObject.PerformerIDs.Loaded() {
if err := galleriesPerformersTableMgr.insertJoins(ctx, id, newObject.PerformerIDs.List()); err != nil { if err := galleriesPerformersTableMgr.insertJoins(ctx, id, newObject.PerformerIDs.List()); err != nil {
return err return err
@ -212,6 +216,11 @@ func (qb *GalleryStore) Update(ctx context.Context, updatedObject *models.Galler
return err return err
} }
if updatedObject.URLs.Loaded() {
if err := galleriesURLsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.URLs.List()); err != nil {
return err
}
}
if updatedObject.PerformerIDs.Loaded() { if updatedObject.PerformerIDs.Loaded() {
if err := galleriesPerformersTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.PerformerIDs.List()); err != nil { if err := galleriesPerformersTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.PerformerIDs.List()); err != nil {
return err return err
@ -257,6 +266,11 @@ func (qb *GalleryStore) UpdatePartial(ctx context.Context, id int, partial model
} }
} }
if partial.URLs != nil {
if err := galleriesURLsTableMgr.modifyJoins(ctx, id, partial.URLs.Values, partial.URLs.Mode); err != nil {
return nil, err
}
}
if partial.PerformerIDs != nil { if partial.PerformerIDs != nil {
if err := galleriesPerformersTableMgr.modifyJoins(ctx, id, partial.PerformerIDs.IDs, partial.PerformerIDs.Mode); err != nil { if err := galleriesPerformersTableMgr.modifyJoins(ctx, id, partial.PerformerIDs.IDs, partial.PerformerIDs.Mode); err != nil {
return nil, err return nil, err
@ -669,7 +683,7 @@ func (qb *GalleryStore) makeFilter(ctx context.Context, galleryFilter *models.Ga
query.handleCriterion(ctx, intCriterionHandler(galleryFilter.Rating100, "galleries.rating", nil)) query.handleCriterion(ctx, intCriterionHandler(galleryFilter.Rating100, "galleries.rating", nil))
// legacy rating handler // legacy rating handler
query.handleCriterion(ctx, rating5CriterionHandler(galleryFilter.Rating, "galleries.rating", nil)) query.handleCriterion(ctx, rating5CriterionHandler(galleryFilter.Rating, "galleries.rating", nil))
query.handleCriterion(ctx, stringCriterionHandler(galleryFilter.URL, "galleries.url")) query.handleCriterion(ctx, galleryURLsCriterionHandler(galleryFilter.URL))
query.handleCriterion(ctx, boolCriterionHandler(galleryFilter.Organized, "galleries.organized", nil)) query.handleCriterion(ctx, boolCriterionHandler(galleryFilter.Organized, "galleries.organized", nil))
query.handleCriterion(ctx, galleryIsMissingCriterionHandler(qb, galleryFilter.IsMissing)) query.handleCriterion(ctx, galleryIsMissingCriterionHandler(qb, galleryFilter.IsMissing))
query.handleCriterion(ctx, galleryTagsCriterionHandler(qb, galleryFilter.Tags)) query.handleCriterion(ctx, galleryTagsCriterionHandler(qb, galleryFilter.Tags))
@ -793,6 +807,18 @@ func (qb *GalleryStore) QueryCount(ctx context.Context, galleryFilter *models.Ga
return query.executeCount(ctx) return query.executeCount(ctx)
} }
func galleryURLsCriterionHandler(url *models.StringCriterionInput) criterionHandlerFunc {
h := stringListCriterionHandlerBuilder{
joinTable: galleriesURLsTable,
stringColumn: galleriesURLColumn,
addJoinTable: func(f *filterBuilder) {
galleriesURLsTableMgr.join(f, "", "galleries.id")
},
}
return h.handler(url)
}
func (qb *GalleryStore) galleryPathCriterionHandler(c *models.StringCriterionInput) criterionHandlerFunc { func (qb *GalleryStore) galleryPathCriterionHandler(c *models.StringCriterionInput) criterionHandlerFunc {
return func(ctx context.Context, f *filterBuilder) { return func(ctx context.Context, f *filterBuilder) {
if c != nil { if c != nil {
@ -874,6 +900,9 @@ func galleryIsMissingCriterionHandler(qb *GalleryStore, isMissing *string) crite
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":
galleriesURLsTableMgr.join(f, "", "galleries.id")
f.addWhere("gallery_urls.url IS NULL")
case "scenes": case "scenes":
f.addLeftJoin("scenes_galleries", "scenes_join", "scenes_join.gallery_id = galleries.id") f.addLeftJoin("scenes_galleries", "scenes_join", "scenes_join.gallery_id = galleries.id")
f.addWhere("scenes_join.gallery_id IS NULL") f.addWhere("scenes_join.gallery_id IS NULL")
@ -1107,6 +1136,10 @@ func (qb *GalleryStore) setGallerySort(query *queryBuilder, findFilter *models.F
query.sortAndPagination += ", COALESCE(galleries.title, galleries.id) COLLATE NATURAL_CI ASC" query.sortAndPagination += ", COALESCE(galleries.title, galleries.id) COLLATE NATURAL_CI ASC"
} }
func (qb *GalleryStore) GetURLs(ctx context.Context, galleryID int) ([]string, error) {
return galleriesURLsTableMgr.get(ctx, galleryID)
}
func (qb *GalleryStore) filesRepository() *filesRepository { func (qb *GalleryStore) filesRepository() *filesRepository {
return &filesRepository{ return &filesRepository{
repository: repository{ repository: repository{

View file

@ -17,6 +17,11 @@ import (
var invalidID = -1 var invalidID = -1
func loadGalleryRelationships(ctx context.Context, expected models.Gallery, actual *models.Gallery) error { func loadGalleryRelationships(ctx context.Context, expected models.Gallery, actual *models.Gallery) error {
if expected.URLs.Loaded() {
if err := actual.LoadURLs(ctx, db.Gallery); err != nil {
return err
}
}
if expected.SceneIDs.Loaded() { if expected.SceneIDs.Loaded() {
if err := actual.LoadSceneIDs(ctx, db.Gallery); err != nil { if err := actual.LoadSceneIDs(ctx, db.Gallery); err != nil {
return err return err
@ -72,7 +77,7 @@ func Test_galleryQueryBuilder_Create(t *testing.T) {
"full", "full",
models.Gallery{ models.Gallery{
Title: title, Title: title,
URL: url, URLs: models.NewRelatedStrings([]string{url}),
Date: &date, Date: &date,
Details: details, Details: details,
Rating: &rating, Rating: &rating,
@ -90,7 +95,7 @@ func Test_galleryQueryBuilder_Create(t *testing.T) {
"with file", "with file",
models.Gallery{ models.Gallery{
Title: title, Title: title,
URL: url, URLs: models.NewRelatedStrings([]string{url}),
Date: &date, Date: &date,
Details: details, Details: details,
Rating: &rating, Rating: &rating,
@ -222,7 +227,7 @@ func Test_galleryQueryBuilder_Update(t *testing.T) {
&models.Gallery{ &models.Gallery{
ID: galleryIDs[galleryIdxWithScene], ID: galleryIDs[galleryIdxWithScene],
Title: title, Title: title,
URL: url, URLs: models.NewRelatedStrings([]string{url}),
Date: &date, Date: &date,
Details: details, Details: details,
Rating: &rating, Rating: &rating,
@ -243,6 +248,7 @@ func Test_galleryQueryBuilder_Update(t *testing.T) {
"clear nullables", "clear nullables",
&models.Gallery{ &models.Gallery{
ID: galleryIDs[galleryIdxWithImage], ID: galleryIDs[galleryIdxWithImage],
URLs: models.NewRelatedStrings([]string{}),
SceneIDs: models.NewRelatedIDs([]int{}), SceneIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}), TagIDs: models.NewRelatedIDs([]int{}),
PerformerIDs: models.NewRelatedIDs([]int{}), PerformerIDs: models.NewRelatedIDs([]int{}),
@ -384,7 +390,7 @@ func clearGalleryPartial() models.GalleryPartial {
return models.GalleryPartial{ return models.GalleryPartial{
Title: models.OptionalString{Set: true, Null: true}, Title: models.OptionalString{Set: true, Null: true},
Details: models.OptionalString{Set: true, Null: true}, Details: models.OptionalString{Set: true, Null: true},
URL: models.OptionalString{Set: true, Null: true}, URLs: &models.UpdateStrings{Mode: models.RelationshipUpdateModeSet},
Date: models.OptionalDate{Set: true, Null: true}, Date: models.OptionalDate{Set: true, Null: true},
Rating: models.OptionalInt{Set: true, Null: true}, Rating: models.OptionalInt{Set: true, Null: true},
StudioID: models.OptionalInt{Set: true, Null: true}, StudioID: models.OptionalInt{Set: true, Null: true},
@ -418,7 +424,10 @@ func Test_galleryQueryBuilder_UpdatePartial(t *testing.T) {
models.GalleryPartial{ models.GalleryPartial{
Title: models.NewOptionalString(title), Title: models.NewOptionalString(title),
Details: models.NewOptionalString(details), Details: models.NewOptionalString(details),
URL: models.NewOptionalString(url), URLs: &models.UpdateStrings{
Values: []string{url},
Mode: models.RelationshipUpdateModeSet,
},
Date: models.NewOptionalDate(date), Date: models.NewOptionalDate(date),
Rating: models.NewOptionalInt(rating), Rating: models.NewOptionalInt(rating),
Organized: models.NewOptionalBool(true), Organized: models.NewOptionalBool(true),
@ -443,7 +452,7 @@ func Test_galleryQueryBuilder_UpdatePartial(t *testing.T) {
ID: galleryIDs[galleryIdxWithImage], ID: galleryIDs[galleryIdxWithImage],
Title: title, Title: title,
Details: details, Details: details,
URL: url, URLs: models.NewRelatedStrings([]string{url}),
Date: &date, Date: &date,
Rating: &rating, Rating: &rating,
Organized: true, Organized: true,
@ -1653,7 +1662,13 @@ func TestGalleryQueryURL(t *testing.T) {
verifyFn := func(g *models.Gallery) { verifyFn := func(g *models.Gallery) {
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)
} }
verifyGalleryQuery(t, filter, verifyFn) verifyGalleryQuery(t, filter, verifyFn)
@ -1683,6 +1698,12 @@ func verifyGalleryQuery(t *testing.T, filter models.GalleryFilterType, verifyFn
galleries := queryGallery(ctx, t, sqb, &filter, nil) galleries := queryGallery(ctx, t, sqb, &filter, nil)
for _, g := range galleries {
if err := g.LoadURLs(ctx, sqb); err != nil {
t.Errorf("Error loading gallery URLs: %v", err)
}
}
// assume it should find at least one // assume it should find at least one
assert.Greater(t, len(galleries), 0) assert.Greater(t, len(galleries), 0)

View file

@ -0,0 +1,76 @@
PRAGMA foreign_keys=OFF;
CREATE TABLE `gallery_urls` (
`gallery_id` integer NOT NULL,
`position` integer NOT NULL,
`url` varchar(255) NOT NULL,
foreign key(`gallery_id`) references `galleries`(`id`) on delete CASCADE,
PRIMARY KEY(`gallery_id`, `position`, `url`)
);
CREATE INDEX `gallery_urls_url` on `gallery_urls` (`url`);
-- drop url
CREATE TABLE `galleries_new` (
`id` integer not null primary key autoincrement,
`folder_id` integer,
`title` varchar(255),
`date` date,
`details` text,
`studio_id` integer,
`rating` tinyint,
`organized` boolean not null default '0',
`created_at` datetime not null,
`updated_at` datetime not null,
foreign key(`studio_id`) references `studios`(`id`) on delete SET NULL,
foreign key(`folder_id`) references `folders`(`id`) on delete SET NULL
);
INSERT INTO `galleries_new`
(
`id`,
`folder_id`,
`title`,
`date`,
`details`,
`studio_id`,
`rating`,
`organized`,
`created_at`,
`updated_at`
)
SELECT
`id`,
`folder_id`,
`title`,
`date`,
`details`,
`studio_id`,
`rating`,
`organized`,
`created_at`,
`updated_at`
FROM `galleries`;
INSERT INTO `gallery_urls`
(
`gallery_id`,
`position`,
`url`
)
SELECT
`id`,
'0',
`url`
FROM `galleries`
WHERE `galleries`.`url` IS NOT NULL AND `galleries`.`url` != '';
DROP INDEX `index_galleries_on_studio_id`;
DROP INDEX `index_galleries_on_folder_id_unique`;
DROP TABLE `galleries`;
ALTER TABLE `galleries_new` rename to `galleries`;
CREATE INDEX `index_galleries_on_studio_id` on `galleries` (`studio_id`);
CREATE UNIQUE INDEX `index_galleries_on_folder_id_unique` on `galleries` (`folder_id`);
PRAGMA foreign_keys=ON;

View file

@ -1291,6 +1291,9 @@ func sceneIsMissingCriterionHandler(qb *SceneStore, isMissing *string) criterion
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":
scenesURLsTableMgr.join(f, "", "scenes.id")
f.addWhere("scene_urls.url IS NULL")
case "galleries": case "galleries":
qb.galleriesRepository().join(f, "galleries_join", "scenes.id") qb.galleriesRepository().join(f, "galleries_join", "scenes.id")
f.addWhere("galleries_join.scene_id IS NULL") f.addWhere("galleries_join.scene_id IS NULL")

View file

@ -1213,7 +1213,16 @@ func getGalleryNullStringValue(index int, field string) sql.NullString {
} }
func getGalleryNullStringPtr(index int, field string) *string { func getGalleryNullStringPtr(index int, field string) *string {
return getStringPtr(getPrefixedStringValue("gallery", index, field)) return getStringPtrFromNullString(getPrefixedNullStringValue("gallery", index, field))
}
func getGalleryEmptyString(index int, field string) string {
v := getGalleryNullStringPtr(index, field)
if v == nil {
return ""
}
return *v
} }
func getGalleryBasename(index int) string { func getGalleryBasename(index int) string {
@ -1246,7 +1255,9 @@ func makeGallery(i int, includeScenes bool) *models.Gallery {
ret := &models.Gallery{ ret := &models.Gallery{
Title: getGalleryStringValue(i, titleField), Title: getGalleryStringValue(i, titleField),
URL: getGalleryNullStringValue(i, urlField).String, URLs: models.NewRelatedStrings([]string{
getGalleryEmptyString(i, urlField),
}),
Rating: getIntPtr(getRating(i)), Rating: getIntPtr(getRating(i)),
Date: getObjectDate(i), Date: getObjectDate(i),
StudioID: studioID, StudioID: studioID,

View file

@ -19,6 +19,7 @@ var (
galleriesTagsJoinTable = goqu.T(galleriesTagsTable) galleriesTagsJoinTable = goqu.T(galleriesTagsTable)
performersGalleriesJoinTable = goqu.T(performersGalleriesTable) performersGalleriesJoinTable = goqu.T(performersGalleriesTable)
galleriesScenesJoinTable = goqu.T(galleriesScenesTable) galleriesScenesJoinTable = goqu.T(galleriesScenesTable)
galleriesURLsJoinTable = goqu.T(galleriesURLsTable)
scenesFilesJoinTable = goqu.T(scenesFilesTable) scenesFilesJoinTable = goqu.T(scenesFilesTable)
scenesTagsJoinTable = goqu.T(scenesTagsTable) scenesTagsJoinTable = goqu.T(scenesTagsTable)
@ -122,6 +123,14 @@ var (
table: goqu.T(galleriesChaptersTable), table: goqu.T(galleriesChaptersTable),
idColumn: goqu.T(galleriesChaptersTable).Col(idColumn), idColumn: goqu.T(galleriesChaptersTable).Col(idColumn),
} }
galleriesURLsTableMgr = &orderedValueTable[string]{
table: table{
table: galleriesURLsJoinTable,
idColumn: galleriesURLsJoinTable.Col(galleryIDColumn),
},
valueColumn: galleriesURLsJoinTable.Col(galleriesURLColumn),
}
) )
var ( var (

View file

@ -25,7 +25,7 @@ import {
} from "src/components/Shared/Select"; } from "src/components/Shared/Select";
import { Icon } from "src/components/Shared/Icon"; import { Icon } from "src/components/Shared/Icon";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { URLField } from "src/components/Shared/URLField"; import { URLListInput } from "src/components/Shared/URLField";
import { useToast } from "src/hooks/Toast"; import { useToast } from "src/hooks/Toast";
import { useFormik } from "formik"; import { useFormik } from "formik";
import FormUtils from "src/utils/form"; import FormUtils from "src/utils/form";
@ -42,6 +42,7 @@ import {
Performer, Performer,
PerformerSelect, PerformerSelect,
} from "src/components/Performers/PerformerSelect"; } from "src/components/Performers/PerformerSelect";
import { yupDateString, yupUniqueStringList } from "src/utils/yup";
interface IProps { interface IProps {
gallery: Partial<GQL.GalleryDataFragment>; gallery: Partial<GQL.GalleryDataFragment>;
@ -84,20 +85,8 @@ export const GalleryEditPanel: React.FC<IProps> = ({
const schema = yup.object({ const schema = yup.object({
title: titleRequired ? yup.string().required() : yup.string().ensure(), title: titleRequired ? yup.string().required() : yup.string().ensure(),
url: yup.string().ensure(), urls: yupUniqueStringList("urls"),
date: yup date: yupDateString(intl),
.string()
.ensure()
.test({
name: "date",
test: (value) => {
if (!value) return true;
if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false;
if (Number.isNaN(Date.parse(value))) return false;
return true;
},
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
}),
rating100: yup.number().nullable().defined(), rating100: yup.number().nullable().defined(),
studio_id: yup.string().required().nullable(), studio_id: yup.string().required().nullable(),
performer_ids: yup.array(yup.string().required()).defined(), performer_ids: yup.array(yup.string().required()).defined(),
@ -108,7 +97,7 @@ export const GalleryEditPanel: React.FC<IProps> = ({
const initialValues = { const initialValues = {
title: gallery?.title ?? "", title: gallery?.title ?? "",
url: gallery?.url ?? "", urls: gallery?.urls ?? [],
date: gallery?.date ?? "", date: gallery?.date ?? "",
rating100: gallery?.rating100 ?? null, rating100: gallery?.rating100 ?? null,
studio_id: gallery?.studio?.id ?? null, studio_id: gallery?.studio?.id ?? null,
@ -313,8 +302,8 @@ export const GalleryEditPanel: React.FC<IProps> = ({
formik.setFieldValue("date", galleryData.date); formik.setFieldValue("date", galleryData.date);
} }
if (galleryData.url) { if (galleryData.urls) {
formik.setFieldValue("url", galleryData.url); formik.setFieldValue("url", galleryData.urls);
} }
if (galleryData.studio?.stored_id) { if (galleryData.studio?.stored_id) {
@ -351,13 +340,13 @@ export const GalleryEditPanel: React.FC<IProps> = ({
} }
} }
async function onScrapeGalleryURL() { async function onScrapeGalleryURL(url: string) {
if (!formik.values.url) { if (!url) {
return; return;
} }
setIsLoading(true); setIsLoading(true);
try { try {
const result = await queryScrapeGalleryURL(formik.values.url); const result = await queryScrapeGalleryURL(url);
if (!result || !result.data || !result.data.scrapeGalleryURL) { if (!result || !result.data || !result.data.scrapeGalleryURL) {
return; return;
} }
@ -392,6 +381,14 @@ export const GalleryEditPanel: React.FC<IProps> = ({
if (isLoading) return <LoadingIndicator />; if (isLoading) return <LoadingIndicator />;
const urlsErrors = Array.isArray(formik.errors.urls)
? formik.errors.urls[0]
: formik.errors.urls;
const urlsErrorMsg = urlsErrors
? intl.formatMessage({ id: "validation.urls_must_be_unique" })
: undefined;
const urlsErrorIdx = urlsErrors?.split(" ").map((e) => parseInt(e));
return ( return (
<div id="gallery-edit-details"> <div id="gallery-edit-details">
<Prompt <Prompt
@ -428,18 +425,20 @@ export const GalleryEditPanel: React.FC<IProps> = ({
<div className="form-container row px-3"> <div className="form-container row px-3">
<div className="col-12 col-lg-6 col-xl-12"> <div className="col-12 col-lg-6 col-xl-12">
{renderTextField("title", intl.formatMessage({ id: "title" }))} {renderTextField("title", intl.formatMessage({ id: "title" }))}
<Form.Group controlId="url" as={Row}> <Form.Group controlId="urls" as={Row}>
<Col xs={3} className="pr-0 url-label"> <Col xs={3} className="pr-0 url-label">
<Form.Label className="col-form-label"> <Form.Label className="col-form-label">
<FormattedMessage id="url" /> <FormattedMessage id="urls" />
</Form.Label> </Form.Label>
</Col> </Col>
<Col xs={9}> <Col xs={9}>
<URLField <URLListInput
{...formik.getFieldProps("url")} value={formik.values.urls ?? []}
onScrapeClick={onScrapeGalleryURL} setValue={(value) => formik.setFieldValue("urls", value)}
errors={urlsErrorMsg}
errorIdx={urlsErrorIdx}
onScrapeClick={(url) => onScrapeGalleryURL(url)}
urlScrapable={urlScrapable} urlScrapable={urlScrapable}
isInvalid={!!formik.getFieldMeta("url").error}
/> />
</Col> </Col>
</Form.Group> </Form.Group>

View file

@ -7,7 +7,7 @@ import * as GQL from "src/core/generated-graphql";
import { mutateGallerySetPrimaryFile } from "src/core/StashService"; import { mutateGallerySetPrimaryFile } from "src/core/StashService";
import { useToast } from "src/hooks/Toast"; import { useToast } from "src/hooks/Toast";
import TextUtils from "src/utils/text"; import TextUtils from "src/utils/text";
import { TextField, URLField } from "src/utils/field"; import { TextField, URLField, URLsField } from "src/utils/field";
interface IFileInfoPanelProps { interface IFileInfoPanelProps {
folder?: Pick<GQL.Folder, "id" | "path">; folder?: Pick<GQL.Folder, "id" | "path">;
@ -147,12 +147,7 @@ export const GalleryFileInfoPanel: React.FC<IGalleryFileInfoPanelProps> = (
return ( return (
<> <>
<dl className="container gallery-file-info details-list"> <dl className="container gallery-file-info details-list">
<URLField <URLsField id="urls" urls={props.gallery.urls} truncate />
id="media_info.downloaded_from"
url={props.gallery.url}
value={props.gallery.url}
truncate
/>
</dl> </dl>
{filesPanel} {filesPanel}

View file

@ -4,6 +4,7 @@ import * as GQL from "src/core/generated-graphql";
import { import {
ScrapeDialog, ScrapeDialog,
ScrapedInputGroupRow, ScrapedInputGroupRow,
ScrapedStringListRow,
ScrapedTextAreaRow, ScrapedTextAreaRow,
} from "src/components/Shared/ScrapeDialog/ScrapeDialog"; } from "src/components/Shared/ScrapeDialog/ScrapeDialog";
import clone from "lodash-es/clone"; import clone from "lodash-es/clone";
@ -23,6 +24,7 @@ import {
useCreateScrapedStudio, useCreateScrapedStudio,
useCreateScrapedTag, useCreateScrapedTag,
} from "src/components/Shared/ScrapeDialog/createObjects"; } from "src/components/Shared/ScrapeDialog/createObjects";
import { uniq } from "lodash-es";
interface IGalleryScrapeDialogProps { interface IGalleryScrapeDialogProps {
gallery: Partial<GQL.GalleryUpdateInput>; gallery: Partial<GQL.GalleryUpdateInput>;
@ -36,29 +38,32 @@ interface IHasStoredID {
stored_id?: string | null; stored_id?: string | null;
} }
export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = ( export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = ({
props: IGalleryScrapeDialogProps gallery,
) => { galleryPerformers,
scraped,
onClose,
}) => {
const intl = useIntl(); const intl = useIntl();
const [title, setTitle] = useState<ScrapeResult<string>>( const [title, setTitle] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(props.gallery.title, props.scraped.title) new ScrapeResult<string>(gallery.title, scraped.title)
); );
const [url, setURL] = useState<ScrapeResult<string>>( const [urls, setURLs] = useState<ScrapeResult<string[]>>(
new ScrapeResult<string>(props.gallery.url, props.scraped.url) new ScrapeResult<string[]>(
); gallery.urls,
const [date, setDate] = useState<ScrapeResult<string>>( scraped.urls
new ScrapeResult<string>(props.gallery.date, props.scraped.date) ? uniq((gallery.urls ?? []).concat(scraped.urls ?? []))
); : undefined
const [studio, setStudio] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(
props.gallery.studio_id,
props.scraped.studio?.stored_id
) )
); );
const [date, setDate] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(gallery.date, scraped.date)
);
const [studio, setStudio] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(gallery.studio_id, scraped.studio?.stored_id)
);
const [newStudio, setNewStudio] = useState<GQL.ScrapedStudio | undefined>( const [newStudio, setNewStudio] = useState<GQL.ScrapedStudio | undefined>(
props.scraped.studio && !props.scraped.studio.stored_id scraped.studio && !scraped.studio.stored_id ? scraped.studio : undefined
? props.scraped.studio
: undefined
); );
function mapStoredIdObjects( function mapStoredIdObjects(
@ -104,30 +109,30 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
>( >(
new ObjectListScrapeResult<GQL.ScrapedPerformer>( new ObjectListScrapeResult<GQL.ScrapedPerformer>(
sortStoredIdObjects( sortStoredIdObjects(
props.galleryPerformers.map((p) => ({ galleryPerformers.map((p) => ({
stored_id: p.id, stored_id: p.id,
name: p.name, name: p.name,
})) }))
), ),
sortStoredIdObjects(props.scraped.performers ?? undefined) sortStoredIdObjects(scraped.performers ?? undefined)
) )
); );
const [newPerformers, setNewPerformers] = useState<GQL.ScrapedPerformer[]>( const [newPerformers, setNewPerformers] = useState<GQL.ScrapedPerformer[]>(
props.scraped.performers?.filter((t) => !t.stored_id) ?? [] scraped.performers?.filter((t) => !t.stored_id) ?? []
); );
const [tags, setTags] = useState<ScrapeResult<string[]>>( const [tags, setTags] = useState<ScrapeResult<string[]>>(
new ScrapeResult<string[]>( new ScrapeResult<string[]>(
sortIdList(props.gallery.tag_ids), sortIdList(gallery.tag_ids),
mapStoredIdObjects(props.scraped.tags ?? undefined) mapStoredIdObjects(scraped.tags ?? undefined)
) )
); );
const [newTags, setNewTags] = useState<GQL.ScrapedTag[]>( const [newTags, setNewTags] = useState<GQL.ScrapedTag[]>(
props.scraped.tags?.filter((t) => !t.stored_id) ?? [] scraped.tags?.filter((t) => !t.stored_id) ?? []
); );
const [details, setDetails] = useState<ScrapeResult<string>>( const [details, setDetails] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(props.gallery.details, props.scraped.details) new ScrapeResult<string>(gallery.details, scraped.details)
); );
const createNewStudio = useCreateScrapedStudio({ const createNewStudio = useCreateScrapedStudio({
@ -152,14 +157,14 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
// don't show the dialog if nothing was scraped // don't show the dialog if nothing was scraped
if ( if (
[title, url, date, studio, performers, tags, details].every( [title, urls, date, studio, performers, tags, details].every(
(r) => !r.scraped (r) => !r.scraped
) && ) &&
!newStudio && !newStudio &&
newPerformers.length === 0 && newPerformers.length === 0 &&
newTags.length === 0 newTags.length === 0
) { ) {
props.onClose(); onClose();
return <></>; return <></>;
} }
@ -168,7 +173,7 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
return { return {
title: title.getNewValue(), title: title.getNewValue(),
url: url.getNewValue(), urls: urls.getNewValue(),
date: date.getNewValue(), date: date.getNewValue(),
studio: newStudioValue studio: newStudioValue
? { ? {
@ -195,10 +200,10 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
result={title} result={title}
onChange={(value) => setTitle(value)} onChange={(value) => setTitle(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 <ScrapedInputGroupRow
title={intl.formatMessage({ id: "date" })} title={intl.formatMessage({ id: "date" })}
@ -244,7 +249,7 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
)} )}
renderScrapeRows={renderScrapeRows} renderScrapeRows={renderScrapeRows}
onClose={(apply) => { onClose={(apply) => {
props.onClose(apply ? makeNewScrapedItem() : undefined); onClose(apply ? makeNewScrapedItem() : undefined);
}} }}
/> />
); );