mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Performer tags (#1132)
* Add scraping support for performer tags * Add performer count to tag cards * Refactor sqlite test setup * Add performer tag filtering in gallery and image * Add bulk update performer * Add Performers tab to tag page * Add count filters and sort bys for tags * Move scene count to icon in performer card #1148
This commit is contained in:
@@ -3,6 +3,11 @@ fragment SlimPerformerData on Performer {
|
|||||||
name
|
name
|
||||||
gender
|
gender
|
||||||
image_path
|
image_path
|
||||||
|
favorite
|
||||||
|
tags {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
stash_ids {
|
stash_ids {
|
||||||
endpoint
|
endpoint
|
||||||
stash_id
|
stash_id
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ fragment PerformerData on Performer {
|
|||||||
favorite
|
favorite
|
||||||
image_path
|
image_path
|
||||||
scene_count
|
scene_count
|
||||||
|
|
||||||
|
tags {
|
||||||
|
...TagData
|
||||||
|
}
|
||||||
|
|
||||||
stash_ids {
|
stash_ids {
|
||||||
stash_id
|
stash_id
|
||||||
endpoint
|
endpoint
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ fragment ScrapedPerformerData on ScrapedPerformer {
|
|||||||
tattoos
|
tattoos
|
||||||
piercings
|
piercings
|
||||||
aliases
|
aliases
|
||||||
|
tags {
|
||||||
|
...ScrapedSceneTagData
|
||||||
|
}
|
||||||
image
|
image
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +39,9 @@ fragment ScrapedScenePerformerData on ScrapedScenePerformer {
|
|||||||
tattoos
|
tattoos
|
||||||
piercings
|
piercings
|
||||||
aliases
|
aliases
|
||||||
|
tags {
|
||||||
|
...ScrapedSceneTagData
|
||||||
|
}
|
||||||
remote_site_id
|
remote_site_id
|
||||||
images
|
images
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ fragment TagData on Tag {
|
|||||||
image_path
|
image_path
|
||||||
scene_count
|
scene_count
|
||||||
scene_marker_count
|
scene_marker_count
|
||||||
|
performer_count
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ mutation PerformerCreate(
|
|||||||
$twitter: String,
|
$twitter: String,
|
||||||
$instagram: String,
|
$instagram: String,
|
||||||
$favorite: Boolean,
|
$favorite: Boolean,
|
||||||
|
$tag_ids: [ID!],
|
||||||
$stash_ids: [StashIDInput!],
|
$stash_ids: [StashIDInput!],
|
||||||
$image: String) {
|
$image: String) {
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ mutation PerformerCreate(
|
|||||||
twitter: $twitter,
|
twitter: $twitter,
|
||||||
instagram: $instagram,
|
instagram: $instagram,
|
||||||
favorite: $favorite,
|
favorite: $favorite,
|
||||||
|
tag_ids: $tag_ids,
|
||||||
stash_ids: $stash_ids,
|
stash_ids: $stash_ids,
|
||||||
image: $image
|
image: $image
|
||||||
}) {
|
}) {
|
||||||
@@ -52,6 +54,14 @@ mutation PerformerUpdate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mutation BulkPerformerUpdate(
|
||||||
|
$input: BulkPerformerUpdateInput!) {
|
||||||
|
|
||||||
|
bulkPerformerUpdate(input: $input) {
|
||||||
|
...PerformerData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mutation PerformerDestroy($id: ID!) {
|
mutation PerformerDestroy($id: ID!) {
|
||||||
performerDestroy(input: { id: $id })
|
performerDestroy(input: { id: $id })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ type Mutation {
|
|||||||
performerUpdate(input: PerformerUpdateInput!): Performer
|
performerUpdate(input: PerformerUpdateInput!): Performer
|
||||||
performerDestroy(input: PerformerDestroyInput!): Boolean!
|
performerDestroy(input: PerformerDestroyInput!): Boolean!
|
||||||
performersDestroy(ids: [ID!]!): Boolean!
|
performersDestroy(ids: [ID!]!): Boolean!
|
||||||
|
bulkPerformerUpdate(input: BulkPerformerUpdateInput!): [Performer!]
|
||||||
|
|
||||||
studioCreate(input: StudioCreateInput!): Studio
|
studioCreate(input: StudioCreateInput!): Studio
|
||||||
studioUpdate(input: StudioUpdateInput!): Studio
|
studioUpdate(input: StudioUpdateInput!): Studio
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ input PerformerFilterType {
|
|||||||
gender: GenderCriterionInput
|
gender: GenderCriterionInput
|
||||||
"""Filter to only include performers missing this property"""
|
"""Filter to only include performers missing this property"""
|
||||||
is_missing: String
|
is_missing: String
|
||||||
|
"""Filter to only include performers with these tags"""
|
||||||
|
tags: MultiCriterionInput
|
||||||
"""Filter by StashID"""
|
"""Filter by StashID"""
|
||||||
stash_id: String
|
stash_id: String
|
||||||
}
|
}
|
||||||
@@ -101,6 +103,8 @@ input SceneFilterType {
|
|||||||
movies: MultiCriterionInput
|
movies: MultiCriterionInput
|
||||||
"""Filter to only include scenes with these tags"""
|
"""Filter to only include scenes with these tags"""
|
||||||
tags: MultiCriterionInput
|
tags: MultiCriterionInput
|
||||||
|
"""Filter to only include scenes with performers with these tags"""
|
||||||
|
performer_tags: MultiCriterionInput
|
||||||
"""Filter to only include scenes with these performers"""
|
"""Filter to only include scenes with these performers"""
|
||||||
performers: MultiCriterionInput
|
performers: MultiCriterionInput
|
||||||
"""Filter by StashID"""
|
"""Filter by StashID"""
|
||||||
@@ -136,11 +140,13 @@ input GalleryFilterType {
|
|||||||
organized: Boolean
|
organized: Boolean
|
||||||
"""Filter by average image resolution"""
|
"""Filter by average image resolution"""
|
||||||
average_resolution: ResolutionEnum
|
average_resolution: ResolutionEnum
|
||||||
"""Filter to only include scenes with this studio"""
|
"""Filter to only include galleries with this studio"""
|
||||||
studios: MultiCriterionInput
|
studios: MultiCriterionInput
|
||||||
"""Filter to only include scenes with these tags"""
|
"""Filter to only include galleries with these tags"""
|
||||||
tags: MultiCriterionInput
|
tags: MultiCriterionInput
|
||||||
"""Filter to only include scenes with these performers"""
|
"""Filter to only include galleries with performers with these tags"""
|
||||||
|
performer_tags: MultiCriterionInput
|
||||||
|
"""Filter to only include galleries with these performers"""
|
||||||
performers: MultiCriterionInput
|
performers: MultiCriterionInput
|
||||||
"""Filter by number of images in this gallery"""
|
"""Filter by number of images in this gallery"""
|
||||||
image_count: IntCriterionInput
|
image_count: IntCriterionInput
|
||||||
@@ -153,6 +159,15 @@ input TagFilterType {
|
|||||||
"""Filter by number of scenes with this tag"""
|
"""Filter by number of scenes with this tag"""
|
||||||
scene_count: IntCriterionInput
|
scene_count: IntCriterionInput
|
||||||
|
|
||||||
|
"""Filter by number of images with this tag"""
|
||||||
|
image_count: IntCriterionInput
|
||||||
|
|
||||||
|
"""Filter by number of galleries with this tag"""
|
||||||
|
gallery_count: IntCriterionInput
|
||||||
|
|
||||||
|
"""Filter by number of performers with this tag"""
|
||||||
|
performer_count: IntCriterionInput
|
||||||
|
|
||||||
"""Filter by number of markers with this tag"""
|
"""Filter by number of markers with this tag"""
|
||||||
marker_count: IntCriterionInput
|
marker_count: IntCriterionInput
|
||||||
}
|
}
|
||||||
@@ -174,6 +189,8 @@ input ImageFilterType {
|
|||||||
studios: MultiCriterionInput
|
studios: MultiCriterionInput
|
||||||
"""Filter to only include images with these tags"""
|
"""Filter to only include images with these tags"""
|
||||||
tags: MultiCriterionInput
|
tags: MultiCriterionInput
|
||||||
|
"""Filter to only include images with performers with these tags"""
|
||||||
|
performer_tags: MultiCriterionInput
|
||||||
"""Filter to only include images with these performers"""
|
"""Filter to only include images with these performers"""
|
||||||
performers: MultiCriterionInput
|
performers: MultiCriterionInput
|
||||||
"""Filter to only include images with these galleries"""
|
"""Filter to only include images with these galleries"""
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type Performer {
|
|||||||
piercings: String
|
piercings: String
|
||||||
aliases: String
|
aliases: String
|
||||||
favorite: Boolean!
|
favorite: Boolean!
|
||||||
|
tags: [Tag!]!
|
||||||
|
|
||||||
image_path: String # Resolver
|
image_path: String # Resolver
|
||||||
scene_count: Int # Resolver
|
scene_count: Int # Resolver
|
||||||
@@ -52,6 +53,7 @@ input PerformerCreateInput {
|
|||||||
twitter: String
|
twitter: String
|
||||||
instagram: String
|
instagram: String
|
||||||
favorite: Boolean
|
favorite: Boolean
|
||||||
|
tag_ids: [ID!]
|
||||||
"""This should be base64 encoded"""
|
"""This should be base64 encoded"""
|
||||||
image: String
|
image: String
|
||||||
stash_ids: [StashIDInput!]
|
stash_ids: [StashIDInput!]
|
||||||
@@ -76,11 +78,34 @@ input PerformerUpdateInput {
|
|||||||
twitter: String
|
twitter: String
|
||||||
instagram: String
|
instagram: String
|
||||||
favorite: Boolean
|
favorite: Boolean
|
||||||
|
tag_ids: [ID!]
|
||||||
"""This should be base64 encoded"""
|
"""This should be base64 encoded"""
|
||||||
image: String
|
image: String
|
||||||
stash_ids: [StashIDInput!]
|
stash_ids: [StashIDInput!]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input BulkPerformerUpdateInput {
|
||||||
|
clientMutationId: String
|
||||||
|
ids: [ID!]
|
||||||
|
url: String
|
||||||
|
gender: GenderEnum
|
||||||
|
birthdate: String
|
||||||
|
ethnicity: String
|
||||||
|
country: String
|
||||||
|
eye_color: String
|
||||||
|
height: String
|
||||||
|
measurements: String
|
||||||
|
fake_tits: String
|
||||||
|
career_length: String
|
||||||
|
tattoos: String
|
||||||
|
piercings: String
|
||||||
|
aliases: String
|
||||||
|
twitter: String
|
||||||
|
instagram: String
|
||||||
|
favorite: Boolean
|
||||||
|
tag_ids: BulkUpdateIds
|
||||||
|
}
|
||||||
|
|
||||||
input PerformerDestroyInput {
|
input PerformerDestroyInput {
|
||||||
id: ID!
|
id: ID!
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ type ScrapedPerformer {
|
|||||||
tattoos: String
|
tattoos: String
|
||||||
piercings: String
|
piercings: String
|
||||||
aliases: String
|
aliases: String
|
||||||
|
# Should be ScrapedPerformerTag - but would be identical types
|
||||||
|
tags: [ScrapedSceneTag!]
|
||||||
|
|
||||||
"""This should be base64 encoded"""
|
"""This should be base64 encoded"""
|
||||||
image: String
|
image: String
|
||||||
@@ -39,5 +41,6 @@ input ScrapedPerformerInput {
|
|||||||
piercings: String
|
piercings: String
|
||||||
aliases: String
|
aliases: String
|
||||||
|
|
||||||
|
# not including tags for the input
|
||||||
# not including image for the input
|
# not including image for the input
|
||||||
}
|
}
|
||||||
@@ -45,6 +45,7 @@ type ScrapedScenePerformer {
|
|||||||
tattoos: String
|
tattoos: String
|
||||||
piercings: String
|
piercings: String
|
||||||
aliases: String
|
aliases: String
|
||||||
|
tags: [ScrapedSceneTag!]
|
||||||
|
|
||||||
remote_site_id: String
|
remote_site_id: String
|
||||||
images: [String!]
|
images: [String!]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ type Tag {
|
|||||||
image_path: String # Resolver
|
image_path: String # Resolver
|
||||||
scene_count: Int # Resolver
|
scene_count: Int # Resolver
|
||||||
scene_marker_count: Int # Resolver
|
scene_marker_count: Int # Resolver
|
||||||
|
performer_count: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
input TagCreateInput {
|
input TagCreateInput {
|
||||||
|
|||||||
@@ -138,6 +138,17 @@ func (r *performerResolver) ImagePath(ctx context.Context, obj *models.Performer
|
|||||||
return &imagePath, nil
|
return &imagePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *performerResolver) Tags(ctx context.Context, obj *models.Performer) (ret []*models.Tag, err error) {
|
||||||
|
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||||
|
ret, err = repo.Tag().FindByPerformerID(obj.ID)
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *performerResolver) SceneCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
|
func (r *performerResolver) SceneCount(ctx context.Context, obj *models.Performer) (ret *int, err error) {
|
||||||
var res int
|
var res int
|
||||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||||
|
|||||||
@@ -31,6 +31,18 @@ func (r *tagResolver) SceneMarkerCount(ctx context.Context, obj *models.Tag) (re
|
|||||||
return &count, err
|
return &count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *tagResolver) PerformerCount(ctx context.Context, obj *models.Tag) (ret *int, err error) {
|
||||||
|
var count int
|
||||||
|
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||||
|
count, err = repo.Performer().CountByTagID(obj.ID)
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &count, err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *tagResolver) ImagePath(ctx context.Context, obj *models.Tag) (*string, error) {
|
func (r *tagResolver) ImagePath(ctx context.Context, obj *models.Tag) (*string, error) {
|
||||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||||
imagePath := urlbuilders.NewTagURLBuilder(baseURL, obj.ID).GetTagImageURL()
|
imagePath := urlbuilders.NewTagURLBuilder(baseURL, obj.ID).GetTagImageURL()
|
||||||
|
|||||||
@@ -94,6 +94,12 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(input.TagIds) > 0 {
|
||||||
|
if err := r.updatePerformerTags(qb, performer.ID, input.TagIds); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// update image table
|
// update image table
|
||||||
if len(imageData) > 0 {
|
if len(imageData) > 0 {
|
||||||
if err := qb.UpdateImage(performer.ID, imageData); err != nil {
|
if err := qb.UpdateImage(performer.ID, imageData); err != nil {
|
||||||
@@ -183,6 +189,13 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save the tags
|
||||||
|
if translator.hasField("tag_ids") {
|
||||||
|
if err := r.updatePerformerTags(qb, performer.ID, input.TagIds); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// update image table
|
// update image table
|
||||||
if len(imageData) > 0 {
|
if len(imageData) > 0 {
|
||||||
if err := qb.UpdateImage(performer.ID, imageData); err != nil {
|
if err := qb.UpdateImage(performer.ID, imageData); err != nil {
|
||||||
@@ -211,6 +224,92 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
|||||||
return performer, nil
|
return performer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) updatePerformerTags(qb models.PerformerReaderWriter, performerID int, tagsIDs []string) error {
|
||||||
|
ids, err := utils.StringSliceToIntSlice(tagsIDs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return qb.UpdateTags(performerID, ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mutationResolver) BulkPerformerUpdate(ctx context.Context, input models.BulkPerformerUpdateInput) ([]*models.Performer, error) {
|
||||||
|
performerIDs, err := utils.StringSliceToIntSlice(input.Ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate performer from the input
|
||||||
|
updatedTime := time.Now()
|
||||||
|
|
||||||
|
translator := changesetTranslator{
|
||||||
|
inputMap: getUpdateInputMap(ctx),
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedPerformer := models.PerformerPartial{
|
||||||
|
UpdatedAt: &models.SQLiteTimestamp{Timestamp: updatedTime},
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedPerformer.URL = translator.nullString(input.URL, "url")
|
||||||
|
updatedPerformer.Birthdate = translator.sqliteDate(input.Birthdate, "birthdate")
|
||||||
|
updatedPerformer.Ethnicity = translator.nullString(input.Ethnicity, "ethnicity")
|
||||||
|
updatedPerformer.Country = translator.nullString(input.Country, "country")
|
||||||
|
updatedPerformer.EyeColor = translator.nullString(input.EyeColor, "eye_color")
|
||||||
|
updatedPerformer.Height = translator.nullString(input.Height, "height")
|
||||||
|
updatedPerformer.Measurements = translator.nullString(input.Measurements, "measurements")
|
||||||
|
updatedPerformer.FakeTits = translator.nullString(input.FakeTits, "fake_tits")
|
||||||
|
updatedPerformer.CareerLength = translator.nullString(input.CareerLength, "career_length")
|
||||||
|
updatedPerformer.Tattoos = translator.nullString(input.Tattoos, "tattoos")
|
||||||
|
updatedPerformer.Piercings = translator.nullString(input.Piercings, "piercings")
|
||||||
|
updatedPerformer.Aliases = translator.nullString(input.Aliases, "aliases")
|
||||||
|
updatedPerformer.Twitter = translator.nullString(input.Twitter, "twitter")
|
||||||
|
updatedPerformer.Instagram = translator.nullString(input.Instagram, "instagram")
|
||||||
|
updatedPerformer.Favorite = translator.nullBool(input.Favorite, "favorite")
|
||||||
|
|
||||||
|
if translator.hasField("gender") {
|
||||||
|
if input.Gender != nil {
|
||||||
|
updatedPerformer.Gender = &sql.NullString{String: input.Gender.String(), Valid: true}
|
||||||
|
} else {
|
||||||
|
updatedPerformer.Gender = &sql.NullString{String: "", Valid: false}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := []*models.Performer{}
|
||||||
|
|
||||||
|
// Start the transaction and save the scene marker
|
||||||
|
if err := r.withTxn(ctx, func(repo models.Repository) error {
|
||||||
|
qb := repo.Performer()
|
||||||
|
|
||||||
|
for _, performerID := range performerIDs {
|
||||||
|
updatedPerformer.ID = performerID
|
||||||
|
|
||||||
|
performer, err := qb.Update(updatedPerformer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = append(ret, performer)
|
||||||
|
|
||||||
|
// Save the tags
|
||||||
|
if translator.hasField("tag_ids") {
|
||||||
|
tagIDs, err := adjustTagIDs(qb, performerID, *input.TagIds)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := qb.UpdateTags(performerID, tagIDs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) PerformerDestroy(ctx context.Context, input models.PerformerDestroyInput) (bool, error) {
|
func (r *mutationResolver) PerformerDestroy(ctx context.Context, input models.PerformerDestroyInput) (bool, error) {
|
||||||
id, err := strconv.Atoi(input.ID)
|
id, err := strconv.Atoi(input.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul
|
|||||||
|
|
||||||
// Save the tags
|
// Save the tags
|
||||||
if translator.hasField("tag_ids") {
|
if translator.hasField("tag_ids") {
|
||||||
tagIDs, err := adjustSceneTagIDs(qb, sceneID, *input.TagIds)
|
tagIDs, err := adjustTagIDs(qb, sceneID, *input.TagIds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -330,7 +330,11 @@ func adjustScenePerformerIDs(qb models.SceneReader, sceneID int, ids models.Bulk
|
|||||||
return adjustIDs(ret, ids), nil
|
return adjustIDs(ret, ids), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func adjustSceneTagIDs(qb models.SceneReader, sceneID int, ids models.BulkUpdateIds) (ret []int, err error) {
|
type tagIDsGetter interface {
|
||||||
|
GetTagIDs(id int) ([]int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func adjustTagIDs(qb tagIDsGetter, sceneID int, ids models.BulkUpdateIds) (ret []int, err error) {
|
||||||
ret, err = qb.GetTagIDs(sceneID)
|
ret, err = qb.GetTagIDs(sceneID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
|
|
||||||
var DB *sqlx.DB
|
var DB *sqlx.DB
|
||||||
var dbPath string
|
var dbPath string
|
||||||
var appSchemaVersion uint = 18
|
var appSchemaVersion uint = 19
|
||||||
var databaseSchemaVersion uint
|
var databaseSchemaVersion uint
|
||||||
|
|
||||||
const sqlite3Driver = "sqlite3ex"
|
const sqlite3Driver = "sqlite3ex"
|
||||||
|
|||||||
9
pkg/database/migrations/19_performer_tags.up.sql
Normal file
9
pkg/database/migrations/19_performer_tags.up.sql
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE `performers_tags` (
|
||||||
|
`performer_id` integer NOT NULL,
|
||||||
|
`tag_id` integer NOT NULL,
|
||||||
|
foreign key(`performer_id`) references `performers`(`id`) on delete CASCADE,
|
||||||
|
foreign key(`tag_id`) references `tags`(`id`) on delete CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX `index_performers_tags_on_tag_id` on `performers_tags` (`tag_id`);
|
||||||
|
CREATE INDEX `index_performers_tags_on_performer_id` on `performers_tags` (`performer_id`);
|
||||||
@@ -2,9 +2,9 @@ package jsonschema
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/json-iterator/go"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ type Performer struct {
|
|||||||
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"`
|
||||||
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"`
|
||||||
|
|||||||
@@ -725,6 +725,18 @@ func (t *ExportTask) exportPerformer(wg *sync.WaitGroup, jobChan <-chan *models.
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tags, err := repo.Tag().FindByPerformerID(p.ID)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("[performers] <%s> error getting performer tags: %s", p.Checksum, err.Error())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
newPerformerJSON.Tags = tag.GetNames(tags)
|
||||||
|
|
||||||
|
if t.includeDependencies {
|
||||||
|
t.tags.IDs = utils.IntAppendUniques(t.tags.IDs, tag.GetIDs(tags))
|
||||||
|
}
|
||||||
|
|
||||||
performerJSON, err := t.json.getPerformer(p.Checksum)
|
performerJSON, err := t.json.getPerformer(p.Checksum)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Debugf("[performers] error reading performer json: %s", err.Error())
|
logger.Debugf("[performers] error reading performer json: %s", err.Error())
|
||||||
|
|||||||
@@ -300,8 +300,8 @@ func (_m *GalleryReaderWriter) GetPerformerIDs(galleryID int) ([]int, error) {
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTagIDs provides a mock function with given fields: galleryID
|
// GetSceneIDs provides a mock function with given fields: galleryID
|
||||||
func (_m *GalleryReaderWriter) GetTagIDs(galleryID int) ([]int, error) {
|
func (_m *GalleryReaderWriter) GetSceneIDs(galleryID int) ([]int, error) {
|
||||||
ret := _m.Called(galleryID)
|
ret := _m.Called(galleryID)
|
||||||
|
|
||||||
var r0 []int
|
var r0 []int
|
||||||
@@ -323,8 +323,8 @@ func (_m *GalleryReaderWriter) GetTagIDs(galleryID int) ([]int, error) {
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSceneIDs provides a mock function with given fields: galleryID
|
// GetTagIDs provides a mock function with given fields: galleryID
|
||||||
func (_m *GalleryReaderWriter) GetSceneIDs(galleryID int) ([]int, error) {
|
func (_m *GalleryReaderWriter) GetTagIDs(galleryID int) ([]int, error) {
|
||||||
ret := _m.Called(galleryID)
|
ret := _m.Called(galleryID)
|
||||||
|
|
||||||
var r0 []int
|
var r0 []int
|
||||||
@@ -464,20 +464,6 @@ func (_m *GalleryReaderWriter) UpdatePerformers(galleryID int, performerIDs []in
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateTags provides a mock function with given fields: galleryID, tagIDs
|
|
||||||
func (_m *GalleryReaderWriter) UpdateTags(galleryID int, tagIDs []int) error {
|
|
||||||
ret := _m.Called(galleryID, tagIDs)
|
|
||||||
|
|
||||||
var r0 error
|
|
||||||
if rf, ok := ret.Get(0).(func(int, []int) error); ok {
|
|
||||||
r0 = rf(galleryID, tagIDs)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateScenes provides a mock function with given fields: galleryID, sceneIDs
|
// UpdateScenes provides a mock function with given fields: galleryID, sceneIDs
|
||||||
func (_m *GalleryReaderWriter) UpdateScenes(galleryID int, sceneIDs []int) error {
|
func (_m *GalleryReaderWriter) UpdateScenes(galleryID int, sceneIDs []int) error {
|
||||||
ret := _m.Called(galleryID, sceneIDs)
|
ret := _m.Called(galleryID, sceneIDs)
|
||||||
@@ -491,3 +477,17 @@ func (_m *GalleryReaderWriter) UpdateScenes(galleryID int, sceneIDs []int) error
|
|||||||
|
|
||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateTags provides a mock function with given fields: galleryID, tagIDs
|
||||||
|
func (_m *GalleryReaderWriter) UpdateTags(galleryID int, tagIDs []int) error {
|
||||||
|
ret := _m.Called(galleryID, tagIDs)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(int, []int) error); ok {
|
||||||
|
r0 = rf(galleryID, tagIDs)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|||||||
@@ -79,6 +79,27 @@ func (_m *PerformerReaderWriter) Count() (int, error) {
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CountByTagID provides a mock function with given fields: tagID
|
||||||
|
func (_m *PerformerReaderWriter) CountByTagID(tagID int) (int, error) {
|
||||||
|
ret := _m.Called(tagID)
|
||||||
|
|
||||||
|
var r0 int
|
||||||
|
if rf, ok := ret.Get(0).(func(int) int); ok {
|
||||||
|
r0 = rf(tagID)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(int)
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(int) error); ok {
|
||||||
|
r1 = rf(tagID)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// Create provides a mock function with given fields: newPerformer
|
// Create provides a mock function with given fields: newPerformer
|
||||||
func (_m *PerformerReaderWriter) Create(newPerformer models.Performer) (*models.Performer, error) {
|
func (_m *PerformerReaderWriter) Create(newPerformer models.Performer) (*models.Performer, error) {
|
||||||
ret := _m.Called(newPerformer)
|
ret := _m.Called(newPerformer)
|
||||||
@@ -337,6 +358,29 @@ func (_m *PerformerReaderWriter) GetStashIDs(performerID int) ([]*models.StashID
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTagIDs provides a mock function with given fields: sceneID
|
||||||
|
func (_m *PerformerReaderWriter) GetTagIDs(sceneID int) ([]int, error) {
|
||||||
|
ret := _m.Called(sceneID)
|
||||||
|
|
||||||
|
var r0 []int
|
||||||
|
if rf, ok := ret.Get(0).(func(int) []int); ok {
|
||||||
|
r0 = rf(sceneID)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]int)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(int) error); ok {
|
||||||
|
r1 = rf(sceneID)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// Query provides a mock function with given fields: performerFilter, findFilter
|
// Query provides a mock function with given fields: performerFilter, findFilter
|
||||||
func (_m *PerformerReaderWriter) Query(performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error) {
|
func (_m *PerformerReaderWriter) Query(performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error) {
|
||||||
ret := _m.Called(performerFilter, findFilter)
|
ret := _m.Called(performerFilter, findFilter)
|
||||||
@@ -440,3 +484,17 @@ func (_m *PerformerReaderWriter) UpdateStashIDs(performerID int, stashIDs []mode
|
|||||||
|
|
||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateTags provides a mock function with given fields: sceneID, tagIDs
|
||||||
|
func (_m *PerformerReaderWriter) UpdateTags(sceneID int, tagIDs []int) error {
|
||||||
|
ret := _m.Called(sceneID, tagIDs)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(int, []int) error); ok {
|
||||||
|
r0 = rf(sceneID, tagIDs)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|||||||
@@ -300,6 +300,29 @@ func (_m *SceneReaderWriter) FindByChecksum(checksum string) (*models.Scene, err
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindByGalleryID provides a mock function with given fields: performerID
|
||||||
|
func (_m *SceneReaderWriter) FindByGalleryID(performerID int) ([]*models.Scene, error) {
|
||||||
|
ret := _m.Called(performerID)
|
||||||
|
|
||||||
|
var r0 []*models.Scene
|
||||||
|
if rf, ok := ret.Get(0).(func(int) []*models.Scene); ok {
|
||||||
|
r0 = rf(performerID)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*models.Scene)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(int) error); ok {
|
||||||
|
r1 = rf(performerID)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// FindByMovieID provides a mock function with given fields: movieID
|
// FindByMovieID provides a mock function with given fields: movieID
|
||||||
func (_m *SceneReaderWriter) FindByMovieID(movieID int) ([]*models.Scene, error) {
|
func (_m *SceneReaderWriter) FindByMovieID(movieID int) ([]*models.Scene, error) {
|
||||||
ret := _m.Called(movieID)
|
ret := _m.Called(movieID)
|
||||||
@@ -392,29 +415,6 @@ func (_m *SceneReaderWriter) FindByPerformerID(performerID int) ([]*models.Scene
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindByGalleryID provides a mock function with given fields: galleryID
|
|
||||||
func (_m *SceneReaderWriter) FindByGalleryID(galleryID int) ([]*models.Scene, error) {
|
|
||||||
ret := _m.Called(galleryID)
|
|
||||||
|
|
||||||
var r0 []*models.Scene
|
|
||||||
if rf, ok := ret.Get(0).(func(int) []*models.Scene); ok {
|
|
||||||
r0 = rf(galleryID)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).([]*models.Scene)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(int) error); ok {
|
|
||||||
r1 = rf(galleryID)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindMany provides a mock function with given fields: ids
|
// FindMany provides a mock function with given fields: ids
|
||||||
func (_m *SceneReaderWriter) FindMany(ids []int) ([]*models.Scene, error) {
|
func (_m *SceneReaderWriter) FindMany(ids []int) ([]*models.Scene, error) {
|
||||||
ret := _m.Called(ids)
|
ret := _m.Called(ids)
|
||||||
@@ -461,6 +461,29 @@ func (_m *SceneReaderWriter) GetCover(sceneID int) ([]byte, error) {
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetGalleryIDs provides a mock function with given fields: sceneID
|
||||||
|
func (_m *SceneReaderWriter) GetGalleryIDs(sceneID int) ([]int, error) {
|
||||||
|
ret := _m.Called(sceneID)
|
||||||
|
|
||||||
|
var r0 []int
|
||||||
|
if rf, ok := ret.Get(0).(func(int) []int); ok {
|
||||||
|
r0 = rf(sceneID)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]int)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(int) error); ok {
|
||||||
|
r1 = rf(sceneID)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// GetMovies provides a mock function with given fields: sceneID
|
// GetMovies provides a mock function with given fields: sceneID
|
||||||
func (_m *SceneReaderWriter) GetMovies(sceneID int) ([]models.MoviesScenes, error) {
|
func (_m *SceneReaderWriter) GetMovies(sceneID int) ([]models.MoviesScenes, error) {
|
||||||
ret := _m.Called(sceneID)
|
ret := _m.Called(sceneID)
|
||||||
@@ -507,8 +530,31 @@ func (_m *SceneReaderWriter) GetPerformerIDs(sceneID int) ([]int, error) {
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGalleryIDs provides a mock function with given fields: sceneID
|
// GetStashIDs provides a mock function with given fields: sceneID
|
||||||
func (_m *SceneReaderWriter) GetGalleryIDs(sceneID int) ([]int, error) {
|
func (_m *SceneReaderWriter) GetStashIDs(sceneID int) ([]*models.StashID, error) {
|
||||||
|
ret := _m.Called(sceneID)
|
||||||
|
|
||||||
|
var r0 []*models.StashID
|
||||||
|
if rf, ok := ret.Get(0).(func(int) []*models.StashID); ok {
|
||||||
|
r0 = rf(sceneID)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*models.StashID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(int) error); ok {
|
||||||
|
r1 = rf(sceneID)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTagIDs provides a mock function with given fields: sceneID
|
||||||
|
func (_m *SceneReaderWriter) GetTagIDs(sceneID int) ([]int, error) {
|
||||||
ret := _m.Called(sceneID)
|
ret := _m.Called(sceneID)
|
||||||
|
|
||||||
var r0 []int
|
var r0 []int
|
||||||
@@ -530,52 +576,6 @@ func (_m *SceneReaderWriter) GetGalleryIDs(sceneID int) ([]int, error) {
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStashIDs provides a mock function with given fields: performerID
|
|
||||||
func (_m *SceneReaderWriter) GetStashIDs(performerID int) ([]*models.StashID, error) {
|
|
||||||
ret := _m.Called(performerID)
|
|
||||||
|
|
||||||
var r0 []*models.StashID
|
|
||||||
if rf, ok := ret.Get(0).(func(int) []*models.StashID); ok {
|
|
||||||
r0 = rf(performerID)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).([]*models.StashID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(int) error); ok {
|
|
||||||
r1 = rf(performerID)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTagIDs provides a mock function with given fields: imageID
|
|
||||||
func (_m *SceneReaderWriter) GetTagIDs(imageID int) ([]int, error) {
|
|
||||||
ret := _m.Called(imageID)
|
|
||||||
|
|
||||||
var r0 []int
|
|
||||||
if rf, ok := ret.Get(0).(func(int) []int); ok {
|
|
||||||
r0 = rf(imageID)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).([]int)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 error
|
|
||||||
if rf, ok := ret.Get(1).(func(int) error); ok {
|
|
||||||
r1 = rf(imageID)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Error(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncrementOCounter provides a mock function with given fields: id
|
// IncrementOCounter provides a mock function with given fields: id
|
||||||
func (_m *SceneReaderWriter) IncrementOCounter(id int) (int, error) {
|
func (_m *SceneReaderWriter) IncrementOCounter(id int) (int, error) {
|
||||||
ret := _m.Called(id)
|
ret := _m.Called(id)
|
||||||
@@ -766,6 +766,20 @@ func (_m *SceneReaderWriter) UpdateFull(updatedScene models.Scene) (*models.Scen
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateGalleries provides a mock function with given fields: sceneID, galleryIDs
|
||||||
|
func (_m *SceneReaderWriter) UpdateGalleries(sceneID int, galleryIDs []int) error {
|
||||||
|
ret := _m.Called(sceneID, galleryIDs)
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(int, []int) error); ok {
|
||||||
|
r0 = rf(sceneID, galleryIDs)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateMovies provides a mock function with given fields: sceneID, movies
|
// UpdateMovies provides a mock function with given fields: sceneID, movies
|
||||||
func (_m *SceneReaderWriter) UpdateMovies(sceneID int, movies []models.MoviesScenes) error {
|
func (_m *SceneReaderWriter) UpdateMovies(sceneID int, movies []models.MoviesScenes) error {
|
||||||
ret := _m.Called(sceneID, movies)
|
ret := _m.Called(sceneID, movies)
|
||||||
@@ -794,20 +808,6 @@ func (_m *SceneReaderWriter) UpdatePerformers(sceneID int, performerIDs []int) e
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateGalleries provides a mock function with given fields: sceneID, galleryIDs
|
|
||||||
func (_m *SceneReaderWriter) UpdateGalleries(sceneID int, galleryIDs []int) error {
|
|
||||||
ret := _m.Called(sceneID, galleryIDs)
|
|
||||||
|
|
||||||
var r0 error
|
|
||||||
if rf, ok := ret.Get(0).(func(int, []int) error); ok {
|
|
||||||
r0 = rf(sceneID, galleryIDs)
|
|
||||||
} else {
|
|
||||||
r0 = ret.Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateStashIDs provides a mock function with given fields: sceneID, stashIDs
|
// UpdateStashIDs provides a mock function with given fields: sceneID, stashIDs
|
||||||
func (_m *SceneReaderWriter) UpdateStashIDs(sceneID int, stashIDs []models.StashID) error {
|
func (_m *SceneReaderWriter) UpdateStashIDs(sceneID int, stashIDs []models.StashID) error {
|
||||||
ret := _m.Called(sceneID, stashIDs)
|
ret := _m.Called(sceneID, stashIDs)
|
||||||
|
|||||||
@@ -245,6 +245,29 @@ func (_m *TagReaderWriter) FindByNames(names []string, nocase bool) ([]*models.T
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindByPerformerID provides a mock function with given fields: performerID
|
||||||
|
func (_m *TagReaderWriter) FindByPerformerID(performerID int) ([]*models.Tag, error) {
|
||||||
|
ret := _m.Called(performerID)
|
||||||
|
|
||||||
|
var r0 []*models.Tag
|
||||||
|
if rf, ok := ret.Get(0).(func(int) []*models.Tag); ok {
|
||||||
|
r0 = rf(performerID)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).([]*models.Tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(1).(func(int) error); ok {
|
||||||
|
r1 = rf(performerID)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// FindBySceneID provides a mock function with given fields: sceneID
|
// FindBySceneID provides a mock function with given fields: sceneID
|
||||||
func (_m *TagReaderWriter) FindBySceneID(sceneID int) ([]*models.Tag, error) {
|
func (_m *TagReaderWriter) FindBySceneID(sceneID int) ([]*models.Tag, error) {
|
||||||
ret := _m.Called(sceneID)
|
ret := _m.Called(sceneID)
|
||||||
|
|||||||
@@ -24,43 +24,45 @@ type ScrapedItem struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ScrapedPerformer struct {
|
type ScrapedPerformer struct {
|
||||||
Name *string `graphql:"name" json:"name"`
|
Name *string `graphql:"name" json:"name"`
|
||||||
Gender *string `graphql:"gender" json:"gender"`
|
Gender *string `graphql:"gender" json:"gender"`
|
||||||
URL *string `graphql:"url" json:"url"`
|
URL *string `graphql:"url" json:"url"`
|
||||||
Twitter *string `graphql:"twitter" json:"twitter"`
|
Twitter *string `graphql:"twitter" json:"twitter"`
|
||||||
Instagram *string `graphql:"instagram" json:"instagram"`
|
Instagram *string `graphql:"instagram" json:"instagram"`
|
||||||
Birthdate *string `graphql:"birthdate" json:"birthdate"`
|
Birthdate *string `graphql:"birthdate" json:"birthdate"`
|
||||||
Ethnicity *string `graphql:"ethnicity" json:"ethnicity"`
|
Ethnicity *string `graphql:"ethnicity" json:"ethnicity"`
|
||||||
Country *string `graphql:"country" json:"country"`
|
Country *string `graphql:"country" json:"country"`
|
||||||
EyeColor *string `graphql:"eye_color" json:"eye_color"`
|
EyeColor *string `graphql:"eye_color" json:"eye_color"`
|
||||||
Height *string `graphql:"height" json:"height"`
|
Height *string `graphql:"height" json:"height"`
|
||||||
Measurements *string `graphql:"measurements" json:"measurements"`
|
Measurements *string `graphql:"measurements" json:"measurements"`
|
||||||
FakeTits *string `graphql:"fake_tits" json:"fake_tits"`
|
FakeTits *string `graphql:"fake_tits" json:"fake_tits"`
|
||||||
CareerLength *string `graphql:"career_length" json:"career_length"`
|
CareerLength *string `graphql:"career_length" json:"career_length"`
|
||||||
Tattoos *string `graphql:"tattoos" json:"tattoos"`
|
Tattoos *string `graphql:"tattoos" json:"tattoos"`
|
||||||
Piercings *string `graphql:"piercings" json:"piercings"`
|
Piercings *string `graphql:"piercings" json:"piercings"`
|
||||||
Aliases *string `graphql:"aliases" json:"aliases"`
|
Aliases *string `graphql:"aliases" json:"aliases"`
|
||||||
Image *string `graphql:"image" json:"image"`
|
Tags []*ScrapedSceneTag `graphql:"tags" json:"tags"`
|
||||||
|
Image *string `graphql:"image" json:"image"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// this type has no Image field
|
// this type has no Image field
|
||||||
type ScrapedPerformerStash struct {
|
type ScrapedPerformerStash struct {
|
||||||
Name *string `graphql:"name" json:"name"`
|
Name *string `graphql:"name" json:"name"`
|
||||||
Gender *string `graphql:"gender" json:"gender"`
|
Gender *string `graphql:"gender" json:"gender"`
|
||||||
URL *string `graphql:"url" json:"url"`
|
URL *string `graphql:"url" json:"url"`
|
||||||
Twitter *string `graphql:"twitter" json:"twitter"`
|
Twitter *string `graphql:"twitter" json:"twitter"`
|
||||||
Instagram *string `graphql:"instagram" json:"instagram"`
|
Instagram *string `graphql:"instagram" json:"instagram"`
|
||||||
Birthdate *string `graphql:"birthdate" json:"birthdate"`
|
Birthdate *string `graphql:"birthdate" json:"birthdate"`
|
||||||
Ethnicity *string `graphql:"ethnicity" json:"ethnicity"`
|
Ethnicity *string `graphql:"ethnicity" json:"ethnicity"`
|
||||||
Country *string `graphql:"country" json:"country"`
|
Country *string `graphql:"country" json:"country"`
|
||||||
EyeColor *string `graphql:"eye_color" json:"eye_color"`
|
EyeColor *string `graphql:"eye_color" json:"eye_color"`
|
||||||
Height *string `graphql:"height" json:"height"`
|
Height *string `graphql:"height" json:"height"`
|
||||||
Measurements *string `graphql:"measurements" json:"measurements"`
|
Measurements *string `graphql:"measurements" json:"measurements"`
|
||||||
FakeTits *string `graphql:"fake_tits" json:"fake_tits"`
|
FakeTits *string `graphql:"fake_tits" json:"fake_tits"`
|
||||||
CareerLength *string `graphql:"career_length" json:"career_length"`
|
CareerLength *string `graphql:"career_length" json:"career_length"`
|
||||||
Tattoos *string `graphql:"tattoos" json:"tattoos"`
|
Tattoos *string `graphql:"tattoos" json:"tattoos"`
|
||||||
Piercings *string `graphql:"piercings" json:"piercings"`
|
Piercings *string `graphql:"piercings" json:"piercings"`
|
||||||
Aliases *string `graphql:"aliases" json:"aliases"`
|
Aliases *string `graphql:"aliases" json:"aliases"`
|
||||||
|
Tags []*ScrapedSceneTag `graphql:"tags" json:"tags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScrapedScene struct {
|
type ScrapedScene struct {
|
||||||
@@ -106,25 +108,26 @@ type ScrapedGalleryStash struct {
|
|||||||
|
|
||||||
type ScrapedScenePerformer struct {
|
type ScrapedScenePerformer struct {
|
||||||
// Set if performer matched
|
// Set if performer matched
|
||||||
ID *string `graphql:"id" json:"id"`
|
ID *string `graphql:"id" json:"id"`
|
||||||
Name string `graphql:"name" json:"name"`
|
Name string `graphql:"name" json:"name"`
|
||||||
Gender *string `graphql:"gender" json:"gender"`
|
Gender *string `graphql:"gender" json:"gender"`
|
||||||
URL *string `graphql:"url" json:"url"`
|
URL *string `graphql:"url" json:"url"`
|
||||||
Twitter *string `graphql:"twitter" json:"twitter"`
|
Twitter *string `graphql:"twitter" json:"twitter"`
|
||||||
Instagram *string `graphql:"instagram" json:"instagram"`
|
Instagram *string `graphql:"instagram" json:"instagram"`
|
||||||
Birthdate *string `graphql:"birthdate" json:"birthdate"`
|
Birthdate *string `graphql:"birthdate" json:"birthdate"`
|
||||||
Ethnicity *string `graphql:"ethnicity" json:"ethnicity"`
|
Ethnicity *string `graphql:"ethnicity" json:"ethnicity"`
|
||||||
Country *string `graphql:"country" json:"country"`
|
Country *string `graphql:"country" json:"country"`
|
||||||
EyeColor *string `graphql:"eye_color" json:"eye_color"`
|
EyeColor *string `graphql:"eye_color" json:"eye_color"`
|
||||||
Height *string `graphql:"height" json:"height"`
|
Height *string `graphql:"height" json:"height"`
|
||||||
Measurements *string `graphql:"measurements" json:"measurements"`
|
Measurements *string `graphql:"measurements" json:"measurements"`
|
||||||
FakeTits *string `graphql:"fake_tits" json:"fake_tits"`
|
FakeTits *string `graphql:"fake_tits" json:"fake_tits"`
|
||||||
CareerLength *string `graphql:"career_length" json:"career_length"`
|
CareerLength *string `graphql:"career_length" json:"career_length"`
|
||||||
Tattoos *string `graphql:"tattoos" json:"tattoos"`
|
Tattoos *string `graphql:"tattoos" json:"tattoos"`
|
||||||
Piercings *string `graphql:"piercings" json:"piercings"`
|
Piercings *string `graphql:"piercings" json:"piercings"`
|
||||||
Aliases *string `graphql:"aliases" json:"aliases"`
|
Aliases *string `graphql:"aliases" json:"aliases"`
|
||||||
RemoteSiteID *string `graphql:"remote_site_id" json:"remote_site_id"`
|
Tags []*ScrapedSceneTag `graphql:"tags" json:"tags"`
|
||||||
Images []string `graphql:"images" json:"images"`
|
RemoteSiteID *string `graphql:"remote_site_id" json:"remote_site_id"`
|
||||||
|
Images []string `graphql:"images" json:"images"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScrapedSceneStudio struct {
|
type ScrapedSceneStudio struct {
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ type PerformerReader interface {
|
|||||||
FindByImageID(imageID int) ([]*Performer, error)
|
FindByImageID(imageID int) ([]*Performer, error)
|
||||||
FindByGalleryID(galleryID int) ([]*Performer, error)
|
FindByGalleryID(galleryID int) ([]*Performer, error)
|
||||||
FindByNames(names []string, nocase bool) ([]*Performer, error)
|
FindByNames(names []string, nocase bool) ([]*Performer, error)
|
||||||
|
CountByTagID(tagID int) (int, error)
|
||||||
Count() (int, error)
|
Count() (int, error)
|
||||||
All() ([]*Performer, error)
|
All() ([]*Performer, error)
|
||||||
AllSlim() ([]*Performer, error)
|
AllSlim() ([]*Performer, error)
|
||||||
Query(performerFilter *PerformerFilterType, findFilter *FindFilterType) ([]*Performer, int, error)
|
Query(performerFilter *PerformerFilterType, findFilter *FindFilterType) ([]*Performer, int, error)
|
||||||
GetImage(performerID int) ([]byte, error)
|
GetImage(performerID int) ([]byte, error)
|
||||||
GetStashIDs(performerID int) ([]*StashID, error)
|
GetStashIDs(performerID int) ([]*StashID, error)
|
||||||
|
GetTagIDs(sceneID int) ([]int, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type PerformerWriter interface {
|
type PerformerWriter interface {
|
||||||
@@ -24,6 +26,7 @@ type PerformerWriter interface {
|
|||||||
UpdateImage(performerID int, image []byte) error
|
UpdateImage(performerID int, image []byte) error
|
||||||
DestroyImage(performerID int) error
|
DestroyImage(performerID int) error
|
||||||
UpdateStashIDs(performerID int, stashIDs []StashID) error
|
UpdateStashIDs(performerID int, stashIDs []StashID) error
|
||||||
|
UpdateTags(sceneID int, tagIDs []int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type PerformerReaderWriter interface {
|
type PerformerReaderWriter interface {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ type TagReader interface {
|
|||||||
Find(id int) (*Tag, error)
|
Find(id int) (*Tag, error)
|
||||||
FindMany(ids []int) ([]*Tag, error)
|
FindMany(ids []int) ([]*Tag, error)
|
||||||
FindBySceneID(sceneID int) ([]*Tag, error)
|
FindBySceneID(sceneID int) ([]*Tag, error)
|
||||||
|
FindByPerformerID(performerID int) ([]*Tag, error)
|
||||||
FindBySceneMarkerID(sceneMarkerID int) ([]*Tag, error)
|
FindBySceneMarkerID(sceneMarkerID int) ([]*Tag, error)
|
||||||
FindByImageID(imageID int) ([]*Tag, error)
|
FindByImageID(imageID int) ([]*Tag, error)
|
||||||
FindByGalleryID(galleryID int) ([]*Tag, error)
|
FindByGalleryID(galleryID int) ([]*Tag, error)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package performer
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/manager/jsonschema"
|
"github.com/stashapp/stash/pkg/manager/jsonschema"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
@@ -10,16 +11,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Importer struct {
|
type Importer struct {
|
||||||
ReaderWriter models.PerformerReaderWriter
|
ReaderWriter models.PerformerReaderWriter
|
||||||
Input jsonschema.Performer
|
TagWriter models.TagReaderWriter
|
||||||
|
Input jsonschema.Performer
|
||||||
|
MissingRefBehaviour models.ImportMissingRefEnum
|
||||||
|
|
||||||
|
ID int
|
||||||
performer models.Performer
|
performer models.Performer
|
||||||
imageData []byte
|
imageData []byte
|
||||||
|
|
||||||
|
tags []*models.Tag
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Importer) PreImport() error {
|
func (i *Importer) PreImport() error {
|
||||||
i.performer = performerJSONToPerformer(i.Input)
|
i.performer = performerJSONToPerformer(i.Input)
|
||||||
|
|
||||||
|
if err := i.populateTags(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
if len(i.Input.Image) > 0 {
|
if len(i.Input.Image) > 0 {
|
||||||
_, i.imageData, err = utils.ProcessBase64Image(i.Input.Image)
|
_, i.imageData, err = utils.ProcessBase64Image(i.Input.Image)
|
||||||
@@ -31,7 +41,82 @@ func (i *Importer) PreImport() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Importer) populateTags() error {
|
||||||
|
if len(i.Input.Tags) > 0 {
|
||||||
|
|
||||||
|
tags, err := importTags(i.TagWriter, i.Input.Tags, i.MissingRefBehaviour)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
i.tags = tags
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func importTags(tagWriter models.TagReaderWriter, names []string, missingRefBehaviour models.ImportMissingRefEnum) ([]*models.Tag, error) {
|
||||||
|
tags, err := tagWriter.FindByNames(names, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluckedNames []string
|
||||||
|
for _, tag := range tags {
|
||||||
|
pluckedNames = append(pluckedNames, tag.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
missingTags := utils.StrFilter(names, func(name string) bool {
|
||||||
|
return !utils.StrInclude(pluckedNames, name)
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(missingTags) > 0 {
|
||||||
|
if missingRefBehaviour == models.ImportMissingRefEnumFail {
|
||||||
|
return nil, fmt.Errorf("tags [%s] not found", strings.Join(missingTags, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if missingRefBehaviour == models.ImportMissingRefEnumCreate {
|
||||||
|
createdTags, err := createTags(tagWriter, missingTags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating tags: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
tags = append(tags, createdTags...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore if MissingRefBehaviour set to Ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTags(tagWriter models.TagWriter, names []string) ([]*models.Tag, error) {
|
||||||
|
var ret []*models.Tag
|
||||||
|
for _, name := range names {
|
||||||
|
newTag := *models.NewTag(name)
|
||||||
|
|
||||||
|
created, err := tagWriter.Create(newTag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = append(ret, created)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (i *Importer) PostImport(id int) error {
|
func (i *Importer) PostImport(id int) error {
|
||||||
|
if len(i.tags) > 0 {
|
||||||
|
var tagIDs []int
|
||||||
|
for _, t := range i.tags {
|
||||||
|
tagIDs = append(tagIDs, t.ID)
|
||||||
|
}
|
||||||
|
if err := i.ReaderWriter.UpdateTags(id, tagIDs); err != nil {
|
||||||
|
return fmt.Errorf("failed to associate tags: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(i.imageData) > 0 {
|
if len(i.imageData) > 0 {
|
||||||
if err := i.ReaderWriter.UpdateImage(id, i.imageData); err != nil {
|
if err := i.ReaderWriter.UpdateImage(id, i.imageData); err != nil {
|
||||||
return fmt.Errorf("error setting performer image: %s", err.Error())
|
return fmt.Errorf("error setting performer image: %s", err.Error())
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package performer
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/manager/jsonschema"
|
"github.com/stashapp/stash/pkg/manager/jsonschema"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/models/mocks"
|
"github.com/stashapp/stash/pkg/models/mocks"
|
||||||
@@ -16,9 +18,15 @@ const invalidImage = "aW1hZ2VCeXRlcw&&"
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
existingPerformerID = 100
|
existingPerformerID = 100
|
||||||
|
existingTagID = 105
|
||||||
|
errTagsID = 106
|
||||||
|
|
||||||
existingPerformerName = "existingPerformerName"
|
existingPerformerName = "existingPerformerName"
|
||||||
performerNameErr = "performerNameErr"
|
performerNameErr = "performerNameErr"
|
||||||
|
|
||||||
|
existingTagName = "existingTagName"
|
||||||
|
existingTagErr = "existingTagErr"
|
||||||
|
missingTagName = "missingTagName"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestImporterName(t *testing.T) {
|
func TestImporterName(t *testing.T) {
|
||||||
@@ -53,6 +61,91 @@ func TestImporterPreImport(t *testing.T) {
|
|||||||
assert.Equal(t, expectedPerformer, i.performer)
|
assert.Equal(t, expectedPerformer, i.performer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestImporterPreImportWithTag(t *testing.T) {
|
||||||
|
tagReaderWriter := &mocks.TagReaderWriter{}
|
||||||
|
|
||||||
|
i := Importer{
|
||||||
|
TagWriter: tagReaderWriter,
|
||||||
|
MissingRefBehaviour: models.ImportMissingRefEnumFail,
|
||||||
|
Input: jsonschema.Performer{
|
||||||
|
Tags: []string{
|
||||||
|
existingTagName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tagReaderWriter.On("FindByNames", []string{existingTagName}, false).Return([]*models.Tag{
|
||||||
|
{
|
||||||
|
ID: existingTagID,
|
||||||
|
Name: existingTagName,
|
||||||
|
},
|
||||||
|
}, nil).Once()
|
||||||
|
tagReaderWriter.On("FindByNames", []string{existingTagErr}, false).Return(nil, errors.New("FindByNames error")).Once()
|
||||||
|
|
||||||
|
err := i.PreImport()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, existingTagID, i.tags[0].ID)
|
||||||
|
|
||||||
|
i.Input.Tags = []string{existingTagErr}
|
||||||
|
err = i.PreImport()
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
tagReaderWriter.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImporterPreImportWithMissingTag(t *testing.T) {
|
||||||
|
tagReaderWriter := &mocks.TagReaderWriter{}
|
||||||
|
|
||||||
|
i := Importer{
|
||||||
|
TagWriter: tagReaderWriter,
|
||||||
|
Input: jsonschema.Performer{
|
||||||
|
Tags: []string{
|
||||||
|
missingTagName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MissingRefBehaviour: models.ImportMissingRefEnumFail,
|
||||||
|
}
|
||||||
|
|
||||||
|
tagReaderWriter.On("FindByNames", []string{missingTagName}, false).Return(nil, nil).Times(3)
|
||||||
|
tagReaderWriter.On("Create", mock.AnythingOfType("models.Tag")).Return(&models.Tag{
|
||||||
|
ID: existingTagID,
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
err := i.PreImport()
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
i.MissingRefBehaviour = models.ImportMissingRefEnumIgnore
|
||||||
|
err = i.PreImport()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
i.MissingRefBehaviour = models.ImportMissingRefEnumCreate
|
||||||
|
err = i.PreImport()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, existingTagID, i.tags[0].ID)
|
||||||
|
|
||||||
|
tagReaderWriter.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImporterPreImportWithMissingTagCreateErr(t *testing.T) {
|
||||||
|
tagReaderWriter := &mocks.TagReaderWriter{}
|
||||||
|
|
||||||
|
i := Importer{
|
||||||
|
TagWriter: tagReaderWriter,
|
||||||
|
Input: jsonschema.Performer{
|
||||||
|
Tags: []string{
|
||||||
|
missingTagName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MissingRefBehaviour: models.ImportMissingRefEnumCreate,
|
||||||
|
}
|
||||||
|
|
||||||
|
tagReaderWriter.On("FindByNames", []string{missingTagName}, false).Return(nil, nil).Once()
|
||||||
|
tagReaderWriter.On("Create", mock.AnythingOfType("models.Tag")).Return(nil, errors.New("Create error"))
|
||||||
|
|
||||||
|
err := i.PreImport()
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestImporterPostImport(t *testing.T) {
|
func TestImporterPostImport(t *testing.T) {
|
||||||
readerWriter := &mocks.PerformerReaderWriter{}
|
readerWriter := &mocks.PerformerReaderWriter{}
|
||||||
|
|
||||||
@@ -111,6 +204,32 @@ func TestImporterFindExistingID(t *testing.T) {
|
|||||||
readerWriter.AssertExpectations(t)
|
readerWriter.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestImporterPostImportUpdateTags(t *testing.T) {
|
||||||
|
readerWriter := &mocks.PerformerReaderWriter{}
|
||||||
|
|
||||||
|
i := Importer{
|
||||||
|
ReaderWriter: readerWriter,
|
||||||
|
tags: []*models.Tag{
|
||||||
|
{
|
||||||
|
ID: existingTagID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
updateErr := errors.New("UpdateTags error")
|
||||||
|
|
||||||
|
readerWriter.On("UpdateTags", performerID, []int{existingTagID}).Return(nil).Once()
|
||||||
|
readerWriter.On("UpdateTags", errTagsID, mock.AnythingOfType("[]int")).Return(updateErr).Once()
|
||||||
|
|
||||||
|
err := i.PostImport(performerID)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
err = i.PostImport(errTagsID)
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
|
||||||
|
readerWriter.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreate(t *testing.T) {
|
func TestCreate(t *testing.T) {
|
||||||
readerWriter := &mocks.PerformerReaderWriter{}
|
readerWriter := &mocks.PerformerReaderWriter{}
|
||||||
|
|
||||||
|
|||||||
@@ -94,10 +94,10 @@ func (s mappedConfig) postProcess(q mappedQuery, attrConfig mappedScraperAttrCon
|
|||||||
type mappedSceneScraperConfig struct {
|
type mappedSceneScraperConfig struct {
|
||||||
mappedConfig
|
mappedConfig
|
||||||
|
|
||||||
Tags mappedConfig `yaml:"Tags"`
|
Tags mappedConfig `yaml:"Tags"`
|
||||||
Performers mappedConfig `yaml:"Performers"`
|
Performers mappedPerformerScraperConfig `yaml:"Performers"`
|
||||||
Studio mappedConfig `yaml:"Studio"`
|
Studio mappedConfig `yaml:"Studio"`
|
||||||
Movies mappedConfig `yaml:"Movies"`
|
Movies mappedConfig `yaml:"Movies"`
|
||||||
}
|
}
|
||||||
type _mappedSceneScraperConfig mappedSceneScraperConfig
|
type _mappedSceneScraperConfig mappedSceneScraperConfig
|
||||||
|
|
||||||
@@ -211,10 +211,54 @@ func (s *mappedGalleryScraperConfig) UnmarshalYAML(unmarshal func(interface{}) e
|
|||||||
|
|
||||||
type mappedPerformerScraperConfig struct {
|
type mappedPerformerScraperConfig struct {
|
||||||
mappedConfig
|
mappedConfig
|
||||||
|
|
||||||
|
Tags mappedConfig `yaml:"Tags"`
|
||||||
}
|
}
|
||||||
|
type _mappedPerformerScraperConfig mappedPerformerScraperConfig
|
||||||
|
|
||||||
|
const (
|
||||||
|
mappedScraperConfigPerformerTags = "Tags"
|
||||||
|
)
|
||||||
|
|
||||||
func (s *mappedPerformerScraperConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (s *mappedPerformerScraperConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
return unmarshal(&s.mappedConfig)
|
// HACK - unmarshal to map first, then remove known scene sub-fields, then
|
||||||
|
// remarshal to yaml and pass that down to the base map
|
||||||
|
parentMap := make(map[string]interface{})
|
||||||
|
if err := unmarshal(parentMap); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// move the known sub-fields to a separate map
|
||||||
|
thisMap := make(map[string]interface{})
|
||||||
|
|
||||||
|
thisMap[mappedScraperConfigPerformerTags] = parentMap[mappedScraperConfigPerformerTags]
|
||||||
|
|
||||||
|
delete(parentMap, mappedScraperConfigPerformerTags)
|
||||||
|
|
||||||
|
// re-unmarshal the sub-fields
|
||||||
|
yml, err := yaml.Marshal(thisMap)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// needs to be a different type to prevent infinite recursion
|
||||||
|
c := _mappedPerformerScraperConfig{}
|
||||||
|
if err := yaml.Unmarshal(yml, &c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*s = mappedPerformerScraperConfig(c)
|
||||||
|
|
||||||
|
yml, err = yaml.Marshal(parentMap)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := yaml.Unmarshal(yml, &s.mappedConfig); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type mappedMovieScraperConfig struct {
|
type mappedMovieScraperConfig struct {
|
||||||
@@ -647,9 +691,23 @@ func (s mappedScraper) scrapePerformer(q mappedQuery) (*models.ScrapedPerformer,
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
performerTagsMap := performerMap.Tags
|
||||||
|
|
||||||
results := performerMap.process(q, s.Common)
|
results := performerMap.process(q, s.Common)
|
||||||
if len(results) > 0 {
|
if len(results) > 0 {
|
||||||
results[0].apply(&ret)
|
results[0].apply(&ret)
|
||||||
|
|
||||||
|
// now apply the tags
|
||||||
|
if performerTagsMap != nil {
|
||||||
|
logger.Debug(`Processing performer tags:`)
|
||||||
|
tagResults := performerTagsMap.process(q, s.Common)
|
||||||
|
|
||||||
|
for _, p := range tagResults {
|
||||||
|
tag := &models.ScrapedSceneTag{}
|
||||||
|
p.apply(tag)
|
||||||
|
ret.Tags = append(ret.Tags, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ret, nil
|
return &ret, nil
|
||||||
@@ -687,19 +745,34 @@ func (s mappedScraper) scrapeScene(q mappedQuery) (*models.ScrapedScene, error)
|
|||||||
sceneStudioMap := sceneScraperConfig.Studio
|
sceneStudioMap := sceneScraperConfig.Studio
|
||||||
sceneMoviesMap := sceneScraperConfig.Movies
|
sceneMoviesMap := sceneScraperConfig.Movies
|
||||||
|
|
||||||
|
scenePerformerTagsMap := scenePerformersMap.Tags
|
||||||
|
|
||||||
logger.Debug(`Processing scene:`)
|
logger.Debug(`Processing scene:`)
|
||||||
results := sceneMap.process(q, s.Common)
|
results := sceneMap.process(q, s.Common)
|
||||||
if len(results) > 0 {
|
if len(results) > 0 {
|
||||||
results[0].apply(&ret)
|
results[0].apply(&ret)
|
||||||
|
|
||||||
|
// process performer tags once
|
||||||
|
var performerTagResults mappedResults
|
||||||
|
if scenePerformerTagsMap != nil {
|
||||||
|
performerTagResults = scenePerformerTagsMap.process(q, s.Common)
|
||||||
|
}
|
||||||
|
|
||||||
// now apply the performers and tags
|
// now apply the performers and tags
|
||||||
if scenePerformersMap != nil {
|
if scenePerformersMap.mappedConfig != nil {
|
||||||
logger.Debug(`Processing scene performers:`)
|
logger.Debug(`Processing scene performers:`)
|
||||||
performerResults := scenePerformersMap.process(q, s.Common)
|
performerResults := scenePerformersMap.process(q, s.Common)
|
||||||
|
|
||||||
for _, p := range performerResults {
|
for _, p := range performerResults {
|
||||||
performer := &models.ScrapedScenePerformer{}
|
performer := &models.ScrapedScenePerformer{}
|
||||||
p.apply(performer)
|
p.apply(performer)
|
||||||
|
|
||||||
|
for _, p := range performerTagResults {
|
||||||
|
tag := &models.ScrapedSceneTag{}
|
||||||
|
p.apply(tag)
|
||||||
|
ret.Tags = append(ret.Tags, tag)
|
||||||
|
}
|
||||||
|
|
||||||
ret.Performers = append(ret.Performers, performer)
|
ret.Performers = append(ret.Performers, performer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,9 +220,11 @@ func (c Cache) ScrapePerformerURL(url string) (*models.ScrapedPerformer, error)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// post-process - set the image if applicable
|
if ret != nil {
|
||||||
if err := setPerformerImage(ret, c.globalConfig); err != nil {
|
err = c.postScrapePerformer(ret)
|
||||||
logger.Warnf("Could not set image using URL %s: %s", *ret.Image, err.Error())
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret, nil
|
return ret, nil
|
||||||
@@ -232,6 +234,49 @@ func (c Cache) ScrapePerformerURL(url string) (*models.ScrapedPerformer, error)
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c Cache) postScrapePerformer(ret *models.ScrapedPerformer) error {
|
||||||
|
if err := c.txnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error {
|
||||||
|
tqb := r.Tag()
|
||||||
|
|
||||||
|
for _, t := range ret.Tags {
|
||||||
|
err := MatchScrapedSceneTag(tqb, t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// post-process - set the image if applicable
|
||||||
|
if err := setPerformerImage(ret, c.globalConfig); err != nil {
|
||||||
|
logger.Warnf("Could not set image using URL %s: %s", *ret.Image, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Cache) postScrapeScenePerformer(ret *models.ScrapedScenePerformer) error {
|
||||||
|
if err := c.txnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error {
|
||||||
|
tqb := r.Tag()
|
||||||
|
|
||||||
|
for _, t := range ret.Tags {
|
||||||
|
err := MatchScrapedSceneTag(tqb, t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c Cache) postScrapeScene(ret *models.ScrapedScene) error {
|
func (c Cache) postScrapeScene(ret *models.ScrapedScene) error {
|
||||||
if err := c.txnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error {
|
if err := c.txnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error {
|
||||||
pqb := r.Performer()
|
pqb := r.Performer()
|
||||||
@@ -240,8 +285,11 @@ func (c Cache) postScrapeScene(ret *models.ScrapedScene) error {
|
|||||||
sqb := r.Studio()
|
sqb := r.Studio()
|
||||||
|
|
||||||
for _, p := range ret.Performers {
|
for _, p := range ret.Performers {
|
||||||
err := MatchScrapedScenePerformer(pqb, p)
|
if err := c.postScrapeScenePerformer(p); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := MatchScrapedScenePerformer(pqb, p); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,13 @@ func (s *stashScraper) scrapePerformerByFragment(scrapedPerformer models.Scraped
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if q.FindPerformer != nil {
|
||||||
|
// the ids of the tags must be nilled
|
||||||
|
for _, t := range q.FindPerformer.Tags {
|
||||||
|
t.ID = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// need to copy back to a scraped performer
|
// need to copy back to a scraped performer
|
||||||
ret := models.ScrapedPerformer{}
|
ret := models.ScrapedPerformer{}
|
||||||
err = copier.Copy(&ret, q.FindPerformer)
|
err = copier.Copy(&ret, q.FindPerformer)
|
||||||
|
|||||||
@@ -322,6 +322,7 @@ func performerFragmentToScrapedScenePerformer(p graphql.PerformerFragment) *mode
|
|||||||
Twitter: findURL(p.Urls, "TWITTER"),
|
Twitter: findURL(p.Urls, "TWITTER"),
|
||||||
RemoteSiteID: &id,
|
RemoteSiteID: &id,
|
||||||
Images: images,
|
Images: images,
|
||||||
|
// TODO - tags not currently supported
|
||||||
// TODO - Image - should be returned as a set of URLs. Will need a
|
// TODO - Image - should be returned as a set of URLs. Will need a
|
||||||
// graphql schema change to accommodate this. Leave off for now.
|
// graphql schema change to accommodate this. Leave off for now.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -520,7 +520,7 @@ func makeSceneXPathConfig() mappedScraper {
|
|||||||
performerConfig := make(mappedConfig)
|
performerConfig := make(mappedConfig)
|
||||||
performerConfig["Name"] = makeSimpleAttrConfig(`$performerElem/@data-mxptext`)
|
performerConfig["Name"] = makeSimpleAttrConfig(`$performerElem/@data-mxptext`)
|
||||||
performerConfig["URL"] = makeSimpleAttrConfig(`$performerElem/@href`)
|
performerConfig["URL"] = makeSimpleAttrConfig(`$performerElem/@href`)
|
||||||
config.Performers = performerConfig
|
config.Performers.mappedConfig = performerConfig
|
||||||
|
|
||||||
studioConfig := make(mappedConfig)
|
studioConfig := make(mappedConfig)
|
||||||
studioConfig["Name"] = makeSimpleAttrConfig(`$studioElem`)
|
studioConfig["Name"] = makeSimpleAttrConfig(`$studioElem`)
|
||||||
@@ -730,7 +730,7 @@ xPathScrapers:
|
|||||||
assert.Equal(t, "//title", sceneConfig.mappedConfig["Title"].Selector)
|
assert.Equal(t, "//title", sceneConfig.mappedConfig["Title"].Selector)
|
||||||
assert.Equal(t, "//tags", sceneConfig.Tags["Name"].Selector)
|
assert.Equal(t, "//tags", sceneConfig.Tags["Name"].Selector)
|
||||||
assert.Equal(t, "//movies", sceneConfig.Movies["Name"].Selector)
|
assert.Equal(t, "//movies", sceneConfig.Movies["Name"].Selector)
|
||||||
assert.Equal(t, "//performers", sceneConfig.Performers["Name"].Selector)
|
assert.Equal(t, "//performers", sceneConfig.Performers.mappedConfig["Name"].Selector)
|
||||||
assert.Equal(t, "//studio", sceneConfig.Studio["Name"].Selector)
|
assert.Equal(t, "//studio", sceneConfig.Studio["Name"].Selector)
|
||||||
|
|
||||||
postProcess := sceneConfig.mappedConfig["Title"].postProcessActions
|
postProcess := sceneConfig.mappedConfig["Title"].postProcessActions
|
||||||
|
|||||||
@@ -259,6 +259,8 @@ func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, fi
|
|||||||
query.addHaving(havingClause)
|
query.addHaving(havingClause)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleGalleryPerformerTagsCriterion(&query, galleryFilter.PerformerTags)
|
||||||
|
|
||||||
query.sortAndPagination = qb.getGallerySort(findFilter) + getPagination(findFilter)
|
query.sortAndPagination = qb.getGallerySort(findFilter) + getPagination(findFilter)
|
||||||
idsResult, countResult, err := query.executeFind()
|
idsResult, countResult, err := query.executeFind()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -344,6 +346,31 @@ func (qb *galleryQueryBuilder) handleAverageResolutionFilter(query *queryBuilder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleGalleryPerformerTagsCriterion(query *queryBuilder, performerTagsFilter *models.MultiCriterionInput) {
|
||||||
|
if performerTagsFilter != nil && len(performerTagsFilter.Value) > 0 {
|
||||||
|
for _, tagID := range performerTagsFilter.Value {
|
||||||
|
query.addArg(tagID)
|
||||||
|
}
|
||||||
|
|
||||||
|
query.body += " LEFT JOIN performers_tags AS performer_tags_join on performers_join.performer_id = performer_tags_join.performer_id"
|
||||||
|
|
||||||
|
if performerTagsFilter.Modifier == models.CriterionModifierIncludes {
|
||||||
|
// includes any of the provided ids
|
||||||
|
query.addWhere("performer_tags_join.tag_id IN " + getInBinding(len(performerTagsFilter.Value)))
|
||||||
|
} else if performerTagsFilter.Modifier == models.CriterionModifierIncludesAll {
|
||||||
|
// includes all of the provided ids
|
||||||
|
query.addWhere("performer_tags_join.tag_id IN " + getInBinding(len(performerTagsFilter.Value)))
|
||||||
|
query.addHaving(fmt.Sprintf("count(distinct performer_tags_join.tag_id) IS %d", len(performerTagsFilter.Value)))
|
||||||
|
} else if performerTagsFilter.Modifier == models.CriterionModifierExcludes {
|
||||||
|
query.addWhere(fmt.Sprintf(`not exists
|
||||||
|
(select performers_galleries.performer_id from performers_galleries
|
||||||
|
left join performers_tags on performers_tags.performer_id = performers_galleries.performer_id where
|
||||||
|
performers_galleries.gallery_id = galleries.id AND
|
||||||
|
performers_tags.tag_id in %s)`, getInBinding(len(performerTagsFilter.Value))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *galleryQueryBuilder) getGallerySort(findFilter *models.FindFilterType) string {
|
func (qb *galleryQueryBuilder) getGallerySort(findFilter *models.FindFilterType) string {
|
||||||
var sort string
|
var sort string
|
||||||
var direction string
|
var direction string
|
||||||
|
|||||||
@@ -519,6 +519,61 @@ func TestGalleryQueryStudio(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGalleryQueryPerformerTags(t *testing.T) {
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
sqb := r.Gallery()
|
||||||
|
tagCriterion := models.MultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(tagIDs[tagIdxWithPerformer]),
|
||||||
|
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
}
|
||||||
|
|
||||||
|
galleryFilter := models.GalleryFilterType{
|
||||||
|
PerformerTags: &tagCriterion,
|
||||||
|
}
|
||||||
|
|
||||||
|
galleries := queryGallery(t, sqb, &galleryFilter, nil)
|
||||||
|
assert.Len(t, galleries, 2)
|
||||||
|
|
||||||
|
// ensure ids are correct
|
||||||
|
for _, gallery := range galleries {
|
||||||
|
assert.True(t, gallery.ID == galleryIDs[galleryIdxWithPerformerTag] || gallery.ID == galleryIDs[galleryIdxWithPerformerTwoTags])
|
||||||
|
}
|
||||||
|
|
||||||
|
tagCriterion = models.MultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
|
||||||
|
strconv.Itoa(tagIDs[tagIdx2WithPerformer]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludesAll,
|
||||||
|
}
|
||||||
|
|
||||||
|
galleries = queryGallery(t, sqb, &galleryFilter, nil)
|
||||||
|
|
||||||
|
assert.Len(t, galleries, 1)
|
||||||
|
assert.Equal(t, galleryIDs[galleryIdxWithPerformerTwoTags], galleries[0].ID)
|
||||||
|
|
||||||
|
tagCriterion = models.MultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierExcludes,
|
||||||
|
}
|
||||||
|
|
||||||
|
q := getGalleryStringValue(galleryIdxWithPerformerTwoTags, titleField)
|
||||||
|
findFilter := models.FindFilterType{
|
||||||
|
Q: &q,
|
||||||
|
}
|
||||||
|
|
||||||
|
galleries = queryGallery(t, sqb, &galleryFilter, &findFilter)
|
||||||
|
assert.Len(t, galleries, 0)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// TODO Count
|
// TODO Count
|
||||||
// TODO All
|
// TODO All
|
||||||
// TODO Query
|
// TODO Query
|
||||||
|
|||||||
@@ -360,6 +360,8 @@ func (qb *imageQueryBuilder) Query(imageFilter *models.ImageFilterType, findFilt
|
|||||||
query.addHaving(havingClause)
|
query.addHaving(havingClause)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleImagePerformerTagsCriterion(&query, imageFilter.PerformerTags)
|
||||||
|
|
||||||
query.sortAndPagination = qb.getImageSort(findFilter) + getPagination(findFilter)
|
query.sortAndPagination = qb.getImageSort(findFilter) + getPagination(findFilter)
|
||||||
idsResult, countResult, err := query.executeFind()
|
idsResult, countResult, err := query.executeFind()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -379,6 +381,31 @@ func (qb *imageQueryBuilder) Query(imageFilter *models.ImageFilterType, findFilt
|
|||||||
return images, countResult, nil
|
return images, countResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleImagePerformerTagsCriterion(query *queryBuilder, performerTagsFilter *models.MultiCriterionInput) {
|
||||||
|
if performerTagsFilter != nil && len(performerTagsFilter.Value) > 0 {
|
||||||
|
for _, tagID := range performerTagsFilter.Value {
|
||||||
|
query.addArg(tagID)
|
||||||
|
}
|
||||||
|
|
||||||
|
query.body += " LEFT JOIN performers_tags AS performer_tags_join on performers_join.performer_id = performer_tags_join.performer_id"
|
||||||
|
|
||||||
|
if performerTagsFilter.Modifier == models.CriterionModifierIncludes {
|
||||||
|
// includes any of the provided ids
|
||||||
|
query.addWhere("performer_tags_join.tag_id IN " + getInBinding(len(performerTagsFilter.Value)))
|
||||||
|
} else if performerTagsFilter.Modifier == models.CriterionModifierIncludesAll {
|
||||||
|
// includes all of the provided ids
|
||||||
|
query.addWhere("performer_tags_join.tag_id IN " + getInBinding(len(performerTagsFilter.Value)))
|
||||||
|
query.addHaving(fmt.Sprintf("count(distinct performer_tags_join.tag_id) IS %d", len(performerTagsFilter.Value)))
|
||||||
|
} else if performerTagsFilter.Modifier == models.CriterionModifierExcludes {
|
||||||
|
query.addWhere(fmt.Sprintf(`not exists
|
||||||
|
(select performers_images.performer_id from performers_images
|
||||||
|
left join performers_tags on performers_tags.performer_id = performers_images.performer_id where
|
||||||
|
performers_images.image_id = images.id AND
|
||||||
|
performers_tags.tag_id in %s)`, getInBinding(len(performerTagsFilter.Value))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *imageQueryBuilder) getImageSort(findFilter *models.FindFilterType) string {
|
func (qb *imageQueryBuilder) getImageSort(findFilter *models.FindFilterType) string {
|
||||||
if findFilter == nil {
|
if findFilter == nil {
|
||||||
return " ORDER BY images.path ASC "
|
return " ORDER BY images.path ASC "
|
||||||
|
|||||||
@@ -619,6 +619,70 @@ func TestImageQueryStudio(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func queryImages(t *testing.T, sqb models.ImageReader, imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) []*models.Image {
|
||||||
|
images, _, err := sqb.Query(imageFilter, findFilter)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error querying images: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return images
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImageQueryPerformerTags(t *testing.T) {
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
sqb := r.Image()
|
||||||
|
tagCriterion := models.MultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(tagIDs[tagIdxWithPerformer]),
|
||||||
|
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
}
|
||||||
|
|
||||||
|
imageFilter := models.ImageFilterType{
|
||||||
|
PerformerTags: &tagCriterion,
|
||||||
|
}
|
||||||
|
|
||||||
|
images := queryImages(t, sqb, &imageFilter, nil)
|
||||||
|
assert.Len(t, images, 2)
|
||||||
|
|
||||||
|
// ensure ids are correct
|
||||||
|
for _, image := range images {
|
||||||
|
assert.True(t, image.ID == imageIDs[imageIdxWithPerformerTag] || image.ID == imageIDs[imageIdxWithPerformerTwoTags])
|
||||||
|
}
|
||||||
|
|
||||||
|
tagCriterion = models.MultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
|
||||||
|
strconv.Itoa(tagIDs[tagIdx2WithPerformer]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludesAll,
|
||||||
|
}
|
||||||
|
|
||||||
|
images = queryImages(t, sqb, &imageFilter, nil)
|
||||||
|
|
||||||
|
assert.Len(t, images, 1)
|
||||||
|
assert.Equal(t, imageIDs[imageIdxWithPerformerTwoTags], images[0].ID)
|
||||||
|
|
||||||
|
tagCriterion = models.MultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierExcludes,
|
||||||
|
}
|
||||||
|
|
||||||
|
q := getImageStringValue(imageIdxWithPerformerTwoTags, titleField)
|
||||||
|
findFilter := models.FindFilterType{
|
||||||
|
Q: &q,
|
||||||
|
}
|
||||||
|
|
||||||
|
images = queryImages(t, sqb, &imageFilter, &findFilter)
|
||||||
|
assert.Len(t, images, 0)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestImageQuerySorting(t *testing.T) {
|
func TestImageQuerySorting(t *testing.T) {
|
||||||
withTxn(func(r models.Repository) error {
|
withTxn(func(r models.Repository) error {
|
||||||
sort := titleField
|
sort := titleField
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ import (
|
|||||||
|
|
||||||
const performerTable = "performers"
|
const performerTable = "performers"
|
||||||
const performerIDColumn = "performer_id"
|
const performerIDColumn = "performer_id"
|
||||||
|
const performersTagsTable = "performers_tags"
|
||||||
|
|
||||||
|
var countPerformersForTagQuery = `
|
||||||
|
SELECT tag_id AS id FROM performers_tags
|
||||||
|
WHERE performers_tags.tag_id = ?
|
||||||
|
GROUP BY performers_tags.performer_id
|
||||||
|
`
|
||||||
|
|
||||||
type performerQueryBuilder struct {
|
type performerQueryBuilder struct {
|
||||||
repository
|
repository
|
||||||
@@ -153,6 +160,11 @@ func (qb *performerQueryBuilder) FindByNames(names []string, nocase bool) ([]*mo
|
|||||||
return qb.queryPerformers(query, args)
|
return qb.queryPerformers(query, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qb *performerQueryBuilder) CountByTagID(tagID int) (int, error) {
|
||||||
|
args := []interface{}{tagID}
|
||||||
|
return qb.runCountQuery(qb.buildCountQuery(countPerformersForTagQuery), args)
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *performerQueryBuilder) Count() (int, error) {
|
func (qb *performerQueryBuilder) Count() (int, error) {
|
||||||
return qb.runCountQuery(qb.buildCountQuery("SELECT performers.id FROM performers"), nil)
|
return qb.runCountQuery(qb.buildCountQuery("SELECT performers.id FROM performers"), nil)
|
||||||
}
|
}
|
||||||
@@ -250,6 +262,18 @@ func (qb *performerQueryBuilder) Query(performerFilter *models.PerformerFilterTy
|
|||||||
// TODO - need better handling of aliases
|
// TODO - need better handling of aliases
|
||||||
query.handleStringCriterionInput(performerFilter.Aliases, tableName+".aliases")
|
query.handleStringCriterionInput(performerFilter.Aliases, tableName+".aliases")
|
||||||
|
|
||||||
|
if tagsFilter := performerFilter.Tags; tagsFilter != nil && len(tagsFilter.Value) > 0 {
|
||||||
|
for _, tagID := range tagsFilter.Value {
|
||||||
|
query.addArg(tagID)
|
||||||
|
}
|
||||||
|
|
||||||
|
query.body += ` left join performers_tags as tags_join on tags_join.performer_id = performers.id
|
||||||
|
LEFT JOIN tags on tags_join.tag_id = tags.id`
|
||||||
|
whereClause, havingClause := getMultiCriterionClause("performers", "tags", "performers_tags", "performer_id", "tag_id", tagsFilter)
|
||||||
|
query.addWhere(whereClause)
|
||||||
|
query.addHaving(havingClause)
|
||||||
|
}
|
||||||
|
|
||||||
query.sortAndPagination = qb.getPerformerSort(findFilter) + getPagination(findFilter)
|
query.sortAndPagination = qb.getPerformerSort(findFilter) + getPagination(findFilter)
|
||||||
idsResult, countResult, err := query.executeFind()
|
idsResult, countResult, err := query.executeFind()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -361,6 +385,26 @@ func (qb *performerQueryBuilder) queryPerformers(query string, args []interface{
|
|||||||
return []*models.Performer(ret), nil
|
return []*models.Performer(ret), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qb *performerQueryBuilder) tagsRepository() *joinRepository {
|
||||||
|
return &joinRepository{
|
||||||
|
repository: repository{
|
||||||
|
tx: qb.tx,
|
||||||
|
tableName: performersTagsTable,
|
||||||
|
idColumn: performerIDColumn,
|
||||||
|
},
|
||||||
|
fkColumn: tagIDColumn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *performerQueryBuilder) GetTagIDs(id int) ([]int, error) {
|
||||||
|
return qb.tagsRepository().getIDs(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *performerQueryBuilder) UpdateTags(id int, tagIDs []int) error {
|
||||||
|
// Delete the existing joins and then create new ones
|
||||||
|
return qb.tagsRepository().replace(id, tagIDs)
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *performerQueryBuilder) imageRepository() *imageRepository {
|
func (qb *performerQueryBuilder) imageRepository() *imageRepository {
|
||||||
return &imageRepository{
|
return &imageRepository{
|
||||||
repository: repository{
|
repository: repository{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package sqlite_test
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -227,6 +228,69 @@ func verifyPerformerAge(t *testing.T, ageCriterion models.IntCriterionInput) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func queryPerformers(t *testing.T, qb models.PerformerReader, performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) []*models.Performer {
|
||||||
|
performers, _, err := qb.Query(performerFilter, findFilter)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error querying performers: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return performers
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPerformerQueryTags(t *testing.T) {
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
sqb := r.Performer()
|
||||||
|
tagCriterion := models.MultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(tagIDs[tagIdxWithPerformer]),
|
||||||
|
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
}
|
||||||
|
|
||||||
|
performerFilter := models.PerformerFilterType{
|
||||||
|
Tags: &tagCriterion,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure ids are correct
|
||||||
|
performers := queryPerformers(t, sqb, &performerFilter, nil)
|
||||||
|
assert.Len(t, performers, 2)
|
||||||
|
for _, performer := range performers {
|
||||||
|
assert.True(t, performer.ID == performerIDs[performerIdxWithTag] || performer.ID == performerIDs[performerIdxWithTwoTags])
|
||||||
|
}
|
||||||
|
|
||||||
|
tagCriterion = models.MultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
|
||||||
|
strconv.Itoa(tagIDs[tagIdx2WithPerformer]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludesAll,
|
||||||
|
}
|
||||||
|
|
||||||
|
performers = queryPerformers(t, sqb, &performerFilter, nil)
|
||||||
|
|
||||||
|
assert.Len(t, performers, 1)
|
||||||
|
assert.Equal(t, sceneIDs[performerIdxWithTwoTags], performers[0].ID)
|
||||||
|
|
||||||
|
tagCriterion = models.MultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierExcludes,
|
||||||
|
}
|
||||||
|
|
||||||
|
q := getSceneStringValue(performerIdxWithTwoTags, titleField)
|
||||||
|
findFilter := models.FindFilterType{
|
||||||
|
Q: &q,
|
||||||
|
}
|
||||||
|
|
||||||
|
performers = queryPerformers(t, sqb, &performerFilter, &findFilter)
|
||||||
|
assert.Len(t, performers, 0)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestPerformerStashIDs(t *testing.T) {
|
func TestPerformerStashIDs(t *testing.T) {
|
||||||
if err := withTxn(func(r models.Repository) error {
|
if err := withTxn(func(r models.Repository) error {
|
||||||
qb := r.Performer()
|
qb := r.Performer()
|
||||||
|
|||||||
@@ -351,6 +351,7 @@ func (qb *sceneQueryBuilder) makeFilter(sceneFilter *models.SceneFilterType) *fi
|
|||||||
query.handleCriterionFunc(sceneStudioCriterionHandler(qb, sceneFilter.Studios))
|
query.handleCriterionFunc(sceneStudioCriterionHandler(qb, sceneFilter.Studios))
|
||||||
query.handleCriterionFunc(sceneMoviesCriterionHandler(qb, sceneFilter.Movies))
|
query.handleCriterionFunc(sceneMoviesCriterionHandler(qb, sceneFilter.Movies))
|
||||||
query.handleCriterionFunc(sceneStashIDsHandler(qb, sceneFilter.StashID))
|
query.handleCriterionFunc(sceneStashIDsHandler(qb, sceneFilter.StashID))
|
||||||
|
query.handleCriterionFunc(scenePerformerTagsCriterionHandler(qb, sceneFilter.PerformerTags))
|
||||||
|
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
@@ -565,6 +566,60 @@ func sceneStashIDsHandler(qb *sceneQueryBuilder, stashID *string) criterionHandl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func scenePerformerTagsCriterionHandler(qb *sceneQueryBuilder, performerTagsFilter *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
|
return func(f *filterBuilder) {
|
||||||
|
if performerTagsFilter != nil && len(performerTagsFilter.Value) > 0 {
|
||||||
|
qb.performersRepository().join(f, "performers_join", "scenes.id")
|
||||||
|
f.addJoin("performers_tags", "performer_tags_join", "performers_join.performer_id = performer_tags_join.performer_id")
|
||||||
|
|
||||||
|
var args []interface{}
|
||||||
|
for _, tagID := range performerTagsFilter.Value {
|
||||||
|
args = append(args, tagID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if performerTagsFilter.Modifier == models.CriterionModifierIncludes {
|
||||||
|
// includes any of the provided ids
|
||||||
|
f.addWhere("performer_tags_join.tag_id IN "+getInBinding(len(performerTagsFilter.Value)), args...)
|
||||||
|
} else if performerTagsFilter.Modifier == models.CriterionModifierIncludesAll {
|
||||||
|
// includes all of the provided ids
|
||||||
|
f.addWhere("performer_tags_join.tag_id IN "+getInBinding(len(performerTagsFilter.Value)), args...)
|
||||||
|
f.addHaving(fmt.Sprintf("count(distinct performer_tags_join.tag_id) IS %d", len(performerTagsFilter.Value)))
|
||||||
|
} else if performerTagsFilter.Modifier == models.CriterionModifierExcludes {
|
||||||
|
f.addWhere(fmt.Sprintf(`not exists
|
||||||
|
(select performers_scenes.performer_id from performers_scenes
|
||||||
|
left join performers_tags on performers_tags.performer_id = performers_scenes.performer_id where
|
||||||
|
performers_scenes.scene_id = scenes.id AND
|
||||||
|
performers_tags.tag_id in %s)`, getInBinding(len(performerTagsFilter.Value))), args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleScenePerformerTagsCriterion(query *queryBuilder, performerTagsFilter *models.MultiCriterionInput) {
|
||||||
|
if performerTagsFilter != nil && len(performerTagsFilter.Value) > 0 {
|
||||||
|
for _, tagID := range performerTagsFilter.Value {
|
||||||
|
query.addArg(tagID)
|
||||||
|
}
|
||||||
|
|
||||||
|
query.body += " LEFT JOIN performers_tags AS performer_tags_join on performers_join.performer_id = performer_tags_join.performer_id"
|
||||||
|
|
||||||
|
if performerTagsFilter.Modifier == models.CriterionModifierIncludes {
|
||||||
|
// includes any of the provided ids
|
||||||
|
query.addWhere("performer_tags_join.tag_id IN " + getInBinding(len(performerTagsFilter.Value)))
|
||||||
|
} else if performerTagsFilter.Modifier == models.CriterionModifierIncludesAll {
|
||||||
|
// includes all of the provided ids
|
||||||
|
query.addWhere("performer_tags_join.tag_id IN " + getInBinding(len(performerTagsFilter.Value)))
|
||||||
|
query.addHaving(fmt.Sprintf("count(distinct performer_tags_join.tag_id) IS %d", len(performerTagsFilter.Value)))
|
||||||
|
} else if performerTagsFilter.Modifier == models.CriterionModifierExcludes {
|
||||||
|
query.addWhere(fmt.Sprintf(`not exists
|
||||||
|
(select performers_scenes.performer_id from performers_scenes
|
||||||
|
left join performers_tags on performers_tags.performer_id = performers_scenes.performer_id where
|
||||||
|
performers_scenes.scene_id = scenes.id AND
|
||||||
|
performers_tags.tag_id in %s)`, getInBinding(len(performerTagsFilter.Value))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *sceneQueryBuilder) getSceneSort(findFilter *models.FindFilterType) string {
|
func (qb *sceneQueryBuilder) getSceneSort(findFilter *models.FindFilterType) string {
|
||||||
if findFilter == nil {
|
if findFilter == nil {
|
||||||
return " ORDER BY scenes.path, scenes.date ASC "
|
return " ORDER BY scenes.path, scenes.date ASC "
|
||||||
|
|||||||
@@ -940,6 +940,61 @@ func TestSceneQueryTags(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSceneQueryPerformerTags(t *testing.T) {
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
sqb := r.Scene()
|
||||||
|
tagCriterion := models.MultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(tagIDs[tagIdxWithPerformer]),
|
||||||
|
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
}
|
||||||
|
|
||||||
|
sceneFilter := models.SceneFilterType{
|
||||||
|
PerformerTags: &tagCriterion,
|
||||||
|
}
|
||||||
|
|
||||||
|
scenes := queryScene(t, sqb, &sceneFilter, nil)
|
||||||
|
assert.Len(t, scenes, 2)
|
||||||
|
|
||||||
|
// ensure ids are correct
|
||||||
|
for _, scene := range scenes {
|
||||||
|
assert.True(t, scene.ID == sceneIDs[sceneIdxWithPerformerTag] || scene.ID == sceneIDs[sceneIdxWithPerformerTwoTags])
|
||||||
|
}
|
||||||
|
|
||||||
|
tagCriterion = models.MultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
|
||||||
|
strconv.Itoa(tagIDs[tagIdx2WithPerformer]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludesAll,
|
||||||
|
}
|
||||||
|
|
||||||
|
scenes = queryScene(t, sqb, &sceneFilter, nil)
|
||||||
|
|
||||||
|
assert.Len(t, scenes, 1)
|
||||||
|
assert.Equal(t, sceneIDs[sceneIdxWithPerformerTwoTags], scenes[0].ID)
|
||||||
|
|
||||||
|
tagCriterion = models.MultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(tagIDs[tagIdx1WithPerformer]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierExcludes,
|
||||||
|
}
|
||||||
|
|
||||||
|
q := getSceneStringValue(sceneIdxWithPerformerTwoTags, titleField)
|
||||||
|
findFilter := models.FindFilterType{
|
||||||
|
Q: &q,
|
||||||
|
}
|
||||||
|
|
||||||
|
scenes = queryScene(t, sqb, &sceneFilter, &findFilter)
|
||||||
|
assert.Len(t, scenes, 0)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestSceneQueryStudio(t *testing.T) {
|
func TestSceneQueryStudio(t *testing.T) {
|
||||||
withTxn(func(r models.Repository) error {
|
withTxn(func(r models.Repository) error {
|
||||||
sqb := r.Scene()
|
sqb := r.Scene()
|
||||||
|
|||||||
@@ -20,110 +20,240 @@ import (
|
|||||||
"github.com/stashapp/stash/pkg/utils"
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const totalScenes = 12
|
const (
|
||||||
const totalImages = 6 // TODO - add one for zip file
|
sceneIdxWithMovie = iota
|
||||||
const performersNameCase = 9
|
sceneIdxWithGallery
|
||||||
const performersNameNoCase = 2
|
sceneIdxWithPerformer
|
||||||
const moviesNameCase = 2
|
sceneIdxWithTwoPerformers
|
||||||
const moviesNameNoCase = 1
|
sceneIdxWithTag
|
||||||
const totalGalleries = 8
|
sceneIdxWithTwoTags
|
||||||
const tagsNameNoCase = 2
|
sceneIdxWithStudio
|
||||||
const tagsNameCase = 12
|
sceneIdxWithMarker
|
||||||
const studiosNameCase = 6
|
sceneIdxWithPerformerTag
|
||||||
const studiosNameNoCase = 1
|
sceneIdxWithPerformerTwoTags
|
||||||
|
// new indexes above
|
||||||
|
lastSceneIdx
|
||||||
|
|
||||||
var sceneIDs []int
|
totalScenes = lastSceneIdx + 3
|
||||||
var imageIDs []int
|
)
|
||||||
var performerIDs []int
|
|
||||||
var movieIDs []int
|
|
||||||
var galleryIDs []int
|
|
||||||
var tagIDs []int
|
|
||||||
var studioIDs []int
|
|
||||||
var markerIDs []int
|
|
||||||
|
|
||||||
var tagNames []string
|
const (
|
||||||
var studioNames []string
|
imageIdxWithGallery = iota
|
||||||
var movieNames []string
|
imageIdxWithPerformer
|
||||||
var performerNames []string
|
imageIdxWithTwoPerformers
|
||||||
|
imageIdxWithTag
|
||||||
|
imageIdxWithTwoTags
|
||||||
|
imageIdxWithStudio
|
||||||
|
imageIdxInZip // TODO - not implemented
|
||||||
|
imageIdxWithPerformerTag
|
||||||
|
imageIdxWithPerformerTwoTags
|
||||||
|
// new indexes above
|
||||||
|
totalImages
|
||||||
|
)
|
||||||
|
|
||||||
const sceneIdxWithMovie = 0
|
const (
|
||||||
const sceneIdxWithGallery = 1
|
performerIdxWithScene = iota
|
||||||
const sceneIdxWithPerformer = 2
|
performerIdx1WithScene
|
||||||
const sceneIdxWithTwoPerformers = 3
|
performerIdx2WithScene
|
||||||
const sceneIdxWithTag = 4
|
performerIdxWithImage
|
||||||
const sceneIdxWithTwoTags = 5
|
performerIdx1WithImage
|
||||||
const sceneIdxWithStudio = 6
|
performerIdx2WithImage
|
||||||
const sceneIdxWithMarker = 7
|
performerIdxWithTag
|
||||||
|
performerIdxWithTwoTags
|
||||||
|
performerIdxWithGallery
|
||||||
|
performerIdx1WithGallery
|
||||||
|
performerIdx2WithGallery
|
||||||
|
// new indexes above
|
||||||
|
// performers with dup names start from the end
|
||||||
|
performerIdx1WithDupName
|
||||||
|
performerIdxWithDupName
|
||||||
|
|
||||||
const imageIdxWithGallery = 0
|
performersNameCase = performerIdx1WithDupName
|
||||||
const imageIdxWithPerformer = 1
|
performersNameNoCase = 2
|
||||||
const imageIdxWithTwoPerformers = 2
|
)
|
||||||
const imageIdxWithTag = 3
|
|
||||||
const imageIdxWithTwoTags = 4
|
|
||||||
const imageIdxWithStudio = 5
|
|
||||||
const imageIdxInZip = 6
|
|
||||||
|
|
||||||
const performerIdxWithScene = 0
|
const (
|
||||||
const performerIdx1WithScene = 1
|
movieIdxWithScene = iota
|
||||||
const performerIdx2WithScene = 2
|
movieIdxWithStudio
|
||||||
const performerIdxWithImage = 3
|
// movies with dup names start from the end
|
||||||
const performerIdx1WithImage = 4
|
movieIdxWithDupName
|
||||||
const performerIdx2WithImage = 5
|
|
||||||
const performerIdxWithGallery = 6
|
|
||||||
const performerIdx1WithGallery = 7
|
|
||||||
const performerIdx2WithGallery = 8
|
|
||||||
|
|
||||||
// performers with dup names start from the end
|
moviesNameCase = movieIdxWithDupName
|
||||||
const performerIdx1WithDupName = 9
|
moviesNameNoCase = 1
|
||||||
const performerIdxWithDupName = 10
|
)
|
||||||
|
|
||||||
const movieIdxWithScene = 0
|
const (
|
||||||
const movieIdxWithStudio = 1
|
galleryIdxWithScene = iota
|
||||||
|
galleryIdxWithImage
|
||||||
|
galleryIdxWithPerformer
|
||||||
|
galleryIdxWithTwoPerformers
|
||||||
|
galleryIdxWithTag
|
||||||
|
galleryIdxWithTwoTags
|
||||||
|
galleryIdxWithStudio
|
||||||
|
galleryIdxWithPerformerTag
|
||||||
|
galleryIdxWithPerformerTwoTags
|
||||||
|
// new indexes above
|
||||||
|
lastGalleryIdx
|
||||||
|
|
||||||
// movies with dup names start from the end
|
totalGalleries = lastGalleryIdx + 1
|
||||||
const movieIdxWithDupName = 2
|
)
|
||||||
|
|
||||||
const galleryIdxWithScene = 0
|
const (
|
||||||
const galleryIdxWithImage = 1
|
tagIdxWithScene = iota
|
||||||
const galleryIdxWithPerformer = 2
|
tagIdx1WithScene
|
||||||
const galleryIdxWithTwoPerformers = 3
|
tagIdx2WithScene
|
||||||
const galleryIdxWithTag = 4
|
tagIdxWithPrimaryMarker
|
||||||
const galleryIdxWithTwoTags = 5
|
tagIdxWithMarker
|
||||||
const galleryIdxWithStudio = 6
|
tagIdxWithCoverImage
|
||||||
|
tagIdxWithImage
|
||||||
|
tagIdx1WithImage
|
||||||
|
tagIdx2WithImage
|
||||||
|
tagIdxWithPerformer
|
||||||
|
tagIdx1WithPerformer
|
||||||
|
tagIdx2WithPerformer
|
||||||
|
tagIdxWithGallery
|
||||||
|
tagIdx1WithGallery
|
||||||
|
tagIdx2WithGallery
|
||||||
|
// new indexes above
|
||||||
|
// tags with dup names start from the end
|
||||||
|
tagIdx1WithDupName
|
||||||
|
tagIdxWithDupName
|
||||||
|
|
||||||
const tagIdxWithScene = 0
|
tagsNameNoCase = 2
|
||||||
const tagIdx1WithScene = 1
|
tagsNameCase = tagIdx1WithDupName
|
||||||
const tagIdx2WithScene = 2
|
)
|
||||||
const tagIdxWithPrimaryMarker = 3
|
|
||||||
const tagIdxWithMarker = 4
|
|
||||||
const tagIdxWithCoverImage = 5
|
|
||||||
const tagIdxWithImage = 6
|
|
||||||
const tagIdx1WithImage = 7
|
|
||||||
const tagIdx2WithImage = 8
|
|
||||||
const tagIdxWithGallery = 9
|
|
||||||
const tagIdx1WithGallery = 10
|
|
||||||
const tagIdx2WithGallery = 11
|
|
||||||
|
|
||||||
// tags with dup names start from the end
|
const (
|
||||||
const tagIdx1WithDupName = 12
|
studioIdxWithScene = iota
|
||||||
const tagIdxWithDupName = 13
|
studioIdxWithMovie
|
||||||
|
studioIdxWithChildStudio
|
||||||
|
studioIdxWithParentStudio
|
||||||
|
studioIdxWithImage
|
||||||
|
studioIdxWithGallery
|
||||||
|
// new indexes above
|
||||||
|
// studios with dup names start from the end
|
||||||
|
studioIdxWithDupName
|
||||||
|
|
||||||
const studioIdxWithScene = 0
|
studiosNameCase = studioIdxWithDupName
|
||||||
const studioIdxWithMovie = 1
|
studiosNameNoCase = 1
|
||||||
const studioIdxWithChildStudio = 2
|
)
|
||||||
const studioIdxWithParentStudio = 3
|
|
||||||
const studioIdxWithImage = 4
|
|
||||||
const studioIdxWithGallery = 5
|
|
||||||
|
|
||||||
// studios with dup names start from the end
|
const (
|
||||||
const studioIdxWithDupName = 6
|
markerIdxWithScene = iota
|
||||||
|
)
|
||||||
|
|
||||||
const markerIdxWithScene = 0
|
const (
|
||||||
|
pathField = "Path"
|
||||||
|
checksumField = "Checksum"
|
||||||
|
titleField = "Title"
|
||||||
|
zipPath = "zipPath.zip"
|
||||||
|
)
|
||||||
|
|
||||||
const pathField = "Path"
|
var (
|
||||||
const checksumField = "Checksum"
|
sceneIDs []int
|
||||||
const titleField = "Title"
|
imageIDs []int
|
||||||
const zipPath = "zipPath.zip"
|
performerIDs []int
|
||||||
|
movieIDs []int
|
||||||
|
galleryIDs []int
|
||||||
|
tagIDs []int
|
||||||
|
studioIDs []int
|
||||||
|
markerIDs []int
|
||||||
|
|
||||||
|
tagNames []string
|
||||||
|
studioNames []string
|
||||||
|
movieNames []string
|
||||||
|
performerNames []string
|
||||||
|
)
|
||||||
|
|
||||||
|
type idAssociation struct {
|
||||||
|
first int
|
||||||
|
second int
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
sceneTagLinks = [][2]int{
|
||||||
|
{sceneIdxWithTag, tagIdxWithScene},
|
||||||
|
{sceneIdxWithTwoTags, tagIdx1WithScene},
|
||||||
|
{sceneIdxWithTwoTags, tagIdx2WithScene},
|
||||||
|
}
|
||||||
|
|
||||||
|
scenePerformerLinks = [][2]int{
|
||||||
|
{sceneIdxWithPerformer, performerIdxWithScene},
|
||||||
|
{sceneIdxWithTwoPerformers, performerIdx1WithScene},
|
||||||
|
{sceneIdxWithTwoPerformers, performerIdx2WithScene},
|
||||||
|
{sceneIdxWithPerformerTag, performerIdxWithTag},
|
||||||
|
{sceneIdxWithPerformerTwoTags, performerIdxWithTwoTags},
|
||||||
|
}
|
||||||
|
|
||||||
|
sceneGalleryLinks = [][2]int{
|
||||||
|
{sceneIdxWithGallery, galleryIdxWithScene},
|
||||||
|
}
|
||||||
|
|
||||||
|
sceneMovieLinks = [][2]int{
|
||||||
|
{sceneIdxWithMovie, movieIdxWithScene},
|
||||||
|
}
|
||||||
|
|
||||||
|
sceneStudioLinks = [][2]int{
|
||||||
|
{sceneIdxWithStudio, studioIdxWithScene},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
imageGalleryLinks = [][2]int{
|
||||||
|
{imageIdxWithGallery, galleryIdxWithImage},
|
||||||
|
}
|
||||||
|
imageStudioLinks = [][2]int{
|
||||||
|
{imageIdxWithStudio, studioIdxWithImage},
|
||||||
|
}
|
||||||
|
imageTagLinks = [][2]int{
|
||||||
|
{imageIdxWithTag, tagIdxWithImage},
|
||||||
|
{imageIdxWithTwoTags, tagIdx1WithImage},
|
||||||
|
{imageIdxWithTwoTags, tagIdx2WithImage},
|
||||||
|
}
|
||||||
|
imagePerformerLinks = [][2]int{
|
||||||
|
{imageIdxWithPerformer, performerIdxWithImage},
|
||||||
|
{imageIdxWithTwoPerformers, performerIdx1WithImage},
|
||||||
|
{imageIdxWithTwoPerformers, performerIdx2WithImage},
|
||||||
|
{imageIdxWithPerformerTag, performerIdxWithTag},
|
||||||
|
{imageIdxWithPerformerTwoTags, performerIdxWithTwoTags},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
galleryPerformerLinks = [][2]int{
|
||||||
|
{galleryIdxWithPerformer, performerIdxWithGallery},
|
||||||
|
{galleryIdxWithTwoPerformers, performerIdx1WithGallery},
|
||||||
|
{galleryIdxWithTwoPerformers, performerIdx2WithGallery},
|
||||||
|
{galleryIdxWithPerformerTag, performerIdxWithTag},
|
||||||
|
{galleryIdxWithPerformerTwoTags, performerIdxWithTwoTags},
|
||||||
|
}
|
||||||
|
|
||||||
|
galleryTagLinks = [][2]int{
|
||||||
|
{galleryIdxWithTag, tagIdxWithGallery},
|
||||||
|
{galleryIdxWithTwoTags, tagIdx1WithGallery},
|
||||||
|
{galleryIdxWithTwoTags, tagIdx2WithGallery},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
movieStudioLinks = [][2]int{
|
||||||
|
{movieIdxWithStudio, studioIdxWithMovie},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
studioParentLinks = [][2]int{
|
||||||
|
{studioIdxWithChildStudio, studioIdxWithParentStudio},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
performerTagLinks = [][2]int{
|
||||||
|
{performerIdxWithTag, tagIdxWithPerformer},
|
||||||
|
{performerIdxWithTwoTags, tagIdx1WithPerformer},
|
||||||
|
{performerIdxWithTwoTags, tagIdx2WithPerformer},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
ret := runTests(m)
|
ret := runTests(m)
|
||||||
@@ -205,12 +335,16 @@ func populateDB() error {
|
|||||||
return fmt.Errorf("error creating studios: %s", err.Error())
|
return fmt.Errorf("error creating studios: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := linkSceneGallery(r.Scene(), sceneIdxWithGallery, galleryIdxWithScene); err != nil {
|
if err := linkPerformerTags(r.Performer()); err != nil {
|
||||||
return fmt.Errorf("error linking scene to gallery: %s", err.Error())
|
return fmt.Errorf("error linking performer tags: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := linkSceneMovie(r.Scene(), sceneIdxWithMovie, movieIdxWithScene); err != nil {
|
if err := linkSceneGalleries(r.Scene()); err != nil {
|
||||||
return fmt.Errorf("error scene to movie: %s", err.Error())
|
return fmt.Errorf("error linking scenes to galleries: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := linkSceneMovies(r.Scene()); err != nil {
|
||||||
|
return fmt.Errorf("error linking scenes to movies: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := linkScenePerformers(r.Scene()); err != nil {
|
if err := linkScenePerformers(r.Scene()); err != nil {
|
||||||
@@ -221,11 +355,11 @@ func populateDB() error {
|
|||||||
return fmt.Errorf("error linking scene tags: %s", err.Error())
|
return fmt.Errorf("error linking scene tags: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := linkSceneStudio(r.Scene(), sceneIdxWithStudio, studioIdxWithScene); err != nil {
|
if err := linkSceneStudios(r.Scene()); err != nil {
|
||||||
return fmt.Errorf("error linking scene studio: %s", err.Error())
|
return fmt.Errorf("error linking scene studios: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := linkImageGallery(r.Gallery(), imageIdxWithGallery, galleryIdxWithImage); err != nil {
|
if err := linkImageGalleries(r.Gallery()); err != nil {
|
||||||
return fmt.Errorf("error linking gallery images: %s", err.Error())
|
return fmt.Errorf("error linking gallery images: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,16 +371,16 @@ func populateDB() error {
|
|||||||
return fmt.Errorf("error linking image tags: %s", err.Error())
|
return fmt.Errorf("error linking image tags: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := linkImageStudio(r.Image(), imageIdxWithStudio, studioIdxWithImage); err != nil {
|
if err := linkImageStudios(r.Image()); err != nil {
|
||||||
return fmt.Errorf("error linking image studio: %s", err.Error())
|
return fmt.Errorf("error linking image studio: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := linkMovieStudio(r.Movie(), movieIdxWithStudio, studioIdxWithMovie); err != nil {
|
if err := linkMovieStudios(r.Movie()); err != nil {
|
||||||
return fmt.Errorf("error linking movie studio: %s", err.Error())
|
return fmt.Errorf("error linking movie studios: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := linkStudioParent(r.Studio(), studioIdxWithChildStudio, studioIdxWithParentStudio); err != nil {
|
if err := linkStudiosParent(r.Studio()); err != nil {
|
||||||
return fmt.Errorf("error linking studio parent: %s", err.Error())
|
return fmt.Errorf("error linking studios parent: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := linkGalleryPerformers(r.Gallery()); err != nil {
|
if err := linkGalleryPerformers(r.Gallery()); err != nil {
|
||||||
@@ -512,6 +646,30 @@ func getTagMarkerCount(id int) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getTagImageCount(id int) int {
|
||||||
|
if id == tagIDs[tagIdx1WithImage] || id == tagIDs[tagIdx2WithImage] || id == tagIDs[tagIdxWithImage] {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTagGalleryCount(id int) int {
|
||||||
|
if id == tagIDs[tagIdx1WithGallery] || id == tagIDs[tagIdx2WithGallery] || id == tagIDs[tagIdxWithGallery] {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTagPerformerCount(id int) int {
|
||||||
|
if id == tagIDs[tagIdx1WithPerformer] || id == tagIDs[tagIdx2WithPerformer] || id == tagIDs[tagIdxWithPerformer] {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
//createTags creates n tags with plain Name and o tags with camel cased NaMe included
|
//createTags creates n tags with plain Name and o tags with camel cased NaMe included
|
||||||
func createTags(tqb models.TagReaderWriter, n int, o int) error {
|
func createTags(tqb models.TagReaderWriter, n int, o int) error {
|
||||||
const namePlain = "Name"
|
const namePlain = "Name"
|
||||||
@@ -624,189 +782,182 @@ func createMarker(mqb models.SceneMarkerReaderWriter, sceneIdx, primaryTagIdx in
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func linkSceneMovie(qb models.SceneReaderWriter, sceneIndex, movieIndex int) error {
|
func doLinks(links [][2]int, fn func(idx1, idx2 int) error) error {
|
||||||
sceneID := sceneIDs[sceneIndex]
|
for _, l := range links {
|
||||||
movies, err := qb.GetMovies(sceneID)
|
if err := fn(l[0], l[1]); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
movies = append(movies, models.MoviesScenes{
|
return nil
|
||||||
MovieID: movieIDs[movieIndex],
|
}
|
||||||
SceneID: sceneID,
|
|
||||||
|
func linkPerformerTags(qb models.PerformerReaderWriter) error {
|
||||||
|
return doLinks(performerTagLinks, func(performerIndex, tagIndex int) error {
|
||||||
|
performerID := performerIDs[performerIndex]
|
||||||
|
tagID := tagIDs[tagIndex]
|
||||||
|
tagIDs, err := qb.GetTagIDs(performerID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tagIDs = utils.IntAppendUnique(tagIDs, tagID)
|
||||||
|
|
||||||
|
return qb.UpdateTags(performerID, tagIDs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func linkSceneMovies(qb models.SceneReaderWriter) error {
|
||||||
|
return doLinks(sceneMovieLinks, func(sceneIndex, movieIndex int) error {
|
||||||
|
sceneID := sceneIDs[sceneIndex]
|
||||||
|
movies, err := qb.GetMovies(sceneID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
movies = append(movies, models.MoviesScenes{
|
||||||
|
MovieID: movieIDs[movieIndex],
|
||||||
|
SceneID: sceneID,
|
||||||
|
})
|
||||||
|
return qb.UpdateMovies(sceneID, movies)
|
||||||
})
|
})
|
||||||
return qb.UpdateMovies(sceneID, movies)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func linkScenePerformers(qb models.SceneReaderWriter) error {
|
func linkScenePerformers(qb models.SceneReaderWriter) error {
|
||||||
if err := linkScenePerformer(qb, sceneIdxWithPerformer, performerIdxWithScene); err != nil {
|
return doLinks(scenePerformerLinks, func(sceneIndex, performerIndex int) error {
|
||||||
|
_, err := scene.AddPerformer(qb, sceneIDs[sceneIndex], performerIDs[performerIndex])
|
||||||
return err
|
return err
|
||||||
}
|
})
|
||||||
if err := linkScenePerformer(qb, sceneIdxWithTwoPerformers, performerIdx1WithScene); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := linkScenePerformer(qb, sceneIdxWithTwoPerformers, performerIdx2WithScene); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func linkScenePerformer(qb models.SceneReaderWriter, sceneIndex, performerIndex int) error {
|
func linkSceneGalleries(qb models.SceneReaderWriter) error {
|
||||||
_, err := scene.AddPerformer(qb, sceneIDs[sceneIndex], performerIDs[performerIndex])
|
return doLinks(sceneGalleryLinks, func(sceneIndex, galleryIndex int) error {
|
||||||
return err
|
_, err := scene.AddGallery(qb, sceneIDs[sceneIndex], galleryIDs[galleryIndex])
|
||||||
}
|
return err
|
||||||
|
})
|
||||||
func linkSceneGallery(qb models.SceneReaderWriter, sceneIndex, galleryIndex int) error {
|
|
||||||
_, err := scene.AddGallery(qb, sceneIDs[sceneIndex], galleryIDs[galleryIndex])
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func linkSceneTags(qb models.SceneReaderWriter) error {
|
func linkSceneTags(qb models.SceneReaderWriter) error {
|
||||||
if err := linkSceneTag(qb, sceneIdxWithTag, tagIdxWithScene); err != nil {
|
return doLinks(sceneTagLinks, func(sceneIndex, tagIndex int) error {
|
||||||
|
_, err := scene.AddTag(qb, sceneIDs[sceneIndex], tagIDs[tagIndex])
|
||||||
return err
|
return err
|
||||||
}
|
})
|
||||||
if err := linkSceneTag(qb, sceneIdxWithTwoTags, tagIdx1WithScene); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := linkSceneTag(qb, sceneIdxWithTwoTags, tagIdx2WithScene); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func linkSceneTag(qb models.SceneReaderWriter, sceneIndex, tagIndex int) error {
|
func linkSceneStudios(sqb models.SceneWriter) error {
|
||||||
_, err := scene.AddTag(qb, sceneIDs[sceneIndex], tagIDs[tagIndex])
|
return doLinks(sceneStudioLinks, func(sceneIndex, studioIndex int) error {
|
||||||
return err
|
scene := models.ScenePartial{
|
||||||
|
ID: sceneIDs[sceneIndex],
|
||||||
|
StudioID: &sql.NullInt64{Int64: int64(studioIDs[studioIndex]), Valid: true},
|
||||||
|
}
|
||||||
|
_, err := sqb.Update(scene)
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func linkSceneStudio(sqb models.SceneWriter, sceneIndex, studioIndex int) error {
|
func linkImageGalleries(gqb models.GalleryReaderWriter) error {
|
||||||
scene := models.ScenePartial{
|
return doLinks(imageGalleryLinks, func(imageIndex, galleryIndex int) error {
|
||||||
ID: sceneIDs[sceneIndex],
|
return gallery.AddImage(gqb, galleryIDs[galleryIndex], imageIDs[imageIndex])
|
||||||
StudioID: &sql.NullInt64{Int64: int64(studioIDs[studioIndex]), Valid: true},
|
})
|
||||||
}
|
|
||||||
_, err := sqb.Update(scene)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func linkImageGallery(gqb models.GalleryReaderWriter, imageIndex, galleryIndex int) error {
|
|
||||||
return gallery.AddImage(gqb, galleryIDs[galleryIndex], imageIDs[imageIndex])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func linkImageTags(iqb models.ImageReaderWriter) error {
|
func linkImageTags(iqb models.ImageReaderWriter) error {
|
||||||
if err := linkImageTag(iqb, imageIdxWithTag, tagIdxWithImage); err != nil {
|
return doLinks(imageTagLinks, func(imageIndex, tagIndex int) error {
|
||||||
return err
|
imageID := imageIDs[imageIndex]
|
||||||
}
|
tags, err := iqb.GetTagIDs(imageID)
|
||||||
if err := linkImageTag(iqb, imageIdxWithTwoTags, tagIdx1WithImage); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := linkImageTag(iqb, imageIdxWithTwoTags, tagIdx2WithImage); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
tags = append(tags, tagIDs[tagIndex])
|
||||||
|
|
||||||
|
return iqb.UpdateTags(imageID, tags)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func linkImageTag(iqb models.ImageReaderWriter, imageIndex, tagIndex int) error {
|
func linkImageStudios(qb models.ImageWriter) error {
|
||||||
imageID := imageIDs[imageIndex]
|
return doLinks(imageStudioLinks, func(imageIndex, studioIndex int) error {
|
||||||
tags, err := iqb.GetTagIDs(imageID)
|
image := models.ImagePartial{
|
||||||
if err != nil {
|
ID: imageIDs[imageIndex],
|
||||||
|
StudioID: &sql.NullInt64{Int64: int64(studioIDs[studioIndex]), Valid: true},
|
||||||
|
}
|
||||||
|
_, err := qb.Update(image)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
})
|
||||||
|
|
||||||
tags = append(tags, tagIDs[tagIndex])
|
|
||||||
|
|
||||||
return iqb.UpdateTags(imageID, tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
func linkImageStudio(qb models.ImageWriter, imageIndex, studioIndex int) error {
|
|
||||||
image := models.ImagePartial{
|
|
||||||
ID: imageIDs[imageIndex],
|
|
||||||
StudioID: &sql.NullInt64{Int64: int64(studioIDs[studioIndex]), Valid: true},
|
|
||||||
}
|
|
||||||
_, err := qb.Update(image)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func linkImagePerformers(qb models.ImageReaderWriter) error {
|
func linkImagePerformers(qb models.ImageReaderWriter) error {
|
||||||
if err := linkImagePerformer(qb, imageIdxWithPerformer, performerIdxWithImage); err != nil {
|
return doLinks(imagePerformerLinks, func(imageIndex, performerIndex int) error {
|
||||||
return err
|
imageID := imageIDs[imageIndex]
|
||||||
}
|
performers, err := qb.GetPerformerIDs(imageID)
|
||||||
if err := linkImagePerformer(qb, imageIdxWithTwoPerformers, performerIdx1WithImage); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := linkImagePerformer(qb, imageIdxWithTwoPerformers, performerIdx2WithImage); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
performers = append(performers, performerIDs[performerIndex])
|
||||||
|
|
||||||
|
return qb.UpdatePerformers(imageID, performers)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func linkImagePerformer(iqb models.ImageReaderWriter, imageIndex, performerIndex int) error {
|
func linkGalleryPerformers(qb models.GalleryReaderWriter) error {
|
||||||
imageID := imageIDs[imageIndex]
|
return doLinks(galleryPerformerLinks, func(galleryIndex, performerIndex int) error {
|
||||||
performers, err := iqb.GetPerformerIDs(imageID)
|
galleryID := imageIDs[galleryIndex]
|
||||||
if err != nil {
|
performers, err := qb.GetPerformerIDs(galleryID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
performers = append(performers, performerIDs[performerIndex])
|
||||||
|
|
||||||
|
return qb.UpdatePerformers(galleryID, performers)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func linkGalleryTags(iqb models.GalleryReaderWriter) error {
|
||||||
|
return doLinks(galleryTagLinks, func(galleryIndex, tagIndex int) error {
|
||||||
|
galleryID := imageIDs[galleryIndex]
|
||||||
|
tags, err := iqb.GetTagIDs(galleryID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tags = append(tags, tagIDs[tagIndex])
|
||||||
|
|
||||||
|
return iqb.UpdateTags(galleryID, tags)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func linkMovieStudios(mqb models.MovieWriter) error {
|
||||||
|
return doLinks(movieStudioLinks, func(movieIndex, studioIndex int) error {
|
||||||
|
movie := models.MoviePartial{
|
||||||
|
ID: movieIDs[movieIndex],
|
||||||
|
StudioID: &sql.NullInt64{Int64: int64(studioIDs[studioIndex]), Valid: true},
|
||||||
|
}
|
||||||
|
_, err := mqb.Update(movie)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
})
|
||||||
|
|
||||||
performers = append(performers, performerIDs[performerIndex])
|
|
||||||
|
|
||||||
return iqb.UpdatePerformers(imageID, performers)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func linkMovieStudio(mqb models.MovieWriter, movieIndex, studioIndex int) error {
|
func linkStudiosParent(qb models.StudioWriter) error {
|
||||||
movie := models.MoviePartial{
|
return doLinks(studioParentLinks, func(parentIndex, childIndex int) error {
|
||||||
ID: movieIDs[movieIndex],
|
studio := models.StudioPartial{
|
||||||
StudioID: &sql.NullInt64{Int64: int64(studioIDs[studioIndex]), Valid: true},
|
ID: studioIDs[childIndex],
|
||||||
}
|
ParentID: &sql.NullInt64{Int64: int64(studioIDs[parentIndex]), Valid: true},
|
||||||
_, err := mqb.Update(movie)
|
}
|
||||||
|
_, err := qb.Update(studio)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
})
|
||||||
|
|
||||||
func linkStudioParent(qb models.StudioWriter, parentIndex, childIndex int) error {
|
|
||||||
studio := models.StudioPartial{
|
|
||||||
ID: studioIDs[childIndex],
|
|
||||||
ParentID: &sql.NullInt64{Int64: int64(studioIDs[parentIndex]), Valid: true},
|
|
||||||
}
|
|
||||||
_, err := qb.Update(studio)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func addTagImage(qb models.TagWriter, tagIndex int) error {
|
func addTagImage(qb models.TagWriter, tagIndex int) error {
|
||||||
return qb.UpdateImage(tagIDs[tagIndex], models.DefaultTagImage)
|
return qb.UpdateImage(tagIDs[tagIndex], models.DefaultTagImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func linkGalleryTags(iqb models.GalleryReaderWriter) error {
|
|
||||||
if err := linkGalleryTag(iqb, galleryIdxWithTag, tagIdxWithGallery); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := linkGalleryTag(iqb, galleryIdxWithTwoTags, tagIdx1WithGallery); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := linkGalleryTag(iqb, galleryIdxWithTwoTags, tagIdx2WithGallery); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func linkGalleryTag(iqb models.GalleryReaderWriter, galleryIndex, tagIndex int) error {
|
|
||||||
galleryID := galleryIDs[galleryIndex]
|
|
||||||
tags, err := iqb.GetTagIDs(galleryID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tags = append(tags, tagIDs[tagIndex])
|
|
||||||
|
|
||||||
return iqb.UpdateTags(galleryID, tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
func linkGalleryStudio(qb models.GalleryWriter, galleryIndex, studioIndex int) error {
|
func linkGalleryStudio(qb models.GalleryWriter, galleryIndex, studioIndex int) error {
|
||||||
gallery := models.GalleryPartial{
|
gallery := models.GalleryPartial{
|
||||||
ID: galleryIDs[galleryIndex],
|
ID: galleryIDs[galleryIndex],
|
||||||
@@ -816,29 +967,3 @@ func linkGalleryStudio(qb models.GalleryWriter, galleryIndex, studioIndex int) e
|
|||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func linkGalleryPerformers(qb models.GalleryReaderWriter) error {
|
|
||||||
if err := linkGalleryPerformer(qb, galleryIdxWithPerformer, performerIdxWithGallery); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := linkGalleryPerformer(qb, galleryIdxWithTwoPerformers, performerIdx1WithGallery); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := linkGalleryPerformer(qb, galleryIdxWithTwoPerformers, performerIdx2WithGallery); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func linkGalleryPerformer(iqb models.GalleryReaderWriter, galleryIndex, performerIndex int) error {
|
|
||||||
galleryID := galleryIDs[galleryIndex]
|
|
||||||
performers, err := iqb.GetPerformerIDs(galleryID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
performers = append(performers, performerIDs[performerIndex])
|
|
||||||
|
|
||||||
return iqb.UpdatePerformers(galleryID, performers)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -113,6 +113,18 @@ func (qb *tagQueryBuilder) FindBySceneID(sceneID int) ([]*models.Tag, error) {
|
|||||||
return qb.queryTags(query, args)
|
return qb.queryTags(query, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qb *tagQueryBuilder) FindByPerformerID(performerID int) ([]*models.Tag, error) {
|
||||||
|
query := `
|
||||||
|
SELECT tags.* FROM tags
|
||||||
|
LEFT JOIN performers_tags as performers_join on performers_join.tag_id = tags.id
|
||||||
|
WHERE performers_join.performer_id = ?
|
||||||
|
GROUP BY tags.id
|
||||||
|
`
|
||||||
|
query += qb.getTagSort(nil)
|
||||||
|
args := []interface{}{performerID}
|
||||||
|
return qb.queryTags(query, args)
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *tagQueryBuilder) FindByImageID(imageID int) ([]*models.Tag, error) {
|
func (qb *tagQueryBuilder) FindByImageID(imageID int) ([]*models.Tag, error) {
|
||||||
query := `
|
query := `
|
||||||
SELECT tags.* FROM tags
|
SELECT tags.* FROM tags
|
||||||
@@ -211,6 +223,12 @@ func (qb *tagQueryBuilder) Query(tagFilter *models.TagFilterType, findFilter *mo
|
|||||||
|
|
||||||
query.body += `
|
query.body += `
|
||||||
left join tags_image on tags_image.tag_id = tags.id
|
left join tags_image on tags_image.tag_id = tags.id
|
||||||
|
left join images_tags on images_tags.tag_id = tags.id
|
||||||
|
left join images on images_tags.image_id = images.id
|
||||||
|
left join galleries_tags on galleries_tags.tag_id = tags.id
|
||||||
|
left join galleries on galleries_tags.gallery_id = galleries.id
|
||||||
|
left join performers_tags on performers_tags.tag_id = tags.id
|
||||||
|
left join performers on performers_tags.performer_id = performers.id
|
||||||
left join scenes_tags on scenes_tags.tag_id = tags.id
|
left join scenes_tags on scenes_tags.tag_id = tags.id
|
||||||
left join scenes on scenes_tags.scene_id = scenes.id`
|
left join scenes on scenes_tags.scene_id = scenes.id`
|
||||||
|
|
||||||
@@ -238,6 +256,30 @@ func (qb *tagQueryBuilder) Query(tagFilter *models.TagFilterType, findFilter *mo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if imageCount := tagFilter.ImageCount; imageCount != nil {
|
||||||
|
clause, count := getIntCriterionWhereClause("count(distinct images_tags.image_id)", *imageCount)
|
||||||
|
query.addHaving(clause)
|
||||||
|
if count == 1 {
|
||||||
|
query.addArg(imageCount.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if galleryCount := tagFilter.GalleryCount; galleryCount != nil {
|
||||||
|
clause, count := getIntCriterionWhereClause("count(distinct galleries_tags.gallery_id)", *galleryCount)
|
||||||
|
query.addHaving(clause)
|
||||||
|
if count == 1 {
|
||||||
|
query.addArg(galleryCount.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if performersCount := tagFilter.PerformerCount; performersCount != nil {
|
||||||
|
clause, count := getIntCriterionWhereClause("count(distinct performers_tags.performer_id)", *performersCount)
|
||||||
|
query.addHaving(clause)
|
||||||
|
if count == 1 {
|
||||||
|
query.addArg(performersCount.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// if markerCount := tagFilter.MarkerCount; markerCount != nil {
|
// if markerCount := tagFilter.MarkerCount; markerCount != nil {
|
||||||
// clause, count := getIntCriterionWhereClause("count(distinct scene_markers.id)", *markerCount)
|
// clause, count := getIntCriterionWhereClause("count(distinct scene_markers.id)", *markerCount)
|
||||||
// query.addHaving(clause)
|
// query.addHaving(clause)
|
||||||
|
|||||||
@@ -238,6 +238,132 @@ func verifyTagMarkerCount(t *testing.T, markerCountCriterion models.IntCriterion
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTagQueryImageCount(t *testing.T) {
|
||||||
|
countCriterion := models.IntCriterionInput{
|
||||||
|
Value: 1,
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyTagImageCount(t, countCriterion)
|
||||||
|
|
||||||
|
countCriterion.Modifier = models.CriterionModifierNotEquals
|
||||||
|
verifyTagImageCount(t, countCriterion)
|
||||||
|
|
||||||
|
countCriterion.Modifier = models.CriterionModifierLessThan
|
||||||
|
verifyTagImageCount(t, countCriterion)
|
||||||
|
|
||||||
|
countCriterion.Value = 0
|
||||||
|
countCriterion.Modifier = models.CriterionModifierGreaterThan
|
||||||
|
verifyTagImageCount(t, countCriterion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyTagImageCount(t *testing.T, imageCountCriterion models.IntCriterionInput) {
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
qb := r.Tag()
|
||||||
|
tagFilter := models.TagFilterType{
|
||||||
|
ImageCount: &imageCountCriterion,
|
||||||
|
}
|
||||||
|
|
||||||
|
tags, _, err := qb.Query(&tagFilter, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error querying tag: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range tags {
|
||||||
|
verifyInt64(t, sql.NullInt64{
|
||||||
|
Int64: int64(getTagImageCount(tag.ID)),
|
||||||
|
Valid: true,
|
||||||
|
}, imageCountCriterion)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTagQueryGalleryCount(t *testing.T) {
|
||||||
|
countCriterion := models.IntCriterionInput{
|
||||||
|
Value: 1,
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyTagGalleryCount(t, countCriterion)
|
||||||
|
|
||||||
|
countCriterion.Modifier = models.CriterionModifierNotEquals
|
||||||
|
verifyTagGalleryCount(t, countCriterion)
|
||||||
|
|
||||||
|
countCriterion.Modifier = models.CriterionModifierLessThan
|
||||||
|
verifyTagGalleryCount(t, countCriterion)
|
||||||
|
|
||||||
|
countCriterion.Value = 0
|
||||||
|
countCriterion.Modifier = models.CriterionModifierGreaterThan
|
||||||
|
verifyTagGalleryCount(t, countCriterion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyTagGalleryCount(t *testing.T, imageCountCriterion models.IntCriterionInput) {
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
qb := r.Tag()
|
||||||
|
tagFilter := models.TagFilterType{
|
||||||
|
GalleryCount: &imageCountCriterion,
|
||||||
|
}
|
||||||
|
|
||||||
|
tags, _, err := qb.Query(&tagFilter, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error querying tag: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range tags {
|
||||||
|
verifyInt64(t, sql.NullInt64{
|
||||||
|
Int64: int64(getTagGalleryCount(tag.ID)),
|
||||||
|
Valid: true,
|
||||||
|
}, imageCountCriterion)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTagQueryPerformerCount(t *testing.T) {
|
||||||
|
countCriterion := models.IntCriterionInput{
|
||||||
|
Value: 1,
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyTagPerformerCount(t, countCriterion)
|
||||||
|
|
||||||
|
countCriterion.Modifier = models.CriterionModifierNotEquals
|
||||||
|
verifyTagPerformerCount(t, countCriterion)
|
||||||
|
|
||||||
|
countCriterion.Modifier = models.CriterionModifierLessThan
|
||||||
|
verifyTagPerformerCount(t, countCriterion)
|
||||||
|
|
||||||
|
countCriterion.Value = 0
|
||||||
|
countCriterion.Modifier = models.CriterionModifierGreaterThan
|
||||||
|
verifyTagPerformerCount(t, countCriterion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyTagPerformerCount(t *testing.T, imageCountCriterion models.IntCriterionInput) {
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
qb := r.Tag()
|
||||||
|
tagFilter := models.TagFilterType{
|
||||||
|
PerformerCount: &imageCountCriterion,
|
||||||
|
}
|
||||||
|
|
||||||
|
tags, _, err := qb.Query(&tagFilter, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error querying tag: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range tags {
|
||||||
|
verifyInt64(t, sql.NullInt64{
|
||||||
|
Int64: int64(getTagPerformerCount(tag.ID)),
|
||||||
|
Valid: true,
|
||||||
|
}, imageCountCriterion)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestTagUpdateTagImage(t *testing.T) {
|
func TestTagUpdateTagImage(t *testing.T) {
|
||||||
if err := withTxn(func(r models.Repository) error {
|
if err := withTxn(func(r models.Repository) error {
|
||||||
qb := r.Tag()
|
qb := r.Tag()
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
### ✨ New Features
|
||||||
|
* Added Performer tags.
|
||||||
|
|
||||||
### 🎨 Improvements
|
### 🎨 Improvements
|
||||||
* Improved performer details and edit UI pages.
|
* Improved performer details and edit UI pages.
|
||||||
* Resolve python executable to `python3` or `python` for python script scrapers.
|
* Resolve python executable to `python3` or `python` for python script scrapers.
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
criterion.type !== "parent_studios" &&
|
criterion.type !== "parent_studios" &&
|
||||||
criterion.type !== "tags" &&
|
criterion.type !== "tags" &&
|
||||||
criterion.type !== "sceneTags" &&
|
criterion.type !== "sceneTags" &&
|
||||||
|
criterion.type !== "performerTags" &&
|
||||||
criterion.type !== "movies"
|
criterion.type !== "movies"
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|||||||
225
ui/v2.5/src/components/Performers/EditPerformersDialog.tsx
Normal file
225
ui/v2.5/src/components/Performers/EditPerformersDialog.tsx
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Form } from "react-bootstrap";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { useBulkPerformerUpdate } from "src/core/StashService";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { Modal } from "src/components/Shared";
|
||||||
|
import { useToast } from "src/hooks";
|
||||||
|
import MultiSet from "../Shared/MultiSet";
|
||||||
|
|
||||||
|
interface IListOperationProps {
|
||||||
|
selected: GQL.SlimPerformerDataFragment[];
|
||||||
|
onClose: (applied: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||||
|
props: IListOperationProps
|
||||||
|
) => {
|
||||||
|
const Toast = useToast();
|
||||||
|
|
||||||
|
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
|
||||||
|
GQL.BulkUpdateIdMode.Add
|
||||||
|
);
|
||||||
|
const [tagIds, setTagIds] = useState<string[]>();
|
||||||
|
const [favorite, setFavorite] = useState<boolean | undefined>();
|
||||||
|
|
||||||
|
const [updatePerformers] = useBulkPerformerUpdate(getPerformerInput());
|
||||||
|
|
||||||
|
// Network state
|
||||||
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
|
||||||
|
const checkboxRef = React.createRef<HTMLInputElement>();
|
||||||
|
|
||||||
|
function makeBulkUpdateIds(
|
||||||
|
ids: string[],
|
||||||
|
mode: GQL.BulkUpdateIdMode
|
||||||
|
): GQL.BulkUpdateIds {
|
||||||
|
return {
|
||||||
|
mode,
|
||||||
|
ids,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPerformerInput(): GQL.BulkPerformerUpdateInput {
|
||||||
|
// need to determine what we are actually setting on each performer
|
||||||
|
const aggregateTagIds = getTagIds(props.selected);
|
||||||
|
|
||||||
|
const performerInput: GQL.BulkPerformerUpdateInput = {
|
||||||
|
ids: props.selected.map((performer) => {
|
||||||
|
return performer.id;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// if tagIds non-empty, then we are setting them
|
||||||
|
if (
|
||||||
|
tagMode === GQL.BulkUpdateIdMode.Set &&
|
||||||
|
(!tagIds || tagIds.length === 0)
|
||||||
|
) {
|
||||||
|
// and all performers have the same ids,
|
||||||
|
if (aggregateTagIds.length > 0) {
|
||||||
|
// then unset the tagIds, otherwise ignore
|
||||||
|
performerInput.tag_ids = makeBulkUpdateIds(tagIds || [], tagMode);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if tagIds non-empty, then we are setting them
|
||||||
|
performerInput.tag_ids = makeBulkUpdateIds(tagIds || [], tagMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (favorite !== undefined) {
|
||||||
|
performerInput.favorite = favorite;
|
||||||
|
}
|
||||||
|
|
||||||
|
return performerInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSave() {
|
||||||
|
setIsUpdating(true);
|
||||||
|
try {
|
||||||
|
await updatePerformers();
|
||||||
|
Toast.success({ content: "Updated performers" });
|
||||||
|
props.onClose(true);
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
}
|
||||||
|
setIsUpdating(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTagIds(state: GQL.SlimPerformerDataFragment[]) {
|
||||||
|
let ret: string[] = [];
|
||||||
|
let first = true;
|
||||||
|
|
||||||
|
state.forEach((performer: GQL.SlimPerformerDataFragment) => {
|
||||||
|
if (first) {
|
||||||
|
ret = performer.tags ? performer.tags.map((t) => t.id).sort() : [];
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
const tIds = performer.tags
|
||||||
|
? performer.tags.map((t) => t.id).sort()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (!_.isEqual(ret, tIds)) {
|
||||||
|
ret = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const state = props.selected;
|
||||||
|
let updateTagIds: string[] = [];
|
||||||
|
let updateFavorite: boolean | undefined;
|
||||||
|
let first = true;
|
||||||
|
|
||||||
|
state.forEach((performer: GQL.SlimPerformerDataFragment) => {
|
||||||
|
const performerTagIDs = (performer.tags ?? []).map((p) => p.id).sort();
|
||||||
|
|
||||||
|
if (first) {
|
||||||
|
updateTagIds = performerTagIDs;
|
||||||
|
first = false;
|
||||||
|
updateFavorite = performer.favorite;
|
||||||
|
} else {
|
||||||
|
if (!_.isEqual(performerTagIDs, updateTagIds)) {
|
||||||
|
updateTagIds = [];
|
||||||
|
}
|
||||||
|
if (performer.favorite !== updateFavorite) {
|
||||||
|
updateFavorite = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (tagMode === GQL.BulkUpdateIdMode.Set) {
|
||||||
|
setTagIds(updateTagIds);
|
||||||
|
}
|
||||||
|
setFavorite(updateFavorite);
|
||||||
|
}, [props.selected, tagMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (checkboxRef.current) {
|
||||||
|
checkboxRef.current.indeterminate = favorite === undefined;
|
||||||
|
}
|
||||||
|
}, [favorite, checkboxRef]);
|
||||||
|
|
||||||
|
function renderMultiSelect(
|
||||||
|
type: "performers" | "tags",
|
||||||
|
ids: string[] | undefined
|
||||||
|
) {
|
||||||
|
let mode = GQL.BulkUpdateIdMode.Add;
|
||||||
|
switch (type) {
|
||||||
|
case "tags":
|
||||||
|
mode = tagMode;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MultiSet
|
||||||
|
type={type}
|
||||||
|
disabled={isUpdating}
|
||||||
|
onUpdate={(items) => {
|
||||||
|
const itemIDs = items.map((i) => i.id);
|
||||||
|
switch (type) {
|
||||||
|
case "tags":
|
||||||
|
setTagIds(itemIDs);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSetMode={(newMode) => {
|
||||||
|
switch (type) {
|
||||||
|
case "tags":
|
||||||
|
setTagMode(newMode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
ids={ids ?? []}
|
||||||
|
mode={mode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cycleFavorite() {
|
||||||
|
if (favorite) {
|
||||||
|
setFavorite(undefined);
|
||||||
|
} else if (favorite === undefined) {
|
||||||
|
setFavorite(false);
|
||||||
|
} else {
|
||||||
|
setFavorite(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
show
|
||||||
|
icon="pencil-alt"
|
||||||
|
header="Edit Performers"
|
||||||
|
accept={{ onClick: onSave, text: "Apply" }}
|
||||||
|
cancel={{
|
||||||
|
onClick: () => props.onClose(false),
|
||||||
|
text: "Cancel",
|
||||||
|
variant: "secondary",
|
||||||
|
}}
|
||||||
|
isRunning={isUpdating}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<Form.Group controlId="tags">
|
||||||
|
<Form.Label>Tags</Form.Label>
|
||||||
|
{renderMultiSelect("tags", tagIds)}
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="favorite">
|
||||||
|
<Form.Check
|
||||||
|
type="checkbox"
|
||||||
|
label="Favorite"
|
||||||
|
checked={favorite}
|
||||||
|
ref={checkboxRef}
|
||||||
|
onChange={() => cycleFavorite()}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return render();
|
||||||
|
};
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { FormattedNumber, FormattedPlural, FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { NavUtils, TextUtils } from "src/utils";
|
import { NavUtils, TextUtils } from "src/utils";
|
||||||
import { BasicCard, CountryFlag, TruncatedText } from "src/components/Shared";
|
import {
|
||||||
|
BasicCard,
|
||||||
|
CountryFlag,
|
||||||
|
HoverPopover,
|
||||||
|
Icon,
|
||||||
|
TagLink,
|
||||||
|
TruncatedText,
|
||||||
|
} from "src/components/Shared";
|
||||||
|
import { Button, ButtonGroup } from "react-bootstrap";
|
||||||
|
|
||||||
interface IPerformerCardProps {
|
interface IPerformerCardProps {
|
||||||
performer: GQL.PerformerDataFragment;
|
performer: GQL.PerformerDataFragment;
|
||||||
@@ -34,6 +42,50 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function maybeRenderScenesPopoverButton() {
|
||||||
|
if (!performer.scene_count) return;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={NavUtils.makePerformerScenesUrl(performer)}>
|
||||||
|
<Button className="minimal">
|
||||||
|
<Icon icon="play-circle" />
|
||||||
|
<span>{performer.scene_count}</span>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderTagPopoverButton() {
|
||||||
|
if (performer.tags.length <= 0) return;
|
||||||
|
|
||||||
|
const popoverContent = performer.tags.map((tag) => (
|
||||||
|
<TagLink key={tag.id} tagType="performer" tag={tag} />
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HoverPopover placement="bottom" content={popoverContent}>
|
||||||
|
<Button className="minimal">
|
||||||
|
<Icon icon="tag" />
|
||||||
|
<span>{performer.tags.length}</span>
|
||||||
|
</Button>
|
||||||
|
</HoverPopover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderPopoverButtonGroup() {
|
||||||
|
if (performer.scene_count || performer.tags.length > 0) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<hr />
|
||||||
|
<ButtonGroup className="card-popovers">
|
||||||
|
{maybeRenderScenesPopoverButton()}
|
||||||
|
{maybeRenderTagPopoverButton()}
|
||||||
|
</ButtonGroup>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BasicCard
|
<BasicCard
|
||||||
className="performer-card"
|
className="performer-card"
|
||||||
@@ -57,19 +109,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
|||||||
<Link to={NavUtils.makePerformersCountryUrl(performer)}>
|
<Link to={NavUtils.makePerformersCountryUrl(performer)}>
|
||||||
<CountryFlag country={performer.country} />
|
<CountryFlag country={performer.country} />
|
||||||
</Link>
|
</Link>
|
||||||
<div className="text-muted">
|
{maybeRenderPopoverButtonGroup()}
|
||||||
Stars in
|
|
||||||
<FormattedNumber value={performer.scene_count ?? 0} />
|
|
||||||
|
|
||||||
<Link to={NavUtils.makePerformerScenesUrl(performer)}>
|
|
||||||
<FormattedPlural
|
|
||||||
value={performer.scene_count ?? 0}
|
|
||||||
one="scene"
|
|
||||||
other="scenes"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
|
import { TagLink } from "src/components/Shared";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { genderToString } from "src/core/StashService";
|
import { genderToString } from "src/core/StashService";
|
||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
@@ -15,6 +16,25 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
// Network state
|
// Network state
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
function renderTagsField() {
|
||||||
|
if (!performer.tags?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dl className="row">
|
||||||
|
<dt className="col-3 col-xl-2">Tags</dt>
|
||||||
|
<dd className="col-9 col-xl-10">
|
||||||
|
<ul className="pl-0">
|
||||||
|
{(performer.tags ?? []).map((tag) => (
|
||||||
|
<TagLink key={tag.id} tagType="performer" tag={tag} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function renderStashIDs() {
|
function renderStashIDs() {
|
||||||
if (!performer.stash_ids?.length) {
|
if (!performer.stash_ids?.length) {
|
||||||
return;
|
return;
|
||||||
@@ -101,6 +121,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
TextUtils.instagramURL
|
TextUtils.instagramURL
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
{renderTagsField()}
|
||||||
{renderStashIDs()}
|
{renderStashIDs()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Col,
|
Col,
|
||||||
InputGroup,
|
InputGroup,
|
||||||
Row,
|
Row,
|
||||||
|
Badge,
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
@@ -20,6 +21,7 @@ import {
|
|||||||
mutateReloadScrapers,
|
mutateReloadScrapers,
|
||||||
usePerformerUpdate,
|
usePerformerUpdate,
|
||||||
usePerformerCreate,
|
usePerformerCreate,
|
||||||
|
useTagCreate,
|
||||||
queryScrapePerformerURL,
|
queryScrapePerformerURL,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import {
|
import {
|
||||||
@@ -28,6 +30,8 @@ import {
|
|||||||
ImageInput,
|
ImageInput,
|
||||||
ScrapePerformerSuggest,
|
ScrapePerformerSuggest,
|
||||||
LoadingIndicator,
|
LoadingIndicator,
|
||||||
|
CollapseButton,
|
||||||
|
TagSelect,
|
||||||
} from "src/components/Shared";
|
} from "src/components/Shared";
|
||||||
import { ImageUtils } from "src/utils";
|
import { ImageUtils } from "src/utils";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
@@ -64,6 +68,8 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
scrapePerformerDetails,
|
scrapePerformerDetails,
|
||||||
setScrapePerformerDetails,
|
setScrapePerformerDetails,
|
||||||
] = useState<GQL.ScrapedPerformerDataFragment>();
|
] = useState<GQL.ScrapedPerformerDataFragment>();
|
||||||
|
const [newTags, setNewTags] = useState<GQL.ScrapedSceneTag[]>();
|
||||||
|
|
||||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
// Network state
|
// Network state
|
||||||
@@ -81,6 +87,8 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
|
|
||||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
||||||
|
|
||||||
|
const [createTag] = useTagCreate({ name: "" });
|
||||||
|
|
||||||
const genderOptions = [""].concat(getGenderStrings());
|
const genderOptions = [""].concat(getGenderStrings());
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
@@ -100,6 +108,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
url: yup.string().optional(),
|
url: yup.string().optional(),
|
||||||
twitter: yup.string().optional(),
|
twitter: yup.string().optional(),
|
||||||
instagram: yup.string().optional(),
|
instagram: yup.string().optional(),
|
||||||
|
tag_ids: yup.array(yup.string().required()).optional(),
|
||||||
stash_ids: yup.mixed<GQL.StashIdInput>().optional(),
|
stash_ids: yup.mixed<GQL.StashIdInput>().optional(),
|
||||||
image: yup.string().optional().nullable(),
|
image: yup.string().optional().nullable(),
|
||||||
});
|
});
|
||||||
@@ -121,6 +130,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
url: performer.url ?? "",
|
url: performer.url ?? "",
|
||||||
twitter: performer.twitter ?? "",
|
twitter: performer.twitter ?? "",
|
||||||
instagram: performer.instagram ?? "",
|
instagram: performer.instagram ?? "",
|
||||||
|
tag_ids: (performer.tags ?? []).map((t) => t.id),
|
||||||
stash_ids: performer.stash_ids ?? undefined,
|
stash_ids: performer.stash_ids ?? undefined,
|
||||||
image: undefined,
|
image: undefined,
|
||||||
};
|
};
|
||||||
@@ -154,6 +164,75 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
return genderToString(retEnum);
|
return genderToString(retEnum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderNewTags() {
|
||||||
|
if (!newTags || newTags.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret = (
|
||||||
|
<>
|
||||||
|
{newTags.map((t) => (
|
||||||
|
<Badge
|
||||||
|
className="tag-item"
|
||||||
|
variant="secondary"
|
||||||
|
key={t.name}
|
||||||
|
onClick={() => createNewTag(t)}
|
||||||
|
>
|
||||||
|
{t.name}
|
||||||
|
<Button className="minimal ml-2">
|
||||||
|
<Icon className="fa-fw" icon="plus" />
|
||||||
|
</Button>
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const minCollapseLength = 10;
|
||||||
|
|
||||||
|
if (newTags.length >= minCollapseLength) {
|
||||||
|
return (
|
||||||
|
<CollapseButton text={`Missing (${newTags.length})`}>
|
||||||
|
{ret}
|
||||||
|
</CollapseButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createNewTag(toCreate: GQL.ScrapedSceneTag) {
|
||||||
|
let tagInput: GQL.TagCreateInput = { name: "" };
|
||||||
|
try {
|
||||||
|
tagInput = Object.assign(tagInput, toCreate);
|
||||||
|
const result = await createTag({
|
||||||
|
variables: tagInput,
|
||||||
|
});
|
||||||
|
|
||||||
|
// add the new tag to the new tags value
|
||||||
|
const newTagIds = formik.values.tag_ids.concat([
|
||||||
|
result.data!.tagCreate!.id,
|
||||||
|
]);
|
||||||
|
formik.setFieldValue("tag_ids", newTagIds);
|
||||||
|
|
||||||
|
// remove the tag from the list
|
||||||
|
const newTagsClone = newTags!.concat();
|
||||||
|
const pIndex = newTagsClone.indexOf(toCreate);
|
||||||
|
newTagsClone.splice(pIndex, 1);
|
||||||
|
|
||||||
|
setNewTags(newTagsClone);
|
||||||
|
|
||||||
|
Toast.success({
|
||||||
|
content: (
|
||||||
|
<span>
|
||||||
|
Created tag: <b>{toCreate.name}</b>
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updatePerformerEditStateFromScraper(
|
function updatePerformerEditStateFromScraper(
|
||||||
state: Partial<GQL.ScrapedPerformerDataFragment>
|
state: Partial<GQL.ScrapedPerformerDataFragment>
|
||||||
) {
|
) {
|
||||||
@@ -210,6 +289,13 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
translateScrapedGender(state.gender ?? undefined)
|
translateScrapedGender(state.gender ?? undefined)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (state.tags) {
|
||||||
|
// map tags to their ids and filter out those not found
|
||||||
|
const newTagIds = state.tags.map((t) => t.stored_id).filter((t) => t);
|
||||||
|
formik.setFieldValue("tag_ids", newTagIds as string[]);
|
||||||
|
|
||||||
|
setNewTags(state.tags.filter((t) => !t.stored_id));
|
||||||
|
}
|
||||||
|
|
||||||
// image is a base64 string
|
// image is a base64 string
|
||||||
// #404: don't overwrite image if it has been modified by the user
|
// #404: don't overwrite image if it has been modified by the user
|
||||||
@@ -354,7 +440,13 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
if (!scrapePerformerDetails) return {};
|
if (!scrapePerformerDetails) return {};
|
||||||
|
|
||||||
// image is not supported
|
// image is not supported
|
||||||
const { __typename, image: _image, ...ret } = scrapePerformerDetails;
|
// remove tags as well
|
||||||
|
const {
|
||||||
|
__typename,
|
||||||
|
image: _image,
|
||||||
|
tags: _tags,
|
||||||
|
...ret
|
||||||
|
} = scrapePerformerDetails;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,10 +576,10 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPerformer: Partial<GQL.PerformerDataFragment> = {
|
const currentPerformer: Partial<GQL.PerformerUpdateInput> = {
|
||||||
...formik.values,
|
...formik.values,
|
||||||
gender: stringToGender(formik.values.gender),
|
gender: stringToGender(formik.values.gender),
|
||||||
image_path: formik.values.image ?? performer.image_path,
|
image: formik.values.image ?? performer.image_path,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -570,6 +662,28 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderTagsField() {
|
||||||
|
return (
|
||||||
|
<Form.Row>
|
||||||
|
<Form.Group as={Col} sm="6">
|
||||||
|
<Form.Label>Tags</Form.Label>
|
||||||
|
<TagSelect
|
||||||
|
menuPortalTarget={document.body}
|
||||||
|
isMulti
|
||||||
|
onSelect={(items) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
"tag_ids",
|
||||||
|
items.map((item) => item.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ids={formik.values.tag_ids}
|
||||||
|
/>
|
||||||
|
{renderNewTags()}
|
||||||
|
</Form.Group>
|
||||||
|
</Form.Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const removeStashID = (stashID: GQL.StashIdInput) => {
|
const removeStashID = (stashID: GQL.StashIdInput) => {
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
"stash_ids",
|
"stash_ids",
|
||||||
@@ -805,6 +919,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form.Row>
|
</Form.Row>
|
||||||
|
{renderTagsField()}
|
||||||
{renderStashIDs()}
|
{renderStashIDs()}
|
||||||
|
|
||||||
{renderButtons()}
|
{renderButtons()}
|
||||||
|
|||||||
@@ -11,8 +11,12 @@ import {
|
|||||||
getGenderStrings,
|
getGenderStrings,
|
||||||
genderToString,
|
genderToString,
|
||||||
stringToGender,
|
stringToGender,
|
||||||
|
useTagCreate,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { Form } from "react-bootstrap";
|
import { Form } from "react-bootstrap";
|
||||||
|
import { TagSelect } from "src/components/Shared";
|
||||||
|
import { useToast } from "src/hooks";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
function renderScrapedGender(
|
function renderScrapedGender(
|
||||||
result: ScrapeResult<string>,
|
result: ScrapeResult<string>,
|
||||||
@@ -62,8 +66,54 @@ function renderScrapedGenderRow(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderScrapedTags(
|
||||||
|
result: ScrapeResult<string[]>,
|
||||||
|
isNew?: boolean,
|
||||||
|
onChange?: (value: string[]) => void
|
||||||
|
) {
|
||||||
|
const resultValue = isNew ? result.newValue : result.originalValue;
|
||||||
|
const value = resultValue ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TagSelect
|
||||||
|
isMulti
|
||||||
|
className="form-control react-select"
|
||||||
|
isDisabled={!isNew}
|
||||||
|
onSelect={(items) => {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(items.map((i) => i.id));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
ids={value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderScrapedTagsRow(
|
||||||
|
result: ScrapeResult<string[]>,
|
||||||
|
onChange: (value: ScrapeResult<string[]>) => void,
|
||||||
|
newTags: GQL.ScrapedSceneTag[],
|
||||||
|
onCreateNew?: (value: GQL.ScrapedSceneTag) => void
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<ScrapeDialogRow
|
||||||
|
title="Tags"
|
||||||
|
result={result}
|
||||||
|
renderOriginalField={() => renderScrapedTags(result)}
|
||||||
|
renderNewField={() =>
|
||||||
|
renderScrapedTags(result, true, (value) =>
|
||||||
|
onChange(result.cloneWithValue(value))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
newValues={newTags}
|
||||||
|
onChange={onChange}
|
||||||
|
onCreateNew={onCreateNew}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface IPerformerScrapeDialogProps {
|
interface IPerformerScrapeDialogProps {
|
||||||
performer: Partial<GQL.PerformerDataFragment>;
|
performer: Partial<GQL.PerformerUpdateInput>;
|
||||||
scraped: GQL.ScrapedPerformer;
|
scraped: GQL.ScrapedPerformer;
|
||||||
|
|
||||||
onClose: (scrapedPerformer?: GQL.ScrapedPerformer) => void;
|
onClose: (scrapedPerformer?: GQL.ScrapedPerformer) => void;
|
||||||
@@ -151,8 +201,64 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [createTag] = useTagCreate({ name: "" });
|
||||||
|
const Toast = useToast();
|
||||||
|
|
||||||
|
interface IHasStoredID {
|
||||||
|
stored_id?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStoredIdObjects(
|
||||||
|
scrapedObjects?: IHasStoredID[]
|
||||||
|
): string[] | undefined {
|
||||||
|
if (!scrapedObjects) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const ret = scrapedObjects
|
||||||
|
.map((p) => p.stored_id)
|
||||||
|
.filter((p) => {
|
||||||
|
return p !== undefined && p !== null;
|
||||||
|
}) as string[];
|
||||||
|
|
||||||
|
if (ret.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort by id numerically
|
||||||
|
ret.sort((a, b) => {
|
||||||
|
return parseInt(a, 10) - parseInt(b, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortIdList(idList?: string[] | null) {
|
||||||
|
if (!idList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret = _.clone(idList);
|
||||||
|
// sort by id numerically
|
||||||
|
ret.sort((a, b) => {
|
||||||
|
return parseInt(a, 10) - parseInt(b, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [tags, setTags] = useState<ScrapeResult<string[]>>(
|
||||||
|
new ScrapeResult<string[]>(
|
||||||
|
sortIdList(props.performer.tag_ids ?? undefined),
|
||||||
|
mapStoredIdObjects(props.scraped.tags ?? undefined)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const [newTags, setNewTags] = useState<GQL.ScrapedSceneTag[]>(
|
||||||
|
props.scraped.tags?.filter((t) => !t.stored_id) ?? []
|
||||||
|
);
|
||||||
|
|
||||||
const [image, setImage] = useState<ScrapeResult<string>>(
|
const [image, setImage] = useState<ScrapeResult<string>>(
|
||||||
new ScrapeResult<string>(props.performer.image_path, props.scraped.image)
|
new ScrapeResult<string>(props.performer.image, props.scraped.image)
|
||||||
);
|
);
|
||||||
|
|
||||||
const allFields = [
|
const allFields = [
|
||||||
@@ -173,6 +279,7 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||||||
instagram,
|
instagram,
|
||||||
gender,
|
gender,
|
||||||
image,
|
image,
|
||||||
|
tags,
|
||||||
];
|
];
|
||||||
// don't show the dialog if nothing was scraped
|
// don't show the dialog if nothing was scraped
|
||||||
if (allFields.every((r) => !r.scraped)) {
|
if (allFields.every((r) => !r.scraped)) {
|
||||||
@@ -180,6 +287,41 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createNewTag(toCreate: GQL.ScrapedSceneTag) {
|
||||||
|
let tagInput: GQL.TagCreateInput = { name: "" };
|
||||||
|
try {
|
||||||
|
tagInput = Object.assign(tagInput, toCreate);
|
||||||
|
const result = await createTag({
|
||||||
|
variables: tagInput,
|
||||||
|
});
|
||||||
|
|
||||||
|
// add the new tag to the new tags value
|
||||||
|
const tagClone = tags.cloneWithValue(tags.newValue);
|
||||||
|
if (!tagClone.newValue) {
|
||||||
|
tagClone.newValue = [];
|
||||||
|
}
|
||||||
|
tagClone.newValue.push(result.data!.tagCreate!.id);
|
||||||
|
setTags(tagClone);
|
||||||
|
|
||||||
|
// remove the tag from the list
|
||||||
|
const newTagsClone = newTags.concat();
|
||||||
|
const pIndex = newTagsClone.indexOf(toCreate);
|
||||||
|
newTagsClone.splice(pIndex, 1);
|
||||||
|
|
||||||
|
setNewTags(newTagsClone);
|
||||||
|
|
||||||
|
Toast.success({
|
||||||
|
content: (
|
||||||
|
<span>
|
||||||
|
Created tag: <b>{toCreate.name}</b>
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function makeNewScrapedItem(): GQL.ScrapedPerformer {
|
function makeNewScrapedItem(): GQL.ScrapedPerformer {
|
||||||
return {
|
return {
|
||||||
name: name.getNewValue(),
|
name: name.getNewValue(),
|
||||||
@@ -198,6 +340,12 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||||||
twitter: twitter.getNewValue(),
|
twitter: twitter.getNewValue(),
|
||||||
instagram: instagram.getNewValue(),
|
instagram: instagram.getNewValue(),
|
||||||
gender: gender.getNewValue(),
|
gender: gender.getNewValue(),
|
||||||
|
tags: tags.getNewValue()?.map((m) => {
|
||||||
|
return {
|
||||||
|
stored_id: m,
|
||||||
|
name: "",
|
||||||
|
};
|
||||||
|
}),
|
||||||
image: image.getNewValue(),
|
image: image.getNewValue(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -281,6 +429,12 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||||||
result={instagram}
|
result={instagram}
|
||||||
onChange={(value) => setInstagram(value)}
|
onChange={(value) => setInstagram(value)}
|
||||||
/>
|
/>
|
||||||
|
{renderScrapedTagsRow(
|
||||||
|
tags,
|
||||||
|
(value) => setTags(value),
|
||||||
|
newTags,
|
||||||
|
createNewTag
|
||||||
|
)}
|
||||||
<ScrapedImageRow
|
<ScrapedImageRow
|
||||||
title="Performer Image"
|
title="Performer Image"
|
||||||
className="performer-image"
|
className="performer-image"
|
||||||
|
|||||||
@@ -17,8 +17,17 @@ import { DisplayMode } from "src/models/list-filter/types";
|
|||||||
import { ExportDialog, DeleteEntityDialog } from "src/components/Shared";
|
import { ExportDialog, DeleteEntityDialog } from "src/components/Shared";
|
||||||
import { PerformerCard } from "./PerformerCard";
|
import { PerformerCard } from "./PerformerCard";
|
||||||
import { PerformerListTable } from "./PerformerListTable";
|
import { PerformerListTable } from "./PerformerListTable";
|
||||||
|
import { EditPerformersDialog } from "./EditPerformersDialog";
|
||||||
|
|
||||||
export const PerformerList: React.FC = () => {
|
interface IPerformerList {
|
||||||
|
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
||||||
|
persistState?: PersistanceLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PerformerList: React.FC<IPerformerList> = ({
|
||||||
|
filterHook,
|
||||||
|
persistState,
|
||||||
|
}) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
||||||
const [isExportAll, setIsExportAll] = useState(false);
|
const [isExportAll, setIsExportAll] = useState(false);
|
||||||
@@ -82,6 +91,17 @@ export const PerformerList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderEditPerformersDialog(
|
||||||
|
selectedPerformers: SlimPerformerDataFragment[],
|
||||||
|
onClose: (applied: boolean) => void
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EditPerformersDialog selected={selectedPerformers} onClose={onClose} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const renderDeleteDialog = (
|
const renderDeleteDialog = (
|
||||||
selectedPerformers: SlimPerformerDataFragment[],
|
selectedPerformers: SlimPerformerDataFragment[],
|
||||||
onClose: (confirmed: boolean) => void
|
onClose: (confirmed: boolean) => void
|
||||||
@@ -98,9 +118,11 @@ export const PerformerList: React.FC = () => {
|
|||||||
const listData = usePerformersList({
|
const listData = usePerformersList({
|
||||||
otherOperations,
|
otherOperations,
|
||||||
renderContent,
|
renderContent,
|
||||||
|
renderEditDialog: renderEditPerformersDialog,
|
||||||
|
filterHook,
|
||||||
addKeybinds,
|
addKeybinds,
|
||||||
selectable: true,
|
selectable: true,
|
||||||
persistState: PersistanceLevel.ALL,
|
persistState,
|
||||||
renderDeleteDialog,
|
renderDeleteDialog,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Route, Switch } from "react-router-dom";
|
import { Route, Switch } from "react-router-dom";
|
||||||
|
import { PersistanceLevel } from "src/hooks/ListHook";
|
||||||
import { Performer } from "./PerformerDetails/Performer";
|
import { Performer } from "./PerformerDetails/Performer";
|
||||||
import { PerformerList } from "./PerformerList";
|
import { PerformerList } from "./PerformerList";
|
||||||
|
|
||||||
const Performers = () => (
|
const Performers = () => (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/performers" component={PerformerList} />
|
<Route
|
||||||
|
exact
|
||||||
|
path="/performers"
|
||||||
|
render={(props) => (
|
||||||
|
<PerformerList persistState={PersistanceLevel.ALL} {...props} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Route path="/performers/:id/:tab?" component={Performer} />
|
<Route path="/performers/:id/:tab?" component={Performer} />
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ interface ITypeProps {
|
|||||||
| "parent_studios"
|
| "parent_studios"
|
||||||
| "tags"
|
| "tags"
|
||||||
| "sceneTags"
|
| "sceneTags"
|
||||||
|
| "performerTags"
|
||||||
| "movies";
|
| "movies";
|
||||||
}
|
}
|
||||||
interface IFilterProps {
|
interface IFilterProps {
|
||||||
@@ -43,6 +44,7 @@ interface IFilterProps {
|
|||||||
isMulti?: boolean;
|
isMulti?: boolean;
|
||||||
isClearable?: boolean;
|
isClearable?: boolean;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
|
menuPortalTarget?: HTMLElement | null;
|
||||||
}
|
}
|
||||||
interface ISelectProps<T extends boolean> {
|
interface ISelectProps<T extends boolean> {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -60,6 +62,7 @@ interface ISelectProps<T extends boolean> {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
showDropdown?: boolean;
|
showDropdown?: boolean;
|
||||||
groupHeader?: string;
|
groupHeader?: string;
|
||||||
|
menuPortalTarget?: HTMLElement | null;
|
||||||
closeMenuOnSelect?: boolean;
|
closeMenuOnSelect?: boolean;
|
||||||
noOptionsMessage?: string | null;
|
noOptionsMessage?: string | null;
|
||||||
}
|
}
|
||||||
@@ -109,6 +112,7 @@ const SelectComponent = <T extends boolean>({
|
|||||||
placeholder,
|
placeholder,
|
||||||
showDropdown = true,
|
showDropdown = true,
|
||||||
groupHeader,
|
groupHeader,
|
||||||
|
menuPortalTarget,
|
||||||
closeMenuOnSelect = true,
|
closeMenuOnSelect = true,
|
||||||
noOptionsMessage = type !== "tags" ? "None" : null,
|
noOptionsMessage = type !== "tags" ? "None" : null,
|
||||||
}: ISelectProps<T> & ITypeProps) => {
|
}: ISelectProps<T> & ITypeProps) => {
|
||||||
@@ -158,6 +162,7 @@ const SelectComponent = <T extends boolean>({
|
|||||||
isLoading,
|
isLoading,
|
||||||
styles,
|
styles,
|
||||||
closeMenuOnSelect,
|
closeMenuOnSelect,
|
||||||
|
menuPortalTarget,
|
||||||
components: {
|
components: {
|
||||||
IndicatorSeparator: () => null,
|
IndicatorSeparator: () => null,
|
||||||
...((!showDropdown || isDisabled) && { DropdownIndicator: () => null }),
|
...((!showDropdown || isDisabled) && { DropdownIndicator: () => null }),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { NavUtils, TextUtils } from "src/utils";
|
|||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
tag?: Partial<TagDataFragment>;
|
tag?: Partial<TagDataFragment>;
|
||||||
|
tagType?: "performer" | "scene";
|
||||||
performer?: Partial<PerformerDataFragment>;
|
performer?: Partial<PerformerDataFragment>;
|
||||||
marker?: Partial<SceneMarkerDataFragment>;
|
marker?: Partial<SceneMarkerDataFragment>;
|
||||||
movie?: Partial<MovieDataFragment>;
|
movie?: Partial<MovieDataFragment>;
|
||||||
@@ -26,7 +27,11 @@ export const TagLink: React.FC<IProps> = (props: IProps) => {
|
|||||||
let link: string = "#";
|
let link: string = "#";
|
||||||
let title: string = "";
|
let title: string = "";
|
||||||
if (props.tag) {
|
if (props.tag) {
|
||||||
link = NavUtils.makeTagScenesUrl(props.tag);
|
if (!props.tagType || props.tagType === "scene") {
|
||||||
|
link = NavUtils.makeTagScenesUrl(props.tag);
|
||||||
|
} else {
|
||||||
|
link = NavUtils.makeTagPerformersUrl(props.tag);
|
||||||
|
}
|
||||||
title = props.tag.name || "";
|
title = props.tag.name || "";
|
||||||
} else if (props.performer) {
|
} else if (props.performer) {
|
||||||
link = NavUtils.makePerformerScenesUrl(props.performer);
|
link = NavUtils.makePerformerScenesUrl(props.performer);
|
||||||
|
|||||||
@@ -47,6 +47,19 @@ export const TagCard: React.FC<IProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function maybeRenderPerformersPopoverButton() {
|
||||||
|
if (!tag.performer_count) return;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={NavUtils.makeTagPerformersUrl(tag)}>
|
||||||
|
<Button className="minimal">
|
||||||
|
<Icon icon="user" />
|
||||||
|
<span>{tag.performer_count}</span>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function maybeRenderPopoverButtonGroup() {
|
function maybeRenderPopoverButtonGroup() {
|
||||||
if (tag) {
|
if (tag) {
|
||||||
return (
|
return (
|
||||||
@@ -55,6 +68,7 @@ export const TagCard: React.FC<IProps> = ({
|
|||||||
<ButtonGroup className="card-popovers">
|
<ButtonGroup className="card-popovers">
|
||||||
{maybeRenderScenesPopoverButton()}
|
{maybeRenderScenesPopoverButton()}
|
||||||
{maybeRenderSceneMarkersPopoverButton()}
|
{maybeRenderSceneMarkersPopoverButton()}
|
||||||
|
{maybeRenderPerformersPopoverButton()}
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { useToast } from "src/hooks";
|
|||||||
import { TagScenesPanel } from "./TagScenesPanel";
|
import { TagScenesPanel } from "./TagScenesPanel";
|
||||||
import { TagMarkersPanel } from "./TagMarkersPanel";
|
import { TagMarkersPanel } from "./TagMarkersPanel";
|
||||||
import { TagImagesPanel } from "./TagImagesPanel";
|
import { TagImagesPanel } from "./TagImagesPanel";
|
||||||
|
import { TagPerformersPanel } from "./TagPerformersPanel";
|
||||||
|
|
||||||
interface ITabParams {
|
interface ITabParams {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -51,7 +52,10 @@ export const Tag: React.FC = () => {
|
|||||||
const [createTag] = useTagCreate(getTagInput() as GQL.TagUpdateInput);
|
const [createTag] = useTagCreate(getTagInput() as GQL.TagUpdateInput);
|
||||||
const [deleteTag] = useTagDestroy(getTagInput() as GQL.TagUpdateInput);
|
const [deleteTag] = useTagDestroy(getTagInput() as GQL.TagUpdateInput);
|
||||||
|
|
||||||
const activeTabKey = tab === "markers" || tab === "images" ? tab : "scenes";
|
const activeTabKey =
|
||||||
|
tab === "markers" || tab === "images" || tab === "performers"
|
||||||
|
? tab
|
||||||
|
: "scenes";
|
||||||
const setActiveTabKey = (newTab: string | null) => {
|
const setActiveTabKey = (newTab: string | null) => {
|
||||||
if (tab !== newTab) {
|
if (tab !== newTab) {
|
||||||
const tabParam = newTab === "scenes" ? "" : `/${newTab}`;
|
const tabParam = newTab === "scenes" ? "" : `/${newTab}`;
|
||||||
@@ -260,6 +264,9 @@ export const Tag: React.FC = () => {
|
|||||||
<Tab eventKey="markers" title="Markers">
|
<Tab eventKey="markers" title="Markers">
|
||||||
<TagMarkersPanel tag={tag} />
|
<TagMarkersPanel tag={tag} />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab eventKey="performers" title="Performers">
|
||||||
|
<TagPerformersPanel tag={tag} />
|
||||||
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import React from "react";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { tagFilterHook } from "src/core/tags";
|
||||||
|
import { PerformerList } from "src/components/Performers/PerformerList";
|
||||||
|
|
||||||
|
interface ITagPerformersPanel {
|
||||||
|
tag: GQL.TagDataFragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TagPerformersPanel: React.FC<ITagPerformersPanel> = ({ tag }) => {
|
||||||
|
return <PerformerList filterHook={tagFilterHook(tag)} />;
|
||||||
|
};
|
||||||
@@ -298,6 +298,15 @@ export const usePerformerUpdate = () =>
|
|||||||
GQL.usePerformerUpdateMutation({
|
GQL.usePerformerUpdateMutation({
|
||||||
update: deleteCache(performerMutationImpactedQueries),
|
update: deleteCache(performerMutationImpactedQueries),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const useBulkPerformerUpdate = (input: GQL.BulkPerformerUpdateInput) =>
|
||||||
|
GQL.useBulkPerformerUpdateMutation({
|
||||||
|
variables: {
|
||||||
|
input,
|
||||||
|
},
|
||||||
|
update: deleteCache(performerMutationImpactedQueries),
|
||||||
|
});
|
||||||
|
|
||||||
export const usePerformerDestroy = () =>
|
export const usePerformerDestroy = () =>
|
||||||
GQL.usePerformerDestroyMutation({
|
GQL.usePerformerDestroyMutation({
|
||||||
refetchQueries: getQueryNames([
|
refetchQueries: getQueryNames([
|
||||||
|
|||||||
@@ -709,6 +709,7 @@ CareerLength
|
|||||||
Tattoos
|
Tattoos
|
||||||
Piercings
|
Piercings
|
||||||
Aliases
|
Aliases
|
||||||
|
Tags (see Tag fields)
|
||||||
Image
|
Image
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export type CriterionType =
|
|||||||
| "movieIsMissing"
|
| "movieIsMissing"
|
||||||
| "tags"
|
| "tags"
|
||||||
| "sceneTags"
|
| "sceneTags"
|
||||||
|
| "performerTags"
|
||||||
| "performers"
|
| "performers"
|
||||||
| "studios"
|
| "studios"
|
||||||
| "movies"
|
| "movies"
|
||||||
@@ -43,7 +44,10 @@ export type CriterionType =
|
|||||||
| "gender"
|
| "gender"
|
||||||
| "parent_studios"
|
| "parent_studios"
|
||||||
| "scene_count"
|
| "scene_count"
|
||||||
| "marker_count";
|
| "marker_count"
|
||||||
|
| "image_count"
|
||||||
|
| "gallery_count"
|
||||||
|
| "performer_count";
|
||||||
|
|
||||||
type Option = string | number | IOptionType;
|
type Option = string | number | IOptionType;
|
||||||
export type CriterionValue = string | number | ILabeledId[];
|
export type CriterionValue = string | number | ILabeledId[];
|
||||||
@@ -83,6 +87,8 @@ export abstract class Criterion {
|
|||||||
return "Tags";
|
return "Tags";
|
||||||
case "sceneTags":
|
case "sceneTags":
|
||||||
return "Scene Tags";
|
return "Scene Tags";
|
||||||
|
case "performerTags":
|
||||||
|
return "Performer Tags";
|
||||||
case "performers":
|
case "performers":
|
||||||
return "Performers";
|
return "Performers";
|
||||||
case "studios":
|
case "studios":
|
||||||
@@ -123,6 +129,12 @@ export abstract class Criterion {
|
|||||||
return "Scene Count";
|
return "Scene Count";
|
||||||
case "marker_count":
|
case "marker_count":
|
||||||
return "Marker Count";
|
return "Marker Count";
|
||||||
|
case "image_count":
|
||||||
|
return "Image Count";
|
||||||
|
case "gallery_count":
|
||||||
|
return "Gallery Count";
|
||||||
|
case "performer_count":
|
||||||
|
return "Performer Count";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,16 @@ export class TagsCriterion extends Criterion {
|
|||||||
public options: IOptionType[] = [];
|
public options: IOptionType[] = [];
|
||||||
public value: ILabeledId[] = [];
|
public value: ILabeledId[] = [];
|
||||||
|
|
||||||
constructor(type: "tags" | "sceneTags") {
|
constructor(type: "tags" | "sceneTags" | "performerTags") {
|
||||||
super();
|
super();
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.parameterName = type;
|
this.parameterName = type;
|
||||||
if (type === "sceneTags") {
|
if (type === "sceneTags") {
|
||||||
this.parameterName = "scene_tags";
|
this.parameterName = "scene_tags";
|
||||||
}
|
}
|
||||||
|
if (type === "performerTags") {
|
||||||
|
this.parameterName = "performer_tags";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public encodeValue() {
|
public encodeValue() {
|
||||||
@@ -39,3 +42,8 @@ export class SceneTagsCriterionOption implements ICriterionOption {
|
|||||||
public label: string = Criterion.getLabel("sceneTags");
|
public label: string = Criterion.getLabel("sceneTags");
|
||||||
public value: CriterionType = "sceneTags";
|
public value: CriterionType = "sceneTags";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class PerformerTagsCriterionOption implements ICriterionOption {
|
||||||
|
public label: string = Criterion.getLabel("performerTags");
|
||||||
|
public value: CriterionType = "performerTags";
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ export function makeCriteria(type: CriterionType = "none") {
|
|||||||
case "o_counter":
|
case "o_counter":
|
||||||
case "scene_count":
|
case "scene_count":
|
||||||
case "marker_count":
|
case "marker_count":
|
||||||
|
case "image_count":
|
||||||
|
case "gallery_count":
|
||||||
|
case "performer_count":
|
||||||
return new NumberCriterion(type, type);
|
return new NumberCriterion(type, type);
|
||||||
case "resolution":
|
case "resolution":
|
||||||
return new ResolutionCriterion();
|
return new ResolutionCriterion();
|
||||||
@@ -72,6 +75,8 @@ export function makeCriteria(type: CriterionType = "none") {
|
|||||||
return new TagsCriterion("tags");
|
return new TagsCriterion("tags");
|
||||||
case "sceneTags":
|
case "sceneTags":
|
||||||
return new TagsCriterion("sceneTags");
|
return new TagsCriterion("sceneTags");
|
||||||
|
case "performerTags":
|
||||||
|
return new TagsCriterion("performerTags");
|
||||||
case "performers":
|
case "performers":
|
||||||
return new PerformersCriterion();
|
return new PerformersCriterion();
|
||||||
case "studios":
|
case "studios":
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ import {
|
|||||||
ParentStudiosCriterionOption,
|
ParentStudiosCriterionOption,
|
||||||
} from "./criteria/studios";
|
} from "./criteria/studios";
|
||||||
import {
|
import {
|
||||||
|
PerformerTagsCriterionOption,
|
||||||
SceneTagsCriterionOption,
|
SceneTagsCriterionOption,
|
||||||
TagsCriterion,
|
TagsCriterion,
|
||||||
TagsCriterionOption,
|
TagsCriterionOption,
|
||||||
@@ -146,6 +147,7 @@ export class ListFilterModel {
|
|||||||
new HasMarkersCriterionOption(),
|
new HasMarkersCriterionOption(),
|
||||||
new SceneIsMissingCriterionOption(),
|
new SceneIsMissingCriterionOption(),
|
||||||
new TagsCriterionOption(),
|
new TagsCriterionOption(),
|
||||||
|
new PerformerTagsCriterionOption(),
|
||||||
new PerformersCriterionOption(),
|
new PerformersCriterionOption(),
|
||||||
new StudiosCriterionOption(),
|
new StudiosCriterionOption(),
|
||||||
new MoviesCriterionOption(),
|
new MoviesCriterionOption(),
|
||||||
@@ -172,6 +174,7 @@ export class ListFilterModel {
|
|||||||
new ResolutionCriterionOption(),
|
new ResolutionCriterionOption(),
|
||||||
new ImageIsMissingCriterionOption(),
|
new ImageIsMissingCriterionOption(),
|
||||||
new TagsCriterionOption(),
|
new TagsCriterionOption(),
|
||||||
|
new PerformerTagsCriterionOption(),
|
||||||
new PerformersCriterionOption(),
|
new PerformersCriterionOption(),
|
||||||
new StudiosCriterionOption(),
|
new StudiosCriterionOption(),
|
||||||
];
|
];
|
||||||
@@ -206,6 +209,7 @@ export class ListFilterModel {
|
|||||||
new FavoriteCriterionOption(),
|
new FavoriteCriterionOption(),
|
||||||
new GenderCriterionOption(),
|
new GenderCriterionOption(),
|
||||||
new PerformerIsMissingCriterionOption(),
|
new PerformerIsMissingCriterionOption(),
|
||||||
|
new TagsCriterionOption(),
|
||||||
...numberCriteria
|
...numberCriteria
|
||||||
.concat(stringCriteria)
|
.concat(stringCriteria)
|
||||||
.map((c) => ListFilterModel.createCriterionOption(c)),
|
.map((c) => ListFilterModel.createCriterionOption(c)),
|
||||||
@@ -245,6 +249,7 @@ export class ListFilterModel {
|
|||||||
new AverageResolutionCriterionOption(),
|
new AverageResolutionCriterionOption(),
|
||||||
new GalleryIsMissingCriterionOption(),
|
new GalleryIsMissingCriterionOption(),
|
||||||
new TagsCriterionOption(),
|
new TagsCriterionOption(),
|
||||||
|
new PerformerTagsCriterionOption(),
|
||||||
new PerformersCriterionOption(),
|
new PerformersCriterionOption(),
|
||||||
new StudiosCriterionOption(),
|
new StudiosCriterionOption(),
|
||||||
];
|
];
|
||||||
@@ -277,13 +282,20 @@ export class ListFilterModel {
|
|||||||
// issues
|
// issues
|
||||||
this.sortByOptions = [
|
this.sortByOptions = [
|
||||||
"name",
|
"name",
|
||||||
"scenes_count" /* , "scene_markers_count"*/,
|
"scenes_count",
|
||||||
|
"images_count",
|
||||||
|
"galleries_count",
|
||||||
|
"performers_count",
|
||||||
|
/* "scene_markers_count" */
|
||||||
];
|
];
|
||||||
this.displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
|
this.displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
|
||||||
this.criterionOptions = [
|
this.criterionOptions = [
|
||||||
new NoneCriterionOption(),
|
new NoneCriterionOption(),
|
||||||
new TagIsMissingCriterionOption(),
|
new TagIsMissingCriterionOption(),
|
||||||
ListFilterModel.createCriterionOption("scene_count"),
|
ListFilterModel.createCriterionOption("scene_count"),
|
||||||
|
ListFilterModel.createCriterionOption("image_count"),
|
||||||
|
ListFilterModel.createCriterionOption("gallery_count"),
|
||||||
|
ListFilterModel.createCriterionOption("performer_count"),
|
||||||
// marker count has been disabled for now due to performance issues
|
// marker count has been disabled for now due to performance issues
|
||||||
// ListFilterModel.createCriterionOption("marker_count"),
|
// ListFilterModel.createCriterionOption("marker_count"),
|
||||||
];
|
];
|
||||||
@@ -527,6 +539,14 @@ export class ListFilterModel {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "performerTags": {
|
||||||
|
const performerTagsCrit = criterion as TagsCriterion;
|
||||||
|
result.performer_tags = {
|
||||||
|
value: performerTagsCrit.value.map((tag) => tag.id),
|
||||||
|
modifier: performerTagsCrit.modifier,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "performers": {
|
case "performers": {
|
||||||
const perfCrit = criterion as PerformersCriterion;
|
const perfCrit = criterion as PerformersCriterion;
|
||||||
result.performers = {
|
result.performers = {
|
||||||
@@ -650,6 +670,15 @@ export class ListFilterModel {
|
|||||||
}
|
}
|
||||||
case "performerIsMissing":
|
case "performerIsMissing":
|
||||||
result.is_missing = (criterion as IsMissingCriterion).value;
|
result.is_missing = (criterion as IsMissingCriterion).value;
|
||||||
|
break;
|
||||||
|
case "tags": {
|
||||||
|
const tagsCrit = criterion as TagsCriterion;
|
||||||
|
result.tags = {
|
||||||
|
value: tagsCrit.value.map((tag) => tag.id),
|
||||||
|
modifier: tagsCrit.modifier,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
// no default
|
// no default
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -778,6 +807,14 @@ export class ListFilterModel {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "performerTags": {
|
||||||
|
const performerTagsCrit = criterion as TagsCriterion;
|
||||||
|
result.performer_tags = {
|
||||||
|
value: performerTagsCrit.value.map((tag) => tag.id),
|
||||||
|
modifier: performerTagsCrit.modifier,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "performers": {
|
case "performers": {
|
||||||
const perfCrit = criterion as PerformersCriterion;
|
const perfCrit = criterion as PerformersCriterion;
|
||||||
result.performers = {
|
result.performers = {
|
||||||
@@ -929,6 +966,14 @@ export class ListFilterModel {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "performerTags": {
|
||||||
|
const performerTagsCrit = criterion as TagsCriterion;
|
||||||
|
result.performer_tags = {
|
||||||
|
value: performerTagsCrit.value.map((tag) => tag.id),
|
||||||
|
modifier: performerTagsCrit.modifier,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "performers": {
|
case "performers": {
|
||||||
const perfCrit = criterion as PerformersCriterion;
|
const perfCrit = criterion as PerformersCriterion;
|
||||||
result.performers = {
|
result.performers = {
|
||||||
@@ -967,6 +1012,30 @@ export class ListFilterModel {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "image_count": {
|
||||||
|
const countCrit = criterion as NumberCriterion;
|
||||||
|
result.image_count = {
|
||||||
|
value: countCrit.value,
|
||||||
|
modifier: countCrit.modifier,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "gallery_count": {
|
||||||
|
const countCrit = criterion as NumberCriterion;
|
||||||
|
result.gallery_count = {
|
||||||
|
value: countCrit.value,
|
||||||
|
modifier: countCrit.modifier,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "performer_count": {
|
||||||
|
const countCrit = criterion as NumberCriterion;
|
||||||
|
result.performer_count = {
|
||||||
|
value: countCrit.value,
|
||||||
|
modifier: countCrit.modifier,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
// disabled due to performance issues
|
// disabled due to performance issues
|
||||||
// case "marker_count": {
|
// case "marker_count": {
|
||||||
// const countCrit = criterion as NumberCriterion;
|
// const countCrit = criterion as NumberCriterion;
|
||||||
|
|||||||
@@ -76,6 +76,15 @@ const makeTagScenesUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
|||||||
return `/scenes?${filter.makeQueryParameters()}`;
|
return `/scenes?${filter.makeQueryParameters()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const makeTagPerformersUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
||||||
|
if (!tag.id) return "#";
|
||||||
|
const filter = new ListFilterModel(FilterMode.Performers);
|
||||||
|
const criterion = new TagsCriterion("tags");
|
||||||
|
criterion.value = [{ id: tag.id, label: tag.name || `Tag ${tag.id}` }];
|
||||||
|
filter.criteria.push(criterion);
|
||||||
|
return `/performers?${filter.makeQueryParameters()}`;
|
||||||
|
};
|
||||||
|
|
||||||
const makeTagSceneMarkersUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
const makeTagSceneMarkersUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
||||||
if (!tag.id) return "#";
|
if (!tag.id) return "#";
|
||||||
const filter = new ListFilterModel(FilterMode.SceneMarkers);
|
const filter = new ListFilterModel(FilterMode.SceneMarkers);
|
||||||
@@ -98,6 +107,7 @@ export default {
|
|||||||
makeStudioScenesUrl,
|
makeStudioScenesUrl,
|
||||||
makeTagSceneMarkersUrl,
|
makeTagSceneMarkersUrl,
|
||||||
makeTagScenesUrl,
|
makeTagScenesUrl,
|
||||||
|
makeTagPerformersUrl,
|
||||||
makeSceneMarkerUrl,
|
makeSceneMarkerUrl,
|
||||||
makeMovieScenesUrl,
|
makeMovieScenesUrl,
|
||||||
makeChildStudiosUrl,
|
makeChildStudiosUrl,
|
||||||
|
|||||||
Reference in New Issue
Block a user