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 instagram
image_path image_path
favorite favorite
ignore_auto_tag
country country
birthdate birthdate
ethnicity ethnicity

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -66,6 +66,9 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
if input.Details != nil { if input.Details != nil {
newStudio.Details = sql.NullString{String: *input.Details, Valid: true} newStudio.Details = sql.NullString{String: *input.Details, Valid: true}
} }
if input.IgnoreAutoTag != nil {
newStudio.IgnoreAutoTag = *input.IgnoreAutoTag
}
// Start the transaction and save the studio // Start the transaction and save the studio
var s *models.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.Details = translator.nullString(input.Details, "details")
updatedStudio.ParentID = translator.nullInt64FromString(input.ParentID, "parent_id") updatedStudio.ParentID = translator.nullInt64FromString(input.ParentID, "parent_id")
updatedStudio.Rating = translator.nullInt64(input.Rating, "rating") updatedStudio.Rating = translator.nullInt64(input.Rating, "rating")
updatedStudio.IgnoreAutoTag = input.IgnoreAutoTag
// Start the transaction and save the studio // Start the transaction and save the studio
var s *models.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}, UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
} }
if input.IgnoreAutoTag != nil {
newTag.IgnoreAutoTag = *input.IgnoreAutoTag
}
var imageData []byte var imageData []byte
var err error var err error
@ -178,8 +182,9 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
} }
updatedTag := models.TagPartial{ updatedTag := models.TagPartial{
ID: tagID, ID: tagID,
UpdatedAt: &models.SQLiteTimestamp{Timestamp: time.Now()}, IgnoreAutoTag: input.IgnoreAutoTag,
UpdatedAt: &models.SQLiteTimestamp{Timestamp: time.Now()},
} }
if input.Name != nil && t.Name != *input.Name { if input.Name != nil && t.Name != *input.Name {

View file

@ -50,7 +50,9 @@ func runTests(m *testing.M) int {
f.Close() f.Close()
databaseFile := f.Name() 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 close and delete the database
defer testTeardown(databaseFile) 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 { if err := j.txnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error {
performerQuery := r.Performer() performerQuery := r.Performer()
ignoreAutoTag := false
perPage := -1
if performerId == "*" { if performerId == "*" {
var err error var err error
performers, err = performerQuery.All() performers, _, err = performerQuery.Query(&models.PerformerFilterType{
IgnoreAutoTag: &ignoreAutoTag,
}, &models.FindFilterType{
PerPage: &perPage,
})
if err != nil { if err != nil {
return fmt.Errorf("error querying performers: %v", err) 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 { if err := j.txnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error {
studioQuery := r.Studio() studioQuery := r.Studio()
ignoreAutoTag := false
perPage := -1
if studioId == "*" { if studioId == "*" {
var err error var err error
studios, err = studioQuery.All() studios, _, err = studioQuery.Query(&models.StudioFilterType{
IgnoreAutoTag: &ignoreAutoTag,
}, &models.FindFilterType{
PerPage: &perPage,
})
if err != nil { if err != nil {
return fmt.Errorf("error querying studios: %v", err) 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 var tags []*models.Tag
if err := j.txnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error { if err := j.txnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error {
tagQuery := r.Tag() tagQuery := r.Tag()
ignoreAutoTag := false
perPage := -1
if tagId == "*" { if tagId == "*" {
var err error var err error
tags, err = tagQuery.All() tags, _, err = tagQuery.Query(&models.TagFilterType{
IgnoreAutoTag: &ignoreAutoTag,
}, &models.FindFilterType{
PerPage: &perPage,
})
if err != nil { if err != nil {
return fmt.Errorf("error querying tags: %v", err) return fmt.Errorf("error querying tags: %v", err)
} }

View file

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

@ -9,33 +9,34 @@ import (
) )
type Performer struct { type Performer struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Gender string `json:"gender,omitempty"` Gender string `json:"gender,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
Twitter string `json:"twitter,omitempty"` Twitter string `json:"twitter,omitempty"`
Instagram string `json:"instagram,omitempty"` Instagram string `json:"instagram,omitempty"`
Birthdate string `json:"birthdate,omitempty"` Birthdate string `json:"birthdate,omitempty"`
Ethnicity string `json:"ethnicity,omitempty"` Ethnicity string `json:"ethnicity,omitempty"`
Country string `json:"country,omitempty"` Country string `json:"country,omitempty"`
EyeColor string `json:"eye_color,omitempty"` EyeColor string `json:"eye_color,omitempty"`
Height string `json:"height,omitempty"` Height string `json:"height,omitempty"`
Measurements string `json:"measurements,omitempty"` Measurements string `json:"measurements,omitempty"`
FakeTits string `json:"fake_tits,omitempty"` FakeTits string `json:"fake_tits,omitempty"`
CareerLength string `json:"career_length,omitempty"` CareerLength string `json:"career_length,omitempty"`
Tattoos string `json:"tattoos,omitempty"` Tattoos string `json:"tattoos,omitempty"`
Piercings string `json:"piercings,omitempty"` Piercings string `json:"piercings,omitempty"`
Aliases string `json:"aliases,omitempty"` Aliases string `json:"aliases,omitempty"`
Favorite bool `json:"favorite,omitempty"` Favorite bool `json:"favorite,omitempty"`
Tags []string `json:"tags,omitempty"` Tags []string `json:"tags,omitempty"`
Image string `json:"image,omitempty"` Image string `json:"image,omitempty"`
CreatedAt models.JSONTime `json:"created_at,omitempty"` CreatedAt models.JSONTime `json:"created_at,omitempty"`
UpdatedAt models.JSONTime `json:"updated_at,omitempty"` UpdatedAt models.JSONTime `json:"updated_at,omitempty"`
Rating int `json:"rating,omitempty"` Rating int `json:"rating,omitempty"`
Details string `json:"details,omitempty"` Details string `json:"details,omitempty"`
DeathDate string `json:"death_date,omitempty"` DeathDate string `json:"death_date,omitempty"`
HairColor string `json:"hair_color,omitempty"` HairColor string `json:"hair_color,omitempty"`
Weight int `json:"weight,omitempty"` Weight int `json:"weight,omitempty"`
StashIDs []models.StashID `json:"stash_ids,omitempty"` StashIDs []models.StashID `json:"stash_ids,omitempty"`
IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"`
} }
func LoadPerformerFile(filePath string) (*Performer, error) { func LoadPerformerFile(filePath string) (*Performer, error) {

View file

@ -9,16 +9,17 @@ import (
) )
type Studio struct { type Studio struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
ParentStudio string `json:"parent_studio,omitempty"` ParentStudio string `json:"parent_studio,omitempty"`
Image string `json:"image,omitempty"` Image string `json:"image,omitempty"`
CreatedAt models.JSONTime `json:"created_at,omitempty"` CreatedAt models.JSONTime `json:"created_at,omitempty"`
UpdatedAt models.JSONTime `json:"updated_at,omitempty"` UpdatedAt models.JSONTime `json:"updated_at,omitempty"`
Rating int `json:"rating,omitempty"` Rating int `json:"rating,omitempty"`
Details string `json:"details,omitempty"` Details string `json:"details,omitempty"`
Aliases []string `json:"aliases,omitempty"` Aliases []string `json:"aliases,omitempty"`
StashIDs []models.StashID `json:"stash_ids,omitempty"` StashIDs []models.StashID `json:"stash_ids,omitempty"`
IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"`
} }
func LoadStudioFile(filePath string) (*Studio, error) { func LoadStudioFile(filePath string) (*Studio, error) {

View file

@ -9,12 +9,13 @@ import (
) )
type Tag struct { type Tag struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Aliases []string `json:"aliases,omitempty"` Aliases []string `json:"aliases,omitempty"`
Image string `json:"image,omitempty"` Image string `json:"image,omitempty"`
Parents []string `json:"parents,omitempty"` Parents []string `json:"parents,omitempty"`
CreatedAt models.JSONTime `json:"created_at,omitempty"` IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"`
UpdatedAt models.JSONTime `json:"updated_at,omitempty"` CreatedAt models.JSONTime `json:"created_at,omitempty"`
UpdatedAt models.JSONTime `json:"updated_at,omitempty"`
} }
func LoadTagFile(filePath string) (*Tag, error) { func LoadTagFile(filePath string) (*Tag, error) {

View file

@ -8,61 +8,63 @@ import (
) )
type Performer struct { type Performer struct {
ID int `db:"id" json:"id"` ID int `db:"id" json:"id"`
Checksum string `db:"checksum" json:"checksum"` Checksum string `db:"checksum" json:"checksum"`
Name sql.NullString `db:"name" json:"name"` Name sql.NullString `db:"name" json:"name"`
Gender sql.NullString `db:"gender" json:"gender"` Gender sql.NullString `db:"gender" json:"gender"`
URL sql.NullString `db:"url" json:"url"` URL sql.NullString `db:"url" json:"url"`
Twitter sql.NullString `db:"twitter" json:"twitter"` Twitter sql.NullString `db:"twitter" json:"twitter"`
Instagram sql.NullString `db:"instagram" json:"instagram"` Instagram sql.NullString `db:"instagram" json:"instagram"`
Birthdate SQLiteDate `db:"birthdate" json:"birthdate"` Birthdate SQLiteDate `db:"birthdate" json:"birthdate"`
Ethnicity sql.NullString `db:"ethnicity" json:"ethnicity"` Ethnicity sql.NullString `db:"ethnicity" json:"ethnicity"`
Country sql.NullString `db:"country" json:"country"` Country sql.NullString `db:"country" json:"country"`
EyeColor sql.NullString `db:"eye_color" json:"eye_color"` EyeColor sql.NullString `db:"eye_color" json:"eye_color"`
Height sql.NullString `db:"height" json:"height"` Height sql.NullString `db:"height" json:"height"`
Measurements sql.NullString `db:"measurements" json:"measurements"` Measurements sql.NullString `db:"measurements" json:"measurements"`
FakeTits sql.NullString `db:"fake_tits" json:"fake_tits"` FakeTits sql.NullString `db:"fake_tits" json:"fake_tits"`
CareerLength sql.NullString `db:"career_length" json:"career_length"` CareerLength sql.NullString `db:"career_length" json:"career_length"`
Tattoos sql.NullString `db:"tattoos" json:"tattoos"` Tattoos sql.NullString `db:"tattoos" json:"tattoos"`
Piercings sql.NullString `db:"piercings" json:"piercings"` Piercings sql.NullString `db:"piercings" json:"piercings"`
Aliases sql.NullString `db:"aliases" json:"aliases"` Aliases sql.NullString `db:"aliases" json:"aliases"`
Favorite sql.NullBool `db:"favorite" json:"favorite"` Favorite sql.NullBool `db:"favorite" json:"favorite"`
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"` CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"` UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
Rating sql.NullInt64 `db:"rating" json:"rating"` Rating sql.NullInt64 `db:"rating" json:"rating"`
Details sql.NullString `db:"details" json:"details"` Details sql.NullString `db:"details" json:"details"`
DeathDate SQLiteDate `db:"death_date" json:"death_date"` DeathDate SQLiteDate `db:"death_date" json:"death_date"`
HairColor sql.NullString `db:"hair_color" json:"hair_color"` HairColor sql.NullString `db:"hair_color" json:"hair_color"`
Weight sql.NullInt64 `db:"weight" json:"weight"` Weight sql.NullInt64 `db:"weight" json:"weight"`
IgnoreAutoTag bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
} }
type PerformerPartial struct { type PerformerPartial struct {
ID int `db:"id" json:"id"` ID int `db:"id" json:"id"`
Checksum *string `db:"checksum" json:"checksum"` Checksum *string `db:"checksum" json:"checksum"`
Name *sql.NullString `db:"name" json:"name"` Name *sql.NullString `db:"name" json:"name"`
Gender *sql.NullString `db:"gender" json:"gender"` Gender *sql.NullString `db:"gender" json:"gender"`
URL *sql.NullString `db:"url" json:"url"` URL *sql.NullString `db:"url" json:"url"`
Twitter *sql.NullString `db:"twitter" json:"twitter"` Twitter *sql.NullString `db:"twitter" json:"twitter"`
Instagram *sql.NullString `db:"instagram" json:"instagram"` Instagram *sql.NullString `db:"instagram" json:"instagram"`
Birthdate *SQLiteDate `db:"birthdate" json:"birthdate"` Birthdate *SQLiteDate `db:"birthdate" json:"birthdate"`
Ethnicity *sql.NullString `db:"ethnicity" json:"ethnicity"` Ethnicity *sql.NullString `db:"ethnicity" json:"ethnicity"`
Country *sql.NullString `db:"country" json:"country"` Country *sql.NullString `db:"country" json:"country"`
EyeColor *sql.NullString `db:"eye_color" json:"eye_color"` EyeColor *sql.NullString `db:"eye_color" json:"eye_color"`
Height *sql.NullString `db:"height" json:"height"` Height *sql.NullString `db:"height" json:"height"`
Measurements *sql.NullString `db:"measurements" json:"measurements"` Measurements *sql.NullString `db:"measurements" json:"measurements"`
FakeTits *sql.NullString `db:"fake_tits" json:"fake_tits"` FakeTits *sql.NullString `db:"fake_tits" json:"fake_tits"`
CareerLength *sql.NullString `db:"career_length" json:"career_length"` CareerLength *sql.NullString `db:"career_length" json:"career_length"`
Tattoos *sql.NullString `db:"tattoos" json:"tattoos"` Tattoos *sql.NullString `db:"tattoos" json:"tattoos"`
Piercings *sql.NullString `db:"piercings" json:"piercings"` Piercings *sql.NullString `db:"piercings" json:"piercings"`
Aliases *sql.NullString `db:"aliases" json:"aliases"` Aliases *sql.NullString `db:"aliases" json:"aliases"`
Favorite *sql.NullBool `db:"favorite" json:"favorite"` Favorite *sql.NullBool `db:"favorite" json:"favorite"`
CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"` CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"` UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`
Rating *sql.NullInt64 `db:"rating" json:"rating"` Rating *sql.NullInt64 `db:"rating" json:"rating"`
Details *sql.NullString `db:"details" json:"details"` Details *sql.NullString `db:"details" json:"details"`
DeathDate *SQLiteDate `db:"death_date" json:"death_date"` DeathDate *SQLiteDate `db:"death_date" json:"death_date"`
HairColor *sql.NullString `db:"hair_color" json:"hair_color"` HairColor *sql.NullString `db:"hair_color" json:"hair_color"`
Weight *sql.NullInt64 `db:"weight" json:"weight"` Weight *sql.NullInt64 `db:"weight" json:"weight"`
IgnoreAutoTag *bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
} }
func NewPerformer(name string) *Performer { func NewPerformer(name string) *Performer {

View file

@ -8,27 +8,29 @@ import (
) )
type Studio struct { type Studio struct {
ID int `db:"id" json:"id"` ID int `db:"id" json:"id"`
Checksum string `db:"checksum" json:"checksum"` Checksum string `db:"checksum" json:"checksum"`
Name sql.NullString `db:"name" json:"name"` Name sql.NullString `db:"name" json:"name"`
URL sql.NullString `db:"url" json:"url"` URL sql.NullString `db:"url" json:"url"`
ParentID sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"` ParentID sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"`
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"` CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"` UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
Rating sql.NullInt64 `db:"rating" json:"rating"` Rating sql.NullInt64 `db:"rating" json:"rating"`
Details sql.NullString `db:"details" json:"details"` Details sql.NullString `db:"details" json:"details"`
IgnoreAutoTag bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
} }
type StudioPartial struct { type StudioPartial struct {
ID int `db:"id" json:"id"` ID int `db:"id" json:"id"`
Checksum *string `db:"checksum" json:"checksum"` Checksum *string `db:"checksum" json:"checksum"`
Name *sql.NullString `db:"name" json:"name"` Name *sql.NullString `db:"name" json:"name"`
URL *sql.NullString `db:"url" json:"url"` URL *sql.NullString `db:"url" json:"url"`
ParentID *sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"` ParentID *sql.NullInt64 `db:"parent_id,omitempty" json:"parent_id"`
CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"` CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"` UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`
Rating *sql.NullInt64 `db:"rating" json:"rating"` Rating *sql.NullInt64 `db:"rating" json:"rating"`
Details *sql.NullString `db:"details" json:"details"` 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" var DefaultStudioImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4wgVBQsJl1CMZAAAASJJREFUeNrt3N0JwyAYhlEj3cj9R3Cm5rbkqtAP+qrnGaCYHPwJpLlaa++mmLpbAERAgAgIEAEBIiBABERAgAgIEAEBIiBABERAgAgIEAHZuVflj40x4i94zhk9vqsVvEq6AsQqMP1EjORx20OACAgQRRx7T+zzcFBxcjNDfoB4ntQqTm5Awo7MlqywZxcgYQ+RlqywJ3ozJAQCSBiEJSsQA0gYBpDAgAARECACAkRAgAgIEAERECACAmSjUv6eAOSB8m8YIGGzBUjYbAESBgMkbBkDEjZbgITBAClcxiqQvEoatreYIWEBASIgJ4Gkf11ntXH3nS9uxfGWfJ5J9hAgAgJEQAQEiIAAERAgAgJEQAQEiIAAERAgAgJEQAQEiL7qBuc6RKLHxr0CAAAAAElFTkSuQmCC"

View file

@ -3,17 +3,19 @@ package models
import "time" import "time"
type Tag struct { type Tag struct {
ID int `db:"id" json:"id"` ID int `db:"id" json:"id"`
Name string `db:"name" json:"name"` // TODO make schema not null Name string `db:"name" json:"name"` // TODO make schema not null
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"` IgnoreAutoTag bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"` CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
} }
type TagPartial struct { type TagPartial struct {
ID int `db:"id" json:"id"` ID int `db:"id" json:"id"`
Name *string `db:"name" json:"name"` // TODO make schema not null Name *string `db:"name" json:"name"` // TODO make schema not null
CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"` IgnoreAutoTag *bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"` CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`
} }
type TagPath struct { type TagPath struct {

View file

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

View file

@ -21,25 +21,26 @@ const (
) )
const ( const (
performerName = "testPerformer" performerName = "testPerformer"
url = "url" url = "url"
aliases = "aliases" aliases = "aliases"
careerLength = "careerLength" careerLength = "careerLength"
country = "country" country = "country"
ethnicity = "ethnicity" ethnicity = "ethnicity"
eyeColor = "eyeColor" eyeColor = "eyeColor"
fakeTits = "fakeTits" fakeTits = "fakeTits"
gender = "gender" gender = "gender"
height = "height" height = "height"
instagram = "instagram" instagram = "instagram"
measurements = "measurements" measurements = "measurements"
piercings = "piercings" piercings = "piercings"
tattoos = "tattoos" tattoos = "tattoos"
twitter = "twitter" twitter = "twitter"
rating = 5 rating = 5
details = "details" details = "details"
hairColor = "hairColor" hairColor = "hairColor"
weight = 60 weight = 60
autoTagIgnored = true
) )
var imageBytes = []byte("imageBytes") var imageBytes = []byte("imageBytes")
@ -106,6 +107,7 @@ func createFullPerformer(id int, name string) *models.Performer {
Int64: weight, Int64: weight,
Valid: true, Valid: true,
}, },
IgnoreAutoTag: autoTagIgnored,
} }
} }
@ -155,6 +157,7 @@ func createFullJSONPerformer(name string, image string) *jsonschema.Performer {
StashIDs: []models.StashID{ StashIDs: []models.StashID{
stashID, stashID,
}, },
IgnoreAutoTag: autoTagIgnored,
} }
} }

View file

@ -178,10 +178,11 @@ func performerJSONToPerformer(performerJSON jsonschema.Performer) models.Perform
checksum := md5.FromString(performerJSON.Name) checksum := md5.FromString(performerJSON.Name)
newPerformer := models.Performer{ newPerformer := models.Performer{
Checksum: checksum, Checksum: checksum,
Favorite: sql.NullBool{Bool: performerJSON.Favorite, Valid: true}, Favorite: sql.NullBool{Bool: performerJSON.Favorite, Valid: true},
CreatedAt: models.SQLiteTimestamp{Timestamp: performerJSON.CreatedAt.GetTime()}, IgnoreAutoTag: performerJSON.IgnoreAutoTag,
UpdatedAt: models.SQLiteTimestamp{Timestamp: performerJSON.UpdatedAt.GetTime()}, CreatedAt: models.SQLiteTimestamp{Timestamp: performerJSON.CreatedAt.GetTime()},
UpdatedAt: models.SQLiteTimestamp{Timestamp: performerJSON.UpdatedAt.GetTime()},
} }
if performerJSON.Name != "" { if performerJSON.Name != "" {

View file

@ -191,7 +191,11 @@ func (qb *performerQueryBuilder) QueryForAutoTag(words []string) ([]*models.Perf
// args = append(args, w+"%") // 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) 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(stringCriterionHandler(filter.Details, tableName+".details"))
query.handleCriterion(boolCriterionHandler(filter.FilterFavorites, tableName+".favorite")) 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.BirthYear, tableName+".birthdate"))
query.handleCriterion(yearFilterCriterionHandler(filter.DeathYear, tableName+".death_date")) query.handleCriterion(yearFilterCriterionHandler(filter.DeathYear, tableName+".death_date"))

View file

@ -6,6 +6,7 @@ package sqlite_test
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"math"
"strconv" "strconv"
"strings" "strings"
"testing" "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) { func TestPerformerQueryForAutoTag(t *testing.T) {
withTxn(func(r models.Repository) error { withTxn(func(r models.Repository) error {
tqb := r.Performer() 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}) performers, err := tqb.QueryForAutoTag([]string{name})
@ -251,8 +272,8 @@ func TestPerformerQueryForAutoTag(t *testing.T) {
} }
assert.Len(t, performers, 2) assert.Len(t, performers, 2)
assert.Equal(t, strings.ToLower(performerNames[performerIdxWithScene]), strings.ToLower(performers[0].Name.String)) assert.Equal(t, strings.ToLower(performerNames[performerIdx1WithScene]), 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[1].Name.String))
return nil return nil
}) })

View file

@ -99,6 +99,8 @@ const (
performersNameCase = performerIdx1WithDupName performersNameCase = performerIdx1WithDupName
performersNameNoCase = 2 performersNameNoCase = 2
totalPerformers = performersNameCase + performersNameNoCase
) )
const ( const (
@ -166,6 +168,8 @@ const (
tagsNameNoCase = 2 tagsNameNoCase = 2
tagsNameCase = tagIdx1WithDupName tagsNameCase = tagIdx1WithDupName
totalTags = tagsNameCase + tagsNameNoCase
) )
const ( const (
@ -190,6 +194,8 @@ const (
studiosNameCase = studioIdxWithDupName studiosNameCase = studioIdxWithDupName
studiosNameNoCase = 1 studiosNameNoCase = 1
totalStudios = studiosNameCase + studiosNameNoCase
) )
const ( const (
@ -422,7 +428,9 @@ func runTests(m *testing.M) int {
f.Close() f.Close()
databaseFile := f.Name() 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 close and delete the database
defer testTeardown(databaseFile) defer testTeardown(databaseFile)
@ -815,6 +823,10 @@ func getPerformerCareerLength(index int) *string {
return &ret 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 // 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 { func createPerformers(pqb models.PerformerReaderWriter, n int, o int) error {
const namePlain = "Name" const namePlain = "Name"
@ -840,10 +852,11 @@ func createPerformers(pqb models.PerformerReaderWriter, n int, o int) error {
String: getPerformerBirthdate(i), String: getPerformerBirthdate(i),
Valid: true, Valid: true,
}, },
DeathDate: getPerformerDeathDate(i), DeathDate: getPerformerDeathDate(i),
Details: sql.NullString{String: getPerformerStringValue(i, "Details"), Valid: true}, Details: sql.NullString{String: getPerformerStringValue(i, "Details"), Valid: true},
Ethnicity: sql.NullString{String: getPerformerStringValue(i, "Ethnicity"), Valid: true}, Ethnicity: sql.NullString{String: getPerformerStringValue(i, "Ethnicity"), Valid: true},
Rating: getRating(i), Rating: getRating(i),
IgnoreAutoTag: getIgnoreAutoTag(i),
} }
careerLength := getPerformerCareerLength(i) careerLength := getPerformerCareerLength(i)
@ -945,7 +958,8 @@ func createTags(tqb models.TagReaderWriter, n int, o int) error {
// tags [ i ] and [ n + o - i - 1 ] should have similar names with only the Name!=NaMe part different // tags [ i ] and [ n + o - i - 1 ] should have similar names with only the Name!=NaMe part different
tag := models.Tag{ tag := models.Tag{
Name: getTagStringValue(index, name), Name: getTagStringValue(index, name),
IgnoreAutoTag: getIgnoreAutoTag(i),
} }
created, err := tqb.Create(tag) created, err := tqb.Create(tag)
@ -1015,9 +1029,10 @@ func createStudios(sqb models.StudioReaderWriter, n int, o int) error {
name = getStudioStringValue(index, name) name = getStudioStringValue(index, name)
studio := models.Studio{ studio := models.Studio{
Name: sql.NullString{String: name, Valid: true}, Name: sql.NullString{String: name, Valid: true},
Checksum: md5.FromString(name), Checksum: md5.FromString(name),
URL: getStudioNullStringValue(index, urlField), URL: getStudioNullStringValue(index, urlField),
IgnoreAutoTag: getIgnoreAutoTag(i),
} }
created, err := createStudioFromModel(sqb, studio) created, err := createStudioFromModel(sqb, studio)

View file

@ -154,7 +154,11 @@ func (qb *studioQueryBuilder) QueryForAutoTag(words []string) ([]*models.Studio,
args = append(args, ww) 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) 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.Details, studioTable+".details"))
query.handleCriterion(stringCriterionHandler(studioFilter.URL, studioTable+".url")) query.handleCriterion(stringCriterionHandler(studioFilter.URL, studioTable+".url"))
query.handleCriterion(intCriterionHandler(studioFilter.Rating, studioTable+".rating")) query.handleCriterion(intCriterionHandler(studioFilter.Rating, studioTable+".rating"))
query.handleCriterion(boolCriterionHandler(studioFilter.IgnoreAutoTag, studioTable+".ignore_auto_tag"))
query.handleCriterion(criterionHandlerFunc(func(f *filterBuilder) { query.handleCriterion(criterionHandlerFunc(func(f *filterBuilder) {
if studioFilter.StashID != nil { if studioFilter.StashID != nil {

View file

@ -7,6 +7,7 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"math"
"strconv" "strconv"
"strings" "strings"
"testing" "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) { func TestStudioQueryForAutoTag(t *testing.T) {
withTxn(func(r models.Repository) error { withTxn(func(r models.Repository) error {
tqb := r.Studio() 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}) studios, err := tqb.QueryForAutoTag([]string{name})
@ -195,12 +216,11 @@ func TestStudioQueryForAutoTag(t *testing.T) {
t.Errorf("Error finding studios: %s", err.Error()) t.Errorf("Error finding studios: %s", err.Error())
} }
assert.Len(t, studios, 2) assert.Len(t, studios, 1)
assert.Equal(t, strings.ToLower(studioNames[studioIdxWithScene]), strings.ToLower(studios[0].Name.String)) assert.Equal(t, strings.ToLower(studioNames[studioIdxWithMovie]), strings.ToLower(studios[0].Name.String))
assert.Equal(t, strings.ToLower(studioNames[studioIdxWithScene]), strings.ToLower(studios[1].Name.String))
// find by alias // find by alias
name = getStudioStringValue(studioIdxWithScene, "Alias") name = getStudioStringValue(studioIdxWithMovie, "Alias")
studios, err = tqb.QueryForAutoTag([]string{name}) studios, err = tqb.QueryForAutoTag([]string{name})
if err != nil { if err != nil {
@ -208,7 +228,7 @@ func TestStudioQueryForAutoTag(t *testing.T) {
} }
assert.Len(t, studios, 1) assert.Len(t, studios, 1)
assert.Equal(t, studioIDs[studioIdxWithScene], studios[0].ID) assert.Equal(t, studioIDs[studioIdxWithMovie], studios[0].ID)
return nil return nil
}) })

View file

@ -245,7 +245,11 @@ func (qb *tagQueryBuilder) QueryForAutoTag(words []string) ([]*models.Tag, error
args = append(args, ww) 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) 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(stringCriterionHandler(tagFilter.Name, tagTable+".name"))
query.handleCriterion(tagAliasCriterionHandler(qb, tagFilter.Aliases)) query.handleCriterion(tagAliasCriterionHandler(qb, tagFilter.Aliases))
query.handleCriterion(boolCriterionHandler(tagFilter.IgnoreAutoTag, tagTable+".ignore_auto_tag"))
query.handleCriterion(tagIsMissingCriterionHandler(qb, tagFilter.IsMissing)) query.handleCriterion(tagIsMissingCriterionHandler(qb, tagFilter.IsMissing))
query.handleCriterion(tagSceneCountCriterionHandler(qb, tagFilter.SceneCount)) query.handleCriterion(tagSceneCountCriterionHandler(qb, tagFilter.SceneCount))

View file

@ -6,6 +6,7 @@ package sqlite_test
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"math"
"strconv" "strconv"
"strings" "strings"
"testing" "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) { func TestTagQueryForAutoTag(t *testing.T) {
withTxn(func(r models.Repository) error { withTxn(func(r models.Repository) error {
tqb := r.Tag() 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}) tags, err := tqb.QueryForAutoTag([]string{name})
@ -85,12 +106,12 @@ func TestTagQueryForAutoTag(t *testing.T) {
} }
assert.Len(t, tags, 2) 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[0].Name))
assert.Equal(t, strings.ToLower(lcName), strings.ToLower(tags[1].Name)) assert.Equal(t, strings.ToLower(lcName), strings.ToLower(tags[1].Name))
// find by alias // find by alias
name = getTagStringValue(tagIdxWithScene, "Alias") name = getTagStringValue(tagIdx1WithScene, "Alias")
tags, err = tqb.QueryForAutoTag([]string{name}) tags, err = tqb.QueryForAutoTag([]string{name})
if err != nil { if err != nil {
@ -98,7 +119,7 @@ func TestTagQueryForAutoTag(t *testing.T) {
} }
assert.Len(t, tags, 1) assert.Len(t, tags, 1)
assert.Equal(t, tagIDs[tagIdxWithScene], tags[0].ID) assert.Equal(t, tagIDs[tagIdx1WithScene], tags[0].ID)
return nil return nil
}) })

View file

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

View file

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

View file

@ -26,13 +26,14 @@ func (i *Importer) PreImport() error {
checksum := md5.FromString(i.Input.Name) checksum := md5.FromString(i.Input.Name)
i.studio = models.Studio{ i.studio = models.Studio{
Checksum: checksum, Checksum: checksum,
Name: sql.NullString{String: i.Input.Name, Valid: true}, Name: sql.NullString{String: i.Input.Name, Valid: true},
URL: sql.NullString{String: i.Input.URL, Valid: true}, URL: sql.NullString{String: i.Input.URL, Valid: true},
Details: sql.NullString{String: i.Input.Details, Valid: true}, Details: sql.NullString{String: i.Input.Details, Valid: true},
CreatedAt: models.SQLiteTimestamp{Timestamp: i.Input.CreatedAt.GetTime()}, IgnoreAutoTag: i.Input.IgnoreAutoTag,
UpdatedAt: models.SQLiteTimestamp{Timestamp: i.Input.UpdatedAt.GetTime()}, CreatedAt: models.SQLiteTimestamp{Timestamp: i.Input.CreatedAt.GetTime()},
Rating: sql.NullInt64{Int64: int64(i.Input.Rating), Valid: true}, UpdatedAt: models.SQLiteTimestamp{Timestamp: i.Input.UpdatedAt.GetTime()},
Rating: sql.NullInt64{Int64: int64(i.Input.Rating), Valid: true},
} }
if err := i.populateParentStudio(); err != nil { if err := i.populateParentStudio(); err != nil {

View file

@ -38,8 +38,9 @@ func TestImporterName(t *testing.T) {
func TestImporterPreImport(t *testing.T) { func TestImporterPreImport(t *testing.T) {
i := Importer{ i := Importer{
Input: jsonschema.Studio{ Input: jsonschema.Studio{
Name: studioName, Name: studioName,
Image: invalidImage, Image: invalidImage,
IgnoreAutoTag: autoTagIgnored,
}, },
} }

View file

@ -11,9 +11,10 @@ import (
// ToJSON converts a Tag object into its JSON equivalent. // ToJSON converts a Tag object into its JSON equivalent.
func ToJSON(reader models.TagReader, tag *models.Tag) (*jsonschema.Tag, error) { func ToJSON(reader models.TagReader, tag *models.Tag) (*jsonschema.Tag, error) {
newTagJSON := jsonschema.Tag{ newTagJSON := jsonschema.Tag{
Name: tag.Name, Name: tag.Name,
CreatedAt: models.JSONTime{Time: tag.CreatedAt.Timestamp}, IgnoreAutoTag: tag.IgnoreAutoTag,
UpdatedAt: models.JSONTime{Time: tag.UpdatedAt.Timestamp}, CreatedAt: models.JSONTime{Time: tag.CreatedAt.Timestamp},
UpdatedAt: models.JSONTime{Time: tag.UpdatedAt.Timestamp},
} }
aliases, err := reader.GetAliases(tag.ID) aliases, err := reader.GetAliases(tag.ID)

View file

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

View file

@ -31,9 +31,10 @@ type Importer struct {
func (i *Importer) PreImport() error { func (i *Importer) PreImport() error {
i.tag = models.Tag{ i.tag = models.Tag{
Name: i.Input.Name, Name: i.Input.Name,
CreatedAt: models.SQLiteTimestamp{Timestamp: i.Input.CreatedAt.GetTime()}, IgnoreAutoTag: i.Input.IgnoreAutoTag,
UpdatedAt: models.SQLiteTimestamp{Timestamp: i.Input.UpdatedAt.GetTime()}, CreatedAt: models.SQLiteTimestamp{Timestamp: i.Input.CreatedAt.GetTime()},
UpdatedAt: models.SQLiteTimestamp{Timestamp: i.Input.UpdatedAt.GetTime()},
} }
var err error var err error

View file

@ -36,8 +36,9 @@ func TestImporterName(t *testing.T) {
func TestImporterPreImport(t *testing.T) { func TestImporterPreImport(t *testing.T) {
i := Importer{ i := Importer{
Input: jsonschema.Tag{ Input: jsonschema.Tag{
Name: tagName, Name: tagName,
Image: invalidImage, 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. ##### 💥 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 ### ✨ 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)) * Add python location in System Settings for script scrapers and plugins. ([#2409](https://github.com/stashapp/stash/pull/2409))
### 🎨 Improvements ### 🎨 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)) * 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)) * 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)) * 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", "hair_color",
"tattoos", "tattoos",
"piercings", "piercings",
"ignore_auto_tag",
]; ];
export const EditPerformersDialog: React.FC<IListOperationProps> = ( export const EditPerformersDialog: React.FC<IListOperationProps> = (
@ -302,6 +303,16 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
mode={tagIds.mode} mode={tagIds.mode}
/> />
</Form.Group> </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> </Form>
</Modal> </Modal>
); );

View file

@ -117,6 +117,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
death_date: yup.string().optional(), death_date: yup.string().optional(),
hair_color: yup.string().optional(), hair_color: yup.string().optional(),
weight: yup.number().optional(), weight: yup.number().optional(),
ignore_auto_tag: yup.boolean().optional(),
}); });
const initialValues = { const initialValues = {
@ -143,6 +144,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
death_date: performer.death_date ?? "", death_date: performer.death_date ?? "",
hair_color: performer.hair_color ?? "", hair_color: performer.hair_color ?? "",
weight: performer.weight ?? undefined, weight: performer.weight ?? undefined,
ignore_auto_tag: performer.ignore_auto_tag ?? false,
}; };
type InputValues = typeof initialValues; type InputValues = typeof initialValues;
@ -944,6 +946,22 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
{renderStashIDs()} {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")} {renderButtons("mt-3")}
</Form> </Form>
</> </>

View file

@ -55,6 +55,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
}, },
message: "aliases must be unique", message: "aliases must be unique",
}), }),
ignore_auto_tag: yup.boolean().optional(),
}); });
const initialValues = { const initialValues = {
@ -66,6 +67,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
parent_id: studio.parent_studio?.id, parent_id: studio.parent_studio?.id,
stash_ids: studio.stash_ids ?? undefined, stash_ids: studio.stash_ids ?? undefined,
aliases: studio.aliases, aliases: studio.aliases,
ignore_auto_tag: studio.ignore_auto_tag ?? false,
}; };
type InputValues = typeof initialValues; type InputValues = typeof initialValues;
@ -317,6 +319,22 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
</Form.Group> </Form.Group>
</Form> </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 <DetailsEditNavbar
objectName={studio?.name ?? intl.formatMessage({ id: "studio" })} objectName={studio?.name ?? intl.formatMessage({ id: "studio" })}
isNew={isNew} isNew={isNew}

View file

@ -53,6 +53,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
}), }),
parent_ids: yup.array(yup.string().required()).optional().nullable(), parent_ids: yup.array(yup.string().required()).optional().nullable(),
child_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 = { const initialValues = {
@ -60,6 +61,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
aliases: tag?.aliases, aliases: tag?.aliases,
parent_ids: (tag?.parents ?? []).map((t) => t.id), parent_ids: (tag?.parents ?? []).map((t) => t.id),
child_ids: (tag?.children ?? []).map((t) => t.id), child_ids: (tag?.children ?? []).map((t) => t.id),
ignore_auto_tag: tag?.ignore_auto_tag ?? false,
}; };
type InputValues = typeof initialValues; type InputValues = typeof initialValues;
@ -211,6 +213,22 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
/> />
</Col> </Col>
</Form.Group> </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> </Form>
<DetailsEditNavbar <DetailsEditNavbar

View file

@ -870,6 +870,11 @@ dl.details-list {
grid-template-columns: minmax(16.67%, auto) 1fr; 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 // Fix Safari styling on dropdowns
select { select {
-webkit-appearance: none; -webkit-appearance: none;

View file

@ -733,6 +733,7 @@
"hasMarkers": "Has Markers", "hasMarkers": "Has Markers",
"height": "Height", "height": "Height",
"help": "Help", "help": "Help",
"ignore_auto_tag": "Ignore Auto Tag",
"image": "Image", "image": "Image",
"image_count": "Image Count", "image_count": "Image Count",
"images": "Images", "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 { export class NumberCriterionOption extends CriterionOption {
constructor( constructor(
messageID: string, messageID: string,

View file

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

View file

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

View file

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

View file

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

View file

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