mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +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
|
||||
gender
|
||||
image_path
|
||||
favorite
|
||||
tags {
|
||||
id
|
||||
name
|
||||
}
|
||||
stash_ids {
|
||||
endpoint
|
||||
stash_id
|
||||
|
||||
@@ -20,6 +20,11 @@ fragment PerformerData on Performer {
|
||||
favorite
|
||||
image_path
|
||||
scene_count
|
||||
|
||||
tags {
|
||||
...TagData
|
||||
}
|
||||
|
||||
stash_ids {
|
||||
stash_id
|
||||
endpoint
|
||||
|
||||
@@ -15,6 +15,9 @@ fragment ScrapedPerformerData on ScrapedPerformer {
|
||||
tattoos
|
||||
piercings
|
||||
aliases
|
||||
tags {
|
||||
...ScrapedSceneTagData
|
||||
}
|
||||
image
|
||||
}
|
||||
|
||||
@@ -36,6 +39,9 @@ fragment ScrapedScenePerformerData on ScrapedScenePerformer {
|
||||
tattoos
|
||||
piercings
|
||||
aliases
|
||||
tags {
|
||||
...ScrapedSceneTagData
|
||||
}
|
||||
remote_site_id
|
||||
images
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@ fragment TagData on Tag {
|
||||
image_path
|
||||
scene_count
|
||||
scene_marker_count
|
||||
performer_count
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ mutation PerformerCreate(
|
||||
$twitter: String,
|
||||
$instagram: String,
|
||||
$favorite: Boolean,
|
||||
$tag_ids: [ID!],
|
||||
$stash_ids: [StashIDInput!],
|
||||
$image: String) {
|
||||
|
||||
@@ -37,6 +38,7 @@ mutation PerformerCreate(
|
||||
twitter: $twitter,
|
||||
instagram: $instagram,
|
||||
favorite: $favorite,
|
||||
tag_ids: $tag_ids,
|
||||
stash_ids: $stash_ids,
|
||||
image: $image
|
||||
}) {
|
||||
@@ -52,6 +54,14 @@ mutation PerformerUpdate(
|
||||
}
|
||||
}
|
||||
|
||||
mutation BulkPerformerUpdate(
|
||||
$input: BulkPerformerUpdateInput!) {
|
||||
|
||||
bulkPerformerUpdate(input: $input) {
|
||||
...PerformerData
|
||||
}
|
||||
}
|
||||
|
||||
mutation PerformerDestroy($id: ID!) {
|
||||
performerDestroy(input: { id: $id })
|
||||
}
|
||||
|
||||
@@ -174,6 +174,7 @@ type Mutation {
|
||||
performerUpdate(input: PerformerUpdateInput!): Performer
|
||||
performerDestroy(input: PerformerDestroyInput!): Boolean!
|
||||
performersDestroy(ids: [ID!]!): Boolean!
|
||||
bulkPerformerUpdate(input: BulkPerformerUpdateInput!): [Performer!]
|
||||
|
||||
studioCreate(input: StudioCreateInput!): Studio
|
||||
studioUpdate(input: StudioUpdateInput!): Studio
|
||||
|
||||
@@ -59,6 +59,8 @@ input PerformerFilterType {
|
||||
gender: GenderCriterionInput
|
||||
"""Filter to only include performers missing this property"""
|
||||
is_missing: String
|
||||
"""Filter to only include performers with these tags"""
|
||||
tags: MultiCriterionInput
|
||||
"""Filter by StashID"""
|
||||
stash_id: String
|
||||
}
|
||||
@@ -101,6 +103,8 @@ input SceneFilterType {
|
||||
movies: MultiCriterionInput
|
||||
"""Filter to only include scenes with these tags"""
|
||||
tags: MultiCriterionInput
|
||||
"""Filter to only include scenes with performers with these tags"""
|
||||
performer_tags: MultiCriterionInput
|
||||
"""Filter to only include scenes with these performers"""
|
||||
performers: MultiCriterionInput
|
||||
"""Filter by StashID"""
|
||||
@@ -136,11 +140,13 @@ input GalleryFilterType {
|
||||
organized: Boolean
|
||||
"""Filter by average image resolution"""
|
||||
average_resolution: ResolutionEnum
|
||||
"""Filter to only include scenes with this studio"""
|
||||
"""Filter to only include galleries with this studio"""
|
||||
studios: MultiCriterionInput
|
||||
"""Filter to only include scenes with these tags"""
|
||||
"""Filter to only include galleries with these tags"""
|
||||
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
|
||||
"""Filter by number of images in this gallery"""
|
||||
image_count: IntCriterionInput
|
||||
@@ -153,6 +159,15 @@ input TagFilterType {
|
||||
"""Filter by number of scenes with this tag"""
|
||||
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"""
|
||||
marker_count: IntCriterionInput
|
||||
}
|
||||
@@ -174,6 +189,8 @@ input ImageFilterType {
|
||||
studios: MultiCriterionInput
|
||||
"""Filter to only include images with these tags"""
|
||||
tags: MultiCriterionInput
|
||||
"""Filter to only include images with performers with these tags"""
|
||||
performer_tags: MultiCriterionInput
|
||||
"""Filter to only include images with these performers"""
|
||||
performers: MultiCriterionInput
|
||||
"""Filter to only include images with these galleries"""
|
||||
|
||||
@@ -27,6 +27,7 @@ type Performer {
|
||||
piercings: String
|
||||
aliases: String
|
||||
favorite: Boolean!
|
||||
tags: [Tag!]!
|
||||
|
||||
image_path: String # Resolver
|
||||
scene_count: Int # Resolver
|
||||
@@ -52,6 +53,7 @@ input PerformerCreateInput {
|
||||
twitter: String
|
||||
instagram: String
|
||||
favorite: Boolean
|
||||
tag_ids: [ID!]
|
||||
"""This should be base64 encoded"""
|
||||
image: String
|
||||
stash_ids: [StashIDInput!]
|
||||
@@ -76,11 +78,34 @@ input PerformerUpdateInput {
|
||||
twitter: String
|
||||
instagram: String
|
||||
favorite: Boolean
|
||||
tag_ids: [ID!]
|
||||
"""This should be base64 encoded"""
|
||||
image: String
|
||||
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 {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ type ScrapedPerformer {
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String
|
||||
# Should be ScrapedPerformerTag - but would be identical types
|
||||
tags: [ScrapedSceneTag!]
|
||||
|
||||
"""This should be base64 encoded"""
|
||||
image: String
|
||||
@@ -39,5 +41,6 @@ input ScrapedPerformerInput {
|
||||
piercings: String
|
||||
aliases: String
|
||||
|
||||
# not including tags for the input
|
||||
# not including image for the input
|
||||
}
|
||||
@@ -45,6 +45,7 @@ type ScrapedScenePerformer {
|
||||
tattoos: String
|
||||
piercings: String
|
||||
aliases: String
|
||||
tags: [ScrapedSceneTag!]
|
||||
|
||||
remote_site_id: String
|
||||
images: [String!]
|
||||
|
||||
@@ -5,6 +5,7 @@ type Tag {
|
||||
image_path: String # Resolver
|
||||
scene_count: Int # Resolver
|
||||
scene_marker_count: Int # Resolver
|
||||
performer_count: Int
|
||||
}
|
||||
|
||||
input TagCreateInput {
|
||||
|
||||
@@ -138,6 +138,17 @@ func (r *performerResolver) ImagePath(ctx context.Context, obj *models.Performer
|
||||
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) {
|
||||
var res int
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||
imagePath := urlbuilders.NewTagURLBuilder(baseURL, obj.ID).GetTagImageURL()
|
||||
|
||||
@@ -94,6 +94,12 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
return err
|
||||
}
|
||||
|
||||
if len(input.TagIds) > 0 {
|
||||
if err := r.updatePerformerTags(qb, performer.ID, input.TagIds); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
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
|
||||
}
|
||||
|
||||
// Save the tags
|
||||
if translator.hasField("tag_ids") {
|
||||
if err := r.updatePerformerTags(qb, performer.ID, input.TagIds); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(imageData) > 0 {
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
id, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -253,7 +253,7 @@ func (r *mutationResolver) BulkSceneUpdate(ctx context.Context, input models.Bul
|
||||
|
||||
// Save the tags
|
||||
if translator.hasField("tag_ids") {
|
||||
tagIDs, err := adjustSceneTagIDs(qb, sceneID, *input.TagIds)
|
||||
tagIDs, err := adjustTagIDs(qb, sceneID, *input.TagIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -330,7 +330,11 @@ func adjustScenePerformerIDs(qb models.SceneReader, sceneID int, ids models.Bulk
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
|
||||
var DB *sqlx.DB
|
||||
var dbPath string
|
||||
var appSchemaVersion uint = 18
|
||||
var appSchemaVersion uint = 19
|
||||
var databaseSchemaVersion uint
|
||||
|
||||
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 (
|
||||
"fmt"
|
||||
"github.com/json-iterator/go"
|
||||
"os"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
@@ -26,6 +26,7 @@ type Performer struct {
|
||||
Piercings string `json:"piercings,omitempty"`
|
||||
Aliases string `json:"aliases,omitempty"`
|
||||
Favorite bool `json:"favorite,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
Image string `json:"image,omitempty"`
|
||||
CreatedAt models.JSONTime `json:"created_at,omitempty"`
|
||||
UpdatedAt models.JSONTime `json:"updated_at,omitempty"`
|
||||
|
||||
@@ -725,6 +725,18 @@ func (t *ExportTask) exportPerformer(wg *sync.WaitGroup, jobChan <-chan *models.
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// GetTagIDs provides a mock function with given fields: galleryID
|
||||
func (_m *GalleryReaderWriter) GetTagIDs(galleryID int) ([]int, error) {
|
||||
// GetSceneIDs provides a mock function with given fields: galleryID
|
||||
func (_m *GalleryReaderWriter) GetSceneIDs(galleryID int) ([]int, error) {
|
||||
ret := _m.Called(galleryID)
|
||||
|
||||
var r0 []int
|
||||
@@ -323,8 +323,8 @@ func (_m *GalleryReaderWriter) GetTagIDs(galleryID int) ([]int, error) {
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetSceneIDs provides a mock function with given fields: galleryID
|
||||
func (_m *GalleryReaderWriter) GetSceneIDs(galleryID int) ([]int, error) {
|
||||
// GetTagIDs provides a mock function with given fields: galleryID
|
||||
func (_m *GalleryReaderWriter) GetTagIDs(galleryID int) ([]int, error) {
|
||||
ret := _m.Called(galleryID)
|
||||
|
||||
var r0 []int
|
||||
@@ -464,20 +464,6 @@ func (_m *GalleryReaderWriter) UpdatePerformers(galleryID int, performerIDs []in
|
||||
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
|
||||
func (_m *GalleryReaderWriter) UpdateScenes(galleryID int, sceneIDs []int) error {
|
||||
ret := _m.Called(galleryID, sceneIDs)
|
||||
@@ -491,3 +477,17 @@ func (_m *GalleryReaderWriter) UpdateScenes(galleryID int, sceneIDs []int) error
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
func (_m *PerformerReaderWriter) Create(newPerformer models.Performer) (*models.Performer, error) {
|
||||
ret := _m.Called(newPerformer)
|
||||
@@ -337,6 +358,29 @@ func (_m *PerformerReaderWriter) GetStashIDs(performerID int) ([]*models.StashID
|
||||
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
|
||||
func (_m *PerformerReaderWriter) Query(performerFilter *models.PerformerFilterType, findFilter *models.FindFilterType) ([]*models.Performer, int, error) {
|
||||
ret := _m.Called(performerFilter, findFilter)
|
||||
@@ -440,3 +484,17 @@ func (_m *PerformerReaderWriter) UpdateStashIDs(performerID int, stashIDs []mode
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
func (_m *SceneReaderWriter) FindByMovieID(movieID int) ([]*models.Scene, error) {
|
||||
ret := _m.Called(movieID)
|
||||
@@ -392,29 +415,6 @@ func (_m *SceneReaderWriter) FindByPerformerID(performerID int) ([]*models.Scene
|
||||
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
|
||||
func (_m *SceneReaderWriter) FindMany(ids []int) ([]*models.Scene, error) {
|
||||
ret := _m.Called(ids)
|
||||
@@ -461,6 +461,29 @@ func (_m *SceneReaderWriter) GetCover(sceneID int) ([]byte, error) {
|
||||
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
|
||||
func (_m *SceneReaderWriter) GetMovies(sceneID int) ([]models.MoviesScenes, error) {
|
||||
ret := _m.Called(sceneID)
|
||||
@@ -507,8 +530,31 @@ func (_m *SceneReaderWriter) GetPerformerIDs(sceneID int) ([]int, error) {
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetGalleryIDs provides a mock function with given fields: sceneID
|
||||
func (_m *SceneReaderWriter) GetGalleryIDs(sceneID int) ([]int, error) {
|
||||
// GetStashIDs provides a mock function with given fields: sceneID
|
||||
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)
|
||||
|
||||
var r0 []int
|
||||
@@ -530,52 +576,6 @@ func (_m *SceneReaderWriter) GetGalleryIDs(sceneID int) ([]int, error) {
|
||||
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
|
||||
func (_m *SceneReaderWriter) IncrementOCounter(id int) (int, error) {
|
||||
ret := _m.Called(id)
|
||||
@@ -766,6 +766,20 @@ func (_m *SceneReaderWriter) UpdateFull(updatedScene models.Scene) (*models.Scen
|
||||
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
|
||||
func (_m *SceneReaderWriter) UpdateMovies(sceneID int, movies []models.MoviesScenes) error {
|
||||
ret := _m.Called(sceneID, movies)
|
||||
@@ -794,20 +808,6 @@ func (_m *SceneReaderWriter) UpdatePerformers(sceneID int, performerIDs []int) e
|
||||
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
|
||||
func (_m *SceneReaderWriter) UpdateStashIDs(sceneID int, stashIDs []models.StashID) error {
|
||||
ret := _m.Called(sceneID, stashIDs)
|
||||
|
||||
@@ -245,6 +245,29 @@ func (_m *TagReaderWriter) FindByNames(names []string, nocase bool) ([]*models.T
|
||||
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
|
||||
func (_m *TagReaderWriter) FindBySceneID(sceneID int) ([]*models.Tag, error) {
|
||||
ret := _m.Called(sceneID)
|
||||
|
||||
@@ -24,43 +24,45 @@ type ScrapedItem struct {
|
||||
}
|
||||
|
||||
type ScrapedPerformer struct {
|
||||
Name *string `graphql:"name" json:"name"`
|
||||
Gender *string `graphql:"gender" json:"gender"`
|
||||
URL *string `graphql:"url" json:"url"`
|
||||
Twitter *string `graphql:"twitter" json:"twitter"`
|
||||
Instagram *string `graphql:"instagram" json:"instagram"`
|
||||
Birthdate *string `graphql:"birthdate" json:"birthdate"`
|
||||
Ethnicity *string `graphql:"ethnicity" json:"ethnicity"`
|
||||
Country *string `graphql:"country" json:"country"`
|
||||
EyeColor *string `graphql:"eye_color" json:"eye_color"`
|
||||
Height *string `graphql:"height" json:"height"`
|
||||
Measurements *string `graphql:"measurements" json:"measurements"`
|
||||
FakeTits *string `graphql:"fake_tits" json:"fake_tits"`
|
||||
CareerLength *string `graphql:"career_length" json:"career_length"`
|
||||
Tattoos *string `graphql:"tattoos" json:"tattoos"`
|
||||
Piercings *string `graphql:"piercings" json:"piercings"`
|
||||
Aliases *string `graphql:"aliases" json:"aliases"`
|
||||
Image *string `graphql:"image" json:"image"`
|
||||
Name *string `graphql:"name" json:"name"`
|
||||
Gender *string `graphql:"gender" json:"gender"`
|
||||
URL *string `graphql:"url" json:"url"`
|
||||
Twitter *string `graphql:"twitter" json:"twitter"`
|
||||
Instagram *string `graphql:"instagram" json:"instagram"`
|
||||
Birthdate *string `graphql:"birthdate" json:"birthdate"`
|
||||
Ethnicity *string `graphql:"ethnicity" json:"ethnicity"`
|
||||
Country *string `graphql:"country" json:"country"`
|
||||
EyeColor *string `graphql:"eye_color" json:"eye_color"`
|
||||
Height *string `graphql:"height" json:"height"`
|
||||
Measurements *string `graphql:"measurements" json:"measurements"`
|
||||
FakeTits *string `graphql:"fake_tits" json:"fake_tits"`
|
||||
CareerLength *string `graphql:"career_length" json:"career_length"`
|
||||
Tattoos *string `graphql:"tattoos" json:"tattoos"`
|
||||
Piercings *string `graphql:"piercings" json:"piercings"`
|
||||
Aliases *string `graphql:"aliases" json:"aliases"`
|
||||
Tags []*ScrapedSceneTag `graphql:"tags" json:"tags"`
|
||||
Image *string `graphql:"image" json:"image"`
|
||||
}
|
||||
|
||||
// this type has no Image field
|
||||
type ScrapedPerformerStash struct {
|
||||
Name *string `graphql:"name" json:"name"`
|
||||
Gender *string `graphql:"gender" json:"gender"`
|
||||
URL *string `graphql:"url" json:"url"`
|
||||
Twitter *string `graphql:"twitter" json:"twitter"`
|
||||
Instagram *string `graphql:"instagram" json:"instagram"`
|
||||
Birthdate *string `graphql:"birthdate" json:"birthdate"`
|
||||
Ethnicity *string `graphql:"ethnicity" json:"ethnicity"`
|
||||
Country *string `graphql:"country" json:"country"`
|
||||
EyeColor *string `graphql:"eye_color" json:"eye_color"`
|
||||
Height *string `graphql:"height" json:"height"`
|
||||
Measurements *string `graphql:"measurements" json:"measurements"`
|
||||
FakeTits *string `graphql:"fake_tits" json:"fake_tits"`
|
||||
CareerLength *string `graphql:"career_length" json:"career_length"`
|
||||
Tattoos *string `graphql:"tattoos" json:"tattoos"`
|
||||
Piercings *string `graphql:"piercings" json:"piercings"`
|
||||
Aliases *string `graphql:"aliases" json:"aliases"`
|
||||
Name *string `graphql:"name" json:"name"`
|
||||
Gender *string `graphql:"gender" json:"gender"`
|
||||
URL *string `graphql:"url" json:"url"`
|
||||
Twitter *string `graphql:"twitter" json:"twitter"`
|
||||
Instagram *string `graphql:"instagram" json:"instagram"`
|
||||
Birthdate *string `graphql:"birthdate" json:"birthdate"`
|
||||
Ethnicity *string `graphql:"ethnicity" json:"ethnicity"`
|
||||
Country *string `graphql:"country" json:"country"`
|
||||
EyeColor *string `graphql:"eye_color" json:"eye_color"`
|
||||
Height *string `graphql:"height" json:"height"`
|
||||
Measurements *string `graphql:"measurements" json:"measurements"`
|
||||
FakeTits *string `graphql:"fake_tits" json:"fake_tits"`
|
||||
CareerLength *string `graphql:"career_length" json:"career_length"`
|
||||
Tattoos *string `graphql:"tattoos" json:"tattoos"`
|
||||
Piercings *string `graphql:"piercings" json:"piercings"`
|
||||
Aliases *string `graphql:"aliases" json:"aliases"`
|
||||
Tags []*ScrapedSceneTag `graphql:"tags" json:"tags"`
|
||||
}
|
||||
|
||||
type ScrapedScene struct {
|
||||
@@ -106,25 +108,26 @@ type ScrapedGalleryStash struct {
|
||||
|
||||
type ScrapedScenePerformer struct {
|
||||
// Set if performer matched
|
||||
ID *string `graphql:"id" json:"id"`
|
||||
Name string `graphql:"name" json:"name"`
|
||||
Gender *string `graphql:"gender" json:"gender"`
|
||||
URL *string `graphql:"url" json:"url"`
|
||||
Twitter *string `graphql:"twitter" json:"twitter"`
|
||||
Instagram *string `graphql:"instagram" json:"instagram"`
|
||||
Birthdate *string `graphql:"birthdate" json:"birthdate"`
|
||||
Ethnicity *string `graphql:"ethnicity" json:"ethnicity"`
|
||||
Country *string `graphql:"country" json:"country"`
|
||||
EyeColor *string `graphql:"eye_color" json:"eye_color"`
|
||||
Height *string `graphql:"height" json:"height"`
|
||||
Measurements *string `graphql:"measurements" json:"measurements"`
|
||||
FakeTits *string `graphql:"fake_tits" json:"fake_tits"`
|
||||
CareerLength *string `graphql:"career_length" json:"career_length"`
|
||||
Tattoos *string `graphql:"tattoos" json:"tattoos"`
|
||||
Piercings *string `graphql:"piercings" json:"piercings"`
|
||||
Aliases *string `graphql:"aliases" json:"aliases"`
|
||||
RemoteSiteID *string `graphql:"remote_site_id" json:"remote_site_id"`
|
||||
Images []string `graphql:"images" json:"images"`
|
||||
ID *string `graphql:"id" json:"id"`
|
||||
Name string `graphql:"name" json:"name"`
|
||||
Gender *string `graphql:"gender" json:"gender"`
|
||||
URL *string `graphql:"url" json:"url"`
|
||||
Twitter *string `graphql:"twitter" json:"twitter"`
|
||||
Instagram *string `graphql:"instagram" json:"instagram"`
|
||||
Birthdate *string `graphql:"birthdate" json:"birthdate"`
|
||||
Ethnicity *string `graphql:"ethnicity" json:"ethnicity"`
|
||||
Country *string `graphql:"country" json:"country"`
|
||||
EyeColor *string `graphql:"eye_color" json:"eye_color"`
|
||||
Height *string `graphql:"height" json:"height"`
|
||||
Measurements *string `graphql:"measurements" json:"measurements"`
|
||||
FakeTits *string `graphql:"fake_tits" json:"fake_tits"`
|
||||
CareerLength *string `graphql:"career_length" json:"career_length"`
|
||||
Tattoos *string `graphql:"tattoos" json:"tattoos"`
|
||||
Piercings *string `graphql:"piercings" json:"piercings"`
|
||||
Aliases *string `graphql:"aliases" json:"aliases"`
|
||||
Tags []*ScrapedSceneTag `graphql:"tags" json:"tags"`
|
||||
RemoteSiteID *string `graphql:"remote_site_id" json:"remote_site_id"`
|
||||
Images []string `graphql:"images" json:"images"`
|
||||
}
|
||||
|
||||
type ScrapedSceneStudio struct {
|
||||
|
||||
@@ -8,12 +8,14 @@ type PerformerReader interface {
|
||||
FindByImageID(imageID int) ([]*Performer, error)
|
||||
FindByGalleryID(galleryID int) ([]*Performer, error)
|
||||
FindByNames(names []string, nocase bool) ([]*Performer, error)
|
||||
CountByTagID(tagID int) (int, error)
|
||||
Count() (int, error)
|
||||
All() ([]*Performer, error)
|
||||
AllSlim() ([]*Performer, error)
|
||||
Query(performerFilter *PerformerFilterType, findFilter *FindFilterType) ([]*Performer, int, error)
|
||||
GetImage(performerID int) ([]byte, error)
|
||||
GetStashIDs(performerID int) ([]*StashID, error)
|
||||
GetTagIDs(sceneID int) ([]int, error)
|
||||
}
|
||||
|
||||
type PerformerWriter interface {
|
||||
@@ -24,6 +26,7 @@ type PerformerWriter interface {
|
||||
UpdateImage(performerID int, image []byte) error
|
||||
DestroyImage(performerID int) error
|
||||
UpdateStashIDs(performerID int, stashIDs []StashID) error
|
||||
UpdateTags(sceneID int, tagIDs []int) error
|
||||
}
|
||||
|
||||
type PerformerReaderWriter interface {
|
||||
|
||||
@@ -4,6 +4,7 @@ type TagReader interface {
|
||||
Find(id int) (*Tag, error)
|
||||
FindMany(ids []int) ([]*Tag, error)
|
||||
FindBySceneID(sceneID int) ([]*Tag, error)
|
||||
FindByPerformerID(performerID int) ([]*Tag, error)
|
||||
FindBySceneMarkerID(sceneMarkerID int) ([]*Tag, error)
|
||||
FindByImageID(imageID int) ([]*Tag, error)
|
||||
FindByGalleryID(galleryID int) ([]*Tag, error)
|
||||
|
||||
@@ -3,6 +3,7 @@ package performer
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/manager/jsonschema"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -10,16 +11,25 @@ import (
|
||||
)
|
||||
|
||||
type Importer struct {
|
||||
ReaderWriter models.PerformerReaderWriter
|
||||
Input jsonschema.Performer
|
||||
ReaderWriter models.PerformerReaderWriter
|
||||
TagWriter models.TagReaderWriter
|
||||
Input jsonschema.Performer
|
||||
MissingRefBehaviour models.ImportMissingRefEnum
|
||||
|
||||
ID int
|
||||
performer models.Performer
|
||||
imageData []byte
|
||||
|
||||
tags []*models.Tag
|
||||
}
|
||||
|
||||
func (i *Importer) PreImport() error {
|
||||
i.performer = performerJSONToPerformer(i.Input)
|
||||
|
||||
if err := i.populateTags(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
if len(i.Input.Image) > 0 {
|
||||
_, i.imageData, err = utils.ProcessBase64Image(i.Input.Image)
|
||||
@@ -31,7 +41,82 @@ func (i *Importer) PreImport() error {
|
||||
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 {
|
||||
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 err := i.ReaderWriter.UpdateImage(id, i.imageData); err != nil {
|
||||
return fmt.Errorf("error setting performer image: %s", err.Error())
|
||||
|
||||
@@ -3,6 +3,8 @@ package performer
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/stashapp/stash/pkg/manager/jsonschema"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/models/mocks"
|
||||
@@ -16,9 +18,15 @@ const invalidImage = "aW1hZ2VCeXRlcw&&"
|
||||
|
||||
const (
|
||||
existingPerformerID = 100
|
||||
existingTagID = 105
|
||||
errTagsID = 106
|
||||
|
||||
existingPerformerName = "existingPerformerName"
|
||||
performerNameErr = "performerNameErr"
|
||||
|
||||
existingTagName = "existingTagName"
|
||||
existingTagErr = "existingTagErr"
|
||||
missingTagName = "missingTagName"
|
||||
)
|
||||
|
||||
func TestImporterName(t *testing.T) {
|
||||
@@ -53,6 +61,91 @@ func TestImporterPreImport(t *testing.T) {
|
||||
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) {
|
||||
readerWriter := &mocks.PerformerReaderWriter{}
|
||||
|
||||
@@ -111,6 +204,32 @@ func TestImporterFindExistingID(t *testing.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) {
|
||||
readerWriter := &mocks.PerformerReaderWriter{}
|
||||
|
||||
|
||||
@@ -94,10 +94,10 @@ func (s mappedConfig) postProcess(q mappedQuery, attrConfig mappedScraperAttrCon
|
||||
type mappedSceneScraperConfig struct {
|
||||
mappedConfig
|
||||
|
||||
Tags mappedConfig `yaml:"Tags"`
|
||||
Performers mappedConfig `yaml:"Performers"`
|
||||
Studio mappedConfig `yaml:"Studio"`
|
||||
Movies mappedConfig `yaml:"Movies"`
|
||||
Tags mappedConfig `yaml:"Tags"`
|
||||
Performers mappedPerformerScraperConfig `yaml:"Performers"`
|
||||
Studio mappedConfig `yaml:"Studio"`
|
||||
Movies mappedConfig `yaml:"Movies"`
|
||||
}
|
||||
type _mappedSceneScraperConfig mappedSceneScraperConfig
|
||||
|
||||
@@ -211,10 +211,54 @@ func (s *mappedGalleryScraperConfig) UnmarshalYAML(unmarshal func(interface{}) e
|
||||
|
||||
type mappedPerformerScraperConfig struct {
|
||||
mappedConfig
|
||||
|
||||
Tags mappedConfig `yaml:"Tags"`
|
||||
}
|
||||
type _mappedPerformerScraperConfig mappedPerformerScraperConfig
|
||||
|
||||
const (
|
||||
mappedScraperConfigPerformerTags = "Tags"
|
||||
)
|
||||
|
||||
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 {
|
||||
@@ -647,9 +691,23 @@ func (s mappedScraper) scrapePerformer(q mappedQuery) (*models.ScrapedPerformer,
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
performerTagsMap := performerMap.Tags
|
||||
|
||||
results := performerMap.process(q, s.Common)
|
||||
if len(results) > 0 {
|
||||
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
|
||||
@@ -687,19 +745,34 @@ func (s mappedScraper) scrapeScene(q mappedQuery) (*models.ScrapedScene, error)
|
||||
sceneStudioMap := sceneScraperConfig.Studio
|
||||
sceneMoviesMap := sceneScraperConfig.Movies
|
||||
|
||||
scenePerformerTagsMap := scenePerformersMap.Tags
|
||||
|
||||
logger.Debug(`Processing scene:`)
|
||||
results := sceneMap.process(q, s.Common)
|
||||
if len(results) > 0 {
|
||||
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
|
||||
if scenePerformersMap != nil {
|
||||
if scenePerformersMap.mappedConfig != nil {
|
||||
logger.Debug(`Processing scene performers:`)
|
||||
performerResults := scenePerformersMap.process(q, s.Common)
|
||||
|
||||
for _, p := range performerResults {
|
||||
performer := &models.ScrapedScenePerformer{}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,9 +220,11 @@ func (c Cache) ScrapePerformerURL(url string) (*models.ScrapedPerformer, error)
|
||||
return nil, 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())
|
||||
if ret != nil {
|
||||
err = c.postScrapePerformer(ret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
@@ -232,6 +234,49 @@ func (c Cache) ScrapePerformerURL(url string) (*models.ScrapedPerformer, error)
|
||||
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 {
|
||||
if err := c.txnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error {
|
||||
pqb := r.Performer()
|
||||
@@ -240,8 +285,11 @@ func (c Cache) postScrapeScene(ret *models.ScrapedScene) error {
|
||||
sqb := r.Studio()
|
||||
|
||||
for _, p := range ret.Performers {
|
||||
err := MatchScrapedScenePerformer(pqb, p)
|
||||
if err != nil {
|
||||
if err := c.postScrapeScenePerformer(p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := MatchScrapedScenePerformer(pqb, p); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,13 @@ func (s *stashScraper) scrapePerformerByFragment(scrapedPerformer models.Scraped
|
||||
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
|
||||
ret := models.ScrapedPerformer{}
|
||||
err = copier.Copy(&ret, q.FindPerformer)
|
||||
|
||||
@@ -322,6 +322,7 @@ func performerFragmentToScrapedScenePerformer(p graphql.PerformerFragment) *mode
|
||||
Twitter: findURL(p.Urls, "TWITTER"),
|
||||
RemoteSiteID: &id,
|
||||
Images: images,
|
||||
// TODO - tags not currently supported
|
||||
// TODO - Image - should be returned as a set of URLs. Will need a
|
||||
// graphql schema change to accommodate this. Leave off for now.
|
||||
}
|
||||
|
||||
@@ -520,7 +520,7 @@ func makeSceneXPathConfig() mappedScraper {
|
||||
performerConfig := make(mappedConfig)
|
||||
performerConfig["Name"] = makeSimpleAttrConfig(`$performerElem/@data-mxptext`)
|
||||
performerConfig["URL"] = makeSimpleAttrConfig(`$performerElem/@href`)
|
||||
config.Performers = performerConfig
|
||||
config.Performers.mappedConfig = performerConfig
|
||||
|
||||
studioConfig := make(mappedConfig)
|
||||
studioConfig["Name"] = makeSimpleAttrConfig(`$studioElem`)
|
||||
@@ -730,7 +730,7 @@ xPathScrapers:
|
||||
assert.Equal(t, "//title", sceneConfig.mappedConfig["Title"].Selector)
|
||||
assert.Equal(t, "//tags", sceneConfig.Tags["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)
|
||||
|
||||
postProcess := sceneConfig.mappedConfig["Title"].postProcessActions
|
||||
|
||||
@@ -259,6 +259,8 @@ func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, fi
|
||||
query.addHaving(havingClause)
|
||||
}
|
||||
|
||||
handleGalleryPerformerTagsCriterion(&query, galleryFilter.PerformerTags)
|
||||
|
||||
query.sortAndPagination = qb.getGallerySort(findFilter) + getPagination(findFilter)
|
||||
idsResult, countResult, err := query.executeFind()
|
||||
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 {
|
||||
var sort 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 All
|
||||
// TODO Query
|
||||
|
||||
@@ -360,6 +360,8 @@ func (qb *imageQueryBuilder) Query(imageFilter *models.ImageFilterType, findFilt
|
||||
query.addHaving(havingClause)
|
||||
}
|
||||
|
||||
handleImagePerformerTagsCriterion(&query, imageFilter.PerformerTags)
|
||||
|
||||
query.sortAndPagination = qb.getImageSort(findFilter) + getPagination(findFilter)
|
||||
idsResult, countResult, err := query.executeFind()
|
||||
if err != nil {
|
||||
@@ -379,6 +381,31 @@ func (qb *imageQueryBuilder) Query(imageFilter *models.ImageFilterType, findFilt
|
||||
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 {
|
||||
if findFilter == nil {
|
||||
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) {
|
||||
withTxn(func(r models.Repository) error {
|
||||
sort := titleField
|
||||
|
||||
@@ -11,6 +11,13 @@ import (
|
||||
|
||||
const performerTable = "performers"
|
||||
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 {
|
||||
repository
|
||||
@@ -153,6 +160,11 @@ func (qb *performerQueryBuilder) FindByNames(names []string, nocase bool) ([]*mo
|
||||
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) {
|
||||
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
|
||||
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)
|
||||
idsResult, countResult, err := query.executeFind()
|
||||
if err != nil {
|
||||
@@ -361,6 +385,26 @@ func (qb *performerQueryBuilder) queryPerformers(query string, args []interface{
|
||||
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 {
|
||||
return &imageRepository{
|
||||
repository: repository{
|
||||
|
||||
@@ -5,6 +5,7 @@ package sqlite_test
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"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) {
|
||||
if err := withTxn(func(r models.Repository) error {
|
||||
qb := r.Performer()
|
||||
|
||||
@@ -351,6 +351,7 @@ func (qb *sceneQueryBuilder) makeFilter(sceneFilter *models.SceneFilterType) *fi
|
||||
query.handleCriterionFunc(sceneStudioCriterionHandler(qb, sceneFilter.Studios))
|
||||
query.handleCriterionFunc(sceneMoviesCriterionHandler(qb, sceneFilter.Movies))
|
||||
query.handleCriterionFunc(sceneStashIDsHandler(qb, sceneFilter.StashID))
|
||||
query.handleCriterionFunc(scenePerformerTagsCriterionHandler(qb, sceneFilter.PerformerTags))
|
||||
|
||||
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 {
|
||||
if findFilter == nil {
|
||||
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) {
|
||||
withTxn(func(r models.Repository) error {
|
||||
sqb := r.Scene()
|
||||
|
||||
@@ -20,110 +20,240 @@ import (
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
const totalScenes = 12
|
||||
const totalImages = 6 // TODO - add one for zip file
|
||||
const performersNameCase = 9
|
||||
const performersNameNoCase = 2
|
||||
const moviesNameCase = 2
|
||||
const moviesNameNoCase = 1
|
||||
const totalGalleries = 8
|
||||
const tagsNameNoCase = 2
|
||||
const tagsNameCase = 12
|
||||
const studiosNameCase = 6
|
||||
const studiosNameNoCase = 1
|
||||
const (
|
||||
sceneIdxWithMovie = iota
|
||||
sceneIdxWithGallery
|
||||
sceneIdxWithPerformer
|
||||
sceneIdxWithTwoPerformers
|
||||
sceneIdxWithTag
|
||||
sceneIdxWithTwoTags
|
||||
sceneIdxWithStudio
|
||||
sceneIdxWithMarker
|
||||
sceneIdxWithPerformerTag
|
||||
sceneIdxWithPerformerTwoTags
|
||||
// new indexes above
|
||||
lastSceneIdx
|
||||
|
||||
var sceneIDs []int
|
||||
var imageIDs []int
|
||||
var performerIDs []int
|
||||
var movieIDs []int
|
||||
var galleryIDs []int
|
||||
var tagIDs []int
|
||||
var studioIDs []int
|
||||
var markerIDs []int
|
||||
totalScenes = lastSceneIdx + 3
|
||||
)
|
||||
|
||||
var tagNames []string
|
||||
var studioNames []string
|
||||
var movieNames []string
|
||||
var performerNames []string
|
||||
const (
|
||||
imageIdxWithGallery = iota
|
||||
imageIdxWithPerformer
|
||||
imageIdxWithTwoPerformers
|
||||
imageIdxWithTag
|
||||
imageIdxWithTwoTags
|
||||
imageIdxWithStudio
|
||||
imageIdxInZip // TODO - not implemented
|
||||
imageIdxWithPerformerTag
|
||||
imageIdxWithPerformerTwoTags
|
||||
// new indexes above
|
||||
totalImages
|
||||
)
|
||||
|
||||
const sceneIdxWithMovie = 0
|
||||
const sceneIdxWithGallery = 1
|
||||
const sceneIdxWithPerformer = 2
|
||||
const sceneIdxWithTwoPerformers = 3
|
||||
const sceneIdxWithTag = 4
|
||||
const sceneIdxWithTwoTags = 5
|
||||
const sceneIdxWithStudio = 6
|
||||
const sceneIdxWithMarker = 7
|
||||
const (
|
||||
performerIdxWithScene = iota
|
||||
performerIdx1WithScene
|
||||
performerIdx2WithScene
|
||||
performerIdxWithImage
|
||||
performerIdx1WithImage
|
||||
performerIdx2WithImage
|
||||
performerIdxWithTag
|
||||
performerIdxWithTwoTags
|
||||
performerIdxWithGallery
|
||||
performerIdx1WithGallery
|
||||
performerIdx2WithGallery
|
||||
// new indexes above
|
||||
// performers with dup names start from the end
|
||||
performerIdx1WithDupName
|
||||
performerIdxWithDupName
|
||||
|
||||
const imageIdxWithGallery = 0
|
||||
const imageIdxWithPerformer = 1
|
||||
const imageIdxWithTwoPerformers = 2
|
||||
const imageIdxWithTag = 3
|
||||
const imageIdxWithTwoTags = 4
|
||||
const imageIdxWithStudio = 5
|
||||
const imageIdxInZip = 6
|
||||
performersNameCase = performerIdx1WithDupName
|
||||
performersNameNoCase = 2
|
||||
)
|
||||
|
||||
const performerIdxWithScene = 0
|
||||
const performerIdx1WithScene = 1
|
||||
const performerIdx2WithScene = 2
|
||||
const performerIdxWithImage = 3
|
||||
const performerIdx1WithImage = 4
|
||||
const performerIdx2WithImage = 5
|
||||
const performerIdxWithGallery = 6
|
||||
const performerIdx1WithGallery = 7
|
||||
const performerIdx2WithGallery = 8
|
||||
const (
|
||||
movieIdxWithScene = iota
|
||||
movieIdxWithStudio
|
||||
// movies with dup names start from the end
|
||||
movieIdxWithDupName
|
||||
|
||||
// performers with dup names start from the end
|
||||
const performerIdx1WithDupName = 9
|
||||
const performerIdxWithDupName = 10
|
||||
moviesNameCase = movieIdxWithDupName
|
||||
moviesNameNoCase = 1
|
||||
)
|
||||
|
||||
const movieIdxWithScene = 0
|
||||
const movieIdxWithStudio = 1
|
||||
const (
|
||||
galleryIdxWithScene = iota
|
||||
galleryIdxWithImage
|
||||
galleryIdxWithPerformer
|
||||
galleryIdxWithTwoPerformers
|
||||
galleryIdxWithTag
|
||||
galleryIdxWithTwoTags
|
||||
galleryIdxWithStudio
|
||||
galleryIdxWithPerformerTag
|
||||
galleryIdxWithPerformerTwoTags
|
||||
// new indexes above
|
||||
lastGalleryIdx
|
||||
|
||||
// movies with dup names start from the end
|
||||
const movieIdxWithDupName = 2
|
||||
totalGalleries = lastGalleryIdx + 1
|
||||
)
|
||||
|
||||
const galleryIdxWithScene = 0
|
||||
const galleryIdxWithImage = 1
|
||||
const galleryIdxWithPerformer = 2
|
||||
const galleryIdxWithTwoPerformers = 3
|
||||
const galleryIdxWithTag = 4
|
||||
const galleryIdxWithTwoTags = 5
|
||||
const galleryIdxWithStudio = 6
|
||||
const (
|
||||
tagIdxWithScene = iota
|
||||
tagIdx1WithScene
|
||||
tagIdx2WithScene
|
||||
tagIdxWithPrimaryMarker
|
||||
tagIdxWithMarker
|
||||
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
|
||||
const tagIdx1WithScene = 1
|
||||
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
|
||||
tagsNameNoCase = 2
|
||||
tagsNameCase = tagIdx1WithDupName
|
||||
)
|
||||
|
||||
// tags with dup names start from the end
|
||||
const tagIdx1WithDupName = 12
|
||||
const tagIdxWithDupName = 13
|
||||
const (
|
||||
studioIdxWithScene = iota
|
||||
studioIdxWithMovie
|
||||
studioIdxWithChildStudio
|
||||
studioIdxWithParentStudio
|
||||
studioIdxWithImage
|
||||
studioIdxWithGallery
|
||||
// new indexes above
|
||||
// studios with dup names start from the end
|
||||
studioIdxWithDupName
|
||||
|
||||
const studioIdxWithScene = 0
|
||||
const studioIdxWithMovie = 1
|
||||
const studioIdxWithChildStudio = 2
|
||||
const studioIdxWithParentStudio = 3
|
||||
const studioIdxWithImage = 4
|
||||
const studioIdxWithGallery = 5
|
||||
studiosNameCase = studioIdxWithDupName
|
||||
studiosNameNoCase = 1
|
||||
)
|
||||
|
||||
// studios with dup names start from the end
|
||||
const studioIdxWithDupName = 6
|
||||
const (
|
||||
markerIdxWithScene = iota
|
||||
)
|
||||
|
||||
const markerIdxWithScene = 0
|
||||
const (
|
||||
pathField = "Path"
|
||||
checksumField = "Checksum"
|
||||
titleField = "Title"
|
||||
zipPath = "zipPath.zip"
|
||||
)
|
||||
|
||||
const pathField = "Path"
|
||||
const checksumField = "Checksum"
|
||||
const titleField = "Title"
|
||||
const zipPath = "zipPath.zip"
|
||||
var (
|
||||
sceneIDs []int
|
||||
imageIDs []int
|
||||
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) {
|
||||
ret := runTests(m)
|
||||
@@ -205,12 +335,16 @@ func populateDB() error {
|
||||
return fmt.Errorf("error creating studios: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := linkSceneGallery(r.Scene(), sceneIdxWithGallery, galleryIdxWithScene); err != nil {
|
||||
return fmt.Errorf("error linking scene to gallery: %s", err.Error())
|
||||
if err := linkPerformerTags(r.Performer()); err != nil {
|
||||
return fmt.Errorf("error linking performer tags: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := linkSceneMovie(r.Scene(), sceneIdxWithMovie, movieIdxWithScene); err != nil {
|
||||
return fmt.Errorf("error scene to movie: %s", err.Error())
|
||||
if err := linkSceneGalleries(r.Scene()); err != nil {
|
||||
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 {
|
||||
@@ -221,11 +355,11 @@ func populateDB() error {
|
||||
return fmt.Errorf("error linking scene tags: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := linkSceneStudio(r.Scene(), sceneIdxWithStudio, studioIdxWithScene); err != nil {
|
||||
return fmt.Errorf("error linking scene studio: %s", err.Error())
|
||||
if err := linkSceneStudios(r.Scene()); err != nil {
|
||||
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())
|
||||
}
|
||||
|
||||
@@ -237,16 +371,16 @@ func populateDB() 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())
|
||||
}
|
||||
|
||||
if err := linkMovieStudio(r.Movie(), movieIdxWithStudio, studioIdxWithMovie); err != nil {
|
||||
return fmt.Errorf("error linking movie studio: %s", err.Error())
|
||||
if err := linkMovieStudios(r.Movie()); err != nil {
|
||||
return fmt.Errorf("error linking movie studios: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := linkStudioParent(r.Studio(), studioIdxWithChildStudio, studioIdxWithParentStudio); err != nil {
|
||||
return fmt.Errorf("error linking studio parent: %s", err.Error())
|
||||
if err := linkStudiosParent(r.Studio()); err != nil {
|
||||
return fmt.Errorf("error linking studios parent: %s", err.Error())
|
||||
}
|
||||
|
||||
if err := linkGalleryPerformers(r.Gallery()); err != nil {
|
||||
@@ -512,6 +646,30 @@ func getTagMarkerCount(id int) int {
|
||||
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
|
||||
func createTags(tqb models.TagReaderWriter, n int, o int) error {
|
||||
const namePlain = "Name"
|
||||
@@ -624,189 +782,182 @@ func createMarker(mqb models.SceneMarkerReaderWriter, sceneIdx, primaryTagIdx in
|
||||
return nil
|
||||
}
|
||||
|
||||
func linkSceneMovie(qb models.SceneReaderWriter, sceneIndex, movieIndex int) error {
|
||||
sceneID := sceneIDs[sceneIndex]
|
||||
movies, err := qb.GetMovies(sceneID)
|
||||
if err != nil {
|
||||
return err
|
||||
func doLinks(links [][2]int, fn func(idx1, idx2 int) error) error {
|
||||
for _, l := range links {
|
||||
if err := fn(l[0], l[1]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
movies = append(movies, models.MoviesScenes{
|
||||
MovieID: movieIDs[movieIndex],
|
||||
SceneID: sceneID,
|
||||
return nil
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
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 {
|
||||
_, err := scene.AddPerformer(qb, sceneIDs[sceneIndex], performerIDs[performerIndex])
|
||||
return err
|
||||
}
|
||||
|
||||
func linkSceneGallery(qb models.SceneReaderWriter, sceneIndex, galleryIndex int) error {
|
||||
_, err := scene.AddGallery(qb, sceneIDs[sceneIndex], galleryIDs[galleryIndex])
|
||||
return err
|
||||
func linkSceneGalleries(qb models.SceneReaderWriter) error {
|
||||
return doLinks(sceneGalleryLinks, func(sceneIndex, galleryIndex int) error {
|
||||
_, err := scene.AddGallery(qb, sceneIDs[sceneIndex], galleryIDs[galleryIndex])
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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 {
|
||||
_, err := scene.AddTag(qb, sceneIDs[sceneIndex], tagIDs[tagIndex])
|
||||
return err
|
||||
func linkSceneStudios(sqb models.SceneWriter) error {
|
||||
return doLinks(sceneStudioLinks, func(sceneIndex, studioIndex int) error {
|
||||
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 {
|
||||
scene := models.ScenePartial{
|
||||
ID: sceneIDs[sceneIndex],
|
||||
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 linkImageGalleries(gqb models.GalleryReaderWriter) error {
|
||||
return doLinks(imageGalleryLinks, func(imageIndex, galleryIndex int) error {
|
||||
return gallery.AddImage(gqb, galleryIDs[galleryIndex], imageIDs[imageIndex])
|
||||
})
|
||||
}
|
||||
|
||||
func linkImageTags(iqb models.ImageReaderWriter) error {
|
||||
if err := linkImageTag(iqb, imageIdxWithTag, tagIdxWithImage); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := linkImageTag(iqb, imageIdxWithTwoTags, tagIdx1WithImage); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := linkImageTag(iqb, imageIdxWithTwoTags, tagIdx2WithImage); err != nil {
|
||||
return err
|
||||
}
|
||||
return doLinks(imageTagLinks, func(imageIndex, tagIndex int) error {
|
||||
imageID := imageIDs[imageIndex]
|
||||
tags, err := iqb.GetTagIDs(imageID)
|
||||
if 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 {
|
||||
imageID := imageIDs[imageIndex]
|
||||
tags, err := iqb.GetTagIDs(imageID)
|
||||
if err != nil {
|
||||
func linkImageStudios(qb models.ImageWriter) error {
|
||||
return doLinks(imageStudioLinks, func(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
|
||||
}
|
||||
|
||||
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 {
|
||||
if err := linkImagePerformer(qb, imageIdxWithPerformer, performerIdxWithImage); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := linkImagePerformer(qb, imageIdxWithTwoPerformers, performerIdx1WithImage); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := linkImagePerformer(qb, imageIdxWithTwoPerformers, performerIdx2WithImage); err != nil {
|
||||
return err
|
||||
}
|
||||
return doLinks(imagePerformerLinks, func(imageIndex, performerIndex int) error {
|
||||
imageID := imageIDs[imageIndex]
|
||||
performers, err := qb.GetPerformerIDs(imageID)
|
||||
if 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 {
|
||||
imageID := imageIDs[imageIndex]
|
||||
performers, err := iqb.GetPerformerIDs(imageID)
|
||||
if err != nil {
|
||||
func linkGalleryPerformers(qb models.GalleryReaderWriter) error {
|
||||
return doLinks(galleryPerformerLinks, func(galleryIndex, performerIndex int) error {
|
||||
galleryID := imageIDs[galleryIndex]
|
||||
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
|
||||
}
|
||||
|
||||
performers = append(performers, performerIDs[performerIndex])
|
||||
|
||||
return iqb.UpdatePerformers(imageID, performers)
|
||||
})
|
||||
}
|
||||
|
||||
func linkMovieStudio(mqb models.MovieWriter, movieIndex, studioIndex int) error {
|
||||
movie := models.MoviePartial{
|
||||
ID: movieIDs[movieIndex],
|
||||
StudioID: &sql.NullInt64{Int64: int64(studioIDs[studioIndex]), Valid: true},
|
||||
}
|
||||
_, err := mqb.Update(movie)
|
||||
func linkStudiosParent(qb models.StudioWriter) error {
|
||||
return doLinks(studioParentLinks, func(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 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
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func addTagImage(qb models.TagWriter, tagIndex int) error {
|
||||
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 {
|
||||
gallery := models.GalleryPartial{
|
||||
ID: galleryIDs[galleryIndex],
|
||||
@@ -816,29 +967,3 @@ func linkGalleryStudio(qb models.GalleryWriter, galleryIndex, studioIndex int) e
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
query := `
|
||||
SELECT tags.* FROM tags
|
||||
@@ -211,6 +223,12 @@ func (qb *tagQueryBuilder) Query(tagFilter *models.TagFilterType, findFilter *mo
|
||||
|
||||
query.body += `
|
||||
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 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 {
|
||||
// clause, count := getIntCriterionWhereClause("count(distinct scene_markers.id)", *markerCount)
|
||||
// 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) {
|
||||
if err := withTxn(func(r models.Repository) error {
|
||||
qb := r.Tag()
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
### ✨ New Features
|
||||
* Added Performer tags.
|
||||
|
||||
### 🎨 Improvements
|
||||
* Improved performer details and edit UI pages.
|
||||
* 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 !== "tags" &&
|
||||
criterion.type !== "sceneTags" &&
|
||||
criterion.type !== "performerTags" &&
|
||||
criterion.type !== "movies"
|
||||
)
|
||||
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 { 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 { 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 {
|
||||
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 (
|
||||
<BasicCard
|
||||
className="performer-card"
|
||||
@@ -57,19 +109,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
||||
<Link to={NavUtils.makePerformersCountryUrl(performer)}>
|
||||
<CountryFlag country={performer.country} />
|
||||
</Link>
|
||||
<div className="text-muted">
|
||||
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>
|
||||
{maybeRenderPopoverButtonGroup()}
|
||||
</>
|
||||
}
|
||||
selected={selected}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { TagLink } from "src/components/Shared";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { genderToString } from "src/core/StashService";
|
||||
import { TextUtils } from "src/utils";
|
||||
@@ -15,6 +16,25 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
// Network state
|
||||
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() {
|
||||
if (!performer.stash_ids?.length) {
|
||||
return;
|
||||
@@ -101,6 +121,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
TextUtils.instagramURL
|
||||
)}
|
||||
/>
|
||||
{renderTagsField()}
|
||||
{renderStashIDs()}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Col,
|
||||
InputGroup,
|
||||
Row,
|
||||
Badge,
|
||||
} from "react-bootstrap";
|
||||
import Mousetrap from "mousetrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
mutateReloadScrapers,
|
||||
usePerformerUpdate,
|
||||
usePerformerCreate,
|
||||
useTagCreate,
|
||||
queryScrapePerformerURL,
|
||||
} from "src/core/StashService";
|
||||
import {
|
||||
@@ -28,6 +30,8 @@ import {
|
||||
ImageInput,
|
||||
ScrapePerformerSuggest,
|
||||
LoadingIndicator,
|
||||
CollapseButton,
|
||||
TagSelect,
|
||||
} from "src/components/Shared";
|
||||
import { ImageUtils } from "src/utils";
|
||||
import { useToast } from "src/hooks";
|
||||
@@ -64,6 +68,8 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
scrapePerformerDetails,
|
||||
setScrapePerformerDetails,
|
||||
] = useState<GQL.ScrapedPerformerDataFragment>();
|
||||
const [newTags, setNewTags] = useState<GQL.ScrapedSceneTag[]>();
|
||||
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
|
||||
// Network state
|
||||
@@ -81,6 +87,8 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
|
||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
||||
|
||||
const [createTag] = useTagCreate({ name: "" });
|
||||
|
||||
const genderOptions = [""].concat(getGenderStrings());
|
||||
|
||||
const schema = yup.object({
|
||||
@@ -100,6 +108,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
url: yup.string().optional(),
|
||||
twitter: yup.string().optional(),
|
||||
instagram: yup.string().optional(),
|
||||
tag_ids: yup.array(yup.string().required()).optional(),
|
||||
stash_ids: yup.mixed<GQL.StashIdInput>().optional(),
|
||||
image: yup.string().optional().nullable(),
|
||||
});
|
||||
@@ -121,6 +130,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
url: performer.url ?? "",
|
||||
twitter: performer.twitter ?? "",
|
||||
instagram: performer.instagram ?? "",
|
||||
tag_ids: (performer.tags ?? []).map((t) => t.id),
|
||||
stash_ids: performer.stash_ids ?? undefined,
|
||||
image: undefined,
|
||||
};
|
||||
@@ -154,6 +164,75 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
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(
|
||||
state: Partial<GQL.ScrapedPerformerDataFragment>
|
||||
) {
|
||||
@@ -210,6 +289,13 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
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
|
||||
// #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 {};
|
||||
|
||||
// image is not supported
|
||||
const { __typename, image: _image, ...ret } = scrapePerformerDetails;
|
||||
// remove tags as well
|
||||
const {
|
||||
__typename,
|
||||
image: _image,
|
||||
tags: _tags,
|
||||
...ret
|
||||
} = scrapePerformerDetails;
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -484,10 +576,10 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPerformer: Partial<GQL.PerformerDataFragment> = {
|
||||
const currentPerformer: Partial<GQL.PerformerUpdateInput> = {
|
||||
...formik.values,
|
||||
gender: stringToGender(formik.values.gender),
|
||||
image_path: formik.values.image ?? performer.image_path,
|
||||
image: formik.values.image ?? performer.image_path,
|
||||
};
|
||||
|
||||
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) => {
|
||||
formik.setFieldValue(
|
||||
"stash_ids",
|
||||
@@ -805,6 +919,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form.Row>
|
||||
{renderTagsField()}
|
||||
{renderStashIDs()}
|
||||
|
||||
{renderButtons()}
|
||||
|
||||
@@ -11,8 +11,12 @@ import {
|
||||
getGenderStrings,
|
||||
genderToString,
|
||||
stringToGender,
|
||||
useTagCreate,
|
||||
} from "src/core/StashService";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { TagSelect } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import _ from "lodash";
|
||||
|
||||
function renderScrapedGender(
|
||||
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 {
|
||||
performer: Partial<GQL.PerformerDataFragment>;
|
||||
performer: Partial<GQL.PerformerUpdateInput>;
|
||||
scraped: GQL.ScrapedPerformer;
|
||||
|
||||
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>>(
|
||||
new ScrapeResult<string>(props.performer.image_path, props.scraped.image)
|
||||
new ScrapeResult<string>(props.performer.image, props.scraped.image)
|
||||
);
|
||||
|
||||
const allFields = [
|
||||
@@ -173,6 +279,7 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||
instagram,
|
||||
gender,
|
||||
image,
|
||||
tags,
|
||||
];
|
||||
// don't show the dialog if nothing was scraped
|
||||
if (allFields.every((r) => !r.scraped)) {
|
||||
@@ -180,6 +287,41 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||
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 {
|
||||
return {
|
||||
name: name.getNewValue(),
|
||||
@@ -198,6 +340,12 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||
twitter: twitter.getNewValue(),
|
||||
instagram: instagram.getNewValue(),
|
||||
gender: gender.getNewValue(),
|
||||
tags: tags.getNewValue()?.map((m) => {
|
||||
return {
|
||||
stored_id: m,
|
||||
name: "",
|
||||
};
|
||||
}),
|
||||
image: image.getNewValue(),
|
||||
};
|
||||
}
|
||||
@@ -281,6 +429,12 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||
result={instagram}
|
||||
onChange={(value) => setInstagram(value)}
|
||||
/>
|
||||
{renderScrapedTagsRow(
|
||||
tags,
|
||||
(value) => setTags(value),
|
||||
newTags,
|
||||
createNewTag
|
||||
)}
|
||||
<ScrapedImageRow
|
||||
title="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 { PerformerCard } from "./PerformerCard";
|
||||
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 [isExportDialogOpen, setIsExportDialogOpen] = 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 = (
|
||||
selectedPerformers: SlimPerformerDataFragment[],
|
||||
onClose: (confirmed: boolean) => void
|
||||
@@ -98,9 +118,11 @@ export const PerformerList: React.FC = () => {
|
||||
const listData = usePerformersList({
|
||||
otherOperations,
|
||||
renderContent,
|
||||
renderEditDialog: renderEditPerformersDialog,
|
||||
filterHook,
|
||||
addKeybinds,
|
||||
selectable: true,
|
||||
persistState: PersistanceLevel.ALL,
|
||||
persistState,
|
||||
renderDeleteDialog,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import React from "react";
|
||||
import { Route, Switch } from "react-router-dom";
|
||||
import { PersistanceLevel } from "src/hooks/ListHook";
|
||||
import { Performer } from "./PerformerDetails/Performer";
|
||||
import { PerformerList } from "./PerformerList";
|
||||
|
||||
const Performers = () => (
|
||||
<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} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
@@ -32,6 +32,7 @@ interface ITypeProps {
|
||||
| "parent_studios"
|
||||
| "tags"
|
||||
| "sceneTags"
|
||||
| "performerTags"
|
||||
| "movies";
|
||||
}
|
||||
interface IFilterProps {
|
||||
@@ -43,6 +44,7 @@ interface IFilterProps {
|
||||
isMulti?: boolean;
|
||||
isClearable?: boolean;
|
||||
isDisabled?: boolean;
|
||||
menuPortalTarget?: HTMLElement | null;
|
||||
}
|
||||
interface ISelectProps<T extends boolean> {
|
||||
className?: string;
|
||||
@@ -60,6 +62,7 @@ interface ISelectProps<T extends boolean> {
|
||||
placeholder?: string;
|
||||
showDropdown?: boolean;
|
||||
groupHeader?: string;
|
||||
menuPortalTarget?: HTMLElement | null;
|
||||
closeMenuOnSelect?: boolean;
|
||||
noOptionsMessage?: string | null;
|
||||
}
|
||||
@@ -109,6 +112,7 @@ const SelectComponent = <T extends boolean>({
|
||||
placeholder,
|
||||
showDropdown = true,
|
||||
groupHeader,
|
||||
menuPortalTarget,
|
||||
closeMenuOnSelect = true,
|
||||
noOptionsMessage = type !== "tags" ? "None" : null,
|
||||
}: ISelectProps<T> & ITypeProps) => {
|
||||
@@ -158,6 +162,7 @@ const SelectComponent = <T extends boolean>({
|
||||
isLoading,
|
||||
styles,
|
||||
closeMenuOnSelect,
|
||||
menuPortalTarget,
|
||||
components: {
|
||||
IndicatorSeparator: () => null,
|
||||
...((!showDropdown || isDisabled) && { DropdownIndicator: () => null }),
|
||||
|
||||
@@ -14,6 +14,7 @@ import { NavUtils, TextUtils } from "src/utils";
|
||||
|
||||
interface IProps {
|
||||
tag?: Partial<TagDataFragment>;
|
||||
tagType?: "performer" | "scene";
|
||||
performer?: Partial<PerformerDataFragment>;
|
||||
marker?: Partial<SceneMarkerDataFragment>;
|
||||
movie?: Partial<MovieDataFragment>;
|
||||
@@ -26,7 +27,11 @@ export const TagLink: React.FC<IProps> = (props: IProps) => {
|
||||
let link: string = "#";
|
||||
let title: string = "";
|
||||
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 || "";
|
||||
} else if (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() {
|
||||
if (tag) {
|
||||
return (
|
||||
@@ -55,6 +68,7 @@ export const TagCard: React.FC<IProps> = ({
|
||||
<ButtonGroup className="card-popovers">
|
||||
{maybeRenderScenesPopoverButton()}
|
||||
{maybeRenderSceneMarkersPopoverButton()}
|
||||
{maybeRenderPerformersPopoverButton()}
|
||||
</ButtonGroup>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useToast } from "src/hooks";
|
||||
import { TagScenesPanel } from "./TagScenesPanel";
|
||||
import { TagMarkersPanel } from "./TagMarkersPanel";
|
||||
import { TagImagesPanel } from "./TagImagesPanel";
|
||||
import { TagPerformersPanel } from "./TagPerformersPanel";
|
||||
|
||||
interface ITabParams {
|
||||
id?: string;
|
||||
@@ -51,7 +52,10 @@ export const Tag: React.FC = () => {
|
||||
const [createTag] = useTagCreate(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) => {
|
||||
if (tab !== newTab) {
|
||||
const tabParam = newTab === "scenes" ? "" : `/${newTab}`;
|
||||
@@ -260,6 +264,9 @@ export const Tag: React.FC = () => {
|
||||
<Tab eventKey="markers" title="Markers">
|
||||
<TagMarkersPanel tag={tag} />
|
||||
</Tab>
|
||||
<Tab eventKey="performers" title="Performers">
|
||||
<TagPerformersPanel tag={tag} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</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({
|
||||
update: deleteCache(performerMutationImpactedQueries),
|
||||
});
|
||||
|
||||
export const useBulkPerformerUpdate = (input: GQL.BulkPerformerUpdateInput) =>
|
||||
GQL.useBulkPerformerUpdateMutation({
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
update: deleteCache(performerMutationImpactedQueries),
|
||||
});
|
||||
|
||||
export const usePerformerDestroy = () =>
|
||||
GQL.usePerformerDestroyMutation({
|
||||
refetchQueries: getQueryNames([
|
||||
|
||||
@@ -709,6 +709,7 @@ CareerLength
|
||||
Tattoos
|
||||
Piercings
|
||||
Aliases
|
||||
Tags (see Tag fields)
|
||||
Image
|
||||
```
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ export type CriterionType =
|
||||
| "movieIsMissing"
|
||||
| "tags"
|
||||
| "sceneTags"
|
||||
| "performerTags"
|
||||
| "performers"
|
||||
| "studios"
|
||||
| "movies"
|
||||
@@ -43,7 +44,10 @@ export type CriterionType =
|
||||
| "gender"
|
||||
| "parent_studios"
|
||||
| "scene_count"
|
||||
| "marker_count";
|
||||
| "marker_count"
|
||||
| "image_count"
|
||||
| "gallery_count"
|
||||
| "performer_count";
|
||||
|
||||
type Option = string | number | IOptionType;
|
||||
export type CriterionValue = string | number | ILabeledId[];
|
||||
@@ -83,6 +87,8 @@ export abstract class Criterion {
|
||||
return "Tags";
|
||||
case "sceneTags":
|
||||
return "Scene Tags";
|
||||
case "performerTags":
|
||||
return "Performer Tags";
|
||||
case "performers":
|
||||
return "Performers";
|
||||
case "studios":
|
||||
@@ -123,6 +129,12 @@ export abstract class Criterion {
|
||||
return "Scene Count";
|
||||
case "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 value: ILabeledId[] = [];
|
||||
|
||||
constructor(type: "tags" | "sceneTags") {
|
||||
constructor(type: "tags" | "sceneTags" | "performerTags") {
|
||||
super();
|
||||
this.type = type;
|
||||
this.parameterName = type;
|
||||
if (type === "sceneTags") {
|
||||
this.parameterName = "scene_tags";
|
||||
}
|
||||
if (type === "performerTags") {
|
||||
this.parameterName = "performer_tags";
|
||||
}
|
||||
}
|
||||
|
||||
public encodeValue() {
|
||||
@@ -39,3 +42,8 @@ export class SceneTagsCriterionOption implements ICriterionOption {
|
||||
public label: string = Criterion.getLabel("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 "scene_count":
|
||||
case "marker_count":
|
||||
case "image_count":
|
||||
case "gallery_count":
|
||||
case "performer_count":
|
||||
return new NumberCriterion(type, type);
|
||||
case "resolution":
|
||||
return new ResolutionCriterion();
|
||||
@@ -72,6 +75,8 @@ export function makeCriteria(type: CriterionType = "none") {
|
||||
return new TagsCriterion("tags");
|
||||
case "sceneTags":
|
||||
return new TagsCriterion("sceneTags");
|
||||
case "performerTags":
|
||||
return new TagsCriterion("performerTags");
|
||||
case "performers":
|
||||
return new PerformersCriterion();
|
||||
case "studios":
|
||||
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
ParentStudiosCriterionOption,
|
||||
} from "./criteria/studios";
|
||||
import {
|
||||
PerformerTagsCriterionOption,
|
||||
SceneTagsCriterionOption,
|
||||
TagsCriterion,
|
||||
TagsCriterionOption,
|
||||
@@ -146,6 +147,7 @@ export class ListFilterModel {
|
||||
new HasMarkersCriterionOption(),
|
||||
new SceneIsMissingCriterionOption(),
|
||||
new TagsCriterionOption(),
|
||||
new PerformerTagsCriterionOption(),
|
||||
new PerformersCriterionOption(),
|
||||
new StudiosCriterionOption(),
|
||||
new MoviesCriterionOption(),
|
||||
@@ -172,6 +174,7 @@ export class ListFilterModel {
|
||||
new ResolutionCriterionOption(),
|
||||
new ImageIsMissingCriterionOption(),
|
||||
new TagsCriterionOption(),
|
||||
new PerformerTagsCriterionOption(),
|
||||
new PerformersCriterionOption(),
|
||||
new StudiosCriterionOption(),
|
||||
];
|
||||
@@ -206,6 +209,7 @@ export class ListFilterModel {
|
||||
new FavoriteCriterionOption(),
|
||||
new GenderCriterionOption(),
|
||||
new PerformerIsMissingCriterionOption(),
|
||||
new TagsCriterionOption(),
|
||||
...numberCriteria
|
||||
.concat(stringCriteria)
|
||||
.map((c) => ListFilterModel.createCriterionOption(c)),
|
||||
@@ -245,6 +249,7 @@ export class ListFilterModel {
|
||||
new AverageResolutionCriterionOption(),
|
||||
new GalleryIsMissingCriterionOption(),
|
||||
new TagsCriterionOption(),
|
||||
new PerformerTagsCriterionOption(),
|
||||
new PerformersCriterionOption(),
|
||||
new StudiosCriterionOption(),
|
||||
];
|
||||
@@ -277,13 +282,20 @@ export class ListFilterModel {
|
||||
// issues
|
||||
this.sortByOptions = [
|
||||
"name",
|
||||
"scenes_count" /* , "scene_markers_count"*/,
|
||||
"scenes_count",
|
||||
"images_count",
|
||||
"galleries_count",
|
||||
"performers_count",
|
||||
/* "scene_markers_count" */
|
||||
];
|
||||
this.displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
|
||||
this.criterionOptions = [
|
||||
new NoneCriterionOption(),
|
||||
new TagIsMissingCriterionOption(),
|
||||
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
|
||||
// ListFilterModel.createCriterionOption("marker_count"),
|
||||
];
|
||||
@@ -527,6 +539,14 @@ export class ListFilterModel {
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "performerTags": {
|
||||
const performerTagsCrit = criterion as TagsCriterion;
|
||||
result.performer_tags = {
|
||||
value: performerTagsCrit.value.map((tag) => tag.id),
|
||||
modifier: performerTagsCrit.modifier,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "performers": {
|
||||
const perfCrit = criterion as PerformersCriterion;
|
||||
result.performers = {
|
||||
@@ -650,6 +670,15 @@ export class ListFilterModel {
|
||||
}
|
||||
case "performerIsMissing":
|
||||
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
|
||||
}
|
||||
});
|
||||
@@ -778,6 +807,14 @@ export class ListFilterModel {
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "performerTags": {
|
||||
const performerTagsCrit = criterion as TagsCriterion;
|
||||
result.performer_tags = {
|
||||
value: performerTagsCrit.value.map((tag) => tag.id),
|
||||
modifier: performerTagsCrit.modifier,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "performers": {
|
||||
const perfCrit = criterion as PerformersCriterion;
|
||||
result.performers = {
|
||||
@@ -929,6 +966,14 @@ export class ListFilterModel {
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "performerTags": {
|
||||
const performerTagsCrit = criterion as TagsCriterion;
|
||||
result.performer_tags = {
|
||||
value: performerTagsCrit.value.map((tag) => tag.id),
|
||||
modifier: performerTagsCrit.modifier,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case "performers": {
|
||||
const perfCrit = criterion as PerformersCriterion;
|
||||
result.performers = {
|
||||
@@ -967,6 +1012,30 @@ export class ListFilterModel {
|
||||
};
|
||||
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
|
||||
// case "marker_count": {
|
||||
// const countCrit = criterion as NumberCriterion;
|
||||
|
||||
@@ -76,6 +76,15 @@ const makeTagScenesUrl = (tag: Partial<GQL.TagDataFragment>) => {
|
||||
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>) => {
|
||||
if (!tag.id) return "#";
|
||||
const filter = new ListFilterModel(FilterMode.SceneMarkers);
|
||||
@@ -98,6 +107,7 @@ export default {
|
||||
makeStudioScenesUrl,
|
||||
makeTagSceneMarkersUrl,
|
||||
makeTagScenesUrl,
|
||||
makeTagPerformersUrl,
|
||||
makeSceneMarkerUrl,
|
||||
makeMovieScenesUrl,
|
||||
makeChildStudiosUrl,
|
||||
|
||||
Reference in New Issue
Block a user