Add ignore autotag flag (#2439)

* Add autoTagIgnored to database schema
* Graphql changes
* UI changes
* Add field to edit performers dialog
* Apply flag to autotag behaviour
This commit is contained in:
WithoutPants 2022-04-04 20:03:39 +10:00 committed by GitHub
parent 2aee6cc18e
commit 61d9f57ce9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 477 additions and 206 deletions

View file

@ -7,6 +7,7 @@ fragment SlimPerformerData on Performer {
instagram
image_path
favorite
ignore_auto_tag
country
birthdate
ethnicity

View file

@ -18,6 +18,7 @@ fragment PerformerData on Performer {
piercings
aliases
favorite
ignore_auto_tag
image_path
scene_count
image_count

View file

@ -14,6 +14,7 @@ fragment StudioData on Studio {
name
image_path
}
ignore_auto_tag
image_path
scene_count
image_count

View file

@ -2,6 +2,7 @@ fragment TagData on Tag {
id
name
aliases
ignore_auto_tag
image_path
scene_count
scene_marker_count

View file

@ -101,6 +101,8 @@ input PerformerFilterType {
death_year: IntCriterionInput
"""Filter by studios where performer appears in scene/image/gallery"""
studios: HierarchicalMultiCriterionInput
"""Filter by autotag ignore value"""
ignore_auto_tag: Boolean
}
input SceneMarkerFilterType {
@ -219,6 +221,8 @@ input StudioFilterType {
url: StringCriterionInput
"""Filter by studio aliases"""
aliases: StringCriterionInput
"""Filter by autotag ignore value"""
ignore_auto_tag: Boolean
}
input GalleryFilterType {
@ -305,6 +309,9 @@ input TagFilterType {
"""Filter by number f child tags the tag has"""
child_count: IntCriterionInput
"""Filter by autotag ignore value"""
ignore_auto_tag: Boolean
}
input ImageFilterType {

View file

@ -28,6 +28,7 @@ type Performer {
aliases: String
favorite: Boolean!
tags: [Tag!]!
ignore_auto_tag: Boolean!
image_path: String # Resolver
scene_count: Int # Resolver
@ -73,6 +74,7 @@ input PerformerCreateInput {
death_date: String
hair_color: String
weight: Int
ignore_auto_tag: Boolean
}
input PerformerUpdateInput {
@ -103,6 +105,7 @@ input PerformerUpdateInput {
death_date: String
hair_color: String
weight: Int
ignore_auto_tag: Boolean
}
input BulkPerformerUpdateInput {
@ -130,6 +133,7 @@ input BulkPerformerUpdateInput {
death_date: String
hair_color: String
weight: Int
ignore_auto_tag: Boolean
}
input PerformerDestroyInput {

View file

@ -6,6 +6,7 @@ type Studio {
parent_studio: Studio
child_studios: [Studio!]!
aliases: [String!]!
ignore_auto_tag: Boolean!
image_path: String # Resolver
scene_count: Int # Resolver
@ -30,6 +31,7 @@ input StudioCreateInput {
rating: Int
details: String
aliases: [String!]
ignore_auto_tag: Boolean
}
input StudioUpdateInput {
@ -43,6 +45,7 @@ input StudioUpdateInput {
rating: Int
details: String
aliases: [String!]
ignore_auto_tag: Boolean
}
input StudioDestroyInput {

View file

@ -2,6 +2,7 @@ type Tag {
id: ID!
name: String!
aliases: [String!]!
ignore_auto_tag: Boolean!
created_at: Time!
updated_at: Time!
@ -19,6 +20,7 @@ type Tag {
input TagCreateInput {
name: String!
aliases: [String!]
ignore_auto_tag: Boolean
"""This should be a URL or a base64 encoded data URL"""
image: String
@ -31,6 +33,7 @@ input TagUpdateInput {
id: ID!
name: String
aliases: [String!]
ignore_auto_tag: Boolean
"""This should be a URL or a base64 encoded data URL"""
image: String

View file

@ -117,6 +117,9 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
weight := int64(*input.Weight)
newPerformer.Weight = sql.NullInt64{Int64: weight, Valid: true}
}
if input.IgnoreAutoTag != nil {
newPerformer.IgnoreAutoTag = *input.IgnoreAutoTag
}
if err := performer.ValidateDeathDate(nil, input.Birthdate, input.DeathDate); err != nil {
if err != nil {
@ -223,6 +226,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
updatedPerformer.DeathDate = translator.sqliteDate(input.DeathDate, "death_date")
updatedPerformer.HairColor = translator.nullString(input.HairColor, "hair_color")
updatedPerformer.Weight = translator.nullInt64(input.Weight, "weight")
updatedPerformer.IgnoreAutoTag = input.IgnoreAutoTag
// Start the transaction and save the p
var p *models.Performer
@ -331,6 +335,7 @@ func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input models
updatedPerformer.DeathDate = translator.sqliteDate(input.DeathDate, "death_date")
updatedPerformer.HairColor = translator.nullString(input.HairColor, "hair_color")
updatedPerformer.Weight = translator.nullInt64(input.Weight, "weight")
updatedPerformer.IgnoreAutoTag = input.IgnoreAutoTag
if translator.hasField("gender") {
if input.Gender != nil {

View file

@ -66,6 +66,9 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
if input.Details != nil {
newStudio.Details = sql.NullString{String: *input.Details, Valid: true}
}
if input.IgnoreAutoTag != nil {
newStudio.IgnoreAutoTag = *input.IgnoreAutoTag
}
// Start the transaction and save the studio
var s *models.Studio
@ -148,6 +151,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
updatedStudio.Details = translator.nullString(input.Details, "details")
updatedStudio.ParentID = translator.nullInt64FromString(input.ParentID, "parent_id")
updatedStudio.Rating = translator.nullInt64(input.Rating, "rating")
updatedStudio.IgnoreAutoTag = input.IgnoreAutoTag
// Start the transaction and save the studio
var s *models.Studio

View file

@ -34,6 +34,10 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
}
if input.IgnoreAutoTag != nil {
newTag.IgnoreAutoTag = *input.IgnoreAutoTag
}
var imageData []byte
var err error
@ -179,6 +183,7 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
updatedTag := models.TagPartial{
ID: tagID,
IgnoreAutoTag: input.IgnoreAutoTag,
UpdatedAt: &models.SQLiteTimestamp{Timestamp: time.Now()},
}

View file

@ -50,7 +50,9 @@ func runTests(m *testing.M) int {
f.Close()
databaseFile := f.Name()
database.Initialize(databaseFile)
if err := database.Initialize(databaseFile); err != nil {
panic(fmt.Sprintf("Could not initialize database: %s", err.Error()))
}
// defer close and delete the database
defer testTeardown(databaseFile)

View file

@ -126,10 +126,16 @@ func (j *autoTagJob) autoTagPerformers(ctx context.Context, progress *job.Progre
if err := j.txnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error {
performerQuery := r.Performer()
ignoreAutoTag := false
perPage := -1
if performerId == "*" {
var err error
performers, err = performerQuery.All()
performers, _, err = performerQuery.Query(&models.PerformerFilterType{
IgnoreAutoTag: &ignoreAutoTag,
}, &models.FindFilterType{
PerPage: &perPage,
})
if err != nil {
return fmt.Errorf("error querying performers: %v", err)
}
@ -193,9 +199,15 @@ func (j *autoTagJob) autoTagStudios(ctx context.Context, progress *job.Progress,
if err := j.txnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error {
studioQuery := r.Studio()
ignoreAutoTag := false
perPage := -1
if studioId == "*" {
var err error
studios, err = studioQuery.All()
studios, _, err = studioQuery.Query(&models.StudioFilterType{
IgnoreAutoTag: &ignoreAutoTag,
}, &models.FindFilterType{
PerPage: &perPage,
})
if err != nil {
return fmt.Errorf("error querying studios: %v", err)
}
@ -264,9 +276,15 @@ func (j *autoTagJob) autoTagTags(ctx context.Context, progress *job.Progress, pa
var tags []*models.Tag
if err := j.txnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error {
tagQuery := r.Tag()
ignoreAutoTag := false
perPage := -1
if tagId == "*" {
var err error
tags, err = tagQuery.All()
tags, _, err = tagQuery.Query(&models.TagFilterType{
IgnoreAutoTag: &ignoreAutoTag,
}, &models.FindFilterType{
PerPage: &perPage,
})
if err != nil {
return fmt.Errorf("error querying tags: %v", err)
}

View file

@ -23,7 +23,7 @@ import (
var DB *sqlx.DB
var WriteMu sync.Mutex
var dbPath string
var appSchemaVersion uint = 29
var appSchemaVersion uint = 30
var databaseSchemaVersion uint
//go:embed migrations/*.sql

View file

@ -0,0 +1,3 @@
ALTER TABLE `performers` ADD COLUMN `ignore_auto_tag` boolean not null default '0';
ALTER TABLE `studios` ADD COLUMN `ignore_auto_tag` boolean not null default '0';
ALTER TABLE `tags` ADD COLUMN `ignore_auto_tag` boolean not null default '0';

View file

@ -36,6 +36,7 @@ type Performer struct {
HairColor string `json:"hair_color,omitempty"`
Weight int `json:"weight,omitempty"`
StashIDs []models.StashID `json:"stash_ids,omitempty"`
IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"`
}
func LoadPerformerFile(filePath string) (*Performer, error) {

View file

@ -19,6 +19,7 @@ type Studio struct {
Details string `json:"details,omitempty"`
Aliases []string `json:"aliases,omitempty"`
StashIDs []models.StashID `json:"stash_ids,omitempty"`
IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"`
}
func LoadStudioFile(filePath string) (*Studio, error) {

View file

@ -13,6 +13,7 @@ type Tag struct {
Aliases []string `json:"aliases,omitempty"`
Image string `json:"image,omitempty"`
Parents []string `json:"parents,omitempty"`
IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"`
CreatedAt models.JSONTime `json:"created_at,omitempty"`
UpdatedAt models.JSONTime `json:"updated_at,omitempty"`
}

View file

@ -34,6 +34,7 @@ type Performer struct {
DeathDate SQLiteDate `db:"death_date" json:"death_date"`
HairColor sql.NullString `db:"hair_color" json:"hair_color"`
Weight sql.NullInt64 `db:"weight" json:"weight"`
IgnoreAutoTag bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
}
type PerformerPartial struct {
@ -63,6 +64,7 @@ type PerformerPartial struct {
DeathDate *SQLiteDate `db:"death_date" json:"death_date"`
HairColor *sql.NullString `db:"hair_color" json:"hair_color"`
Weight *sql.NullInt64 `db:"weight" json:"weight"`
IgnoreAutoTag *bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
}
func NewPerformer(name string) *Performer {

View file

@ -17,6 +17,7 @@ type Studio struct {
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
Rating sql.NullInt64 `db:"rating" json:"rating"`
Details sql.NullString `db:"details" json:"details"`
IgnoreAutoTag bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
}
type StudioPartial struct {
@ -29,6 +30,7 @@ type StudioPartial struct {
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`
Rating *sql.NullInt64 `db:"rating" json:"rating"`
Details *sql.NullString `db:"details" json:"details"`
IgnoreAutoTag *bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
}
var DefaultStudioImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4wgVBQsJl1CMZAAAASJJREFUeNrt3N0JwyAYhlEj3cj9R3Cm5rbkqtAP+qrnGaCYHPwJpLlaa++mmLpbAERAgAgIEAEBIiBABERAgAgIEAEBIiBABERAgAgIEAHZuVflj40x4i94zhk9vqsVvEq6AsQqMP1EjORx20OACAgQRRx7T+zzcFBxcjNDfoB4ntQqTm5Awo7MlqywZxcgYQ+RlqywJ3ozJAQCSBiEJSsQA0gYBpDAgAARECACAkRAgAgIEAERECACAmSjUv6eAOSB8m8YIGGzBUjYbAESBgMkbBkDEjZbgITBAClcxiqQvEoatreYIWEBASIgJ4Gkf11ntXH3nS9uxfGWfJ5J9hAgAgJEQAQEiIAAERAgAgJEQAQEiIAAERAgAgJEQAQEiL7qBuc6RKLHxr0CAAAAAElFTkSuQmCC"

View file

@ -5,6 +5,7 @@ import "time"
type Tag struct {
ID int `db:"id" json:"id"`
Name string `db:"name" json:"name"` // TODO make schema not null
IgnoreAutoTag bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
}
@ -12,6 +13,7 @@ type Tag struct {
type TagPartial struct {
ID int `db:"id" json:"id"`
Name *string `db:"name" json:"name"` // TODO make schema not null
IgnoreAutoTag *bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`
}

View file

@ -11,6 +11,7 @@ import (
// ToJSON converts a Performer object into its JSON equivalent.
func ToJSON(reader models.PerformerReader, performer *models.Performer) (*jsonschema.Performer, error) {
newPerformerJSON := jsonschema.Performer{
IgnoreAutoTag: performer.IgnoreAutoTag,
CreatedAt: models.JSONTime{Time: performer.CreatedAt.Timestamp},
UpdatedAt: models.JSONTime{Time: performer.UpdatedAt.Timestamp},
}

View file

@ -40,6 +40,7 @@ const (
details = "details"
hairColor = "hairColor"
weight = 60
autoTagIgnored = true
)
var imageBytes = []byte("imageBytes")
@ -106,6 +107,7 @@ func createFullPerformer(id int, name string) *models.Performer {
Int64: weight,
Valid: true,
},
IgnoreAutoTag: autoTagIgnored,
}
}
@ -155,6 +157,7 @@ func createFullJSONPerformer(name string, image string) *jsonschema.Performer {
StashIDs: []models.StashID{
stashID,
},
IgnoreAutoTag: autoTagIgnored,
}
}

View file

@ -180,6 +180,7 @@ func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Perform
newPerformer := models.Performer{
Checksum: checksum,
Favorite: sql.NullBool{Bool: performerJSON.Favorite, Valid: true},
IgnoreAutoTag: performerJSON.IgnoreAutoTag,
CreatedAt: models.SQLiteTimestamp{Timestamp: performerJSON.CreatedAt.GetTime()},
UpdatedAt: models.SQLiteTimestamp{Timestamp: performerJSON.UpdatedAt.GetTime()},
}

View file

@ -191,7 +191,11 @@ func (qb *performerQueryBuilder) QueryForAutoTag(words []string) ([]*models.Perf
// args = append(args, w+"%")
}
where := strings.Join(whereClauses, " OR ")
whereOr := "(" + strings.Join(whereClauses, " OR ") + ")"
where := strings.Join([]string{
"ignore_auto_tag = 0",
whereOr,
}, " AND ")
return qb.queryPerformers(query+" WHERE "+where, args)
}
@ -244,6 +248,7 @@ func (qb *performerQueryBuilder) makeFilter(filter *models.PerformerFilterType)
query.handleCriterion(stringCriterionHandler(filter.Details, tableName+".details"))
query.handleCriterion(boolCriterionHandler(filter.FilterFavorites, tableName+".favorite"))
query.handleCriterion(boolCriterionHandler(filter.IgnoreAutoTag, tableName+".ignore_auto_tag"))
query.handleCriterion(yearFilterCriterionHandler(filter.BirthYear, tableName+".birthdate"))
query.handleCriterion(yearFilterCriterionHandler(filter.DeathYear, tableName+".death_date"))

View file

@ -6,6 +6,7 @@ package sqlite_test
import (
"database/sql"
"fmt"
"math"
"strconv"
"strings"
"testing"
@ -238,11 +239,31 @@ func TestPerformerIllegalQuery(t *testing.T) {
})
}
func TestPerformerQueryIgnoreAutoTag(t *testing.T) {
withTxn(func(r models.Repository) error {
ignoreAutoTag := true
performerFilter := models.PerformerFilterType{
IgnoreAutoTag: &ignoreAutoTag,
}
sqb := r.Performer()
performers := queryPerformers(t, sqb, &performerFilter, nil)
assert.Len(t, performers, int(math.Ceil(float64(totalPerformers)/5)))
for _, p := range performers {
assert.True(t, p.IgnoreAutoTag)
}
return nil
})
}
func TestPerformerQueryForAutoTag(t *testing.T) {
withTxn(func(r models.Repository) error {
tqb := r.Performer()
name := performerNames[performerIdxWithScene] // find a performer by name
name := performerNames[performerIdx1WithScene] // find a performer by name
performers, err := tqb.QueryForAutoTag([]string{name})
@ -251,8 +272,8 @@ func TestPerformerQueryForAutoTag(t *testing.T) {
}
assert.Len(t, performers, 2)
assert.Equal(t, strings.ToLower(performerNames[performerIdxWithScene]), strings.ToLower(performers[0].Name.String))
assert.Equal(t, strings.ToLower(performerNames[performerIdxWithScene]), strings.ToLower(performers[1].Name.String))
assert.Equal(t, strings.ToLower(performerNames[performerIdx1WithScene]), strings.ToLower(performers[0].Name.String))
assert.Equal(t, strings.ToLower(performerNames[performerIdx1WithScene]), strings.ToLower(performers[1].Name.String))
return nil
})

View file

@ -99,6 +99,8 @@ const (
performersNameCase = performerIdx1WithDupName
performersNameNoCase = 2
totalPerformers = performersNameCase + performersNameNoCase
)
const (
@ -166,6 +168,8 @@ const (
tagsNameNoCase = 2
tagsNameCase = tagIdx1WithDupName
totalTags = tagsNameCase + tagsNameNoCase
)
const (
@ -190,6 +194,8 @@ const (
studiosNameCase = studioIdxWithDupName
studiosNameNoCase = 1
totalStudios = studiosNameCase + studiosNameNoCase
)
const (
@ -422,7 +428,9 @@ func runTests(m *testing.M) int {
f.Close()
databaseFile := f.Name()
database.Initialize(databaseFile)
if err := database.Initialize(databaseFile); err != nil {
panic(fmt.Sprintf("Could not initialize database: %s", err.Error()))
}
// defer close and delete the database
defer testTeardown(databaseFile)
@ -815,6 +823,10 @@ func getPerformerCareerLength(index int) *string {
return &ret
}
func getIgnoreAutoTag(index int) bool {
return index%5 == 0
}
// createPerformers creates n performers with plain Name and o performers with camel cased NaMe included
func createPerformers(pqb models.PerformerReaderWriter, n int, o int) error {
const namePlain = "Name"
@ -844,6 +856,7 @@ func createPerformers(pqb models.PerformerReaderWriter, n int, o int) error {
Details: sql.NullString{String: getPerformerStringValue(i, "Details"), Valid: true},
Ethnicity: sql.NullString{String: getPerformerStringValue(i, "Ethnicity"), Valid: true},
Rating: getRating(i),
IgnoreAutoTag: getIgnoreAutoTag(i),
}
careerLength := getPerformerCareerLength(i)
@ -946,6 +959,7 @@ func createTags(tqb models.TagReaderWriter, n int, o int) error {
tag := models.Tag{
Name: getTagStringValue(index, name),
IgnoreAutoTag: getIgnoreAutoTag(i),
}
created, err := tqb.Create(tag)
@ -1018,6 +1032,7 @@ func createStudios(sqb models.StudioReaderWriter, n int, o int) error {
Name: sql.NullString{String: name, Valid: true},
Checksum: md5.FromString(name),
URL: getStudioNullStringValue(index, urlField),
IgnoreAutoTag: getIgnoreAutoTag(i),
}
created, err := createStudioFromModel(sqb, studio)

View file

@ -154,7 +154,11 @@ func (qb *studioQueryBuilder) QueryForAutoTag(words []string) ([]*models.Studio,
args = append(args, ww)
}
where := strings.Join(whereClauses, " OR ")
whereOr := "(" + strings.Join(whereClauses, " OR ") + ")"
where := strings.Join([]string{
"studios.ignore_auto_tag = 0",
whereOr,
}, " AND ")
return qb.queryStudios(query+" WHERE "+where, args)
}
@ -206,6 +210,7 @@ func (qb *studioQueryBuilder) makeFilter(studioFilter *models.StudioFilterType)
query.handleCriterion(stringCriterionHandler(studioFilter.Details, studioTable+".details"))
query.handleCriterion(stringCriterionHandler(studioFilter.URL, studioTable+".url"))
query.handleCriterion(intCriterionHandler(studioFilter.Rating, studioTable+".rating"))
query.handleCriterion(boolCriterionHandler(studioFilter.IgnoreAutoTag, studioTable+".ignore_auto_tag"))
query.handleCriterion(criterionHandlerFunc(func(f *filterBuilder) {
if studioFilter.StashID != nil {

View file

@ -7,6 +7,7 @@ import (
"database/sql"
"errors"
"fmt"
"math"
"strconv"
"strings"
"testing"
@ -183,11 +184,31 @@ func TestStudioIllegalQuery(t *testing.T) {
})
}
func TestStudioQueryIgnoreAutoTag(t *testing.T) {
withTxn(func(r models.Repository) error {
ignoreAutoTag := true
studioFilter := models.StudioFilterType{
IgnoreAutoTag: &ignoreAutoTag,
}
sqb := r.Studio()
studios := queryStudio(t, sqb, &studioFilter, nil)
assert.Len(t, studios, int(math.Ceil(float64(totalStudios)/5)))
for _, s := range studios {
assert.True(t, s.IgnoreAutoTag)
}
return nil
})
}
func TestStudioQueryForAutoTag(t *testing.T) {
withTxn(func(r models.Repository) error {
tqb := r.Studio()
name := studioNames[studioIdxWithScene] // find a studio by name
name := studioNames[studioIdxWithMovie] // find a studio by name
studios, err := tqb.QueryForAutoTag([]string{name})
@ -195,12 +216,11 @@ func TestStudioQueryForAutoTag(t *testing.T) {
t.Errorf("Error finding studios: %s", err.Error())
}
assert.Len(t, studios, 2)
assert.Equal(t, strings.ToLower(studioNames[studioIdxWithScene]), strings.ToLower(studios[0].Name.String))
assert.Equal(t, strings.ToLower(studioNames[studioIdxWithScene]), strings.ToLower(studios[1].Name.String))
assert.Len(t, studios, 1)
assert.Equal(t, strings.ToLower(studioNames[studioIdxWithMovie]), strings.ToLower(studios[0].Name.String))
// find by alias
name = getStudioStringValue(studioIdxWithScene, "Alias")
name = getStudioStringValue(studioIdxWithMovie, "Alias")
studios, err = tqb.QueryForAutoTag([]string{name})
if err != nil {
@ -208,7 +228,7 @@ func TestStudioQueryForAutoTag(t *testing.T) {
}
assert.Len(t, studios, 1)
assert.Equal(t, studioIDs[studioIdxWithScene], studios[0].ID)
assert.Equal(t, studioIDs[studioIdxWithMovie], studios[0].ID)
return nil
})

View file

@ -245,7 +245,11 @@ func (qb *tagQueryBuilder) QueryForAutoTag(words []string) ([]*models.Tag, error
args = append(args, ww)
}
where := strings.Join(whereClauses, " OR ")
whereOr := "(" + strings.Join(whereClauses, " OR ") + ")"
where := strings.Join([]string{
"tags.ignore_auto_tag = 0",
whereOr,
}, " AND ")
return qb.queryTags(query+" WHERE "+where, args)
}
@ -295,6 +299,7 @@ func (qb *tagQueryBuilder) makeFilter(tagFilter *models.TagFilterType) *filterBu
query.handleCriterion(stringCriterionHandler(tagFilter.Name, tagTable+".name"))
query.handleCriterion(tagAliasCriterionHandler(qb, tagFilter.Aliases))
query.handleCriterion(boolCriterionHandler(tagFilter.IgnoreAutoTag, tagTable+".ignore_auto_tag"))
query.handleCriterion(tagIsMissingCriterionHandler(qb, tagFilter.IsMissing))
query.handleCriterion(tagSceneCountCriterionHandler(qb, tagFilter.SceneCount))

View file

@ -6,6 +6,7 @@ package sqlite_test
import (
"database/sql"
"fmt"
"math"
"strconv"
"strings"
"testing"
@ -72,11 +73,31 @@ func TestTagFindByName(t *testing.T) {
})
}
func TestTagQueryIgnoreAutoTag(t *testing.T) {
withTxn(func(r models.Repository) error {
ignoreAutoTag := true
tagFilter := models.TagFilterType{
IgnoreAutoTag: &ignoreAutoTag,
}
sqb := r.Tag()
tags := queryTags(t, sqb, &tagFilter, nil)
assert.Len(t, tags, int(math.Ceil(float64(totalTags)/5)))
for _, s := range tags {
assert.True(t, s.IgnoreAutoTag)
}
return nil
})
}
func TestTagQueryForAutoTag(t *testing.T) {
withTxn(func(r models.Repository) error {
tqb := r.Tag()
name := tagNames[tagIdxWithScene] // find a tag by name
name := tagNames[tagIdx1WithScene] // find a tag by name
tags, err := tqb.QueryForAutoTag([]string{name})
@ -85,12 +106,12 @@ func TestTagQueryForAutoTag(t *testing.T) {
}
assert.Len(t, tags, 2)
lcName := tagNames[tagIdxWithScene]
lcName := tagNames[tagIdx1WithScene]
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")
name = getTagStringValue(tagIdx1WithScene, "Alias")
tags, err = tqb.QueryForAutoTag([]string{name})
if err != nil {
@ -98,7 +119,7 @@ func TestTagQueryForAutoTag(t *testing.T) {
}
assert.Len(t, tags, 1)
assert.Equal(t, tagIDs[tagIdxWithScene], tags[0].ID)
assert.Equal(t, tagIDs[tagIdx1WithScene], tags[0].ID)
return nil
})

View file

@ -11,6 +11,7 @@ import (
// ToJSON converts a Studio object into its JSON equivalent.
func ToJSON(reader models.StudioReader, studio *models.Studio) (*jsonschema.Studio, error) {
newStudioJSON := jsonschema.Studio{
IgnoreAutoTag: studio.IgnoreAutoTag,
CreatedAt: models.JSONTime{Time: studio.CreatedAt.Timestamp},
UpdatedAt: models.JSONTime{Time: studio.UpdatedAt.Timestamp},
}

View file

@ -31,6 +31,7 @@ const (
details = "details"
rating = 5
parentStudioName = "parentStudio"
autoTagIgnored = true
)
var parentStudio models.Studio = models.Studio{
@ -67,6 +68,7 @@ func createFullStudio(id int, parentID int) models.Studio {
Timestamp: updateTime,
},
Rating: models.NullInt64(rating),
IgnoreAutoTag: autoTagIgnored,
}
if parentID != 0 {
@ -106,6 +108,7 @@ func createFullJSONStudio(parentStudio, image string, aliases []string) *jsonsch
StashIDs: []models.StashID{
stashID,
},
IgnoreAutoTag: autoTagIgnored,
}
}

View file

@ -30,6 +30,7 @@ func (i *Importer) PreImport() error {
Name: sql.NullString{String: i.Input.Name, Valid: true},
URL: sql.NullString{String: i.Input.URL, Valid: true},
Details: sql.NullString{String: i.Input.Details, Valid: true},
IgnoreAutoTag: i.Input.IgnoreAutoTag,
CreatedAt: models.SQLiteTimestamp{Timestamp: i.Input.CreatedAt.GetTime()},
UpdatedAt: models.SQLiteTimestamp{Timestamp: i.Input.UpdatedAt.GetTime()},
Rating: sql.NullInt64{Int64: int64(i.Input.Rating), Valid: true},

View file

@ -40,6 +40,7 @@ func TestImporterPreImport(t *testing.T) {
Input: jsonschema.Studio{
Name: studioName,
Image: invalidImage,
IgnoreAutoTag: autoTagIgnored,
},
}

View file

@ -12,6 +12,7 @@ import (
func ToJSON(reader models.TagReader, tag *models.Tag) (*jsonschema.Tag, error) {
newTagJSON := jsonschema.Tag{
Name: tag.Name,
IgnoreAutoTag: tag.IgnoreAutoTag,
CreatedAt: models.JSONTime{Time: tag.CreatedAt.Timestamp},
UpdatedAt: models.JSONTime{Time: tag.UpdatedAt.Timestamp},
}

View file

@ -24,6 +24,7 @@ const (
const tagName = "testTag"
var (
autoTagIgnored = true
createTime = time.Date(2001, 01, 01, 0, 0, 0, 0, time.UTC)
updateTime = time.Date(2002, 01, 01, 0, 0, 0, 0, time.UTC)
)
@ -32,6 +33,7 @@ func createTag(id int) models.Tag {
return models.Tag{
ID: id,
Name: tagName,
IgnoreAutoTag: autoTagIgnored,
CreatedAt: models.SQLiteTimestamp{
Timestamp: createTime,
},
@ -45,6 +47,7 @@ func createJSONTag(aliases []string, image string, parents []string) *jsonschema
return &jsonschema.Tag{
Name: tagName,
Aliases: aliases,
IgnoreAutoTag: autoTagIgnored,
CreatedAt: models.JSONTime{
Time: createTime,
},

View file

@ -32,6 +32,7 @@ type Importer struct {
func (i *Importer) PreImport() error {
i.tag = models.Tag{
Name: i.Input.Name,
IgnoreAutoTag: i.Input.IgnoreAutoTag,
CreatedAt: models.SQLiteTimestamp{Timestamp: i.Input.CreatedAt.GetTime()},
UpdatedAt: models.SQLiteTimestamp{Timestamp: i.Input.UpdatedAt.GetTime()},
}

View file

@ -38,6 +38,7 @@ func TestImporterPreImport(t *testing.T) {
Input: jsonschema.Tag{
Name: tagName,
Image: invalidImage,
IgnoreAutoTag: autoTagIgnored,
},
}

View file

@ -1,10 +1,11 @@
##### 💥 Note: Image Slideshow Delay (in Interface Settings) is now in seconds rather than milliseconds and has not been converted. Please adjust your settings as needed.
### ✨ New Features
* Add Ignore Auto Tag flag to Performers, Studios and Tags. ([#2439](https://github.com/stashapp/stash/pull/2439))
* Add python location in System Settings for script scrapers and plugins. ([#2409](https://github.com/stashapp/stash/pull/2409))
### 🎨 Improvements
* Revamped scene details page. ([#2466](https://github.com/stashapp/stash/pull/2466))
* Restyled scene details page on mobile devices. ([#2466](https://github.com/stashapp/stash/pull/2466))
* Added support for Handy APIv2. ([#2193](https://github.com/stashapp/stash/pull/2193))
* Hide tabs with no content in Performer, Studio and Tag pages. ([#2468](https://github.com/stashapp/stash/pull/2468))
* Added support for bulk editing most performer fields. ([#2467](https://github.com/stashapp/stash/pull/2467))

View file

@ -46,6 +46,7 @@ const performerFields = [
"hair_color",
"tattoos",
"piercings",
"ignore_auto_tag",
];
export const EditPerformersDialog: React.FC<IListOperationProps> = (
@ -302,6 +303,16 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
mode={tagIds.mode}
/>
</Form.Group>
<Form.Group controlId="ignore-auto-tags">
<IndeterminateCheckbox
label={intl.formatMessage({ id: "ignore_auto_tag" })}
setChecked={(checked) =>
setUpdateField({ ignore_auto_tag: checked })
}
checked={updateInput.ignore_auto_tag ?? undefined}
/>
</Form.Group>
</Form>
</Modal>
);

View file

@ -117,6 +117,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
death_date: yup.string().optional(),
hair_color: yup.string().optional(),
weight: yup.number().optional(),
ignore_auto_tag: yup.boolean().optional(),
});
const initialValues = {
@ -143,6 +144,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
death_date: performer.death_date ?? "",
hair_color: performer.hair_color ?? "",
weight: performer.weight ?? undefined,
ignore_auto_tag: performer.ignore_auto_tag ?? false,
};
type InputValues = typeof initialValues;
@ -944,6 +946,22 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
{renderStashIDs()}
<hr />
<Form.Group controlId="ignore-auto-tag" as={Row}>
<Form.Label column sm={labelXS} xl={labelXL}>
<FormattedMessage id="ignore_auto_tag" />
</Form.Label>
<Col sm={fieldXS} xl={fieldXL}>
<Form.Check
{...formik.getFieldProps({
name: "ignore_auto_tag",
type: "checkbox",
})}
/>
</Col>
</Form.Group>
{renderButtons("mt-3")}
</Form>
</>

View file

@ -55,6 +55,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
},
message: "aliases must be unique",
}),
ignore_auto_tag: yup.boolean().optional(),
});
const initialValues = {
@ -66,6 +67,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
parent_id: studio.parent_studio?.id,
stash_ids: studio.stash_ids ?? undefined,
aliases: studio.aliases,
ignore_auto_tag: studio.ignore_auto_tag ?? false,
};
type InputValues = typeof initialValues;
@ -317,6 +319,22 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
</Form.Group>
</Form>
<hr />
<Form.Group controlId="ignore-auto-tag" as={Row}>
<Form.Label column xs={3}>
<FormattedMessage id="ignore_auto_tag" />
</Form.Label>
<Col xs={9}>
<Form.Check
{...formik.getFieldProps({
name: "ignore_auto_tag",
type: "checkbox",
})}
/>
</Col>
</Form.Group>
<DetailsEditNavbar
objectName={studio?.name ?? intl.formatMessage({ id: "studio" })}
isNew={isNew}

View file

@ -53,6 +53,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
}),
parent_ids: yup.array(yup.string().required()).optional().nullable(),
child_ids: yup.array(yup.string().required()).optional().nullable(),
ignore_auto_tag: yup.boolean().optional(),
});
const initialValues = {
@ -60,6 +61,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
aliases: tag?.aliases,
parent_ids: (tag?.parents ?? []).map((t) => t.id),
child_ids: (tag?.children ?? []).map((t) => t.id),
ignore_auto_tag: tag?.ignore_auto_tag ?? false,
};
type InputValues = typeof initialValues;
@ -211,6 +213,22 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
/>
</Col>
</Form.Group>
<hr />
<Form.Group controlId="ignore-auto-tag" as={Row}>
<Form.Label column xs={labelXS} xl={labelXL}>
<FormattedMessage id="ignore_auto_tag" />
</Form.Label>
<Col xs={fieldXS} xl={fieldXL}>
<Form.Check
{...formik.getFieldProps({
name: "ignore_auto_tag",
type: "checkbox",
})}
/>
</Col>
</Form.Group>
</Form>
<DetailsEditNavbar

View file

@ -870,6 +870,11 @@ dl.details-list {
grid-template-columns: minmax(16.67%, auto) 1fr;
}
// middle align checkboxes
.form-group .form-check .form-check-input {
vertical-align: middle;
}
// Fix Safari styling on dropdowns
select {
-webkit-appearance: none;

View file

@ -733,6 +733,7 @@
"hasMarkers": "Has Markers",
"height": "Height",
"help": "Help",
"ignore_auto_tag": "Ignore Auto Tag",
"image": "Image",
"image_count": "Image Count",
"images": "Images",

View file

@ -291,6 +291,18 @@ export class BooleanCriterion extends StringCriterion {
}
}
export function createBooleanCriterionOption(
value: CriterionType,
messageID?: string,
parameterName?: string
) {
return new BooleanCriterionOption(
messageID ?? value,
value,
parameterName ?? messageID ?? value
);
}
export class NumberCriterionOption extends CriterionOption {
constructor(
messageID: string,

View file

@ -8,6 +8,8 @@ import {
MandatoryNumberCriterionOption,
StringCriterionOption,
ILabeledIdCriterion,
BooleanCriterion,
BooleanCriterionOption,
} from "./criterion";
import { OrganizedCriterion } from "./organized";
import { FavoriteCriterion, PerformerFavoriteCriterion } from "./favorite";
@ -173,5 +175,7 @@ export function makeCriteria(type: CriterionType = "none") {
"child_count"
)
);
case "ignore_auto_tag":
return new BooleanCriterion(new BooleanCriterionOption(type, type));
}
}

View file

@ -2,6 +2,7 @@ import {
createNumberCriterionOption,
createMandatoryNumberCriterionOption,
createStringCriterionOption,
createBooleanCriterionOption,
} from "./criteria/criterion";
import { FavoriteCriterionOption } from "./criteria/favorite";
import { GenderCriterionOption } from "./criteria/gender";
@ -79,6 +80,7 @@ const criterionOptions = [
createMandatoryNumberCriterionOption("scene_count"),
createMandatoryNumberCriterionOption("image_count"),
createMandatoryNumberCriterionOption("gallery_count"),
createBooleanCriterionOption("ignore_auto_tag"),
...numberCriteria.map((c) => createNumberCriterionOption(c)),
...stringCriteria.map((c) => createStringCriterionOption(c)),
];

View file

@ -1,4 +1,5 @@
import {
createBooleanCriterionOption,
createMandatoryNumberCriterionOption,
createMandatoryStringCriterionOption,
createStringCriterionOption,
@ -34,6 +35,7 @@ const criterionOptions = [
ParentStudiosCriterionOption,
StudioIsMissingCriterionOption,
RatingCriterionOption,
createBooleanCriterionOption("ignore_auto_tag"),
createMandatoryNumberCriterionOption("scene_count"),
createMandatoryNumberCriterionOption("image_count"),
createMandatoryNumberCriterionOption("gallery_count"),

View file

@ -1,4 +1,5 @@
import {
createBooleanCriterionOption,
createMandatoryNumberCriterionOption,
createMandatoryStringCriterionOption,
createStringCriterionOption,
@ -43,6 +44,7 @@ const criterionOptions = [
createMandatoryStringCriterionOption("name"),
TagIsMissingCriterionOption,
createStringCriterionOption("aliases"),
createBooleanCriterionOption("ignore_auto_tag"),
createMandatoryNumberCriterionOption("scene_count"),
createMandatoryNumberCriterionOption("image_count"),
createMandatoryNumberCriterionOption("gallery_count"),

View file

@ -127,4 +127,5 @@ export type CriterionType =
| "child_tag_count"
| "performer_favorite"
| "performer_age"
| "duplicated";
| "duplicated"
| "ignore_auto_tag";