Skip to content

Commit

Permalink
Multiple image URLs (stashapp#4000)
Browse files Browse the repository at this point in the history
* Backend changes - ported from scene impl
* Front end changes
* Refactor URL mutation code
  • Loading branch information
WithoutPants authored and halkeye committed Sep 1, 2024
1 parent d62ed80 commit 1d880ef
Show file tree
Hide file tree
Showing 29 changed files with 457 additions and 173 deletions.
2 changes: 1 addition & 1 deletion graphql/documents/data/image-slim.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ fragment SlimImageData on Image {
id
title
date
url
urls
rating100
organized
o_counter
Expand Down
2 changes: 1 addition & 1 deletion graphql/documents/data/image.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ fragment ImageData on Image {
title
rating100
date
url
urls
organized
o_counter
created_at
Expand Down
9 changes: 6 additions & 3 deletions graphql/schema/types/image.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ type Image {
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100
rating100: Int
url: String
url: String @deprecated(reason: "Use urls")
urls: [String!]!
date: String
o_counter: Int
organized: Boolean!
Expand Down Expand Up @@ -48,7 +49,8 @@ input ImageUpdateInput {
# rating expressed as 1-100
rating100: Int
organized: Boolean
url: String
url: String @deprecated(reason: "Use urls")
urls: [String!]
date: String

studio_id: ID
Expand All @@ -68,7 +70,8 @@ input BulkImageUpdateInput {
# rating expressed as 1-100
rating100: Int
organized: Boolean
url: String
url: String @deprecated(reason: "Use urls")
urls: BulkUpdateStrings
date: String

studio_id: ID
Expand Down
2 changes: 1 addition & 1 deletion graphql/schema/types/scene.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ type Scene {
details: String
director: String
url: String @deprecated(reason: "Use urls")
urls: [String!]
urls: [String!]!
date: String
# rating expressed as 1-5
rating: Int @deprecated(reason: "Use 1-100 range with rating100")
Expand Down
40 changes: 40 additions & 0 deletions internal/api/changeset_translator.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,46 @@ func (t changesetTranslator) updateIdsBulk(value *BulkUpdateIds, field string) (
}, nil
}

func (t changesetTranslator) optionalURLs(value []string, legacyValue *string) *models.UpdateStrings {
const (
legacyField = "url"
field = "urls"
)

// prefer urls over url
if t.hasField(field) {
return t.updateStrings(value, field)
} else if t.hasField(legacyField) {
var valueSlice []string
if legacyValue != nil {
valueSlice = []string{*legacyValue}
}
return t.updateStrings(valueSlice, legacyField)
}

return nil
}

func (t changesetTranslator) optionalURLsBulk(value *BulkUpdateStrings, legacyValue *string) *models.UpdateStrings {
const (
legacyField = "url"
field = "urls"
)

// prefer urls over url
if t.hasField("urls") {
return t.updateStringsBulk(value, field)
} else if t.hasField(legacyField) {
var valueSlice []string
if legacyValue != nil {
valueSlice = []string{*legacyValue}
}
return t.updateStrings(valueSlice, legacyField)
}

return nil
}

func (t changesetTranslator) updateStrings(value []string, field string) *models.UpdateStrings {
if !t.hasField(field) {
return nil
Expand Down
29 changes: 29 additions & 0 deletions internal/api/resolver_model_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,32 @@ func (r *imageResolver) Performers(ctx context.Context, obj *models.Image) (ret
ret, errs = loaders.From(ctx).PerformerByID.LoadAll(obj.PerformerIDs.List())
return ret, firstError(errs)
}

func (r *imageResolver) URL(ctx context.Context, obj *models.Image) (*string, error) {
if !obj.URLs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadURLs(ctx, r.repository.Image)
}); err != nil {
return nil, err
}
}

urls := obj.URLs.List()
if len(urls) == 0 {
return nil, nil
}

return &urls[0], nil
}

func (r *imageResolver) Urls(ctx context.Context, obj *models.Image) ([]string, error) {
if !obj.URLs.Loaded() {
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
return obj.LoadURLs(ctx, r.repository.Image)
}); err != nil {
return nil, err
}
}

return obj.URLs.List(), nil
}
6 changes: 4 additions & 2 deletions internal/api/resolver_mutation_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp

updatedImage.Title = translator.optionalString(input.Title, "title")
updatedImage.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
updatedImage.URL = translator.optionalString(input.URL, "url")
updatedImage.Organized = translator.optionalBool(input.Organized, "organized")

updatedImage.Date, err = translator.optionalDate(input.Date, "date")
Expand All @@ -120,6 +119,8 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp
return nil, fmt.Errorf("converting studio id: %w", err)
}

updatedImage.URLs = translator.optionalURLs(input.Urls, input.URL)

updatedImage.PrimaryFileID, err = translator.fileIDPtrFromString(input.PrimaryFileID)
if err != nil {
return nil, fmt.Errorf("converting primary file id: %w", err)
Expand Down Expand Up @@ -203,7 +204,6 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU

updatedImage.Title = translator.optionalString(input.Title, "title")
updatedImage.Rating = translator.optionalRatingConversion(input.Rating, input.Rating100)
updatedImage.URL = translator.optionalString(input.URL, "url")
updatedImage.Organized = translator.optionalBool(input.Organized, "organized")

updatedImage.Date, err = translator.optionalDate(input.Date, "date")
Expand All @@ -215,6 +215,8 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU
return nil, fmt.Errorf("converting studio id: %w", err)
}

updatedImage.URLs = translator.optionalURLsBulk(input.Urls, input.URL)

updatedImage.GalleryIDs, err = translator.updateIdsBulk(input.GalleryIds, "gallery_ids")
if err != nil {
return nil, fmt.Errorf("converting gallery ids: %w", err)
Expand Down
22 changes: 2 additions & 20 deletions internal/api/resolver_mutation_scene.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,16 +186,7 @@ func scenePartialFromInput(input models.SceneUpdateInput, translator changesetTr
return nil, fmt.Errorf("converting studio id: %w", err)
}

// prefer urls over url
if translator.hasField("urls") {
updatedScene.URLs = translator.updateStrings(input.Urls, "urls")
} else if translator.hasField("url") {
var urls []string
if input.URL != nil {
urls = []string{*input.URL}
}
updatedScene.URLs = translator.updateStrings(urls, "url")
}
updatedScene.URLs = translator.optionalURLs(input.Urls, input.URL)

updatedScene.PrimaryFileID, err = translator.fileIDPtrFromString(input.PrimaryFileID)
if err != nil {
Expand Down Expand Up @@ -342,16 +333,7 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input BulkSceneU
return nil, fmt.Errorf("converting studio id: %w", err)
}

// prefer urls over url
if translator.hasField("urls") {
updatedScene.URLs = translator.updateStringsBulk(input.Urls, "urls")
} else if translator.hasField("url") {
var urls []string
if input.URL != nil {
urls = []string{*input.URL}
}
updatedScene.URLs = translator.updateStrings(urls, "url")
}
updatedScene.URLs = translator.optionalURLsBulk(input.Urls, input.URL)

updatedScene.PerformerIDs, err = translator.updateIdsBulk(input.PerformerIds, "performer_ids")
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions internal/manager/task_export.go
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,11 @@ func exportImage(ctx context.Context, wg *sync.WaitGroup, jobChan <-chan *models
continue
}

if err := s.LoadURLs(ctx, repo.Image); err != nil {
logger.Errorf("[images] <%s> error getting image urls: %s", imageHash, err.Error())
continue
}

newImageJSON := image.ToBasicJSON(s)

// export files
Expand Down
15 changes: 1 addition & 14 deletions pkg/image/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
func ToBasicJSON(image *models.Image) *jsonschema.Image {
newImageJSON := jsonschema.Image{
Title: image.Title,
URL: image.URL,
URLs: image.URLs.List(),
CreatedAt: json.JSONTime{Time: image.CreatedAt},
UpdatedAt: json.JSONTime{Time: image.UpdatedAt},
}
Expand All @@ -37,19 +37,6 @@ func ToBasicJSON(image *models.Image) *jsonschema.Image {
return &newImageJSON
}

// func getImageFileJSON(image *models.Image) *jsonschema.ImageFile {
// ret := &jsonschema.ImageFile{}

// f := image.PrimaryFile()

// ret.ModTime = json.JSONTime{Time: f.ModTime}
// ret.Size = f.Size
// ret.Width = f.Width
// ret.Height = f.Height

// return ret
// }

// GetStudioName returns the name of the provided image's studio. It returns an
// empty string if there is no studio assigned to the image.
func GetStudioName(ctx context.Context, reader models.StudioGetter, image *models.Image) (string, error) {
Expand Down
4 changes: 2 additions & 2 deletions pkg/image/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func createFullImage(id int) models.Image {
OCounter: ocounter,
Rating: &rating,
Date: &dateObj,
URL: url,
URLs: models.NewRelatedStrings([]string{url}),
Organized: organized,
CreatedAt: createTime,
UpdatedAt: updateTime,
Expand All @@ -66,7 +66,7 @@ func createFullJSONImage() *jsonschema.Image {
OCounter: ocounter,
Rating: rating,
Date: date,
URL: url,
URLs: []string{url},
Organized: organized,
Files: []string{path},
CreatedAt: json.JSONTime{
Expand Down
9 changes: 5 additions & 4 deletions pkg/image/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,6 @@ func (i *Importer) PreImport(ctx context.Context) error {

func (i *Importer) imageJSONToImage(imageJSON jsonschema.Image) models.Image {
newImage := models.Image{
// Checksum: imageJSON.Checksum,
// Path: i.Path,
PerformerIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}),
GalleryIDs: models.NewRelatedIDs([]int{}),
Expand All @@ -81,9 +79,12 @@ func (i *Importer) imageJSONToImage(imageJSON jsonschema.Image) models.Image {
if imageJSON.Rating != 0 {
newImage.Rating = &imageJSON.Rating
}
if imageJSON.URL != "" {
newImage.URL = imageJSON.URL
if len(imageJSON.URLs) > 0 {
newImage.URLs = models.NewRelatedStrings(imageJSON.URLs)
} else if imageJSON.URL != "" {
newImage.URLs = models.NewRelatedStrings([]string{imageJSON.URL})
}

if imageJSON.Date != "" {
d, err := models.ParseDate(imageJSON.Date)
if err == nil {
Expand Down
12 changes: 8 additions & 4 deletions pkg/models/jsonschema/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ import (
)

type Image struct {
Title string `json:"title,omitempty"`
Studio string `json:"studio,omitempty"`
Rating int `json:"rating,omitempty"`
URL string `json:"url,omitempty"`
Title string `json:"title,omitempty"`
Studio string `json:"studio,omitempty"`
Rating int `json:"rating,omitempty"`

// deprecated - for import only
URL string `json:"url,omitempty"`

URLs []string `json:"urls,omitempty"`
Date string `json:"date,omitempty"`
Organized bool `json:"organized,omitempty"`
OCounter int `json:"o_counter,omitempty"`
Expand Down
4 changes: 3 additions & 1 deletion pkg/models/jsonschema/scene.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ type Scene struct {
Title string `json:"title,omitempty"`
Code string `json:"code,omitempty"`
Studio string `json:"studio,omitempty"`

// deprecated - for import only
URL string `json:"url,omitempty"`
URL string `json:"url,omitempty"`

URLs []string `json:"urls,omitempty"`
Date string `json:"date,omitempty"`
Rating int `json:"rating,omitempty"`
Expand Down
23 changes: 23 additions & 0 deletions pkg/models/mocks/ImageReaderWriter.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 13 additions & 7 deletions pkg/models/model_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ type Image struct {

Title string `json:"title"`
// Rating expressed in 1-100 scale
Rating *int `json:"rating"`
Organized bool `json:"organized"`
OCounter int `json:"o_counter"`
StudioID *int `json:"studio_id"`
URL string `json:"url"`
Date *Date `json:"date"`
Rating *int `json:"rating"`
Organized bool `json:"organized"`
OCounter int `json:"o_counter"`
StudioID *int `json:"studio_id"`
URLs RelatedStrings `json:"urls"`
Date *Date `json:"date"`

// transient - not persisted
Files RelatedFiles
Expand Down Expand Up @@ -48,7 +48,7 @@ type ImagePartial struct {
Title OptionalString
// Rating expressed in 1-100 scale
Rating OptionalInt
URL OptionalString
URLs *UpdateStrings
Date OptionalDate
Organized OptionalBool
OCounter OptionalInt
Expand All @@ -69,6 +69,12 @@ func NewImagePartial() ImagePartial {
}
}

func (i *Image) LoadURLs(ctx context.Context, l URLLoader) error {
return i.URLs.load(func() ([]string, error) {
return l.GetURLs(ctx, i.ID)
})
}

func (i *Image) LoadFiles(ctx context.Context, l FileLoader) error {
return i.Files.load(func() ([]File, error) {
return l.GetFiles(ctx, i.ID)
Expand Down
1 change: 1 addition & 0 deletions pkg/models/repository_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ type ImageReader interface {
ImageQueryer
ImageCounter

URLLoader
FileIDLoader
GalleryIDLoader
PerformerIDLoader
Expand Down
Loading

0 comments on commit 1d880ef

Please sign in to comment.