diff --git a/pkg/sqlite/gallery.go b/pkg/sqlite/gallery.go index a9b0f7d6c..45f56b042 100644 --- a/pkg/sqlite/gallery.go +++ b/pkg/sqlite/gallery.go @@ -233,7 +233,7 @@ func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, fi } query.body += " LEFT JOIN tags on tags_join.tag_id = tags.id" - whereClause, havingClause := getMultiCriterionClause("galleries", "tags", "tags_join", "gallery_id", "tag_id", tagsFilter) + whereClause, havingClause := getMultiCriterionClause("galleries", "tags", "galleries_tags", "gallery_id", "tag_id", tagsFilter) query.addWhere(whereClause) query.addHaving(havingClause) } @@ -244,7 +244,7 @@ func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, fi } query.body += " LEFT JOIN performers ON performers_join.performer_id = performers.id" - whereClause, havingClause := getMultiCriterionClause("galleries", "performers", "performers_join", "gallery_id", "performer_id", performersFilter) + whereClause, havingClause := getMultiCriterionClause("galleries", "performers", "performers_galleries", "gallery_id", "performer_id", performersFilter) query.addWhere(whereClause) query.addHaving(havingClause) } diff --git a/pkg/sqlite/gallery_test.go b/pkg/sqlite/gallery_test.go index c06ff19a2..108a28e54 100644 --- a/pkg/sqlite/gallery_test.go +++ b/pkg/sqlite/gallery_test.go @@ -3,6 +3,7 @@ package sqlite_test import ( + "strconv" "testing" "github.com/stretchr/testify/assert" @@ -272,6 +273,252 @@ func TestGalleryQueryIsMissingScene(t *testing.T) { }) } +func queryGallery(t *testing.T, sqb models.GalleryReader, galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) []*models.Gallery { + galleries, _, err := sqb.Query(galleryFilter, findFilter) + if err != nil { + t.Errorf("Error querying gallery: %s", err.Error()) + } + + return galleries +} + +func TestGalleryQueryIsMissingStudio(t *testing.T) { + withTxn(func(r models.Repository) error { + sqb := r.Gallery() + isMissing := "studio" + galleryFilter := models.GalleryFilterType{ + IsMissing: &isMissing, + } + + q := getGalleryStringValue(galleryIdxWithStudio, titleField) + findFilter := models.FindFilterType{ + Q: &q, + } + + galleries := queryGallery(t, sqb, &galleryFilter, &findFilter) + + assert.Len(t, galleries, 0) + + findFilter.Q = nil + galleries = queryGallery(t, sqb, &galleryFilter, &findFilter) + + // ensure non of the ids equal the one with studio + for _, gallery := range galleries { + assert.NotEqual(t, galleryIDs[galleryIdxWithStudio], gallery.ID) + } + + return nil + }) +} + +func TestGalleryQueryIsMissingPerformers(t *testing.T) { + withTxn(func(r models.Repository) error { + sqb := r.Gallery() + isMissing := "performers" + galleryFilter := models.GalleryFilterType{ + IsMissing: &isMissing, + } + + q := getGalleryStringValue(galleryIdxWithPerformer, titleField) + findFilter := models.FindFilterType{ + Q: &q, + } + + galleries := queryGallery(t, sqb, &galleryFilter, &findFilter) + + assert.Len(t, galleries, 0) + + findFilter.Q = nil + galleries = queryGallery(t, sqb, &galleryFilter, &findFilter) + + assert.True(t, len(galleries) > 0) + + // ensure non of the ids equal the one with movies + for _, gallery := range galleries { + assert.NotEqual(t, galleryIDs[galleryIdxWithPerformer], gallery.ID) + } + + return nil + }) +} + +func TestGalleryQueryIsMissingTags(t *testing.T) { + withTxn(func(r models.Repository) error { + sqb := r.Gallery() + isMissing := "tags" + galleryFilter := models.GalleryFilterType{ + IsMissing: &isMissing, + } + + q := getGalleryStringValue(galleryIdxWithTwoTags, titleField) + findFilter := models.FindFilterType{ + Q: &q, + } + + galleries := queryGallery(t, sqb, &galleryFilter, &findFilter) + + assert.Len(t, galleries, 0) + + findFilter.Q = nil + galleries = queryGallery(t, sqb, &galleryFilter, &findFilter) + + assert.True(t, len(galleries) > 0) + + return nil + }) +} + +func TestGalleryQueryPerformers(t *testing.T) { + withTxn(func(r models.Repository) error { + sqb := r.Gallery() + performerCriterion := models.MultiCriterionInput{ + Value: []string{ + strconv.Itoa(performerIDs[performerIdxWithGallery]), + strconv.Itoa(performerIDs[performerIdx1WithGallery]), + }, + Modifier: models.CriterionModifierIncludes, + } + + galleryFilter := models.GalleryFilterType{ + Performers: &performerCriterion, + } + + 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[galleryIdxWithPerformer] || gallery.ID == galleryIDs[galleryIdxWithTwoPerformers]) + } + + performerCriterion = models.MultiCriterionInput{ + Value: []string{ + strconv.Itoa(performerIDs[performerIdx1WithGallery]), + strconv.Itoa(performerIDs[performerIdx2WithGallery]), + }, + Modifier: models.CriterionModifierIncludesAll, + } + + galleries = queryGallery(t, sqb, &galleryFilter, nil) + + assert.Len(t, galleries, 1) + assert.Equal(t, galleryIDs[galleryIdxWithTwoPerformers], galleries[0].ID) + + performerCriterion = models.MultiCriterionInput{ + Value: []string{ + strconv.Itoa(performerIDs[performerIdx1WithGallery]), + }, + Modifier: models.CriterionModifierExcludes, + } + + q := getGalleryStringValue(galleryIdxWithTwoPerformers, titleField) + findFilter := models.FindFilterType{ + Q: &q, + } + + galleries = queryGallery(t, sqb, &galleryFilter, &findFilter) + assert.Len(t, galleries, 0) + + return nil + }) +} + +func TestGalleryQueryTags(t *testing.T) { + withTxn(func(r models.Repository) error { + sqb := r.Gallery() + tagCriterion := models.MultiCriterionInput{ + Value: []string{ + strconv.Itoa(tagIDs[tagIdxWithGallery]), + strconv.Itoa(tagIDs[tagIdx1WithGallery]), + }, + Modifier: models.CriterionModifierIncludes, + } + + galleryFilter := models.GalleryFilterType{ + Tags: &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[galleryIdxWithTag] || gallery.ID == galleryIDs[galleryIdxWithTwoTags]) + } + + tagCriterion = models.MultiCriterionInput{ + Value: []string{ + strconv.Itoa(tagIDs[tagIdx1WithGallery]), + strconv.Itoa(tagIDs[tagIdx2WithGallery]), + }, + Modifier: models.CriterionModifierIncludesAll, + } + + galleries = queryGallery(t, sqb, &galleryFilter, nil) + + assert.Len(t, galleries, 1) + assert.Equal(t, galleryIDs[galleryIdxWithTwoTags], galleries[0].ID) + + tagCriterion = models.MultiCriterionInput{ + Value: []string{ + strconv.Itoa(tagIDs[tagIdx1WithGallery]), + }, + Modifier: models.CriterionModifierExcludes, + } + + q := getGalleryStringValue(galleryIdxWithTwoTags, titleField) + findFilter := models.FindFilterType{ + Q: &q, + } + + galleries = queryGallery(t, sqb, &galleryFilter, &findFilter) + assert.Len(t, galleries, 0) + + return nil + }) +} + +func TestGalleryQueryStudio(t *testing.T) { + withTxn(func(r models.Repository) error { + sqb := r.Gallery() + studioCriterion := models.MultiCriterionInput{ + Value: []string{ + strconv.Itoa(studioIDs[studioIdxWithGallery]), + }, + Modifier: models.CriterionModifierIncludes, + } + + galleryFilter := models.GalleryFilterType{ + Studios: &studioCriterion, + } + + galleries := queryGallery(t, sqb, &galleryFilter, nil) + + assert.Len(t, galleries, 1) + + // ensure id is correct + assert.Equal(t, galleryIDs[galleryIdxWithStudio], galleries[0].ID) + + studioCriterion = models.MultiCriterionInput{ + Value: []string{ + strconv.Itoa(studioIDs[studioIdxWithGallery]), + }, + Modifier: models.CriterionModifierExcludes, + } + + q := getGalleryStringValue(galleryIdxWithStudio, 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 diff --git a/pkg/sqlite/performer_test.go b/pkg/sqlite/performer_test.go index 530430403..f488d5d93 100644 --- a/pkg/sqlite/performer_test.go +++ b/pkg/sqlite/performer_test.go @@ -44,6 +44,14 @@ func TestPerformerFindBySceneID(t *testing.T) { } func TestPerformerFindByNames(t *testing.T) { + getNames := func(p []*models.Performer) []string { + var ret []string + for _, pp := range p { + ret = append(ret, pp.Name.String) + } + return ret + } + withTxn(func(r models.Repository) error { var names []string @@ -72,19 +80,20 @@ func TestPerformerFindByNames(t *testing.T) { if err != nil { t.Errorf("Error finding performers: %s", err.Error()) } - assert.Len(t, performers, 2) // performerIdxWithScene and performerIdx1WithScene - assert.Equal(t, performerNames[performerIdxWithScene], performers[0].Name.String) - assert.Equal(t, performerNames[performerIdx1WithScene], performers[1].Name.String) + retNames := getNames(performers) + assert.Equal(t, names, retNames) performers, err = pqb.FindByNames(names, true) // find performers by names ( 2 names nocase) if err != nil { t.Errorf("Error finding performers: %s", err.Error()) } - assert.Len(t, performers, 4) // performerIdxWithScene and performerIdxWithDupName , performerIdx1WithScene and performerIdx1WithDupName - assert.Equal(t, performerNames[performerIdxWithScene], performers[0].Name.String) - assert.Equal(t, performerNames[performerIdx1WithScene], performers[1].Name.String) - assert.Equal(t, performerNames[performerIdx1WithDupName], performers[2].Name.String) - assert.Equal(t, performerNames[performerIdxWithDupName], performers[3].Name.String) + retNames = getNames(performers) + assert.Equal(t, []string{ + performerNames[performerIdxWithScene], + performerNames[performerIdx1WithScene], + performerNames[performerIdx1WithDupName], + performerNames[performerIdxWithDupName], + }, retNames) return nil }) diff --git a/pkg/sqlite/setup_test.go b/pkg/sqlite/setup_test.go index 81019ce9d..8293d36ba 100644 --- a/pkg/sqlite/setup_test.go +++ b/pkg/sqlite/setup_test.go @@ -22,14 +22,14 @@ import ( const totalScenes = 12 const totalImages = 6 // TODO - add one for zip file -const performersNameCase = 6 +const performersNameCase = 9 const performersNameNoCase = 2 const moviesNameCase = 2 const moviesNameNoCase = 1 -const totalGalleries = 3 +const totalGalleries = 8 const tagsNameNoCase = 2 -const tagsNameCase = 9 -const studiosNameCase = 5 +const tagsNameCase = 12 +const studiosNameCase = 6 const studiosNameNoCase = 1 var sceneIDs []int @@ -69,10 +69,13 @@ const performerIdx2WithScene = 2 const performerIdxWithImage = 3 const performerIdx1WithImage = 4 const performerIdx2WithImage = 5 +const performerIdxWithGallery = 6 +const performerIdx1WithGallery = 7 +const performerIdx2WithGallery = 8 // performers with dup names start from the end -const performerIdx1WithDupName = 6 -const performerIdxWithDupName = 7 +const performerIdx1WithDupName = 9 +const performerIdxWithDupName = 10 const movieIdxWithScene = 0 const movieIdxWithStudio = 1 @@ -82,6 +85,11 @@ const movieIdxWithDupName = 2 const galleryIdxWithScene = 0 const galleryIdxWithImage = 1 +const galleryIdxWithPerformer = 2 +const galleryIdxWithTwoPerformers = 3 +const galleryIdxWithTag = 4 +const galleryIdxWithTwoTags = 5 +const galleryIdxWithStudio = 6 const tagIdxWithScene = 0 const tagIdx1WithScene = 1 @@ -92,19 +100,23 @@ const tagIdxWithCoverImage = 5 const tagIdxWithImage = 6 const tagIdx1WithImage = 7 const tagIdx2WithImage = 8 +const tagIdxWithGallery = 9 +const tagIdx1WithGallery = 10 +const tagIdx2WithGallery = 11 // tags with dup names start from the end -const tagIdx1WithDupName = 9 -const tagIdxWithDupName = 10 +const tagIdx1WithDupName = 12 +const tagIdxWithDupName = 13 const studioIdxWithScene = 0 const studioIdxWithMovie = 1 const studioIdxWithChildStudio = 2 const studioIdxWithParentStudio = 3 const studioIdxWithImage = 4 +const studioIdxWithGallery = 5 // studios with dup names start from the end -const studioIdxWithDupName = 5 +const studioIdxWithDupName = 6 const markerIdxWithScene = 0 @@ -237,6 +249,18 @@ func populateDB() error { return fmt.Errorf("error linking studio parent: %s", err.Error()) } + if err := linkGalleryPerformers(r.Gallery()); err != nil { + return fmt.Errorf("error linking gallery performers: %s", err.Error()) + } + + if err := linkGalleryTags(r.Gallery()); err != nil { + return fmt.Errorf("error linking gallery tags: %s", err.Error()) + } + + if err := linkGalleryStudio(r.Gallery(), galleryIdxWithStudio, studioIdxWithGallery); err != nil { + return fmt.Errorf("error linking gallery studio: %s", err.Error()) + } + if err := createMarker(r.SceneMarker(), sceneIdxWithMarker, tagIdxWithPrimaryMarker, []int{tagIdxWithMarker}); err != nil { return fmt.Errorf("error creating scene marker: %s", err.Error()) } @@ -756,3 +780,65 @@ func linkStudioParent(qb models.StudioWriter, parentIndex, childIndex int) error 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], + StudioID: &sql.NullInt64{Int64: int64(studioIDs[studioIndex]), Valid: true}, + } + _, err := qb.UpdatePartial(gallery) + + 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) +} diff --git a/ui/v2.5/src/components/Changelog/versions/v060.md b/ui/v2.5/src/components/Changelog/versions/v060.md index ea6769ed4..947bffbec 100644 --- a/ui/v2.5/src/components/Changelog/versions/v060.md +++ b/ui/v2.5/src/components/Changelog/versions/v060.md @@ -8,6 +8,7 @@ * Added Rescan button to scene, image, gallery details overflow button. ### 🐛 Bug fixes +* Fix SQL error when filtering galleries excluding performers or tags. * Fix version checking for armv7 and arm64. * Change "Is NULL" filter to include empty string values. * Prevent scene card previews playing in full-screen on iOS devices.