Studio aliases (#1660)

* Add migration to create studio aliases table
* Refactor studioQueryBuilder.Query to use filterBuilder
* Expand GraphQL API with aliases support for studio
* Add aliases support for studios to the UI
* List aliases in details panel
* Allow editing aliases in edit panel
* Add 'aliases' filter when searching
* Find studios by alias in filter / select
* Add auto-tagging based on studio aliases
* Support studio aliases for filename parsing
* Support importing and exporting of studio aliases
* Search for studio alias as well during scraping
This commit is contained in:
gitgiggety
2021-09-09 10:13:42 +02:00
committed by GitHub
parent c91ffe1e58
commit 04e5ac9c2f
34 changed files with 909 additions and 164 deletions

View File

@@ -42,6 +42,13 @@ func ToJSON(reader models.StudioReader, studio *models.Studio) (*jsonschema.Stud
newStudioJSON.Rating = int(studio.Rating.Int64)
}
aliases, err := reader.GetAliases(studio.ID)
if err != nil {
return nil, fmt.Errorf("error getting studio aliases: %s", err.Error())
}
newStudioJSON.Aliases = aliases
image, err := reader.GetImage(studio.ID)
if err != nil {
return nil, fmt.Errorf("error getting studio image: %s", err.Error())

View File

@@ -18,6 +18,7 @@ const (
errImageID = 3
missingParentStudioID = 4
errStudioID = 5
errAliasID = 6
parentStudioID = 10
missingStudioID = 11
@@ -77,7 +78,7 @@ func createEmptyStudio(id int) models.Studio {
}
}
func createFullJSONStudio(parentStudio, image string) *jsonschema.Studio {
func createFullJSONStudio(parentStudio, image string, aliases []string) *jsonschema.Studio {
return &jsonschema.Studio{
Name: studioName,
URL: url,
@@ -91,6 +92,7 @@ func createFullJSONStudio(parentStudio, image string) *jsonschema.Studio {
ParentStudio: parentStudio,
Image: image,
Rating: rating,
Aliases: aliases,
}
}
@@ -117,7 +119,7 @@ func initTestTable() {
scenarios = []testScenario{
testScenario{
createFullStudio(studioID, parentStudioID),
createFullJSONStudio(parentStudioName, image),
createFullJSONStudio(parentStudioName, image, []string{"alias"}),
false,
},
testScenario{
@@ -132,7 +134,7 @@ func initTestTable() {
},
testScenario{
createFullStudio(missingParentStudioID, missingStudioID),
createFullJSONStudio("", image),
createFullJSONStudio("", image, nil),
false,
},
testScenario{
@@ -140,6 +142,11 @@ func initTestTable() {
nil,
true,
},
testScenario{
createFullStudio(errAliasID, parentStudioID),
nil,
true,
},
}
}
@@ -155,6 +162,7 @@ func TestToJSON(t *testing.T) {
mockStudioReader.On("GetImage", errImageID).Return(nil, imageErr).Once()
mockStudioReader.On("GetImage", missingParentStudioID).Return(imageBytes, nil).Maybe()
mockStudioReader.On("GetImage", errStudioID).Return(imageBytes, nil).Maybe()
mockStudioReader.On("GetImage", errAliasID).Return(imageBytes, nil).Maybe()
parentStudioErr := errors.New("error getting parent studio")
@@ -162,6 +170,14 @@ func TestToJSON(t *testing.T) {
mockStudioReader.On("Find", missingStudioID).Return(nil, nil)
mockStudioReader.On("Find", errParentStudioID).Return(nil, parentStudioErr)
aliasErr := errors.New("error getting aliases")
mockStudioReader.On("GetAliases", studioID).Return([]string{"alias"}, nil).Once()
mockStudioReader.On("GetAliases", noImageID).Return(nil, nil).Once()
mockStudioReader.On("GetAliases", errImageID).Return(nil, nil).Once()
mockStudioReader.On("GetAliases", missingParentStudioID).Return(nil, nil).Once()
mockStudioReader.On("GetAliases", errAliasID).Return(nil, aliasErr).Once()
for i, s := range scenarios {
studio := s.input
json, err := ToJSON(mockStudioReader, &studio)

View File

@@ -101,6 +101,10 @@ func (i *Importer) PostImport(id int) error {
}
}
if err := i.ReaderWriter.UpdateAliases(id, i.Input.Aliases); err != nil {
return fmt.Errorf("error setting tag aliases: %s", err.Error())
}
return nil
}

View File

@@ -53,7 +53,7 @@ func TestImporterPreImport(t *testing.T) {
assert.Nil(t, err)
i.Input = *createFullJSONStudio(studioName, image)
i.Input = *createFullJSONStudio(studioName, image, []string{"alias"})
i.Input.ParentStudio = ""
err = i.PreImport()
@@ -151,13 +151,22 @@ func TestImporterPostImport(t *testing.T) {
i := Importer{
ReaderWriter: readerWriter,
imageData: imageBytes,
Input: jsonschema.Studio{
Aliases: []string{"alias"},
},
imageData: imageBytes,
}
updateStudioImageErr := errors.New("UpdateImage error")
updateTagAliasErr := errors.New("UpdateAlias error")
readerWriter.On("UpdateImage", studioID, imageBytes).Return(nil).Once()
readerWriter.On("UpdateImage", errImageID, imageBytes).Return(updateStudioImageErr).Once()
readerWriter.On("UpdateImage", errAliasID, imageBytes).Return(nil).Once()
readerWriter.On("UpdateAliases", studioID, i.Input.Aliases).Return(nil).Once()
readerWriter.On("UpdateAliases", errImageID, i.Input.Aliases).Return(nil).Maybe()
readerWriter.On("UpdateAliases", errAliasID, i.Input.Aliases).Return(updateTagAliasErr).Once()
err := i.PostImport(studioID)
assert.Nil(t, err)
@@ -165,6 +174,9 @@ func TestImporterPostImport(t *testing.T) {
err = i.PostImport(errImageID)
assert.NotNil(t, err)
err = i.PostImport(errAliasID)
assert.NotNil(t, err)
readerWriter.AssertExpectations(t)
}

51
pkg/studio/query.go Normal file
View File

@@ -0,0 +1,51 @@
package studio
import "github.com/stashapp/stash/pkg/models"
func ByName(qb models.StudioReader, name string) (*models.Studio, error) {
f := &models.StudioFilterType{
Name: &models.StringCriterionInput{
Value: name,
Modifier: models.CriterionModifierEquals,
},
}
pp := 1
ret, count, err := qb.Query(f, &models.FindFilterType{
PerPage: &pp,
})
if err != nil {
return nil, err
}
if count > 0 {
return ret[0], nil
}
return nil, nil
}
func ByAlias(qb models.StudioReader, alias string) (*models.Studio, error) {
f := &models.StudioFilterType{
Aliases: &models.StringCriterionInput{
Value: alias,
Modifier: models.CriterionModifierEquals,
},
}
pp := 1
ret, count, err := qb.Query(f, &models.FindFilterType{
PerPage: &pp,
})
if err != nil {
return nil, err
}
if count > 0 {
return ret[0], nil
}
return nil, nil
}

65
pkg/studio/update.go Normal file
View File

@@ -0,0 +1,65 @@
package studio
import (
"fmt"
"github.com/stashapp/stash/pkg/models"
)
type NameExistsError struct {
Name string
}
func (e *NameExistsError) Error() string {
return fmt.Sprintf("studio with name '%s' already exists", e.Name)
}
type NameUsedByAliasError struct {
Name string
OtherStudio string
}
func (e *NameUsedByAliasError) Error() string {
return fmt.Sprintf("name '%s' is used as alias for '%s'", e.Name, e.OtherStudio)
}
// EnsureStudioNameUnique returns an error if the studio name provided
// is used as a name or alias of another existing tag.
func EnsureStudioNameUnique(id int, name string, qb models.StudioReader) error {
// ensure name is unique
sameNameStudio, err := ByName(qb, name)
if err != nil {
return err
}
if sameNameStudio != nil && id != sameNameStudio.ID {
return &NameExistsError{
Name: name,
}
}
// query by alias
sameNameStudio, err = ByAlias(qb, name)
if err != nil {
return err
}
if sameNameStudio != nil && id != sameNameStudio.ID {
return &NameUsedByAliasError{
Name: name,
OtherStudio: sameNameStudio.Name.String,
}
}
return nil
}
func EnsureAliasesUnique(id int, aliases []string, qb models.StudioReader) error {
for _, a := range aliases {
if err := EnsureStudioNameUnique(id, a, qb); err != nil {
return err
}
}
return nil
}