mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Tag aliases (#1412)
* Add Tag Update/UpdateFull * Tag alias implementation * Refactor tag page * Add aliases in UI * Include tag aliases in q filter * Include aliases in tag select * Add aliases to auto-tagger * Use aliases in scraper * Add tag aliases for filename parser
This commit is contained in:
@@ -479,3 +479,28 @@ func (m *countCriterionHandlerBuilder) handler(criterion *models.IntCriterionInp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handler for StringCriterion for string list fields
|
||||
type stringListCriterionHandlerBuilder struct {
|
||||
// table joining primary and foreign objects
|
||||
joinTable string
|
||||
// string field on the join table
|
||||
stringColumn string
|
||||
|
||||
addJoinTable func(f *filterBuilder)
|
||||
}
|
||||
|
||||
func (m *stringListCriterionHandlerBuilder) handler(criterion *models.StringCriterionInput) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
if criterion != nil && len(criterion.Value) > 0 {
|
||||
var args []interface{}
|
||||
for _, tagID := range criterion.Value {
|
||||
args = append(args, tagID)
|
||||
}
|
||||
|
||||
m.addJoinTable(f)
|
||||
|
||||
stringCriterionHandler(criterion, m.joinTable+"."+m.stringColumn)(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,6 +343,45 @@ func (r *imageRepository) replace(id int, image []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
type stringRepository struct {
|
||||
repository
|
||||
stringColumn string
|
||||
}
|
||||
|
||||
func (r *stringRepository) get(id int) ([]string, error) {
|
||||
query := fmt.Sprintf("SELECT %s from %s WHERE %s = ?", r.stringColumn, r.tableName, r.idColumn)
|
||||
var ret []string
|
||||
err := r.queryFunc(query, []interface{}{id}, func(rows *sqlx.Rows) error {
|
||||
var out string
|
||||
if err := rows.Scan(&out); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret = append(ret, out)
|
||||
return nil
|
||||
})
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func (r *stringRepository) insert(id int, s string) (sql.Result, error) {
|
||||
stmt := fmt.Sprintf("INSERT INTO %s (%s, %s) VALUES (?, ?)", r.tableName, r.idColumn, r.stringColumn)
|
||||
return r.tx.Exec(stmt, id, s)
|
||||
}
|
||||
|
||||
func (r *stringRepository) replace(id int, newStrings []string) error {
|
||||
if err := r.destroy([]int{id}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, s := range newStrings {
|
||||
if _, err := r.insert(id, s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type stashIDRepository struct {
|
||||
repository
|
||||
}
|
||||
|
||||
@@ -852,6 +852,12 @@ func createTags(tqb models.TagReaderWriter, n int, o int) error {
|
||||
return fmt.Errorf("Error creating tag %v+: %s", tag, err.Error())
|
||||
}
|
||||
|
||||
// add alias
|
||||
alias := getTagStringValue(i, "Alias")
|
||||
if err := tqb.UpdateAliases(created.ID, []string{alias}); err != nil {
|
||||
return fmt.Errorf("error setting tag alias: %s", err.Error())
|
||||
}
|
||||
|
||||
tagIDs = append(tagIDs, created.ID)
|
||||
tagNames = append(tagNames, created.Name)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
|
||||
const tagTable = "tags"
|
||||
const tagIDColumn = "tag_id"
|
||||
const tagAliasesTable = "tag_aliases"
|
||||
const tagAliasColumn = "alias"
|
||||
|
||||
type tagQueryBuilder struct {
|
||||
repository
|
||||
@@ -35,7 +37,16 @@ func (qb *tagQueryBuilder) Create(newObject models.Tag) (*models.Tag, error) {
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (qb *tagQueryBuilder) Update(updatedObject models.Tag) (*models.Tag, error) {
|
||||
func (qb *tagQueryBuilder) Update(updatedObject models.TagPartial) (*models.Tag, error) {
|
||||
const partial = true
|
||||
if err := qb.update(updatedObject.ID, updatedObject, partial); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return qb.Find(updatedObject.ID)
|
||||
}
|
||||
|
||||
func (qb *tagQueryBuilder) UpdateFull(updatedObject models.Tag) (*models.Tag, error) {
|
||||
const partial = false
|
||||
if err := qb.update(updatedObject.ID, updatedObject, partial); err != nil {
|
||||
return nil, err
|
||||
@@ -197,13 +208,19 @@ func (qb *tagQueryBuilder) QueryForAutoTag(words []string) ([]*models.Tag, error
|
||||
// TODO - Query needs to be changed to support queries of this type, and
|
||||
// this method should be removed
|
||||
query := selectAll(tagTable)
|
||||
query += " LEFT JOIN tag_aliases ON tag_aliases.tag_id = tags.id"
|
||||
|
||||
var whereClauses []string
|
||||
var args []interface{}
|
||||
|
||||
for _, w := range words {
|
||||
whereClauses = append(whereClauses, "name like ?")
|
||||
args = append(args, "%"+w+"%")
|
||||
ww := "%" + w + "%"
|
||||
whereClauses = append(whereClauses, "tags.name like ?")
|
||||
args = append(args, ww)
|
||||
|
||||
// include aliases
|
||||
whereClauses = append(whereClauses, "tag_aliases.alias like ?")
|
||||
args = append(args, ww)
|
||||
}
|
||||
|
||||
where := strings.Join(whereClauses, " OR ")
|
||||
@@ -262,6 +279,9 @@ func (qb *tagQueryBuilder) makeFilter(tagFilter *models.TagFilterType) *filterBu
|
||||
// }
|
||||
// }
|
||||
|
||||
query.handleCriterionFunc(stringCriterionHandler(tagFilter.Name, tagTable+".name"))
|
||||
query.handleCriterionFunc(tagAliasCriterionHandler(qb, tagFilter.Aliases))
|
||||
|
||||
query.handleCriterionFunc(tagIsMissingCriterionHandler(qb, tagFilter.IsMissing))
|
||||
query.handleCriterionFunc(tagSceneCountCriterionHandler(qb, tagFilter.SceneCount))
|
||||
query.handleCriterionFunc(tagImageCountCriterionHandler(qb, tagFilter.ImageCount))
|
||||
@@ -297,7 +317,8 @@ func (qb *tagQueryBuilder) Query(tagFilter *models.TagFilterType, findFilter *mo
|
||||
// Disabling querying/sorting on marker count for now.
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
searchColumns := []string{"tags.name"}
|
||||
query.join(tagAliasesTable, "", "tag_aliases.tag_id = tags.id")
|
||||
searchColumns := []string{"tags.name", "tag_aliases.alias"}
|
||||
clause, thisArgs := getSearchBinding(searchColumns, *q, false)
|
||||
query.addWhere(clause)
|
||||
query.addArg(thisArgs...)
|
||||
@@ -328,6 +349,18 @@ func (qb *tagQueryBuilder) Query(tagFilter *models.TagFilterType, findFilter *mo
|
||||
return tags, countResult, nil
|
||||
}
|
||||
|
||||
func tagAliasCriterionHandler(qb *tagQueryBuilder, alias *models.StringCriterionInput) criterionHandlerFunc {
|
||||
h := stringListCriterionHandlerBuilder{
|
||||
joinTable: tagAliasesTable,
|
||||
stringColumn: tagAliasColumn,
|
||||
addJoinTable: func(f *filterBuilder) {
|
||||
qb.aliasRepository().join(f, "", "tags.id")
|
||||
},
|
||||
}
|
||||
|
||||
return h.handler(alias)
|
||||
}
|
||||
|
||||
func tagIsMissingCriterionHandler(qb *tagQueryBuilder, isMissing *string) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
if isMissing != nil && *isMissing != "" {
|
||||
@@ -484,3 +517,22 @@ func (qb *tagQueryBuilder) UpdateImage(tagID int, image []byte) error {
|
||||
func (qb *tagQueryBuilder) DestroyImage(tagID int) error {
|
||||
return qb.imageRepository().destroy([]int{tagID})
|
||||
}
|
||||
|
||||
func (qb *tagQueryBuilder) aliasRepository() *stringRepository {
|
||||
return &stringRepository{
|
||||
repository: repository{
|
||||
tx: qb.tx,
|
||||
tableName: tagAliasesTable,
|
||||
idColumn: tagIDColumn,
|
||||
},
|
||||
stringColumn: tagAliasColumn,
|
||||
}
|
||||
}
|
||||
|
||||
func (qb *tagQueryBuilder) GetAliases(tagID int) ([]string, error) {
|
||||
return qb.aliasRepository().get(tagID)
|
||||
}
|
||||
|
||||
func (qb *tagQueryBuilder) UpdateAliases(tagID int, aliases []string) error {
|
||||
return qb.aliasRepository().replace(tagID, aliases)
|
||||
}
|
||||
|
||||
@@ -83,8 +83,20 @@ func TestTagQueryForAutoTag(t *testing.T) {
|
||||
}
|
||||
|
||||
assert.Len(t, tags, 2)
|
||||
assert.Equal(t, strings.ToLower(tagNames[tagIdxWithScene]), strings.ToLower(tags[0].Name))
|
||||
assert.Equal(t, strings.ToLower(tagNames[tagIdxWithScene]), strings.ToLower(tags[1].Name))
|
||||
lcName := tagNames[tagIdxWithScene]
|
||||
assert.Equal(t, strings.ToLower(lcName), strings.ToLower(tags[0].Name))
|
||||
assert.Equal(t, strings.ToLower(lcName), strings.ToLower(tags[1].Name))
|
||||
|
||||
// find by alias
|
||||
name = getTagStringValue(tagIdxWithScene, "Alias")
|
||||
tags, err = tqb.QueryForAutoTag([]string{name})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Error finding tags: %s", err.Error())
|
||||
}
|
||||
|
||||
assert.Len(t, tags, 1)
|
||||
assert.Equal(t, tagIDs[tagIdxWithScene], tags[0].ID)
|
||||
|
||||
return nil
|
||||
})
|
||||
@@ -137,6 +149,100 @@ func TestTagFindByNames(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestTagQueryName(t *testing.T) {
|
||||
const tagIdx = 1
|
||||
tagName := getSceneStringValue(tagIdx, "Name")
|
||||
|
||||
nameCriterion := &models.StringCriterionInput{
|
||||
Value: tagName,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
}
|
||||
|
||||
tagFilter := &models.TagFilterType{
|
||||
Name: nameCriterion,
|
||||
}
|
||||
|
||||
verifyFn := func(tag *models.Tag, r models.Repository) {
|
||||
verifyString(t, tag.Name, *nameCriterion)
|
||||
}
|
||||
|
||||
verifyTagQuery(t, tagFilter, nil, verifyFn)
|
||||
|
||||
nameCriterion.Modifier = models.CriterionModifierNotEquals
|
||||
verifyTagQuery(t, tagFilter, nil, verifyFn)
|
||||
|
||||
nameCriterion.Modifier = models.CriterionModifierMatchesRegex
|
||||
nameCriterion.Value = "tag_.*1_Name"
|
||||
verifyTagQuery(t, tagFilter, nil, verifyFn)
|
||||
|
||||
nameCriterion.Modifier = models.CriterionModifierNotMatchesRegex
|
||||
verifyTagQuery(t, tagFilter, nil, verifyFn)
|
||||
}
|
||||
|
||||
func TestTagQueryAlias(t *testing.T) {
|
||||
const tagIdx = 1
|
||||
tagName := getSceneStringValue(tagIdx, "Alias")
|
||||
|
||||
aliasCriterion := &models.StringCriterionInput{
|
||||
Value: tagName,
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
}
|
||||
|
||||
tagFilter := &models.TagFilterType{
|
||||
Aliases: aliasCriterion,
|
||||
}
|
||||
|
||||
verifyFn := func(tag *models.Tag, r models.Repository) {
|
||||
aliases, err := r.Tag().GetAliases(tag.ID)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying tags: %s", err.Error())
|
||||
}
|
||||
|
||||
var alias string
|
||||
if len(aliases) > 0 {
|
||||
alias = aliases[0]
|
||||
}
|
||||
|
||||
verifyString(t, alias, *aliasCriterion)
|
||||
}
|
||||
|
||||
verifyTagQuery(t, tagFilter, nil, verifyFn)
|
||||
|
||||
aliasCriterion.Modifier = models.CriterionModifierNotEquals
|
||||
verifyTagQuery(t, tagFilter, nil, verifyFn)
|
||||
|
||||
aliasCriterion.Modifier = models.CriterionModifierMatchesRegex
|
||||
aliasCriterion.Value = "tag_.*1_Alias"
|
||||
verifyTagQuery(t, tagFilter, nil, verifyFn)
|
||||
|
||||
aliasCriterion.Modifier = models.CriterionModifierNotMatchesRegex
|
||||
verifyTagQuery(t, tagFilter, nil, verifyFn)
|
||||
}
|
||||
|
||||
func verifyTagQuery(t *testing.T, tagFilter *models.TagFilterType, findFilter *models.FindFilterType, verifyFn func(t *models.Tag, r models.Repository)) {
|
||||
withTxn(func(r models.Repository) error {
|
||||
sqb := r.Tag()
|
||||
|
||||
tags := queryTags(t, sqb, tagFilter, findFilter)
|
||||
|
||||
for _, tag := range tags {
|
||||
verifyFn(tag, r)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func queryTags(t *testing.T, qb models.TagReader, tagFilter *models.TagFilterType, findFilter *models.FindFilterType) []*models.Tag {
|
||||
t.Helper()
|
||||
tags, _, err := qb.Query(tagFilter, findFilter)
|
||||
if err != nil {
|
||||
t.Errorf("Error querying tags: %s", err.Error())
|
||||
}
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
func TestTagQueryIsMissingImage(t *testing.T) {
|
||||
withTxn(func(r models.Repository) error {
|
||||
qb := r.Tag()
|
||||
@@ -461,6 +567,39 @@ func TestTagDestroyTagImage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagUpdateAlias(t *testing.T) {
|
||||
if err := withTxn(func(r models.Repository) error {
|
||||
qb := r.Tag()
|
||||
|
||||
// create tag to test against
|
||||
const name = "TestTagUpdateAlias"
|
||||
tag := models.Tag{
|
||||
Name: name,
|
||||
}
|
||||
created, err := qb.Create(tag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating tag: %s", err.Error())
|
||||
}
|
||||
|
||||
aliases := []string{"alias1", "alias2"}
|
||||
err = qb.UpdateAliases(created.ID, aliases)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error updating tag aliases: %s", err.Error())
|
||||
}
|
||||
|
||||
// ensure aliases set
|
||||
storedAliases, err := qb.GetAliases(created.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error getting aliases: %s", err.Error())
|
||||
}
|
||||
assert.Equal(t, aliases, storedAliases)
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Create
|
||||
// TODO Update
|
||||
// TODO Destroy
|
||||
|
||||
Reference in New Issue
Block a user