mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
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:
parent
2aee6cc18e
commit
61d9f57ce9
52 changed files with 477 additions and 206 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
3
pkg/database/migrations/30_ignore_autotag.up..sql
Normal file
3
pkg/database/migrations/30_ignore_autotag.up..sql
Normal 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';
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 != "" {
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue