mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
Autotag support for images and galleries (#1345)
* Add compound queries for images and galleries * Implement image and gallery auto tagging
This commit is contained in:
@@ -164,6 +164,10 @@ input StudioFilterType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input GalleryFilterType {
|
input GalleryFilterType {
|
||||||
|
AND: GalleryFilterType
|
||||||
|
OR: GalleryFilterType
|
||||||
|
NOT: GalleryFilterType
|
||||||
|
|
||||||
"""Filter by path"""
|
"""Filter by path"""
|
||||||
path: StringCriterionInput
|
path: StringCriterionInput
|
||||||
"""Filter to only include galleries missing this property"""
|
"""Filter to only include galleries missing this property"""
|
||||||
@@ -219,6 +223,10 @@ input TagFilterType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input ImageFilterType {
|
input ImageFilterType {
|
||||||
|
AND: ImageFilterType
|
||||||
|
OR: ImageFilterType
|
||||||
|
NOT: ImageFilterType
|
||||||
|
|
||||||
"""Filter by path"""
|
"""Filter by path"""
|
||||||
path: StringCriterionInput
|
path: StringCriterionInput
|
||||||
"""Filter by rating"""
|
"""Filter by rating"""
|
||||||
|
|||||||
117
pkg/autotag/gallery.go
Normal file
117
pkg/autotag/gallery.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package autotag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/gallery"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func galleryPathsFilter(paths []string) *models.GalleryFilterType {
|
||||||
|
if paths == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sep := string(filepath.Separator)
|
||||||
|
|
||||||
|
var ret *models.GalleryFilterType
|
||||||
|
var or *models.GalleryFilterType
|
||||||
|
for _, p := range paths {
|
||||||
|
newOr := &models.GalleryFilterType{}
|
||||||
|
if or != nil {
|
||||||
|
or.Or = newOr
|
||||||
|
} else {
|
||||||
|
ret = newOr
|
||||||
|
}
|
||||||
|
|
||||||
|
or = newOr
|
||||||
|
|
||||||
|
if !strings.HasSuffix(p, sep) {
|
||||||
|
p = p + sep
|
||||||
|
}
|
||||||
|
|
||||||
|
or.Path = &models.StringCriterionInput{
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
Value: p + "%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMatchingGalleries(name string, paths []string, galleryReader models.GalleryReader) ([]*models.Gallery, error) {
|
||||||
|
regex := getPathQueryRegex(name)
|
||||||
|
organized := false
|
||||||
|
filter := models.GalleryFilterType{
|
||||||
|
Path: &models.StringCriterionInput{
|
||||||
|
Value: "(?i)" + regex,
|
||||||
|
Modifier: models.CriterionModifierMatchesRegex,
|
||||||
|
},
|
||||||
|
Organized: &organized,
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.And = galleryPathsFilter(paths)
|
||||||
|
|
||||||
|
pp := models.PerPageAll
|
||||||
|
gallerys, _, err := galleryReader.Query(&filter, &models.FindFilterType{
|
||||||
|
PerPage: &pp,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error querying gallerys with regex '%s': %s", regex, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret []*models.Gallery
|
||||||
|
for _, p := range gallerys {
|
||||||
|
if nameMatchesPath(name, p.Path.String) {
|
||||||
|
ret = append(ret, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getGalleryFileTagger(s *models.Gallery) tagger {
|
||||||
|
return tagger{
|
||||||
|
ID: s.ID,
|
||||||
|
Type: "gallery",
|
||||||
|
Name: s.GetTitle(),
|
||||||
|
Path: s.Path.String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GalleryPerformers tags the provided gallery with performers whose name matches the gallery's path.
|
||||||
|
func GalleryPerformers(s *models.Gallery, rw models.GalleryReaderWriter, performerReader models.PerformerReader) error {
|
||||||
|
t := getGalleryFileTagger(s)
|
||||||
|
|
||||||
|
return t.tagPerformers(performerReader, func(subjectID, otherID int) (bool, error) {
|
||||||
|
return gallery.AddPerformer(rw, subjectID, otherID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GalleryStudios tags the provided gallery with the first studio whose name matches the gallery's path.
|
||||||
|
//
|
||||||
|
// Gallerys will not be tagged if studio is already set.
|
||||||
|
func GalleryStudios(s *models.Gallery, rw models.GalleryReaderWriter, studioReader models.StudioReader) error {
|
||||||
|
if s.StudioID.Valid {
|
||||||
|
// don't modify
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t := getGalleryFileTagger(s)
|
||||||
|
|
||||||
|
return t.tagStudios(studioReader, func(subjectID, otherID int) (bool, error) {
|
||||||
|
return addGalleryStudio(rw, subjectID, otherID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GalleryTags tags the provided gallery with tags whose name matches the gallery's path.
|
||||||
|
func GalleryTags(s *models.Gallery, rw models.GalleryReaderWriter, tagReader models.TagReader) error {
|
||||||
|
t := getGalleryFileTagger(s)
|
||||||
|
|
||||||
|
return t.tagTags(tagReader, func(subjectID, otherID int) (bool, error) {
|
||||||
|
return gallery.AddTag(rw, subjectID, otherID)
|
||||||
|
})
|
||||||
|
}
|
||||||
145
pkg/autotag/gallery_test.go
Normal file
145
pkg/autotag/gallery_test.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package autotag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/models/mocks"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
const galleryExt = "zip"
|
||||||
|
|
||||||
|
func TestGalleryPerformers(t *testing.T) {
|
||||||
|
const galleryID = 1
|
||||||
|
const performerName = "performer name"
|
||||||
|
const performerID = 2
|
||||||
|
performer := models.Performer{
|
||||||
|
ID: performerID,
|
||||||
|
Name: models.NullString(performerName),
|
||||||
|
}
|
||||||
|
|
||||||
|
const reversedPerformerName = "name performer"
|
||||||
|
const reversedPerformerID = 3
|
||||||
|
reversedPerformer := models.Performer{
|
||||||
|
ID: reversedPerformerID,
|
||||||
|
Name: models.NullString(reversedPerformerName),
|
||||||
|
}
|
||||||
|
|
||||||
|
testTables := generateTestTable(performerName, galleryExt)
|
||||||
|
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
for _, test := range testTables {
|
||||||
|
mockPerformerReader := &mocks.PerformerReaderWriter{}
|
||||||
|
mockGalleryReader := &mocks.GalleryReaderWriter{}
|
||||||
|
|
||||||
|
mockPerformerReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Performer{&performer, &reversedPerformer}, nil).Once()
|
||||||
|
|
||||||
|
if test.Matches {
|
||||||
|
mockGalleryReader.On("GetPerformerIDs", galleryID).Return(nil, nil).Once()
|
||||||
|
mockGalleryReader.On("UpdatePerformers", galleryID, []int{performerID}).Return(nil).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
gallery := models.Gallery{
|
||||||
|
ID: galleryID,
|
||||||
|
Path: models.NullString(test.Path),
|
||||||
|
}
|
||||||
|
err := GalleryPerformers(&gallery, mockGalleryReader, mockPerformerReader)
|
||||||
|
|
||||||
|
assert.Nil(err)
|
||||||
|
mockPerformerReader.AssertExpectations(t)
|
||||||
|
mockGalleryReader.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGalleryStudios(t *testing.T) {
|
||||||
|
const galleryID = 1
|
||||||
|
const studioName = "studio name"
|
||||||
|
const studioID = 2
|
||||||
|
studio := models.Studio{
|
||||||
|
ID: studioID,
|
||||||
|
Name: models.NullString(studioName),
|
||||||
|
}
|
||||||
|
|
||||||
|
const reversedStudioName = "name studio"
|
||||||
|
const reversedStudioID = 3
|
||||||
|
reversedStudio := models.Studio{
|
||||||
|
ID: reversedStudioID,
|
||||||
|
Name: models.NullString(reversedStudioName),
|
||||||
|
}
|
||||||
|
|
||||||
|
testTables := generateTestTable(studioName, galleryExt)
|
||||||
|
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
for _, test := range testTables {
|
||||||
|
mockStudioReader := &mocks.StudioReaderWriter{}
|
||||||
|
mockGalleryReader := &mocks.GalleryReaderWriter{}
|
||||||
|
|
||||||
|
mockStudioReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Studio{&studio, &reversedStudio}, nil).Once()
|
||||||
|
|
||||||
|
if test.Matches {
|
||||||
|
mockGalleryReader.On("Find", galleryID).Return(&models.Gallery{}, nil).Once()
|
||||||
|
expectedStudioID := models.NullInt64(studioID)
|
||||||
|
mockGalleryReader.On("UpdatePartial", models.GalleryPartial{
|
||||||
|
ID: galleryID,
|
||||||
|
StudioID: &expectedStudioID,
|
||||||
|
}).Return(nil, nil).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
gallery := models.Gallery{
|
||||||
|
ID: galleryID,
|
||||||
|
Path: models.NullString(test.Path),
|
||||||
|
}
|
||||||
|
err := GalleryStudios(&gallery, mockGalleryReader, mockStudioReader)
|
||||||
|
|
||||||
|
assert.Nil(err)
|
||||||
|
mockStudioReader.AssertExpectations(t)
|
||||||
|
mockGalleryReader.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGalleryTags(t *testing.T) {
|
||||||
|
const galleryID = 1
|
||||||
|
const tagName = "tag name"
|
||||||
|
const tagID = 2
|
||||||
|
tag := models.Tag{
|
||||||
|
ID: tagID,
|
||||||
|
Name: tagName,
|
||||||
|
}
|
||||||
|
|
||||||
|
const reversedTagName = "name tag"
|
||||||
|
const reversedTagID = 3
|
||||||
|
reversedTag := models.Tag{
|
||||||
|
ID: reversedTagID,
|
||||||
|
Name: reversedTagName,
|
||||||
|
}
|
||||||
|
|
||||||
|
testTables := generateTestTable(tagName, galleryExt)
|
||||||
|
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
for _, test := range testTables {
|
||||||
|
mockTagReader := &mocks.TagReaderWriter{}
|
||||||
|
mockGalleryReader := &mocks.GalleryReaderWriter{}
|
||||||
|
|
||||||
|
mockTagReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Tag{&tag, &reversedTag}, nil).Once()
|
||||||
|
|
||||||
|
if test.Matches {
|
||||||
|
mockGalleryReader.On("GetTagIDs", galleryID).Return(nil, nil).Once()
|
||||||
|
mockGalleryReader.On("UpdateTags", galleryID, []int{tagID}).Return(nil).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
gallery := models.Gallery{
|
||||||
|
ID: galleryID,
|
||||||
|
Path: models.NullString(test.Path),
|
||||||
|
}
|
||||||
|
err := GalleryTags(&gallery, mockGalleryReader, mockTagReader)
|
||||||
|
|
||||||
|
assert.Nil(err)
|
||||||
|
mockTagReader.AssertExpectations(t)
|
||||||
|
mockGalleryReader.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
117
pkg/autotag/image.go
Normal file
117
pkg/autotag/image.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package autotag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/image"
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func imagePathsFilter(paths []string) *models.ImageFilterType {
|
||||||
|
if paths == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sep := string(filepath.Separator)
|
||||||
|
|
||||||
|
var ret *models.ImageFilterType
|
||||||
|
var or *models.ImageFilterType
|
||||||
|
for _, p := range paths {
|
||||||
|
newOr := &models.ImageFilterType{}
|
||||||
|
if or != nil {
|
||||||
|
or.Or = newOr
|
||||||
|
} else {
|
||||||
|
ret = newOr
|
||||||
|
}
|
||||||
|
|
||||||
|
or = newOr
|
||||||
|
|
||||||
|
if !strings.HasSuffix(p, sep) {
|
||||||
|
p = p + sep
|
||||||
|
}
|
||||||
|
|
||||||
|
or.Path = &models.StringCriterionInput{
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
Value: p + "%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMatchingImages(name string, paths []string, imageReader models.ImageReader) ([]*models.Image, error) {
|
||||||
|
regex := getPathQueryRegex(name)
|
||||||
|
organized := false
|
||||||
|
filter := models.ImageFilterType{
|
||||||
|
Path: &models.StringCriterionInput{
|
||||||
|
Value: "(?i)" + regex,
|
||||||
|
Modifier: models.CriterionModifierMatchesRegex,
|
||||||
|
},
|
||||||
|
Organized: &organized,
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.And = imagePathsFilter(paths)
|
||||||
|
|
||||||
|
pp := models.PerPageAll
|
||||||
|
images, _, err := imageReader.Query(&filter, &models.FindFilterType{
|
||||||
|
PerPage: &pp,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error querying images with regex '%s': %s", regex, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret []*models.Image
|
||||||
|
for _, p := range images {
|
||||||
|
if nameMatchesPath(name, p.Path) {
|
||||||
|
ret = append(ret, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getImageFileTagger(s *models.Image) tagger {
|
||||||
|
return tagger{
|
||||||
|
ID: s.ID,
|
||||||
|
Type: "image",
|
||||||
|
Name: s.GetTitle(),
|
||||||
|
Path: s.Path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImagePerformers tags the provided image with performers whose name matches the image's path.
|
||||||
|
func ImagePerformers(s *models.Image, rw models.ImageReaderWriter, performerReader models.PerformerReader) error {
|
||||||
|
t := getImageFileTagger(s)
|
||||||
|
|
||||||
|
return t.tagPerformers(performerReader, func(subjectID, otherID int) (bool, error) {
|
||||||
|
return image.AddPerformer(rw, subjectID, otherID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImageStudios tags the provided image with the first studio whose name matches the image's path.
|
||||||
|
//
|
||||||
|
// Images will not be tagged if studio is already set.
|
||||||
|
func ImageStudios(s *models.Image, rw models.ImageReaderWriter, studioReader models.StudioReader) error {
|
||||||
|
if s.StudioID.Valid {
|
||||||
|
// don't modify
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t := getImageFileTagger(s)
|
||||||
|
|
||||||
|
return t.tagStudios(studioReader, func(subjectID, otherID int) (bool, error) {
|
||||||
|
return addImageStudio(rw, subjectID, otherID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImageTags tags the provided image with tags whose name matches the image's path.
|
||||||
|
func ImageTags(s *models.Image, rw models.ImageReaderWriter, tagReader models.TagReader) error {
|
||||||
|
t := getImageFileTagger(s)
|
||||||
|
|
||||||
|
return t.tagTags(tagReader, func(subjectID, otherID int) (bool, error) {
|
||||||
|
return image.AddTag(rw, subjectID, otherID)
|
||||||
|
})
|
||||||
|
}
|
||||||
145
pkg/autotag/image_test.go
Normal file
145
pkg/autotag/image_test.go
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
package autotag
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/models/mocks"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
const imageExt = "jpg"
|
||||||
|
|
||||||
|
func TestImagePerformers(t *testing.T) {
|
||||||
|
const imageID = 1
|
||||||
|
const performerName = "performer name"
|
||||||
|
const performerID = 2
|
||||||
|
performer := models.Performer{
|
||||||
|
ID: performerID,
|
||||||
|
Name: models.NullString(performerName),
|
||||||
|
}
|
||||||
|
|
||||||
|
const reversedPerformerName = "name performer"
|
||||||
|
const reversedPerformerID = 3
|
||||||
|
reversedPerformer := models.Performer{
|
||||||
|
ID: reversedPerformerID,
|
||||||
|
Name: models.NullString(reversedPerformerName),
|
||||||
|
}
|
||||||
|
|
||||||
|
testTables := generateTestTable(performerName, imageExt)
|
||||||
|
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
for _, test := range testTables {
|
||||||
|
mockPerformerReader := &mocks.PerformerReaderWriter{}
|
||||||
|
mockImageReader := &mocks.ImageReaderWriter{}
|
||||||
|
|
||||||
|
mockPerformerReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Performer{&performer, &reversedPerformer}, nil).Once()
|
||||||
|
|
||||||
|
if test.Matches {
|
||||||
|
mockImageReader.On("GetPerformerIDs", imageID).Return(nil, nil).Once()
|
||||||
|
mockImageReader.On("UpdatePerformers", imageID, []int{performerID}).Return(nil).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
image := models.Image{
|
||||||
|
ID: imageID,
|
||||||
|
Path: test.Path,
|
||||||
|
}
|
||||||
|
err := ImagePerformers(&image, mockImageReader, mockPerformerReader)
|
||||||
|
|
||||||
|
assert.Nil(err)
|
||||||
|
mockPerformerReader.AssertExpectations(t)
|
||||||
|
mockImageReader.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImageStudios(t *testing.T) {
|
||||||
|
const imageID = 1
|
||||||
|
const studioName = "studio name"
|
||||||
|
const studioID = 2
|
||||||
|
studio := models.Studio{
|
||||||
|
ID: studioID,
|
||||||
|
Name: models.NullString(studioName),
|
||||||
|
}
|
||||||
|
|
||||||
|
const reversedStudioName = "name studio"
|
||||||
|
const reversedStudioID = 3
|
||||||
|
reversedStudio := models.Studio{
|
||||||
|
ID: reversedStudioID,
|
||||||
|
Name: models.NullString(reversedStudioName),
|
||||||
|
}
|
||||||
|
|
||||||
|
testTables := generateTestTable(studioName, imageExt)
|
||||||
|
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
for _, test := range testTables {
|
||||||
|
mockStudioReader := &mocks.StudioReaderWriter{}
|
||||||
|
mockImageReader := &mocks.ImageReaderWriter{}
|
||||||
|
|
||||||
|
mockStudioReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Studio{&studio, &reversedStudio}, nil).Once()
|
||||||
|
|
||||||
|
if test.Matches {
|
||||||
|
mockImageReader.On("Find", imageID).Return(&models.Image{}, nil).Once()
|
||||||
|
expectedStudioID := models.NullInt64(studioID)
|
||||||
|
mockImageReader.On("Update", models.ImagePartial{
|
||||||
|
ID: imageID,
|
||||||
|
StudioID: &expectedStudioID,
|
||||||
|
}).Return(nil, nil).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
image := models.Image{
|
||||||
|
ID: imageID,
|
||||||
|
Path: test.Path,
|
||||||
|
}
|
||||||
|
err := ImageStudios(&image, mockImageReader, mockStudioReader)
|
||||||
|
|
||||||
|
assert.Nil(err)
|
||||||
|
mockStudioReader.AssertExpectations(t)
|
||||||
|
mockImageReader.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImageTags(t *testing.T) {
|
||||||
|
const imageID = 1
|
||||||
|
const tagName = "tag name"
|
||||||
|
const tagID = 2
|
||||||
|
tag := models.Tag{
|
||||||
|
ID: tagID,
|
||||||
|
Name: tagName,
|
||||||
|
}
|
||||||
|
|
||||||
|
const reversedTagName = "name tag"
|
||||||
|
const reversedTagID = 3
|
||||||
|
reversedTag := models.Tag{
|
||||||
|
ID: reversedTagID,
|
||||||
|
Name: reversedTagName,
|
||||||
|
}
|
||||||
|
|
||||||
|
testTables := generateTestTable(tagName, imageExt)
|
||||||
|
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
for _, test := range testTables {
|
||||||
|
mockTagReader := &mocks.TagReaderWriter{}
|
||||||
|
mockImageReader := &mocks.ImageReaderWriter{}
|
||||||
|
|
||||||
|
mockTagReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Tag{&tag, &reversedTag}, nil).Once()
|
||||||
|
|
||||||
|
if test.Matches {
|
||||||
|
mockImageReader.On("GetTagIDs", imageID).Return(nil, nil).Once()
|
||||||
|
mockImageReader.On("UpdateTags", imageID, []int{tagID}).Return(nil).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
image := models.Image{
|
||||||
|
ID: imageID,
|
||||||
|
Path: test.Path,
|
||||||
|
}
|
||||||
|
err := ImageTags(&image, mockImageReader, mockTagReader)
|
||||||
|
|
||||||
|
assert.Nil(err)
|
||||||
|
mockTagReader.AssertExpectations(t)
|
||||||
|
mockImageReader.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,8 @@ const testName = "Foo's Bar"
|
|||||||
const existingStudioName = "ExistingStudio"
|
const existingStudioName = "ExistingStudio"
|
||||||
|
|
||||||
const existingStudioSceneName = testName + ".dontChangeStudio.mp4"
|
const existingStudioSceneName = testName + ".dontChangeStudio.mp4"
|
||||||
|
const existingStudioImageName = testName + ".dontChangeStudio.mp4"
|
||||||
|
const existingStudioGalleryName = testName + ".dontChangeStudio.mp4"
|
||||||
|
|
||||||
var existingStudioID int
|
var existingStudioID int
|
||||||
|
|
||||||
@@ -109,7 +111,7 @@ func createTag(qb models.TagWriter) error {
|
|||||||
|
|
||||||
func createScenes(sqb models.SceneReaderWriter) error {
|
func createScenes(sqb models.SceneReaderWriter) error {
|
||||||
// create the scenes
|
// create the scenes
|
||||||
scenePatterns, falseScenePatterns := generateScenePaths(testName)
|
scenePatterns, falseScenePatterns := generateTestPaths(testName, sceneExt)
|
||||||
|
|
||||||
for _, fn := range scenePatterns {
|
for _, fn := range scenePatterns {
|
||||||
err := createScene(sqb, makeScene(fn, true))
|
err := createScene(sqb, makeScene(fn, true))
|
||||||
@@ -169,6 +171,130 @@ func createScene(sqb models.SceneWriter, scene *models.Scene) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createImages(sqb models.ImageReaderWriter) error {
|
||||||
|
// create the images
|
||||||
|
imagePatterns, falseImagePatterns := generateTestPaths(testName, imageExt)
|
||||||
|
|
||||||
|
for _, fn := range imagePatterns {
|
||||||
|
err := createImage(sqb, makeImage(fn, true))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, fn := range falseImagePatterns {
|
||||||
|
err := createImage(sqb, makeImage(fn, false))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add organized images
|
||||||
|
for _, fn := range imagePatterns {
|
||||||
|
s := makeImage("organized"+fn, false)
|
||||||
|
s.Organized = true
|
||||||
|
err := createImage(sqb, s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create image with existing studio io
|
||||||
|
studioImage := makeImage(existingStudioImageName, true)
|
||||||
|
studioImage.StudioID = sql.NullInt64{Valid: true, Int64: int64(existingStudioID)}
|
||||||
|
err := createImage(sqb, studioImage)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeImage(name string, expectedResult bool) *models.Image {
|
||||||
|
image := &models.Image{
|
||||||
|
Checksum: utils.MD5FromString(name),
|
||||||
|
Path: name,
|
||||||
|
}
|
||||||
|
|
||||||
|
// if expectedResult is true then we expect it to match, set the title accordingly
|
||||||
|
if expectedResult {
|
||||||
|
image.Title = sql.NullString{Valid: true, String: name}
|
||||||
|
}
|
||||||
|
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
func createImage(sqb models.ImageWriter, image *models.Image) error {
|
||||||
|
_, err := sqb.Create(*image)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to create image with name '%s': %s", image.Path, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createGalleries(sqb models.GalleryReaderWriter) error {
|
||||||
|
// create the galleries
|
||||||
|
galleryPatterns, falseGalleryPatterns := generateTestPaths(testName, galleryExt)
|
||||||
|
|
||||||
|
for _, fn := range galleryPatterns {
|
||||||
|
err := createGallery(sqb, makeGallery(fn, true))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, fn := range falseGalleryPatterns {
|
||||||
|
err := createGallery(sqb, makeGallery(fn, false))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add organized galleries
|
||||||
|
for _, fn := range galleryPatterns {
|
||||||
|
s := makeGallery("organized"+fn, false)
|
||||||
|
s.Organized = true
|
||||||
|
err := createGallery(sqb, s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create gallery with existing studio io
|
||||||
|
studioGallery := makeGallery(existingStudioGalleryName, true)
|
||||||
|
studioGallery.StudioID = sql.NullInt64{Valid: true, Int64: int64(existingStudioID)}
|
||||||
|
err := createGallery(sqb, studioGallery)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeGallery(name string, expectedResult bool) *models.Gallery {
|
||||||
|
gallery := &models.Gallery{
|
||||||
|
Checksum: utils.MD5FromString(name),
|
||||||
|
Path: models.NullString(name),
|
||||||
|
}
|
||||||
|
|
||||||
|
// if expectedResult is true then we expect it to match, set the title accordingly
|
||||||
|
if expectedResult {
|
||||||
|
gallery.Title = sql.NullString{Valid: true, String: name}
|
||||||
|
}
|
||||||
|
|
||||||
|
return gallery
|
||||||
|
}
|
||||||
|
|
||||||
|
func createGallery(sqb models.GalleryWriter, gallery *models.Gallery) error {
|
||||||
|
_, err := sqb.Create(*gallery)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to create gallery with name '%s': %s", gallery.Path.String, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func withTxn(f func(r models.Repository) error) error {
|
func withTxn(f func(r models.Repository) error) error {
|
||||||
t := sqlite.NewTransactionManager()
|
t := sqlite.NewTransactionManager()
|
||||||
return t.WithTxn(context.TODO(), f)
|
return t.WithTxn(context.TODO(), f)
|
||||||
@@ -204,6 +330,16 @@ func populateDB() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = createImages(r.Image())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = createGalleries(r.Gallery())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -212,7 +348,7 @@ func populateDB() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParsePerformers(t *testing.T) {
|
func TestParsePerformerScenes(t *testing.T) {
|
||||||
var performers []*models.Performer
|
var performers []*models.Performer
|
||||||
if err := withTxn(func(r models.Repository) error {
|
if err := withTxn(func(r models.Repository) error {
|
||||||
var err error
|
var err error
|
||||||
@@ -259,7 +395,7 @@ func TestParsePerformers(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseStudios(t *testing.T) {
|
func TestParseStudioScenes(t *testing.T) {
|
||||||
var studios []*models.Studio
|
var studios []*models.Studio
|
||||||
if err := withTxn(func(r models.Repository) error {
|
if err := withTxn(func(r models.Repository) error {
|
||||||
var err error
|
var err error
|
||||||
@@ -310,7 +446,7 @@ func TestParseStudios(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseTags(t *testing.T) {
|
func TestParseTagScenes(t *testing.T) {
|
||||||
var tags []*models.Tag
|
var tags []*models.Tag
|
||||||
if err := withTxn(func(r models.Repository) error {
|
if err := withTxn(func(r models.Repository) error {
|
||||||
var err error
|
var err error
|
||||||
@@ -356,3 +492,293 @@ func TestParseTags(t *testing.T) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParsePerformerImages(t *testing.T) {
|
||||||
|
var performers []*models.Performer
|
||||||
|
if err := withTxn(func(r models.Repository) error {
|
||||||
|
var err error
|
||||||
|
performers, err = r.Performer().All()
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
t.Errorf("Error getting performer: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range performers {
|
||||||
|
if err := withTxn(func(r models.Repository) error {
|
||||||
|
return PerformerImages(p, nil, r.Image())
|
||||||
|
}); err != nil {
|
||||||
|
t.Errorf("Error auto-tagging performers: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify that images were tagged correctly
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
pqb := r.Performer()
|
||||||
|
|
||||||
|
images, err := r.Image().All()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, image := range images {
|
||||||
|
performers, err := pqb.FindByImageID(image.ID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error getting image performers: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// title is only set on images where we expect performer to be set
|
||||||
|
if image.Title.String == image.Path && len(performers) == 0 {
|
||||||
|
t.Errorf("Did not set performer '%s' for path '%s'", testName, image.Path)
|
||||||
|
} else if image.Title.String != image.Path && len(performers) > 0 {
|
||||||
|
t.Errorf("Incorrectly set performer '%s' for path '%s'", testName, image.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseStudioImages(t *testing.T) {
|
||||||
|
var studios []*models.Studio
|
||||||
|
if err := withTxn(func(r models.Repository) error {
|
||||||
|
var err error
|
||||||
|
studios, err = r.Studio().All()
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
t.Errorf("Error getting studio: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range studios {
|
||||||
|
if err := withTxn(func(r models.Repository) error {
|
||||||
|
return StudioImages(s, nil, r.Image())
|
||||||
|
}); err != nil {
|
||||||
|
t.Errorf("Error auto-tagging performers: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify that images were tagged correctly
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
images, err := r.Image().All()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, image := range images {
|
||||||
|
// check for existing studio id image first
|
||||||
|
if image.Path == existingStudioImageName {
|
||||||
|
if image.StudioID.Int64 != int64(existingStudioID) {
|
||||||
|
t.Error("Incorrectly overwrote studio ID for image with existing studio ID")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// title is only set on images where we expect studio to be set
|
||||||
|
if image.Title.String == image.Path {
|
||||||
|
if !image.StudioID.Valid {
|
||||||
|
t.Errorf("Did not set studio '%s' for path '%s'", testName, image.Path)
|
||||||
|
} else if image.StudioID.Int64 != int64(studios[1].ID) {
|
||||||
|
t.Errorf("Incorrect studio id %d set for path '%s'", image.StudioID.Int64, image.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if image.Title.String != image.Path && image.StudioID.Int64 == int64(studios[1].ID) {
|
||||||
|
t.Errorf("Incorrectly set studio '%s' for path '%s'", testName, image.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTagImages(t *testing.T) {
|
||||||
|
var tags []*models.Tag
|
||||||
|
if err := withTxn(func(r models.Repository) error {
|
||||||
|
var err error
|
||||||
|
tags, err = r.Tag().All()
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
t.Errorf("Error getting performer: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range tags {
|
||||||
|
if err := withTxn(func(r models.Repository) error {
|
||||||
|
return TagImages(s, nil, r.Image())
|
||||||
|
}); err != nil {
|
||||||
|
t.Errorf("Error auto-tagging performers: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify that images were tagged correctly
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
images, err := r.Image().All()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
tqb := r.Tag()
|
||||||
|
|
||||||
|
for _, image := range images {
|
||||||
|
tags, err := tqb.FindByImageID(image.ID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error getting image tags: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// title is only set on images where we expect performer to be set
|
||||||
|
if image.Title.String == image.Path && len(tags) == 0 {
|
||||||
|
t.Errorf("Did not set tag '%s' for path '%s'", testName, image.Path)
|
||||||
|
} else if image.Title.String != image.Path && len(tags) > 0 {
|
||||||
|
t.Errorf("Incorrectly set tag '%s' for path '%s'", testName, image.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsePerformerGalleries(t *testing.T) {
|
||||||
|
var performers []*models.Performer
|
||||||
|
if err := withTxn(func(r models.Repository) error {
|
||||||
|
var err error
|
||||||
|
performers, err = r.Performer().All()
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
t.Errorf("Error getting performer: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range performers {
|
||||||
|
if err := withTxn(func(r models.Repository) error {
|
||||||
|
return PerformerGalleries(p, nil, r.Gallery())
|
||||||
|
}); err != nil {
|
||||||
|
t.Errorf("Error auto-tagging performers: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify that galleries were tagged correctly
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
pqb := r.Performer()
|
||||||
|
|
||||||
|
galleries, err := r.Gallery().All()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, gallery := range galleries {
|
||||||
|
performers, err := pqb.FindByGalleryID(gallery.ID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error getting gallery performers: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// title is only set on galleries where we expect performer to be set
|
||||||
|
if gallery.Title.String == gallery.Path.String && len(performers) == 0 {
|
||||||
|
t.Errorf("Did not set performer '%s' for path '%s'", testName, gallery.Path.String)
|
||||||
|
} else if gallery.Title.String != gallery.Path.String && len(performers) > 0 {
|
||||||
|
t.Errorf("Incorrectly set performer '%s' for path '%s'", testName, gallery.Path.String)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseStudioGalleries(t *testing.T) {
|
||||||
|
var studios []*models.Studio
|
||||||
|
if err := withTxn(func(r models.Repository) error {
|
||||||
|
var err error
|
||||||
|
studios, err = r.Studio().All()
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
t.Errorf("Error getting studio: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range studios {
|
||||||
|
if err := withTxn(func(r models.Repository) error {
|
||||||
|
return StudioGalleries(s, nil, r.Gallery())
|
||||||
|
}); err != nil {
|
||||||
|
t.Errorf("Error auto-tagging performers: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify that galleries were tagged correctly
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
galleries, err := r.Gallery().All()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, gallery := range galleries {
|
||||||
|
// check for existing studio id gallery first
|
||||||
|
if gallery.Path.String == existingStudioGalleryName {
|
||||||
|
if gallery.StudioID.Int64 != int64(existingStudioID) {
|
||||||
|
t.Error("Incorrectly overwrote studio ID for gallery with existing studio ID")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// title is only set on galleries where we expect studio to be set
|
||||||
|
if gallery.Title.String == gallery.Path.String {
|
||||||
|
if !gallery.StudioID.Valid {
|
||||||
|
t.Errorf("Did not set studio '%s' for path '%s'", testName, gallery.Path.String)
|
||||||
|
} else if gallery.StudioID.Int64 != int64(studios[1].ID) {
|
||||||
|
t.Errorf("Incorrect studio id %d set for path '%s'", gallery.StudioID.Int64, gallery.Path.String)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if gallery.Title.String != gallery.Path.String && gallery.StudioID.Int64 == int64(studios[1].ID) {
|
||||||
|
t.Errorf("Incorrectly set studio '%s' for path '%s'", testName, gallery.Path.String)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseTagGalleries(t *testing.T) {
|
||||||
|
var tags []*models.Tag
|
||||||
|
if err := withTxn(func(r models.Repository) error {
|
||||||
|
var err error
|
||||||
|
tags, err = r.Tag().All()
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
t.Errorf("Error getting performer: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range tags {
|
||||||
|
if err := withTxn(func(r models.Repository) error {
|
||||||
|
return TagGalleries(s, nil, r.Gallery())
|
||||||
|
}); err != nil {
|
||||||
|
t.Errorf("Error auto-tagging performers: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify that galleries were tagged correctly
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
galleries, err := r.Gallery().All()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
tqb := r.Tag()
|
||||||
|
|
||||||
|
for _, gallery := range galleries {
|
||||||
|
tags, err := tqb.FindByGalleryID(gallery.ID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error getting gallery tags: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// title is only set on galleries where we expect performer to be set
|
||||||
|
if gallery.Title.String == gallery.Path.String && len(tags) == 0 {
|
||||||
|
t.Errorf("Did not set tag '%s' for path '%s'", testName, gallery.Path.String)
|
||||||
|
} else if gallery.Title.String != gallery.Path.String && len(tags) > 0 {
|
||||||
|
t.Errorf("Incorrectly set tag '%s' for path '%s'", testName, gallery.Path.String)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package autotag
|
package autotag
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/stashapp/stash/pkg/gallery"
|
||||||
|
"github.com/stashapp/stash/pkg/image"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/scene"
|
"github.com/stashapp/stash/pkg/scene"
|
||||||
)
|
)
|
||||||
@@ -40,3 +42,21 @@ func PerformerScenes(p *models.Performer, paths []string, rw models.SceneReaderW
|
|||||||
return scene.AddPerformer(rw, otherID, subjectID)
|
return scene.AddPerformer(rw, otherID, subjectID)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PerformerImages searches for images whose path matches the provided performer name and tags the image with the performer.
|
||||||
|
func PerformerImages(p *models.Performer, paths []string, rw models.ImageReaderWriter) error {
|
||||||
|
t := getPerformerTagger(p)
|
||||||
|
|
||||||
|
return t.tagImages(paths, rw, func(subjectID, otherID int) (bool, error) {
|
||||||
|
return image.AddPerformer(rw, otherID, subjectID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PerformerGalleries searches for galleries whose path matches the provided performer name and tags the gallery with the performer.
|
||||||
|
func PerformerGalleries(p *models.Performer, paths []string, rw models.GalleryReaderWriter) error {
|
||||||
|
t := getPerformerTagger(p)
|
||||||
|
|
||||||
|
return t.tagGalleries(paths, rw, func(subjectID, otherID int) (bool, error) {
|
||||||
|
return gallery.AddPerformer(rw, otherID, subjectID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func testPerformerScenes(t *testing.T, performerName, expectedRegex string) {
|
|||||||
const performerID = 2
|
const performerID = 2
|
||||||
|
|
||||||
var scenes []*models.Scene
|
var scenes []*models.Scene
|
||||||
matchingPaths, falsePaths := generateScenePaths(performerName)
|
matchingPaths, falsePaths := generateTestPaths(performerName, "mp4")
|
||||||
for i, p := range append(matchingPaths, falsePaths...) {
|
for i, p := range append(matchingPaths, falsePaths...) {
|
||||||
scenes = append(scenes, &models.Scene{
|
scenes = append(scenes, &models.Scene{
|
||||||
ID: i + 1,
|
ID: i + 1,
|
||||||
@@ -79,3 +79,147 @@ func testPerformerScenes(t *testing.T, performerName, expectedRegex string) {
|
|||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
mockSceneReader.AssertExpectations(t)
|
mockSceneReader.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPerformerImages(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
performerName string
|
||||||
|
expectedRegex string
|
||||||
|
}
|
||||||
|
|
||||||
|
performerNames := []test{
|
||||||
|
{
|
||||||
|
"performer name",
|
||||||
|
`(?i)(?:^|_|[^\w\d])performer[.\-_ ]*name(?:$|_|[^\w\d])`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"performer + name",
|
||||||
|
`(?i)(?:^|_|[^\w\d])performer[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\w\d])`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range performerNames {
|
||||||
|
testPerformerImages(t, p.performerName, p.expectedRegex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPerformerImages(t *testing.T, performerName, expectedRegex string) {
|
||||||
|
mockImageReader := &mocks.ImageReaderWriter{}
|
||||||
|
|
||||||
|
const performerID = 2
|
||||||
|
|
||||||
|
var images []*models.Image
|
||||||
|
matchingPaths, falsePaths := generateTestPaths(performerName, imageExt)
|
||||||
|
for i, p := range append(matchingPaths, falsePaths...) {
|
||||||
|
images = append(images, &models.Image{
|
||||||
|
ID: i + 1,
|
||||||
|
Path: p,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
performer := models.Performer{
|
||||||
|
ID: performerID,
|
||||||
|
Name: models.NullString(performerName),
|
||||||
|
}
|
||||||
|
|
||||||
|
organized := false
|
||||||
|
perPage := models.PerPageAll
|
||||||
|
|
||||||
|
expectedImageFilter := &models.ImageFilterType{
|
||||||
|
Organized: &organized,
|
||||||
|
Path: &models.StringCriterionInput{
|
||||||
|
Value: expectedRegex,
|
||||||
|
Modifier: models.CriterionModifierMatchesRegex,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedFindFilter := &models.FindFilterType{
|
||||||
|
PerPage: &perPage,
|
||||||
|
}
|
||||||
|
|
||||||
|
mockImageReader.On("Query", expectedImageFilter, expectedFindFilter).Return(images, len(images), nil).Once()
|
||||||
|
|
||||||
|
for i := range matchingPaths {
|
||||||
|
imageID := i + 1
|
||||||
|
mockImageReader.On("GetPerformerIDs", imageID).Return(nil, nil).Once()
|
||||||
|
mockImageReader.On("UpdatePerformers", imageID, []int{performerID}).Return(nil).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := PerformerImages(&performer, nil, mockImageReader)
|
||||||
|
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
assert.Nil(err)
|
||||||
|
mockImageReader.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPerformerGalleries(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
performerName string
|
||||||
|
expectedRegex string
|
||||||
|
}
|
||||||
|
|
||||||
|
performerNames := []test{
|
||||||
|
{
|
||||||
|
"performer name",
|
||||||
|
`(?i)(?:^|_|[^\w\d])performer[.\-_ ]*name(?:$|_|[^\w\d])`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"performer + name",
|
||||||
|
`(?i)(?:^|_|[^\w\d])performer[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\w\d])`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range performerNames {
|
||||||
|
testPerformerGalleries(t, p.performerName, p.expectedRegex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPerformerGalleries(t *testing.T, performerName, expectedRegex string) {
|
||||||
|
mockGalleryReader := &mocks.GalleryReaderWriter{}
|
||||||
|
|
||||||
|
const performerID = 2
|
||||||
|
|
||||||
|
var galleries []*models.Gallery
|
||||||
|
matchingPaths, falsePaths := generateTestPaths(performerName, galleryExt)
|
||||||
|
for i, p := range append(matchingPaths, falsePaths...) {
|
||||||
|
galleries = append(galleries, &models.Gallery{
|
||||||
|
ID: i + 1,
|
||||||
|
Path: models.NullString(p),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
performer := models.Performer{
|
||||||
|
ID: performerID,
|
||||||
|
Name: models.NullString(performerName),
|
||||||
|
}
|
||||||
|
|
||||||
|
organized := false
|
||||||
|
perPage := models.PerPageAll
|
||||||
|
|
||||||
|
expectedGalleryFilter := &models.GalleryFilterType{
|
||||||
|
Organized: &organized,
|
||||||
|
Path: &models.StringCriterionInput{
|
||||||
|
Value: expectedRegex,
|
||||||
|
Modifier: models.CriterionModifierMatchesRegex,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedFindFilter := &models.FindFilterType{
|
||||||
|
PerPage: &perPage,
|
||||||
|
}
|
||||||
|
|
||||||
|
mockGalleryReader.On("Query", expectedGalleryFilter, expectedFindFilter).Return(galleries, len(galleries), nil).Once()
|
||||||
|
|
||||||
|
for i := range matchingPaths {
|
||||||
|
galleryID := i + 1
|
||||||
|
mockGalleryReader.On("GetPerformerIDs", galleryID).Return(nil, nil).Once()
|
||||||
|
mockGalleryReader.On("UpdatePerformers", galleryID, []int{performerID}).Return(nil).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := PerformerGalleries(&performer, nil, mockGalleryReader)
|
||||||
|
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
assert.Nil(err)
|
||||||
|
mockGalleryReader.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/stashapp/stash/pkg/scene"
|
"github.com/stashapp/stash/pkg/scene"
|
||||||
)
|
)
|
||||||
|
|
||||||
func pathsFilter(paths []string) *models.SceneFilterType {
|
func scenePathsFilter(paths []string) *models.SceneFilterType {
|
||||||
if paths == nil {
|
if paths == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ func getMatchingScenes(name string, paths []string, sceneReader models.SceneRead
|
|||||||
Organized: &organized,
|
Organized: &organized,
|
||||||
}
|
}
|
||||||
|
|
||||||
filter.And = pathsFilter(paths)
|
filter.And = scenePathsFilter(paths)
|
||||||
|
|
||||||
pp := models.PerPageAll
|
pp := models.PerPageAll
|
||||||
scenes, _, err := sceneReader.Query(&filter, &models.FindFilterType{
|
scenes, _, err := sceneReader.Query(&filter, &models.FindFilterType{
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import (
|
|||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const sceneExt = "mp4"
|
||||||
|
|
||||||
var testSeparators = []string{
|
var testSeparators = []string{
|
||||||
".",
|
".",
|
||||||
"-",
|
"-",
|
||||||
@@ -26,88 +28,88 @@ var testEndSeparators = []string{
|
|||||||
",",
|
",",
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateNamePatterns(name, separator string) []string {
|
func generateNamePatterns(name, separator, ext string) []string {
|
||||||
var ret []string
|
var ret []string
|
||||||
ret = append(ret, fmt.Sprintf("%s%saaa.mp4", name, separator))
|
ret = append(ret, fmt.Sprintf("%s%saaa.%s", name, separator, ext))
|
||||||
ret = append(ret, fmt.Sprintf("aaa%s%s.mp4", separator, name))
|
ret = append(ret, fmt.Sprintf("aaa%s%s.%s", separator, name, ext))
|
||||||
ret = append(ret, fmt.Sprintf("aaa%s%s%sbbb.mp4", separator, name, separator))
|
ret = append(ret, fmt.Sprintf("aaa%s%s%sbbb.%s", separator, name, separator, ext))
|
||||||
ret = append(ret, fmt.Sprintf("dir/%s%saaa.mp4", name, separator))
|
ret = append(ret, fmt.Sprintf("dir/%s%saaa.%s", name, separator, ext))
|
||||||
ret = append(ret, fmt.Sprintf("dir\\%s%saaa.mp4", name, separator))
|
ret = append(ret, fmt.Sprintf("dir\\%s%saaa.%s", name, separator, ext))
|
||||||
ret = append(ret, fmt.Sprintf("%s%saaa/dir/bbb.mp4", name, separator))
|
ret = append(ret, fmt.Sprintf("%s%saaa/dir/bbb.%s", name, separator, ext))
|
||||||
ret = append(ret, fmt.Sprintf("%s%saaa\\dir\\bbb.mp4", name, separator))
|
ret = append(ret, fmt.Sprintf("%s%saaa\\dir\\bbb.%s", name, separator, ext))
|
||||||
ret = append(ret, fmt.Sprintf("dir/%s%s/aaa.mp4", name, separator))
|
ret = append(ret, fmt.Sprintf("dir/%s%s/aaa.%s", name, separator, ext))
|
||||||
ret = append(ret, fmt.Sprintf("dir\\%s%s\\aaa.mp4", name, separator))
|
ret = append(ret, fmt.Sprintf("dir\\%s%s\\aaa.%s", name, separator, ext))
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateSplitNamePatterns(name, separator string) []string {
|
func generateSplitNamePatterns(name, separator, ext string) []string {
|
||||||
var ret []string
|
var ret []string
|
||||||
splitted := strings.Split(name, " ")
|
splitted := strings.Split(name, " ")
|
||||||
// only do this for names that are split into two
|
// only do this for names that are split into two
|
||||||
if len(splitted) == 2 {
|
if len(splitted) == 2 {
|
||||||
ret = append(ret, fmt.Sprintf("%s%s%s.mp4", splitted[0], separator, splitted[1]))
|
ret = append(ret, fmt.Sprintf("%s%s%s.%s", splitted[0], separator, splitted[1], ext))
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateFalseNamePatterns(name string, separator string) []string {
|
func generateFalseNamePatterns(name string, separator, ext string) []string {
|
||||||
splitted := strings.Split(name, " ")
|
splitted := strings.Split(name, " ")
|
||||||
|
|
||||||
var ret []string
|
var ret []string
|
||||||
// only do this for names that are split into two
|
// only do this for names that are split into two
|
||||||
if len(splitted) == 2 {
|
if len(splitted) == 2 {
|
||||||
ret = append(ret, fmt.Sprintf("%s%saaa%s%s.mp4", splitted[0], separator, separator, splitted[1]))
|
ret = append(ret, fmt.Sprintf("%s%saaa%s%s.%s", splitted[0], separator, separator, splitted[1], ext))
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateScenePaths(testName string) (scenePatterns []string, falseScenePatterns []string) {
|
func generateTestPaths(testName, ext string) (scenePatterns []string, falseScenePatterns []string) {
|
||||||
separators := append(testSeparators, testEndSeparators...)
|
separators := append(testSeparators, testEndSeparators...)
|
||||||
|
|
||||||
for _, separator := range separators {
|
for _, separator := range separators {
|
||||||
scenePatterns = append(scenePatterns, generateNamePatterns(testName, separator)...)
|
scenePatterns = append(scenePatterns, generateNamePatterns(testName, separator, ext)...)
|
||||||
scenePatterns = append(scenePatterns, generateNamePatterns(strings.ToLower(testName), separator)...)
|
scenePatterns = append(scenePatterns, generateNamePatterns(strings.ToLower(testName), separator, ext)...)
|
||||||
scenePatterns = append(scenePatterns, generateNamePatterns(strings.ReplaceAll(testName, " ", ""), separator)...)
|
scenePatterns = append(scenePatterns, generateNamePatterns(strings.ReplaceAll(testName, " ", ""), separator, ext)...)
|
||||||
falseScenePatterns = append(falseScenePatterns, generateFalseNamePatterns(testName, separator)...)
|
falseScenePatterns = append(falseScenePatterns, generateFalseNamePatterns(testName, separator, ext)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// add test cases for intra-name separators
|
// add test cases for intra-name separators
|
||||||
for _, separator := range testSeparators {
|
for _, separator := range testSeparators {
|
||||||
if separator != " " {
|
if separator != " " {
|
||||||
scenePatterns = append(scenePatterns, generateNamePatterns(strings.Replace(testName, " ", separator, -1), separator)...)
|
scenePatterns = append(scenePatterns, generateNamePatterns(strings.Replace(testName, " ", separator, -1), separator, ext)...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add basic false scenarios
|
// add basic false scenarios
|
||||||
falseScenePatterns = append(falseScenePatterns, fmt.Sprintf("aaa%s.mp4", testName))
|
falseScenePatterns = append(falseScenePatterns, fmt.Sprintf("aaa%s.%s", testName, ext))
|
||||||
falseScenePatterns = append(falseScenePatterns, fmt.Sprintf("%saaa.mp4", testName))
|
falseScenePatterns = append(falseScenePatterns, fmt.Sprintf("%saaa.%s", testName, ext))
|
||||||
|
|
||||||
// add path separator false scenarios
|
// add path separator false scenarios
|
||||||
falseScenePatterns = append(falseScenePatterns, generateFalseNamePatterns(testName, "/")...)
|
falseScenePatterns = append(falseScenePatterns, generateFalseNamePatterns(testName, "/", ext)...)
|
||||||
falseScenePatterns = append(falseScenePatterns, generateFalseNamePatterns(testName, "\\")...)
|
falseScenePatterns = append(falseScenePatterns, generateFalseNamePatterns(testName, "\\", ext)...)
|
||||||
|
|
||||||
// split patterns only valid for ._- and whitespace
|
// split patterns only valid for ._- and whitespace
|
||||||
for _, separator := range testSeparators {
|
for _, separator := range testSeparators {
|
||||||
scenePatterns = append(scenePatterns, generateSplitNamePatterns(testName, separator)...)
|
scenePatterns = append(scenePatterns, generateSplitNamePatterns(testName, separator, ext)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// false patterns for other separators
|
// false patterns for other separators
|
||||||
for _, separator := range testEndSeparators {
|
for _, separator := range testEndSeparators {
|
||||||
falseScenePatterns = append(falseScenePatterns, generateSplitNamePatterns(testName, separator)...)
|
falseScenePatterns = append(falseScenePatterns, generateSplitNamePatterns(testName, separator, ext)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type pathTestTable struct {
|
type pathTestTable struct {
|
||||||
ScenePath string
|
Path string
|
||||||
Matches bool
|
Matches bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateTestTable(testName string) []pathTestTable {
|
func generateTestTable(testName, ext string) []pathTestTable {
|
||||||
var ret []pathTestTable
|
var ret []pathTestTable
|
||||||
|
|
||||||
var scenePatterns []string
|
var scenePatterns []string
|
||||||
@@ -116,15 +118,15 @@ func generateTestTable(testName string) []pathTestTable {
|
|||||||
separators := append(testSeparators, testEndSeparators...)
|
separators := append(testSeparators, testEndSeparators...)
|
||||||
|
|
||||||
for _, separator := range separators {
|
for _, separator := range separators {
|
||||||
scenePatterns = append(scenePatterns, generateNamePatterns(testName, separator)...)
|
scenePatterns = append(scenePatterns, generateNamePatterns(testName, separator, ext)...)
|
||||||
scenePatterns = append(scenePatterns, generateNamePatterns(strings.ToLower(testName), separator)...)
|
scenePatterns = append(scenePatterns, generateNamePatterns(strings.ToLower(testName), separator, ext)...)
|
||||||
falseScenePatterns = append(falseScenePatterns, generateFalseNamePatterns(testName, separator)...)
|
falseScenePatterns = append(falseScenePatterns, generateFalseNamePatterns(testName, separator, ext)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, p := range scenePatterns {
|
for _, p := range scenePatterns {
|
||||||
t := pathTestTable{
|
t := pathTestTable{
|
||||||
ScenePath: p,
|
Path: p,
|
||||||
Matches: true,
|
Matches: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = append(ret, t)
|
ret = append(ret, t)
|
||||||
@@ -132,8 +134,8 @@ func generateTestTable(testName string) []pathTestTable {
|
|||||||
|
|
||||||
for _, p := range falseScenePatterns {
|
for _, p := range falseScenePatterns {
|
||||||
t := pathTestTable{
|
t := pathTestTable{
|
||||||
ScenePath: p,
|
Path: p,
|
||||||
Matches: false,
|
Matches: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = append(ret, t)
|
ret = append(ret, t)
|
||||||
@@ -158,7 +160,7 @@ func TestScenePerformers(t *testing.T) {
|
|||||||
Name: models.NullString(reversedPerformerName),
|
Name: models.NullString(reversedPerformerName),
|
||||||
}
|
}
|
||||||
|
|
||||||
testTables := generateTestTable(performerName)
|
testTables := generateTestTable(performerName, sceneExt)
|
||||||
|
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
@@ -175,7 +177,7 @@ func TestScenePerformers(t *testing.T) {
|
|||||||
|
|
||||||
scene := models.Scene{
|
scene := models.Scene{
|
||||||
ID: sceneID,
|
ID: sceneID,
|
||||||
Path: test.ScenePath,
|
Path: test.Path,
|
||||||
}
|
}
|
||||||
err := ScenePerformers(&scene, mockSceneReader, mockPerformerReader)
|
err := ScenePerformers(&scene, mockSceneReader, mockPerformerReader)
|
||||||
|
|
||||||
@@ -201,7 +203,7 @@ func TestSceneStudios(t *testing.T) {
|
|||||||
Name: models.NullString(reversedStudioName),
|
Name: models.NullString(reversedStudioName),
|
||||||
}
|
}
|
||||||
|
|
||||||
testTables := generateTestTable(studioName)
|
testTables := generateTestTable(studioName, sceneExt)
|
||||||
|
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
@@ -222,7 +224,7 @@ func TestSceneStudios(t *testing.T) {
|
|||||||
|
|
||||||
scene := models.Scene{
|
scene := models.Scene{
|
||||||
ID: sceneID,
|
ID: sceneID,
|
||||||
Path: test.ScenePath,
|
Path: test.Path,
|
||||||
}
|
}
|
||||||
err := SceneStudios(&scene, mockSceneReader, mockStudioReader)
|
err := SceneStudios(&scene, mockSceneReader, mockStudioReader)
|
||||||
|
|
||||||
@@ -248,7 +250,7 @@ func TestSceneTags(t *testing.T) {
|
|||||||
Name: reversedTagName,
|
Name: reversedTagName,
|
||||||
}
|
}
|
||||||
|
|
||||||
testTables := generateTestTable(tagName)
|
testTables := generateTestTable(tagName, sceneExt)
|
||||||
|
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
@@ -265,7 +267,7 @@ func TestSceneTags(t *testing.T) {
|
|||||||
|
|
||||||
scene := models.Scene{
|
scene := models.Scene{
|
||||||
ID: sceneID,
|
ID: sceneID,
|
||||||
Path: test.ScenePath,
|
Path: test.Path,
|
||||||
}
|
}
|
||||||
err := SceneTags(&scene, mockSceneReader, mockTagReader)
|
err := SceneTags(&scene, mockSceneReader, mockTagReader)
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,54 @@ func addSceneStudio(sceneWriter models.SceneReaderWriter, sceneID, studioID int)
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addImageStudio(imageWriter models.ImageReaderWriter, imageID, studioID int) (bool, error) {
|
||||||
|
// don't set if already set
|
||||||
|
image, err := imageWriter.Find(imageID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if image.StudioID.Valid {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the studio id
|
||||||
|
s := sql.NullInt64{Int64: int64(studioID), Valid: true}
|
||||||
|
imagePartial := models.ImagePartial{
|
||||||
|
ID: imageID,
|
||||||
|
StudioID: &s,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := imageWriter.Update(imagePartial); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addGalleryStudio(galleryWriter models.GalleryReaderWriter, galleryID, studioID int) (bool, error) {
|
||||||
|
// don't set if already set
|
||||||
|
gallery, err := galleryWriter.Find(galleryID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if gallery.StudioID.Valid {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the studio id
|
||||||
|
s := sql.NullInt64{Int64: int64(studioID), Valid: true}
|
||||||
|
galleryPartial := models.GalleryPartial{
|
||||||
|
ID: galleryID,
|
||||||
|
StudioID: &s,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := galleryWriter.UpdatePartial(galleryPartial); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
func getStudioTagger(p *models.Studio) tagger {
|
func getStudioTagger(p *models.Studio) tagger {
|
||||||
return tagger{
|
return tagger{
|
||||||
ID: p.ID,
|
ID: p.ID,
|
||||||
@@ -64,3 +112,21 @@ func StudioScenes(p *models.Studio, paths []string, rw models.SceneReaderWriter)
|
|||||||
return addSceneStudio(rw, otherID, subjectID)
|
return addSceneStudio(rw, otherID, subjectID)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StudioImages searches for images whose path matches the provided studio name and tags the image with the studio, if studio is not already set on the image.
|
||||||
|
func StudioImages(p *models.Studio, paths []string, rw models.ImageReaderWriter) error {
|
||||||
|
t := getStudioTagger(p)
|
||||||
|
|
||||||
|
return t.tagImages(paths, rw, func(subjectID, otherID int) (bool, error) {
|
||||||
|
return addImageStudio(rw, otherID, subjectID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// StudioGalleries searches for galleries whose path matches the provided studio name and tags the gallery with the studio, if studio is not already set on the gallery.
|
||||||
|
func StudioGalleries(p *models.Studio, paths []string, rw models.GalleryReaderWriter) error {
|
||||||
|
t := getStudioTagger(p)
|
||||||
|
|
||||||
|
return t.tagGalleries(paths, rw, func(subjectID, otherID int) (bool, error) {
|
||||||
|
return addGalleryStudio(rw, otherID, subjectID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func testStudioScenes(t *testing.T, studioName, expectedRegex string) {
|
|||||||
const studioID = 2
|
const studioID = 2
|
||||||
|
|
||||||
var scenes []*models.Scene
|
var scenes []*models.Scene
|
||||||
matchingPaths, falsePaths := generateScenePaths(studioName)
|
matchingPaths, falsePaths := generateTestPaths(studioName, sceneExt)
|
||||||
for i, p := range append(matchingPaths, falsePaths...) {
|
for i, p := range append(matchingPaths, falsePaths...) {
|
||||||
scenes = append(scenes, &models.Scene{
|
scenes = append(scenes, &models.Scene{
|
||||||
ID: i + 1,
|
ID: i + 1,
|
||||||
@@ -83,3 +83,155 @@ func testStudioScenes(t *testing.T, studioName, expectedRegex string) {
|
|||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
mockSceneReader.AssertExpectations(t)
|
mockSceneReader.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStudioImages(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
studioName string
|
||||||
|
expectedRegex string
|
||||||
|
}
|
||||||
|
|
||||||
|
studioNames := []test{
|
||||||
|
{
|
||||||
|
"studio name",
|
||||||
|
`(?i)(?:^|_|[^\w\d])studio[.\-_ ]*name(?:$|_|[^\w\d])`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"studio + name",
|
||||||
|
`(?i)(?:^|_|[^\w\d])studio[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\w\d])`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range studioNames {
|
||||||
|
testStudioImages(t, p.studioName, p.expectedRegex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStudioImages(t *testing.T, studioName, expectedRegex string) {
|
||||||
|
mockImageReader := &mocks.ImageReaderWriter{}
|
||||||
|
|
||||||
|
const studioID = 2
|
||||||
|
|
||||||
|
var images []*models.Image
|
||||||
|
matchingPaths, falsePaths := generateTestPaths(studioName, imageExt)
|
||||||
|
for i, p := range append(matchingPaths, falsePaths...) {
|
||||||
|
images = append(images, &models.Image{
|
||||||
|
ID: i + 1,
|
||||||
|
Path: p,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
studio := models.Studio{
|
||||||
|
ID: studioID,
|
||||||
|
Name: models.NullString(studioName),
|
||||||
|
}
|
||||||
|
|
||||||
|
organized := false
|
||||||
|
perPage := models.PerPageAll
|
||||||
|
|
||||||
|
expectedImageFilter := &models.ImageFilterType{
|
||||||
|
Organized: &organized,
|
||||||
|
Path: &models.StringCriterionInput{
|
||||||
|
Value: expectedRegex,
|
||||||
|
Modifier: models.CriterionModifierMatchesRegex,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedFindFilter := &models.FindFilterType{
|
||||||
|
PerPage: &perPage,
|
||||||
|
}
|
||||||
|
|
||||||
|
mockImageReader.On("Query", expectedImageFilter, expectedFindFilter).Return(images, len(images), nil).Once()
|
||||||
|
|
||||||
|
for i := range matchingPaths {
|
||||||
|
imageID := i + 1
|
||||||
|
mockImageReader.On("Find", imageID).Return(&models.Image{}, nil).Once()
|
||||||
|
expectedStudioID := models.NullInt64(studioID)
|
||||||
|
mockImageReader.On("Update", models.ImagePartial{
|
||||||
|
ID: imageID,
|
||||||
|
StudioID: &expectedStudioID,
|
||||||
|
}).Return(nil, nil).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := StudioImages(&studio, nil, mockImageReader)
|
||||||
|
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
assert.Nil(err)
|
||||||
|
mockImageReader.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStudioGalleries(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
studioName string
|
||||||
|
expectedRegex string
|
||||||
|
}
|
||||||
|
|
||||||
|
studioNames := []test{
|
||||||
|
{
|
||||||
|
"studio name",
|
||||||
|
`(?i)(?:^|_|[^\w\d])studio[.\-_ ]*name(?:$|_|[^\w\d])`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"studio + name",
|
||||||
|
`(?i)(?:^|_|[^\w\d])studio[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\w\d])`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range studioNames {
|
||||||
|
testStudioGalleries(t, p.studioName, p.expectedRegex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStudioGalleries(t *testing.T, studioName, expectedRegex string) {
|
||||||
|
mockGalleryReader := &mocks.GalleryReaderWriter{}
|
||||||
|
|
||||||
|
const studioID = 2
|
||||||
|
|
||||||
|
var galleries []*models.Gallery
|
||||||
|
matchingPaths, falsePaths := generateTestPaths(studioName, galleryExt)
|
||||||
|
for i, p := range append(matchingPaths, falsePaths...) {
|
||||||
|
galleries = append(galleries, &models.Gallery{
|
||||||
|
ID: i + 1,
|
||||||
|
Path: models.NullString(p),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
studio := models.Studio{
|
||||||
|
ID: studioID,
|
||||||
|
Name: models.NullString(studioName),
|
||||||
|
}
|
||||||
|
|
||||||
|
organized := false
|
||||||
|
perPage := models.PerPageAll
|
||||||
|
|
||||||
|
expectedGalleryFilter := &models.GalleryFilterType{
|
||||||
|
Organized: &organized,
|
||||||
|
Path: &models.StringCriterionInput{
|
||||||
|
Value: expectedRegex,
|
||||||
|
Modifier: models.CriterionModifierMatchesRegex,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedFindFilter := &models.FindFilterType{
|
||||||
|
PerPage: &perPage,
|
||||||
|
}
|
||||||
|
|
||||||
|
mockGalleryReader.On("Query", expectedGalleryFilter, expectedFindFilter).Return(galleries, len(galleries), nil).Once()
|
||||||
|
|
||||||
|
for i := range matchingPaths {
|
||||||
|
galleryID := i + 1
|
||||||
|
mockGalleryReader.On("Find", galleryID).Return(&models.Gallery{}, nil).Once()
|
||||||
|
expectedStudioID := models.NullInt64(studioID)
|
||||||
|
mockGalleryReader.On("UpdatePartial", models.GalleryPartial{
|
||||||
|
ID: galleryID,
|
||||||
|
StudioID: &expectedStudioID,
|
||||||
|
}).Return(nil, nil).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := StudioGalleries(&studio, nil, mockGalleryReader)
|
||||||
|
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
assert.Nil(err)
|
||||||
|
mockGalleryReader.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package autotag
|
package autotag
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/stashapp/stash/pkg/gallery"
|
||||||
|
"github.com/stashapp/stash/pkg/image"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/scene"
|
"github.com/stashapp/stash/pkg/scene"
|
||||||
)
|
)
|
||||||
@@ -39,3 +41,21 @@ func TagScenes(p *models.Tag, paths []string, rw models.SceneReaderWriter) error
|
|||||||
return scene.AddTag(rw, otherID, subjectID)
|
return scene.AddTag(rw, otherID, subjectID)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TagImages searches for images whose path matches the provided tag name and tags the image with the tag.
|
||||||
|
func TagImages(p *models.Tag, paths []string, rw models.ImageReaderWriter) error {
|
||||||
|
t := getTagTagger(p)
|
||||||
|
|
||||||
|
return t.tagImages(paths, rw, func(subjectID, otherID int) (bool, error) {
|
||||||
|
return image.AddTag(rw, otherID, subjectID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TagGalleries searches for galleries whose path matches the provided tag name and tags the gallery with the tag.
|
||||||
|
func TagGalleries(p *models.Tag, paths []string, rw models.GalleryReaderWriter) error {
|
||||||
|
t := getTagTagger(p)
|
||||||
|
|
||||||
|
return t.tagGalleries(paths, rw, func(subjectID, otherID int) (bool, error) {
|
||||||
|
return gallery.AddTag(rw, otherID, subjectID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func testTagScenes(t *testing.T, tagName, expectedRegex string) {
|
|||||||
const tagID = 2
|
const tagID = 2
|
||||||
|
|
||||||
var scenes []*models.Scene
|
var scenes []*models.Scene
|
||||||
matchingPaths, falsePaths := generateScenePaths(tagName)
|
matchingPaths, falsePaths := generateTestPaths(tagName, "mp4")
|
||||||
for i, p := range append(matchingPaths, falsePaths...) {
|
for i, p := range append(matchingPaths, falsePaths...) {
|
||||||
scenes = append(scenes, &models.Scene{
|
scenes = append(scenes, &models.Scene{
|
||||||
ID: i + 1,
|
ID: i + 1,
|
||||||
@@ -79,3 +79,147 @@ func testTagScenes(t *testing.T, tagName, expectedRegex string) {
|
|||||||
assert.Nil(err)
|
assert.Nil(err)
|
||||||
mockSceneReader.AssertExpectations(t)
|
mockSceneReader.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTagImages(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
tagName string
|
||||||
|
expectedRegex string
|
||||||
|
}
|
||||||
|
|
||||||
|
tagNames := []test{
|
||||||
|
{
|
||||||
|
"tag name",
|
||||||
|
`(?i)(?:^|_|[^\w\d])tag[.\-_ ]*name(?:$|_|[^\w\d])`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag + name",
|
||||||
|
`(?i)(?:^|_|[^\w\d])tag[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\w\d])`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range tagNames {
|
||||||
|
testTagImages(t, p.tagName, p.expectedRegex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTagImages(t *testing.T, tagName, expectedRegex string) {
|
||||||
|
mockImageReader := &mocks.ImageReaderWriter{}
|
||||||
|
|
||||||
|
const tagID = 2
|
||||||
|
|
||||||
|
var images []*models.Image
|
||||||
|
matchingPaths, falsePaths := generateTestPaths(tagName, "mp4")
|
||||||
|
for i, p := range append(matchingPaths, falsePaths...) {
|
||||||
|
images = append(images, &models.Image{
|
||||||
|
ID: i + 1,
|
||||||
|
Path: p,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := models.Tag{
|
||||||
|
ID: tagID,
|
||||||
|
Name: tagName,
|
||||||
|
}
|
||||||
|
|
||||||
|
organized := false
|
||||||
|
perPage := models.PerPageAll
|
||||||
|
|
||||||
|
expectedImageFilter := &models.ImageFilterType{
|
||||||
|
Organized: &organized,
|
||||||
|
Path: &models.StringCriterionInput{
|
||||||
|
Value: expectedRegex,
|
||||||
|
Modifier: models.CriterionModifierMatchesRegex,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedFindFilter := &models.FindFilterType{
|
||||||
|
PerPage: &perPage,
|
||||||
|
}
|
||||||
|
|
||||||
|
mockImageReader.On("Query", expectedImageFilter, expectedFindFilter).Return(images, len(images), nil).Once()
|
||||||
|
|
||||||
|
for i := range matchingPaths {
|
||||||
|
imageID := i + 1
|
||||||
|
mockImageReader.On("GetTagIDs", imageID).Return(nil, nil).Once()
|
||||||
|
mockImageReader.On("UpdateTags", imageID, []int{tagID}).Return(nil).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := TagImages(&tag, nil, mockImageReader)
|
||||||
|
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
assert.Nil(err)
|
||||||
|
mockImageReader.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTagGalleries(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
tagName string
|
||||||
|
expectedRegex string
|
||||||
|
}
|
||||||
|
|
||||||
|
tagNames := []test{
|
||||||
|
{
|
||||||
|
"tag name",
|
||||||
|
`(?i)(?:^|_|[^\w\d])tag[.\-_ ]*name(?:$|_|[^\w\d])`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag + name",
|
||||||
|
`(?i)(?:^|_|[^\w\d])tag[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\w\d])`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range tagNames {
|
||||||
|
testTagGalleries(t, p.tagName, p.expectedRegex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTagGalleries(t *testing.T, tagName, expectedRegex string) {
|
||||||
|
mockGalleryReader := &mocks.GalleryReaderWriter{}
|
||||||
|
|
||||||
|
const tagID = 2
|
||||||
|
|
||||||
|
var galleries []*models.Gallery
|
||||||
|
matchingPaths, falsePaths := generateTestPaths(tagName, "mp4")
|
||||||
|
for i, p := range append(matchingPaths, falsePaths...) {
|
||||||
|
galleries = append(galleries, &models.Gallery{
|
||||||
|
ID: i + 1,
|
||||||
|
Path: models.NullString(p),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := models.Tag{
|
||||||
|
ID: tagID,
|
||||||
|
Name: tagName,
|
||||||
|
}
|
||||||
|
|
||||||
|
organized := false
|
||||||
|
perPage := models.PerPageAll
|
||||||
|
|
||||||
|
expectedGalleryFilter := &models.GalleryFilterType{
|
||||||
|
Organized: &organized,
|
||||||
|
Path: &models.StringCriterionInput{
|
||||||
|
Value: expectedRegex,
|
||||||
|
Modifier: models.CriterionModifierMatchesRegex,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedFindFilter := &models.FindFilterType{
|
||||||
|
PerPage: &perPage,
|
||||||
|
}
|
||||||
|
|
||||||
|
mockGalleryReader.On("Query", expectedGalleryFilter, expectedFindFilter).Return(galleries, len(galleries), nil).Once()
|
||||||
|
|
||||||
|
for i := range matchingPaths {
|
||||||
|
galleryID := i + 1
|
||||||
|
mockGalleryReader.On("GetTagIDs", galleryID).Return(nil, nil).Once()
|
||||||
|
mockGalleryReader.On("UpdateTags", galleryID, []int{tagID}).Return(nil).Once()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := TagGalleries(&tag, nil, mockGalleryReader)
|
||||||
|
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
assert.Nil(err)
|
||||||
|
mockGalleryReader.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|||||||
@@ -196,3 +196,45 @@ func (t *tagger) tagScenes(paths []string, sceneReader models.SceneReader, addFu
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *tagger) tagImages(paths []string, imageReader models.ImageReader, addFunc addLinkFunc) error {
|
||||||
|
others, err := getMatchingImages(t.Name, paths, imageReader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range others {
|
||||||
|
added, err := addFunc(t.ID, p.ID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return t.addError("image", p.GetTitle(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if added {
|
||||||
|
t.addLog("image", p.GetTitle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tagger) tagGalleries(paths []string, galleryReader models.GalleryReader, addFunc addLinkFunc) error {
|
||||||
|
others, err := getMatchingGalleries(t.Name, paths, galleryReader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range others {
|
||||||
|
added, err := addFunc(t.ID, p.ID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return t.addError("gallery", p.GetTitle(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if added {
|
||||||
|
t.addLog("gallery", p.GetTitle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,3 +21,43 @@ func AddImage(qb models.GalleryReaderWriter, galleryID int, imageID int) error {
|
|||||||
imageIDs = utils.IntAppendUnique(imageIDs, imageID)
|
imageIDs = utils.IntAppendUnique(imageIDs, imageID)
|
||||||
return qb.UpdateImages(galleryID, imageIDs)
|
return qb.UpdateImages(galleryID, imageIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AddPerformer(qb models.GalleryReaderWriter, id int, performerID int) (bool, error) {
|
||||||
|
performerIDs, err := qb.GetPerformerIDs(id)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldLen := len(performerIDs)
|
||||||
|
performerIDs = utils.IntAppendUnique(performerIDs, performerID)
|
||||||
|
|
||||||
|
if len(performerIDs) != oldLen {
|
||||||
|
if err := qb.UpdatePerformers(id, performerIDs); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddTag(qb models.GalleryReaderWriter, id int, tagID int) (bool, error) {
|
||||||
|
tagIDs, err := qb.GetTagIDs(id)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldLen := len(tagIDs)
|
||||||
|
tagIDs = utils.IntAppendUnique(tagIDs, tagID)
|
||||||
|
|
||||||
|
if len(tagIDs) != oldLen {
|
||||||
|
if err := qb.UpdateTags(id, tagIDs); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package image
|
package image
|
||||||
|
|
||||||
import "github.com/stashapp/stash/pkg/models"
|
import (
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
func UpdateFileModTime(qb models.ImageWriter, id int, modTime models.NullSQLiteTimestamp) (*models.Image, error) {
|
func UpdateFileModTime(qb models.ImageWriter, id int, modTime models.NullSQLiteTimestamp) (*models.Image, error) {
|
||||||
return qb.Update(models.ImagePartial{
|
return qb.Update(models.ImagePartial{
|
||||||
@@ -8,3 +11,43 @@ func UpdateFileModTime(qb models.ImageWriter, id int, modTime models.NullSQLiteT
|
|||||||
FileModTime: &modTime,
|
FileModTime: &modTime,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AddPerformer(qb models.ImageReaderWriter, id int, performerID int) (bool, error) {
|
||||||
|
performerIDs, err := qb.GetPerformerIDs(id)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldLen := len(performerIDs)
|
||||||
|
performerIDs = utils.IntAppendUnique(performerIDs, performerID)
|
||||||
|
|
||||||
|
if len(performerIDs) != oldLen {
|
||||||
|
if err := qb.UpdatePerformers(id, performerIDs); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddTag(qb models.ImageReaderWriter, id int, tagID int) (bool, error) {
|
||||||
|
tagIDs, err := qb.GetTagIDs(id)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldLen := len(tagIDs)
|
||||||
|
tagIDs = utils.IntAppendUnique(tagIDs, tagID)
|
||||||
|
|
||||||
|
if len(tagIDs) != oldLen {
|
||||||
|
if err := qb.UpdateTags(id, tagIDs); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -650,7 +648,7 @@ func (s *singleton) AutoTag(input models.AutoTagMetadataInput) {
|
|||||||
|
|
||||||
if s.isFileBasedAutoTag(input) {
|
if s.isFileBasedAutoTag(input) {
|
||||||
// doing file-based auto-tag
|
// doing file-based auto-tag
|
||||||
s.autoTagScenes(input.Paths, len(input.Performers) > 0, len(input.Studios) > 0, len(input.Tags) > 0)
|
s.autoTagFiles(input.Paths, len(input.Performers) > 0, len(input.Studios) > 0, len(input.Tags) > 0)
|
||||||
} else {
|
} else {
|
||||||
// doing specific performer/studio/tag auto-tag
|
// doing specific performer/studio/tag auto-tag
|
||||||
s.autoTagSpecific(input)
|
s.autoTagSpecific(input)
|
||||||
@@ -658,90 +656,17 @@ func (s *singleton) AutoTag(input models.AutoTagMetadataInput) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *singleton) autoTagScenes(paths []string, performers, studios, tags bool) {
|
func (s *singleton) autoTagFiles(paths []string, performers, studios, tags bool) {
|
||||||
if err := s.TxnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error {
|
t := autoTagFilesTask{
|
||||||
ret := &models.SceneFilterType{}
|
paths: paths,
|
||||||
or := ret
|
performers: performers,
|
||||||
sep := string(filepath.Separator)
|
studios: studios,
|
||||||
|
tags: tags,
|
||||||
for _, p := range paths {
|
txnManager: s.TxnManager,
|
||||||
if !strings.HasSuffix(p, sep) {
|
status: &s.Status,
|
||||||
p = p + sep
|
|
||||||
}
|
|
||||||
|
|
||||||
if ret.Path == nil {
|
|
||||||
or = ret
|
|
||||||
} else {
|
|
||||||
newOr := &models.SceneFilterType{}
|
|
||||||
or.Or = newOr
|
|
||||||
or = newOr
|
|
||||||
}
|
|
||||||
|
|
||||||
or.Path = &models.StringCriterionInput{
|
|
||||||
Modifier: models.CriterionModifierEquals,
|
|
||||||
Value: p + "%",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
organized := false
|
|
||||||
ret.Organized = &organized
|
|
||||||
|
|
||||||
// batch process scenes
|
|
||||||
batchSize := 1000
|
|
||||||
page := 1
|
|
||||||
findFilter := &models.FindFilterType{
|
|
||||||
PerPage: &batchSize,
|
|
||||||
Page: &page,
|
|
||||||
}
|
|
||||||
|
|
||||||
more := true
|
|
||||||
processed := 0
|
|
||||||
for more {
|
|
||||||
scenes, total, err := r.Scene().Query(ret, findFilter)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if processed == 0 {
|
|
||||||
logger.Infof("Starting autotag of %d scenes", total)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ss := range scenes {
|
|
||||||
if s.Status.stopping {
|
|
||||||
logger.Info("Stopping due to user request")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
t := autoTagSceneTask{
|
|
||||||
txnManager: s.TxnManager,
|
|
||||||
scene: ss,
|
|
||||||
performers: performers,
|
|
||||||
studios: studios,
|
|
||||||
tags: tags,
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
wg.Add(1)
|
|
||||||
go t.Start(&wg)
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
processed++
|
|
||||||
s.Status.setProgress(processed, total)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(scenes) != batchSize {
|
|
||||||
more = false
|
|
||||||
} else {
|
|
||||||
page++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
logger.Error(err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Finished autotag")
|
t.process()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *singleton) autoTagSpecific(input models.AutoTagMetadataInput) {
|
func (s *singleton) autoTagSpecific(input models.AutoTagMetadataInput) {
|
||||||
@@ -838,7 +763,17 @@ func (s *singleton) autoTagPerformers(paths []string, performerIds []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.TxnManager.WithTxn(context.TODO(), func(r models.Repository) error {
|
if err := s.TxnManager.WithTxn(context.TODO(), func(r models.Repository) error {
|
||||||
return autotag.PerformerScenes(performer, paths, r.Scene())
|
if err := autotag.PerformerScenes(performer, paths, r.Scene()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := autotag.PerformerImages(performer, paths, r.Image()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := autotag.PerformerGalleries(performer, paths, r.Gallery()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("error auto-tagging performer '%s': %s", performer.Name.String, err.Error())
|
return fmt.Errorf("error auto-tagging performer '%s': %s", performer.Name.String, err.Error())
|
||||||
}
|
}
|
||||||
@@ -895,7 +830,17 @@ func (s *singleton) autoTagStudios(paths []string, studioIds []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.TxnManager.WithTxn(context.TODO(), func(r models.Repository) error {
|
if err := s.TxnManager.WithTxn(context.TODO(), func(r models.Repository) error {
|
||||||
return autotag.StudioScenes(studio, paths, r.Scene())
|
if err := autotag.StudioScenes(studio, paths, r.Scene()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := autotag.StudioImages(studio, paths, r.Image()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := autotag.StudioGalleries(studio, paths, r.Gallery()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("error auto-tagging studio '%s': %s", studio.Name.String, err.Error())
|
return fmt.Errorf("error auto-tagging studio '%s': %s", studio.Name.String, err.Error())
|
||||||
}
|
}
|
||||||
@@ -946,7 +891,17 @@ func (s *singleton) autoTagTags(paths []string, tagIds []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.TxnManager.WithTxn(context.TODO(), func(r models.Repository) error {
|
if err := s.TxnManager.WithTxn(context.TODO(), func(r models.Repository) error {
|
||||||
return autotag.TagScenes(tag, paths, r.Scene())
|
if err := autotag.TagScenes(tag, paths, r.Scene()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := autotag.TagImages(tag, paths, r.Image()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := autotag.TagGalleries(tag, paths, r.Gallery()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("error auto-tagging tag '%s': %s", tag.Name, err.Error())
|
return fmt.Errorf("error auto-tagging tag '%s': %s", tag.Name, err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package manager
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/autotag"
|
"github.com/stashapp/stash/pkg/autotag"
|
||||||
@@ -9,6 +11,317 @@ import (
|
|||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type autoTagFilesTask struct {
|
||||||
|
paths []string
|
||||||
|
performers bool
|
||||||
|
studios bool
|
||||||
|
tags bool
|
||||||
|
|
||||||
|
txnManager models.TransactionManager
|
||||||
|
status *TaskStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *autoTagFilesTask) makeSceneFilter() *models.SceneFilterType {
|
||||||
|
ret := &models.SceneFilterType{}
|
||||||
|
or := ret
|
||||||
|
sep := string(filepath.Separator)
|
||||||
|
|
||||||
|
for _, p := range t.paths {
|
||||||
|
if !strings.HasSuffix(p, sep) {
|
||||||
|
p = p + sep
|
||||||
|
}
|
||||||
|
|
||||||
|
if ret.Path == nil {
|
||||||
|
or = ret
|
||||||
|
} else {
|
||||||
|
newOr := &models.SceneFilterType{}
|
||||||
|
or.Or = newOr
|
||||||
|
or = newOr
|
||||||
|
}
|
||||||
|
|
||||||
|
or.Path = &models.StringCriterionInput{
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
Value: p + "%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
organized := false
|
||||||
|
ret.Organized = &organized
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *autoTagFilesTask) makeImageFilter() *models.ImageFilterType {
|
||||||
|
ret := &models.ImageFilterType{}
|
||||||
|
or := ret
|
||||||
|
sep := string(filepath.Separator)
|
||||||
|
|
||||||
|
for _, p := range t.paths {
|
||||||
|
if !strings.HasSuffix(p, sep) {
|
||||||
|
p = p + sep
|
||||||
|
}
|
||||||
|
|
||||||
|
if ret.Path == nil {
|
||||||
|
or = ret
|
||||||
|
} else {
|
||||||
|
newOr := &models.ImageFilterType{}
|
||||||
|
or.Or = newOr
|
||||||
|
or = newOr
|
||||||
|
}
|
||||||
|
|
||||||
|
or.Path = &models.StringCriterionInput{
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
Value: p + "%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
organized := false
|
||||||
|
ret.Organized = &organized
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *autoTagFilesTask) makeGalleryFilter() *models.GalleryFilterType {
|
||||||
|
ret := &models.GalleryFilterType{}
|
||||||
|
or := ret
|
||||||
|
sep := string(filepath.Separator)
|
||||||
|
|
||||||
|
for _, p := range t.paths {
|
||||||
|
if !strings.HasSuffix(p, sep) {
|
||||||
|
p = p + sep
|
||||||
|
}
|
||||||
|
|
||||||
|
if ret.Path == nil {
|
||||||
|
or = ret
|
||||||
|
} else {
|
||||||
|
newOr := &models.GalleryFilterType{}
|
||||||
|
or.Or = newOr
|
||||||
|
or = newOr
|
||||||
|
}
|
||||||
|
|
||||||
|
or.Path = &models.StringCriterionInput{
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
Value: p + "%",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
organized := false
|
||||||
|
ret.Organized = &organized
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *autoTagFilesTask) getCount(r models.ReaderRepository) (int, error) {
|
||||||
|
pp := 0
|
||||||
|
findFilter := &models.FindFilterType{
|
||||||
|
PerPage: &pp,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, sceneCount, err := r.Scene().Query(t.makeSceneFilter(), findFilter)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, imageCount, err := r.Image().Query(t.makeImageFilter(), findFilter)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, galleryCount, err := r.Gallery().Query(t.makeGalleryFilter(), findFilter)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sceneCount + imageCount + galleryCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *autoTagFilesTask) batchFindFilter(batchSize int) *models.FindFilterType {
|
||||||
|
page := 1
|
||||||
|
return &models.FindFilterType{
|
||||||
|
PerPage: &batchSize,
|
||||||
|
Page: &page,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *autoTagFilesTask) processScenes(r models.ReaderRepository) error {
|
||||||
|
if t.status.stopping {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
batchSize := 1000
|
||||||
|
|
||||||
|
findFilter := t.batchFindFilter(batchSize)
|
||||||
|
sceneFilter := t.makeSceneFilter()
|
||||||
|
|
||||||
|
more := true
|
||||||
|
for more {
|
||||||
|
scenes, _, err := r.Scene().Query(sceneFilter, findFilter)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ss := range scenes {
|
||||||
|
if t.status.stopping {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tt := autoTagSceneTask{
|
||||||
|
txnManager: t.txnManager,
|
||||||
|
scene: ss,
|
||||||
|
performers: t.performers,
|
||||||
|
studios: t.studios,
|
||||||
|
tags: t.tags,
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
go tt.Start(&wg)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
t.status.incrementProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(scenes) != batchSize {
|
||||||
|
more = false
|
||||||
|
} else {
|
||||||
|
*findFilter.Page++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *autoTagFilesTask) processImages(r models.ReaderRepository) error {
|
||||||
|
if t.status.stopping {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
batchSize := 1000
|
||||||
|
|
||||||
|
findFilter := t.batchFindFilter(batchSize)
|
||||||
|
imageFilter := t.makeImageFilter()
|
||||||
|
|
||||||
|
more := true
|
||||||
|
for more {
|
||||||
|
images, _, err := r.Image().Query(imageFilter, findFilter)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ss := range images {
|
||||||
|
if t.status.stopping {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tt := autoTagImageTask{
|
||||||
|
txnManager: t.txnManager,
|
||||||
|
image: ss,
|
||||||
|
performers: t.performers,
|
||||||
|
studios: t.studios,
|
||||||
|
tags: t.tags,
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
go tt.Start(&wg)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
t.status.incrementProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(images) != batchSize {
|
||||||
|
more = false
|
||||||
|
} else {
|
||||||
|
*findFilter.Page++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *autoTagFilesTask) processGalleries(r models.ReaderRepository) error {
|
||||||
|
if t.status.stopping {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
batchSize := 1000
|
||||||
|
|
||||||
|
findFilter := t.batchFindFilter(batchSize)
|
||||||
|
galleryFilter := t.makeGalleryFilter()
|
||||||
|
|
||||||
|
more := true
|
||||||
|
for more {
|
||||||
|
galleries, _, err := r.Gallery().Query(galleryFilter, findFilter)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ss := range galleries {
|
||||||
|
if t.status.stopping {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tt := autoTagGalleryTask{
|
||||||
|
txnManager: t.txnManager,
|
||||||
|
gallery: ss,
|
||||||
|
performers: t.performers,
|
||||||
|
studios: t.studios,
|
||||||
|
tags: t.tags,
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
go tt.Start(&wg)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
t.status.incrementProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(galleries) != batchSize {
|
||||||
|
more = false
|
||||||
|
} else {
|
||||||
|
*findFilter.Page++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *autoTagFilesTask) process() {
|
||||||
|
if err := t.txnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error {
|
||||||
|
total, err := t.getCount(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t.status.total = total
|
||||||
|
|
||||||
|
logger.Infof("Starting autotag of %d files", total)
|
||||||
|
|
||||||
|
if err := t.processScenes(r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.processImages(r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.processGalleries(r); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.status.stopping {
|
||||||
|
logger.Info("Stopping due to user request")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
logger.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Finished autotag")
|
||||||
|
}
|
||||||
|
|
||||||
type autoTagSceneTask struct {
|
type autoTagSceneTask struct {
|
||||||
txnManager models.TransactionManager
|
txnManager models.TransactionManager
|
||||||
scene *models.Scene
|
scene *models.Scene
|
||||||
@@ -42,3 +355,71 @@ func (t *autoTagSceneTask) Start(wg *sync.WaitGroup) {
|
|||||||
logger.Error(err.Error())
|
logger.Error(err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type autoTagImageTask struct {
|
||||||
|
txnManager models.TransactionManager
|
||||||
|
image *models.Image
|
||||||
|
|
||||||
|
performers bool
|
||||||
|
studios bool
|
||||||
|
tags bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *autoTagImageTask) Start(wg *sync.WaitGroup) {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := t.txnManager.WithTxn(context.TODO(), func(r models.Repository) error {
|
||||||
|
if t.performers {
|
||||||
|
if err := autotag.ImagePerformers(t.image, r.Image(), r.Performer()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.studios {
|
||||||
|
if err := autotag.ImageStudios(t.image, r.Image(), r.Studio()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.tags {
|
||||||
|
if err := autotag.ImageTags(t.image, r.Image(), r.Tag()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
logger.Error(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type autoTagGalleryTask struct {
|
||||||
|
txnManager models.TransactionManager
|
||||||
|
gallery *models.Gallery
|
||||||
|
|
||||||
|
performers bool
|
||||||
|
studios bool
|
||||||
|
tags bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *autoTagGalleryTask) Start(wg *sync.WaitGroup) {
|
||||||
|
defer wg.Done()
|
||||||
|
if err := t.txnManager.WithTxn(context.TODO(), func(r models.Repository) error {
|
||||||
|
if t.performers {
|
||||||
|
if err := autotag.GalleryPerformers(t.gallery, r.Gallery(), r.Performer()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.studios {
|
||||||
|
if err := autotag.GalleryStudios(t.gallery, r.Gallery(), r.Studio()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.tags {
|
||||||
|
if err := autotag.GalleryTags(t.gallery, r.Gallery(), r.Tag()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
logger.Error(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Gallery struct {
|
type Gallery struct {
|
||||||
@@ -39,6 +40,20 @@ type GalleryPartial struct {
|
|||||||
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTitle returns the title of the scene. If the Title field is empty,
|
||||||
|
// then the base filename is returned.
|
||||||
|
func (s Gallery) GetTitle() string {
|
||||||
|
if s.Title.String != "" {
|
||||||
|
return s.Title.String
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Path.Valid {
|
||||||
|
return filepath.Base(s.Path.String)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
const DefaultGthumbWidth int = 640
|
const DefaultGthumbWidth int = 640
|
||||||
|
|
||||||
type Galleries []*Gallery
|
type Galleries []*Gallery
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Image stores the metadata for a single image.
|
// Image stores the metadata for a single image.
|
||||||
@@ -40,6 +41,16 @@ type ImagePartial struct {
|
|||||||
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTitle returns the title of the image. If the Title field is empty,
|
||||||
|
// then the base filename is returned.
|
||||||
|
func (s Image) GetTitle() string {
|
||||||
|
if s.Title.String != "" {
|
||||||
|
return s.Title.String
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Base(s.Path)
|
||||||
|
}
|
||||||
|
|
||||||
// ImageFileType represents the file metadata for an image.
|
// ImageFileType represents the file metadata for an image.
|
||||||
type ImageFileType struct {
|
type ImageFileType struct {
|
||||||
Size *int `graphql:"size" json:"size"`
|
Size *int `graphql:"size" json:"size"`
|
||||||
|
|||||||
@@ -159,7 +159,69 @@ func (qb *galleryQueryBuilder) All() ([]*models.Gallery, error) {
|
|||||||
return qb.queryGalleries(selectAll("galleries")+qb.getGallerySort(nil), nil)
|
return qb.queryGalleries(selectAll("galleries")+qb.getGallerySort(nil), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *galleryQueryBuilder) makeQuery(galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) queryBuilder {
|
func (qb *galleryQueryBuilder) validateFilter(galleryFilter *models.GalleryFilterType) error {
|
||||||
|
const and = "AND"
|
||||||
|
const or = "OR"
|
||||||
|
const not = "NOT"
|
||||||
|
|
||||||
|
if galleryFilter.And != nil {
|
||||||
|
if galleryFilter.Or != nil {
|
||||||
|
return illegalFilterCombination(and, or)
|
||||||
|
}
|
||||||
|
if galleryFilter.Not != nil {
|
||||||
|
return illegalFilterCombination(and, not)
|
||||||
|
}
|
||||||
|
|
||||||
|
return qb.validateFilter(galleryFilter.And)
|
||||||
|
}
|
||||||
|
|
||||||
|
if galleryFilter.Or != nil {
|
||||||
|
if galleryFilter.Not != nil {
|
||||||
|
return illegalFilterCombination(or, not)
|
||||||
|
}
|
||||||
|
|
||||||
|
return qb.validateFilter(galleryFilter.Or)
|
||||||
|
}
|
||||||
|
|
||||||
|
if galleryFilter.Not != nil {
|
||||||
|
return qb.validateFilter(galleryFilter.Not)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *galleryQueryBuilder) makeFilter(galleryFilter *models.GalleryFilterType) *filterBuilder {
|
||||||
|
query := &filterBuilder{}
|
||||||
|
|
||||||
|
if galleryFilter.And != nil {
|
||||||
|
query.and(qb.makeFilter(galleryFilter.And))
|
||||||
|
}
|
||||||
|
if galleryFilter.Or != nil {
|
||||||
|
query.or(qb.makeFilter(galleryFilter.Or))
|
||||||
|
}
|
||||||
|
if galleryFilter.Not != nil {
|
||||||
|
query.not(qb.makeFilter(galleryFilter.Not))
|
||||||
|
}
|
||||||
|
|
||||||
|
query.handleCriterionFunc(boolCriterionHandler(galleryFilter.IsZip, "galleries.zip"))
|
||||||
|
query.handleCriterionFunc(stringCriterionHandler(galleryFilter.Path, "galleries.path"))
|
||||||
|
query.handleCriterionFunc(intCriterionHandler(galleryFilter.Rating, "galleries.rating"))
|
||||||
|
query.handleCriterionFunc(stringCriterionHandler(galleryFilter.URL, "galleries.url"))
|
||||||
|
query.handleCriterionFunc(boolCriterionHandler(galleryFilter.Organized, "galleries.organized"))
|
||||||
|
query.handleCriterionFunc(galleryIsMissingCriterionHandler(qb, galleryFilter.IsMissing))
|
||||||
|
query.handleCriterionFunc(galleryTagsCriterionHandler(qb, galleryFilter.Tags))
|
||||||
|
query.handleCriterionFunc(galleryTagCountCriterionHandler(qb, galleryFilter.TagCount))
|
||||||
|
query.handleCriterionFunc(galleryPerformersCriterionHandler(qb, galleryFilter.Performers))
|
||||||
|
query.handleCriterionFunc(galleryPerformerCountCriterionHandler(qb, galleryFilter.PerformerCount))
|
||||||
|
query.handleCriterionFunc(galleryStudioCriterionHandler(qb, galleryFilter.Studios))
|
||||||
|
query.handleCriterionFunc(galleryPerformerTagsCriterionHandler(qb, galleryFilter.PerformerTags))
|
||||||
|
query.handleCriterionFunc(galleryAverageResolutionCriterionHandler(qb, galleryFilter.AverageResolution))
|
||||||
|
query.handleCriterionFunc(galleryImageCountCriterionHandler(qb, galleryFilter.ImageCount))
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *galleryQueryBuilder) makeQuery(galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) {
|
||||||
if galleryFilter == nil {
|
if galleryFilter == nil {
|
||||||
galleryFilter = &models.GalleryFilterType{}
|
galleryFilter = &models.GalleryFilterType{}
|
||||||
}
|
}
|
||||||
@@ -169,15 +231,7 @@ func (qb *galleryQueryBuilder) makeQuery(galleryFilter *models.GalleryFilterType
|
|||||||
|
|
||||||
query := qb.newQuery()
|
query := qb.newQuery()
|
||||||
|
|
||||||
query.body = selectDistinctIDs("galleries")
|
query.body = selectDistinctIDs(galleryTable)
|
||||||
query.body += `
|
|
||||||
left join performers_galleries as performers_join on performers_join.gallery_id = galleries.id
|
|
||||||
left join scenes_galleries as scenes_join on scenes_join.gallery_id = galleries.id
|
|
||||||
left join studios as studio on studio.id = galleries.studio_id
|
|
||||||
left join galleries_tags as tags_join on tags_join.gallery_id = galleries.id
|
|
||||||
left join galleries_images as images_join on images_join.gallery_id = galleries.id
|
|
||||||
left join images on images_join.image_id = images.id
|
|
||||||
`
|
|
||||||
|
|
||||||
if q := findFilter.Q; q != nil && *q != "" {
|
if q := findFilter.Q; q != nil && *q != "" {
|
||||||
searchColumns := []string{"galleries.title", "galleries.path", "galleries.checksum"}
|
searchColumns := []string{"galleries.title", "galleries.path", "galleries.checksum"}
|
||||||
@@ -186,110 +240,23 @@ func (qb *galleryQueryBuilder) makeQuery(galleryFilter *models.GalleryFilterType
|
|||||||
query.addArg(thisArgs...)
|
query.addArg(thisArgs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if zipFilter := galleryFilter.IsZip; zipFilter != nil {
|
if err := qb.validateFilter(galleryFilter); err != nil {
|
||||||
var favStr string
|
return nil, err
|
||||||
if *zipFilter == true {
|
|
||||||
favStr = "1"
|
|
||||||
} else {
|
|
||||||
favStr = "0"
|
|
||||||
}
|
|
||||||
query.addWhere("galleries.zip = " + favStr)
|
|
||||||
}
|
}
|
||||||
|
filter := qb.makeFilter(galleryFilter)
|
||||||
|
|
||||||
query.handleStringCriterionInput(galleryFilter.Path, "galleries.path")
|
query.addFilter(filter)
|
||||||
query.handleIntCriterionInput(galleryFilter.Rating, "galleries.rating")
|
|
||||||
query.handleStringCriterionInput(galleryFilter.URL, "galleries.url")
|
|
||||||
query.handleCountCriterion(galleryFilter.ImageCount, galleryTable, galleriesImagesTable, galleryIDColumn)
|
|
||||||
qb.handleAverageResolutionFilter(&query, galleryFilter.AverageResolution)
|
|
||||||
|
|
||||||
if Organized := galleryFilter.Organized; Organized != nil {
|
|
||||||
var organized string
|
|
||||||
if *Organized == true {
|
|
||||||
organized = "1"
|
|
||||||
} else {
|
|
||||||
organized = "0"
|
|
||||||
}
|
|
||||||
query.addWhere("galleries.organized = " + organized)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isMissingFilter := galleryFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
|
|
||||||
switch *isMissingFilter {
|
|
||||||
case "scenes":
|
|
||||||
query.addWhere("scenes_join.gallery_id IS NULL")
|
|
||||||
case "studio":
|
|
||||||
query.addWhere("galleries.studio_id IS NULL")
|
|
||||||
case "performers":
|
|
||||||
query.addWhere("performers_join.gallery_id IS NULL")
|
|
||||||
case "date":
|
|
||||||
query.addWhere("galleries.date IS \"\" OR galleries.date IS \"0001-01-01\"")
|
|
||||||
case "tags":
|
|
||||||
query.addWhere("tags_join.gallery_id IS NULL")
|
|
||||||
default:
|
|
||||||
query.addWhere("galleries." + *isMissingFilter + " IS NULL")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if tagsFilter := galleryFilter.Tags; tagsFilter != nil && len(tagsFilter.Value) > 0 {
|
|
||||||
for _, tagID := range tagsFilter.Value {
|
|
||||||
query.addArg(tagID)
|
|
||||||
}
|
|
||||||
|
|
||||||
query.body += " LEFT JOIN tags on tags_join.tag_id = tags.id"
|
|
||||||
whereClause, havingClause := getMultiCriterionClause("galleries", "tags", "galleries_tags", "gallery_id", "tag_id", tagsFilter)
|
|
||||||
query.addWhere(whereClause)
|
|
||||||
query.addHaving(havingClause)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tagCountFilter := galleryFilter.TagCount; tagCountFilter != nil {
|
|
||||||
clause, count := getCountCriterionClause(galleryTable, galleriesTagsTable, galleryIDColumn, *tagCountFilter)
|
|
||||||
|
|
||||||
if count == 1 {
|
|
||||||
query.addArg(tagCountFilter.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
query.addWhere(clause)
|
|
||||||
}
|
|
||||||
|
|
||||||
if performersFilter := galleryFilter.Performers; performersFilter != nil && len(performersFilter.Value) > 0 {
|
|
||||||
for _, performerID := range performersFilter.Value {
|
|
||||||
query.addArg(performerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
query.body += " LEFT JOIN performers ON performers_join.performer_id = performers.id"
|
|
||||||
whereClause, havingClause := getMultiCriterionClause("galleries", "performers", "performers_galleries", "gallery_id", "performer_id", performersFilter)
|
|
||||||
query.addWhere(whereClause)
|
|
||||||
query.addHaving(havingClause)
|
|
||||||
}
|
|
||||||
|
|
||||||
if performerCountFilter := galleryFilter.PerformerCount; performerCountFilter != nil {
|
|
||||||
clause, count := getCountCriterionClause(galleryTable, performersGalleriesTable, galleryIDColumn, *performerCountFilter)
|
|
||||||
|
|
||||||
if count == 1 {
|
|
||||||
query.addArg(performerCountFilter.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
query.addWhere(clause)
|
|
||||||
}
|
|
||||||
|
|
||||||
if studiosFilter := galleryFilter.Studios; studiosFilter != nil && len(studiosFilter.Value) > 0 {
|
|
||||||
for _, studioID := range studiosFilter.Value {
|
|
||||||
query.addArg(studioID)
|
|
||||||
}
|
|
||||||
|
|
||||||
whereClause, havingClause := getMultiCriterionClause("galleries", "studio", "", "", "studio_id", studiosFilter)
|
|
||||||
query.addWhere(whereClause)
|
|
||||||
query.addHaving(havingClause)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleGalleryPerformerTagsCriterion(&query, galleryFilter.PerformerTags)
|
|
||||||
|
|
||||||
query.sortAndPagination = qb.getGallerySort(findFilter) + getPagination(findFilter)
|
query.sortAndPagination = qb.getGallerySort(findFilter) + getPagination(findFilter)
|
||||||
|
|
||||||
return query
|
return &query, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) ([]*models.Gallery, int, error) {
|
func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) ([]*models.Gallery, int, error) {
|
||||||
query := qb.makeQuery(galleryFilter, findFilter)
|
query, err := qb.makeQuery(galleryFilter, findFilter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
idsResult, countResult, err := query.executeFind()
|
idsResult, countResult, err := query.executeFind()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -310,98 +277,155 @@ func (qb *galleryQueryBuilder) Query(galleryFilter *models.GalleryFilterType, fi
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (qb *galleryQueryBuilder) QueryCount(galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) (int, error) {
|
func (qb *galleryQueryBuilder) QueryCount(galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) (int, error) {
|
||||||
query := qb.makeQuery(galleryFilter, findFilter)
|
query, err := qb.makeQuery(galleryFilter, findFilter)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
return query.executeCount()
|
return query.executeCount()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *galleryQueryBuilder) handleAverageResolutionFilter(query *queryBuilder, resolutionFilter *models.ResolutionEnum) {
|
func galleryIsMissingCriterionHandler(qb *galleryQueryBuilder, isMissing *string) criterionHandlerFunc {
|
||||||
if resolutionFilter == nil {
|
return func(f *filterBuilder) {
|
||||||
return
|
if isMissing != nil && *isMissing != "" {
|
||||||
}
|
switch *isMissing {
|
||||||
|
case "scenes":
|
||||||
if resolution := resolutionFilter.String(); resolutionFilter.IsValid() {
|
f.addJoin("scenes_galleries", "scenes_join", "scenes_join.gallery_id = galleries.id")
|
||||||
var low int
|
f.addWhere("scenes_join.gallery_id IS NULL")
|
||||||
var high int
|
case "studio":
|
||||||
|
f.addWhere("galleries.studio_id IS NULL")
|
||||||
switch resolution {
|
case "performers":
|
||||||
case "VERY_LOW":
|
qb.performersRepository().join(f, "performers_join", "galleries.id")
|
||||||
high = 240
|
f.addWhere("performers_join.gallery_id IS NULL")
|
||||||
case "LOW":
|
case "date":
|
||||||
low = 240
|
f.addWhere("galleries.date IS \"\" OR galleries.date IS \"0001-01-01\"")
|
||||||
high = 360
|
case "tags":
|
||||||
case "R360P":
|
qb.tagsRepository().join(f, "tags_join", "galleries.id")
|
||||||
low = 360
|
f.addWhere("tags_join.gallery_id IS NULL")
|
||||||
high = 480
|
default:
|
||||||
case "STANDARD":
|
f.addWhere("(galleries." + *isMissing + " IS NULL OR TRIM(galleries." + *isMissing + ") = '')")
|
||||||
low = 480
|
|
||||||
high = 540
|
|
||||||
case "WEB_HD":
|
|
||||||
low = 540
|
|
||||||
high = 720
|
|
||||||
case "STANDARD_HD":
|
|
||||||
low = 720
|
|
||||||
high = 1080
|
|
||||||
case "FULL_HD":
|
|
||||||
low = 1080
|
|
||||||
high = 1440
|
|
||||||
case "QUAD_HD":
|
|
||||||
low = 1440
|
|
||||||
high = 1920
|
|
||||||
case "VR_HD":
|
|
||||||
low = 1920
|
|
||||||
high = 2160
|
|
||||||
case "FOUR_K":
|
|
||||||
low = 2160
|
|
||||||
high = 2880
|
|
||||||
case "FIVE_K":
|
|
||||||
low = 2880
|
|
||||||
high = 3384
|
|
||||||
case "SIX_K":
|
|
||||||
low = 3384
|
|
||||||
high = 4320
|
|
||||||
case "EIGHT_K":
|
|
||||||
low = 4320
|
|
||||||
}
|
|
||||||
|
|
||||||
havingClause := ""
|
|
||||||
if low != 0 {
|
|
||||||
havingClause = "avg(MIN(images.width, images.height)) >= " + strconv.Itoa(low)
|
|
||||||
}
|
|
||||||
if high != 0 {
|
|
||||||
if havingClause != "" {
|
|
||||||
havingClause += " AND "
|
|
||||||
}
|
}
|
||||||
havingClause += "avg(MIN(images.width, images.height)) < " + strconv.Itoa(high)
|
|
||||||
}
|
|
||||||
|
|
||||||
if havingClause != "" {
|
|
||||||
query.addHaving(havingClause)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleGalleryPerformerTagsCriterion(query *queryBuilder, performerTagsFilter *models.MultiCriterionInput) {
|
func (qb *galleryQueryBuilder) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder {
|
||||||
if performerTagsFilter != nil && len(performerTagsFilter.Value) > 0 {
|
return multiCriterionHandlerBuilder{
|
||||||
for _, tagID := range performerTagsFilter.Value {
|
primaryTable: galleryTable,
|
||||||
query.addArg(tagID)
|
foreignTable: foreignTable,
|
||||||
|
joinTable: joinTable,
|
||||||
|
primaryFK: galleryIDColumn,
|
||||||
|
foreignFK: foreignFK,
|
||||||
|
addJoinsFunc: addJoinsFunc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryTagsCriterionHandler(qb *galleryQueryBuilder, tags *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
|
addJoinsFunc := func(f *filterBuilder) {
|
||||||
|
qb.tagsRepository().join(f, "tags_join", "galleries.id")
|
||||||
|
f.addJoin(tagTable, "", "tags_join.tag_id = tags.id")
|
||||||
|
}
|
||||||
|
h := qb.getMultiCriterionHandlerBuilder(tagTable, galleriesTagsTable, tagIDColumn, addJoinsFunc)
|
||||||
|
|
||||||
|
return h.handler(tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryTagCountCriterionHandler(qb *galleryQueryBuilder, tagCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||||
|
h := countCriterionHandlerBuilder{
|
||||||
|
primaryTable: galleryTable,
|
||||||
|
joinTable: galleriesTagsTable,
|
||||||
|
primaryFK: galleryIDColumn,
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.handler(tagCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryPerformersCriterionHandler(qb *galleryQueryBuilder, performers *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
|
addJoinsFunc := func(f *filterBuilder) {
|
||||||
|
qb.performersRepository().join(f, "performers_join", "galleries.id")
|
||||||
|
f.addJoin(performerTable, "", "performers_join.performer_id = performers.id")
|
||||||
|
}
|
||||||
|
h := qb.getMultiCriterionHandlerBuilder(performerTable, performersGalleriesTable, performerIDColumn, addJoinsFunc)
|
||||||
|
|
||||||
|
return h.handler(performers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryPerformerCountCriterionHandler(qb *galleryQueryBuilder, performerCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||||
|
h := countCriterionHandlerBuilder{
|
||||||
|
primaryTable: galleryTable,
|
||||||
|
joinTable: performersGalleriesTable,
|
||||||
|
primaryFK: galleryIDColumn,
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.handler(performerCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryImageCountCriterionHandler(qb *galleryQueryBuilder, imageCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||||
|
h := countCriterionHandlerBuilder{
|
||||||
|
primaryTable: galleryTable,
|
||||||
|
joinTable: galleriesImagesTable,
|
||||||
|
primaryFK: galleryIDColumn,
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.handler(imageCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryStudioCriterionHandler(qb *galleryQueryBuilder, studios *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
|
addJoinsFunc := func(f *filterBuilder) {
|
||||||
|
f.addJoin(studioTable, "studio", "studio.id = galleries.studio_id")
|
||||||
|
}
|
||||||
|
h := qb.getMultiCriterionHandlerBuilder("studio", "", studioIDColumn, addJoinsFunc)
|
||||||
|
|
||||||
|
return h.handler(studios)
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryPerformerTagsCriterionHandler(qb *galleryQueryBuilder, performerTagsFilter *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
|
return func(f *filterBuilder) {
|
||||||
|
if performerTagsFilter != nil && len(performerTagsFilter.Value) > 0 {
|
||||||
|
qb.performersRepository().join(f, "performers_join", "galleries.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_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))), args...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
query.body += " LEFT JOIN performers_tags AS performer_tags_join on performers_join.performer_id = performer_tags_join.performer_id"
|
func galleryAverageResolutionCriterionHandler(qb *galleryQueryBuilder, resolution *models.ResolutionEnum) criterionHandlerFunc {
|
||||||
|
return func(f *filterBuilder) {
|
||||||
|
if resolution != nil && resolution.IsValid() {
|
||||||
|
qb.imagesRepository().join(f, "images_join", "galleries.id")
|
||||||
|
f.addJoin("images", "", "images_join.image_id = images.id")
|
||||||
|
|
||||||
if performerTagsFilter.Modifier == models.CriterionModifierIncludes {
|
min := resolution.GetMinResolution()
|
||||||
// includes any of the provided ids
|
max := resolution.GetMaxResolution()
|
||||||
query.addWhere("performer_tags_join.tag_id IN " + getInBinding(len(performerTagsFilter.Value)))
|
|
||||||
} else if performerTagsFilter.Modifier == models.CriterionModifierIncludesAll {
|
const widthHeight = "avg(MIN(images.width, images.height))"
|
||||||
// includes all of the provided ids
|
|
||||||
query.addWhere("performer_tags_join.tag_id IN " + getInBinding(len(performerTagsFilter.Value)))
|
if min > 0 {
|
||||||
query.addHaving(fmt.Sprintf("count(distinct performer_tags_join.tag_id) IS %d", len(performerTagsFilter.Value)))
|
f.addHaving(widthHeight + " >= " + strconv.Itoa(min))
|
||||||
} else if performerTagsFilter.Modifier == models.CriterionModifierExcludes {
|
}
|
||||||
query.addWhere(fmt.Sprintf(`not exists
|
|
||||||
(select performers_galleries.performer_id from performers_galleries
|
if max > 0 {
|
||||||
left join performers_tags on performers_tags.performer_id = performers_galleries.performer_id where
|
f.addHaving(widthHeight + " < " + strconv.Itoa(max))
|
||||||
performers_galleries.gallery_id = galleries.id AND
|
}
|
||||||
performers_tags.tag_id in %s)`, getInBinding(len(performerTagsFilter.Value))))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,6 +442,8 @@ func (qb *galleryQueryBuilder) getGallerySort(findFilter *models.FindFilterType)
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch sort {
|
switch sort {
|
||||||
|
case "images_count":
|
||||||
|
return getCountSort(galleryTable, galleriesImagesTable, galleryIDColumn, direction)
|
||||||
case "tag_count":
|
case "tag_count":
|
||||||
return getCountSort(galleryTable, galleriesTagsTable, galleryIDColumn, direction)
|
return getCountSort(galleryTable, galleriesTagsTable, galleryIDColumn, direction)
|
||||||
case "performer_count":
|
case "performer_count":
|
||||||
|
|||||||
@@ -193,6 +193,143 @@ func verifyGalleriesPath(t *testing.T, sqb models.GalleryReader, pathCriterion m
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGalleryQueryPathOr(t *testing.T) {
|
||||||
|
const gallery1Idx = 1
|
||||||
|
const gallery2Idx = 2
|
||||||
|
|
||||||
|
gallery1Path := getGalleryStringValue(gallery1Idx, "Path")
|
||||||
|
gallery2Path := getGalleryStringValue(gallery2Idx, "Path")
|
||||||
|
|
||||||
|
galleryFilter := models.GalleryFilterType{
|
||||||
|
Path: &models.StringCriterionInput{
|
||||||
|
Value: gallery1Path,
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
},
|
||||||
|
Or: &models.GalleryFilterType{
|
||||||
|
Path: &models.StringCriterionInput{
|
||||||
|
Value: gallery2Path,
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
sqb := r.Gallery()
|
||||||
|
|
||||||
|
galleries := queryGallery(t, sqb, &galleryFilter, nil)
|
||||||
|
|
||||||
|
assert.Len(t, galleries, 2)
|
||||||
|
assert.Equal(t, gallery1Path, galleries[0].Path.String)
|
||||||
|
assert.Equal(t, gallery2Path, galleries[1].Path.String)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGalleryQueryPathAndRating(t *testing.T) {
|
||||||
|
const galleryIdx = 1
|
||||||
|
galleryPath := getGalleryStringValue(galleryIdx, "Path")
|
||||||
|
galleryRating := getRating(galleryIdx)
|
||||||
|
|
||||||
|
galleryFilter := models.GalleryFilterType{
|
||||||
|
Path: &models.StringCriterionInput{
|
||||||
|
Value: galleryPath,
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
},
|
||||||
|
And: &models.GalleryFilterType{
|
||||||
|
Rating: &models.IntCriterionInput{
|
||||||
|
Value: int(galleryRating.Int64),
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
sqb := r.Gallery()
|
||||||
|
|
||||||
|
galleries := queryGallery(t, sqb, &galleryFilter, nil)
|
||||||
|
|
||||||
|
assert.Len(t, galleries, 1)
|
||||||
|
assert.Equal(t, galleryPath, galleries[0].Path.String)
|
||||||
|
assert.Equal(t, galleryRating.Int64, galleries[0].Rating.Int64)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGalleryQueryPathNotRating(t *testing.T) {
|
||||||
|
const galleryIdx = 1
|
||||||
|
|
||||||
|
galleryRating := getRating(galleryIdx)
|
||||||
|
|
||||||
|
pathCriterion := models.StringCriterionInput{
|
||||||
|
Value: "gallery_.*1_Path",
|
||||||
|
Modifier: models.CriterionModifierMatchesRegex,
|
||||||
|
}
|
||||||
|
|
||||||
|
ratingCriterion := models.IntCriterionInput{
|
||||||
|
Value: int(galleryRating.Int64),
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
}
|
||||||
|
|
||||||
|
galleryFilter := models.GalleryFilterType{
|
||||||
|
Path: &pathCriterion,
|
||||||
|
Not: &models.GalleryFilterType{
|
||||||
|
Rating: &ratingCriterion,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
sqb := r.Gallery()
|
||||||
|
|
||||||
|
galleries := queryGallery(t, sqb, &galleryFilter, nil)
|
||||||
|
|
||||||
|
for _, gallery := range galleries {
|
||||||
|
verifyNullString(t, gallery.Path, pathCriterion)
|
||||||
|
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
||||||
|
verifyInt64(t, gallery.Rating, ratingCriterion)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGalleryIllegalQuery(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
const galleryIdx = 1
|
||||||
|
subFilter := models.GalleryFilterType{
|
||||||
|
Path: &models.StringCriterionInput{
|
||||||
|
Value: getGalleryStringValue(galleryIdx, "Path"),
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
galleryFilter := &models.GalleryFilterType{
|
||||||
|
And: &subFilter,
|
||||||
|
Or: &subFilter,
|
||||||
|
}
|
||||||
|
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
sqb := r.Gallery()
|
||||||
|
|
||||||
|
_, _, err := sqb.Query(galleryFilter, nil)
|
||||||
|
assert.NotNil(err)
|
||||||
|
|
||||||
|
galleryFilter.Or = nil
|
||||||
|
galleryFilter.Not = &subFilter
|
||||||
|
_, _, err = sqb.Query(galleryFilter, nil)
|
||||||
|
assert.NotNil(err)
|
||||||
|
|
||||||
|
galleryFilter.And = nil
|
||||||
|
galleryFilter.Or = &subFilter
|
||||||
|
_, _, err = sqb.Query(galleryFilter, nil)
|
||||||
|
assert.NotNil(err)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestGalleryQueryURL(t *testing.T) {
|
func TestGalleryQueryURL(t *testing.T) {
|
||||||
const sceneIdx = 1
|
const sceneIdx = 1
|
||||||
galleryURL := getGalleryStringValue(sceneIdx, urlField)
|
galleryURL := getGalleryStringValue(sceneIdx, urlField)
|
||||||
@@ -712,6 +849,22 @@ func verifyGalleriesPerformerCount(t *testing.T, performerCountCriterion models.
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGalleryQueryAverageResolution(t *testing.T) {
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
qb := r.Gallery()
|
||||||
|
resolution := models.ResolutionEnumLow
|
||||||
|
galleryFilter := models.GalleryFilterType{
|
||||||
|
AverageResolution: &resolution,
|
||||||
|
}
|
||||||
|
|
||||||
|
// not verifying average - just ensure we get at least one
|
||||||
|
galleries := queryGallery(t, qb, &galleryFilter, nil)
|
||||||
|
assert.Greater(t, len(galleries), 0)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestGalleryQueryImageCount(t *testing.T) {
|
func TestGalleryQueryImageCount(t *testing.T) {
|
||||||
const imageCount = 0
|
const imageCount = 0
|
||||||
imageCountCriterion := models.IntCriterionInput{
|
imageCountCriterion := models.IntCriterionInput{
|
||||||
|
|||||||
@@ -12,35 +12,6 @@ const imageIDColumn = "image_id"
|
|||||||
const performersImagesTable = "performers_images"
|
const performersImagesTable = "performers_images"
|
||||||
const imagesTagsTable = "images_tags"
|
const imagesTagsTable = "images_tags"
|
||||||
|
|
||||||
var imagesForPerformerQuery = selectAll(imageTable) + `
|
|
||||||
LEFT JOIN performers_images as performers_join on performers_join.image_id = images.id
|
|
||||||
WHERE performers_join.performer_id = ?
|
|
||||||
GROUP BY images.id
|
|
||||||
`
|
|
||||||
|
|
||||||
var countImagesForPerformerQuery = `
|
|
||||||
SELECT performer_id FROM performers_images as performers_join
|
|
||||||
WHERE performer_id = ?
|
|
||||||
GROUP BY image_id
|
|
||||||
`
|
|
||||||
|
|
||||||
var imagesForStudioQuery = selectAll(imageTable) + `
|
|
||||||
JOIN studios ON studios.id = images.studio_id
|
|
||||||
WHERE studios.id = ?
|
|
||||||
GROUP BY images.id
|
|
||||||
`
|
|
||||||
var imagesForMovieQuery = selectAll(imageTable) + `
|
|
||||||
LEFT JOIN movies_images as movies_join on movies_join.image_id = images.id
|
|
||||||
WHERE movies_join.movie_id = ?
|
|
||||||
GROUP BY images.id
|
|
||||||
`
|
|
||||||
|
|
||||||
var countImagesForTagQuery = `
|
|
||||||
SELECT tag_id AS id FROM images_tags
|
|
||||||
WHERE images_tags.tag_id = ?
|
|
||||||
GROUP BY images_tags.image_id
|
|
||||||
`
|
|
||||||
|
|
||||||
var imagesForGalleryQuery = selectAll(imageTable) + `
|
var imagesForGalleryQuery = selectAll(imageTable) + `
|
||||||
LEFT JOIN galleries_images as galleries_join on galleries_join.image_id = images.id
|
LEFT JOIN galleries_images as galleries_join on galleries_join.image_id = images.id
|
||||||
WHERE galleries_join.gallery_id = ?
|
WHERE galleries_join.gallery_id = ?
|
||||||
@@ -216,7 +187,69 @@ func (qb *imageQueryBuilder) All() ([]*models.Image, error) {
|
|||||||
return qb.queryImages(selectAll(imageTable)+qb.getImageSort(nil), nil)
|
return qb.queryImages(selectAll(imageTable)+qb.getImageSort(nil), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *imageQueryBuilder) makeQuery(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) queryBuilder {
|
func (qb *imageQueryBuilder) validateFilter(imageFilter *models.ImageFilterType) error {
|
||||||
|
const and = "AND"
|
||||||
|
const or = "OR"
|
||||||
|
const not = "NOT"
|
||||||
|
|
||||||
|
if imageFilter.And != nil {
|
||||||
|
if imageFilter.Or != nil {
|
||||||
|
return illegalFilterCombination(and, or)
|
||||||
|
}
|
||||||
|
if imageFilter.Not != nil {
|
||||||
|
return illegalFilterCombination(and, not)
|
||||||
|
}
|
||||||
|
|
||||||
|
return qb.validateFilter(imageFilter.And)
|
||||||
|
}
|
||||||
|
|
||||||
|
if imageFilter.Or != nil {
|
||||||
|
if imageFilter.Not != nil {
|
||||||
|
return illegalFilterCombination(or, not)
|
||||||
|
}
|
||||||
|
|
||||||
|
return qb.validateFilter(imageFilter.Or)
|
||||||
|
}
|
||||||
|
|
||||||
|
if imageFilter.Not != nil {
|
||||||
|
return qb.validateFilter(imageFilter.Not)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *imageQueryBuilder) makeFilter(imageFilter *models.ImageFilterType) *filterBuilder {
|
||||||
|
query := &filterBuilder{}
|
||||||
|
|
||||||
|
if imageFilter.And != nil {
|
||||||
|
query.and(qb.makeFilter(imageFilter.And))
|
||||||
|
}
|
||||||
|
if imageFilter.Or != nil {
|
||||||
|
query.or(qb.makeFilter(imageFilter.Or))
|
||||||
|
}
|
||||||
|
if imageFilter.Not != nil {
|
||||||
|
query.not(qb.makeFilter(imageFilter.Not))
|
||||||
|
}
|
||||||
|
|
||||||
|
query.handleCriterionFunc(stringCriterionHandler(imageFilter.Path, "images.path"))
|
||||||
|
query.handleCriterionFunc(intCriterionHandler(imageFilter.Rating, "images.rating"))
|
||||||
|
query.handleCriterionFunc(intCriterionHandler(imageFilter.OCounter, "images.o_counter"))
|
||||||
|
query.handleCriterionFunc(boolCriterionHandler(imageFilter.Organized, "images.organized"))
|
||||||
|
query.handleCriterionFunc(resolutionCriterionHandler(imageFilter.Resolution, "images.height", "images.width"))
|
||||||
|
query.handleCriterionFunc(imageIsMissingCriterionHandler(qb, imageFilter.IsMissing))
|
||||||
|
|
||||||
|
query.handleCriterionFunc(imageTagsCriterionHandler(qb, imageFilter.Tags))
|
||||||
|
query.handleCriterionFunc(imageTagCountCriterionHandler(qb, imageFilter.TagCount))
|
||||||
|
query.handleCriterionFunc(imageGalleriesCriterionHandler(qb, imageFilter.Galleries))
|
||||||
|
query.handleCriterionFunc(imagePerformersCriterionHandler(qb, imageFilter.Performers))
|
||||||
|
query.handleCriterionFunc(imagePerformerCountCriterionHandler(qb, imageFilter.PerformerCount))
|
||||||
|
query.handleCriterionFunc(imageStudioCriterionHandler(qb, imageFilter.Studios))
|
||||||
|
query.handleCriterionFunc(imagePerformerTagsCriterionHandler(qb, imageFilter.PerformerTags))
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
func (qb *imageQueryBuilder) makeQuery(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) (*queryBuilder, error) {
|
||||||
if imageFilter == nil {
|
if imageFilter == nil {
|
||||||
imageFilter = &models.ImageFilterType{}
|
imageFilter = &models.ImageFilterType{}
|
||||||
}
|
}
|
||||||
@@ -227,12 +260,6 @@ func (qb *imageQueryBuilder) makeQuery(imageFilter *models.ImageFilterType, find
|
|||||||
query := qb.newQuery()
|
query := qb.newQuery()
|
||||||
|
|
||||||
query.body = selectDistinctIDs(imageTable)
|
query.body = selectDistinctIDs(imageTable)
|
||||||
query.body += `
|
|
||||||
left join performers_images as performers_join on performers_join.image_id = images.id
|
|
||||||
left join studios as studio on studio.id = images.studio_id
|
|
||||||
left join images_tags as tags_join on tags_join.image_id = images.id
|
|
||||||
left join galleries_images as galleries_join on galleries_join.image_id = images.id
|
|
||||||
`
|
|
||||||
|
|
||||||
if q := findFilter.Q; q != nil && *q != "" {
|
if q := findFilter.Q; q != nil && *q != "" {
|
||||||
searchColumns := []string{"images.title", "images.path", "images.checksum"}
|
searchColumns := []string{"images.title", "images.path", "images.checksum"}
|
||||||
@@ -241,154 +268,23 @@ func (qb *imageQueryBuilder) makeQuery(imageFilter *models.ImageFilterType, find
|
|||||||
query.addArg(thisArgs...)
|
query.addArg(thisArgs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
query.handleStringCriterionInput(imageFilter.Path, "images.path")
|
if err := qb.validateFilter(imageFilter); err != nil {
|
||||||
|
return nil, err
|
||||||
if rating := imageFilter.Rating; rating != nil {
|
|
||||||
clause, count := getIntCriterionWhereClause("images.rating", *imageFilter.Rating)
|
|
||||||
query.addWhere(clause)
|
|
||||||
if count == 1 {
|
|
||||||
query.addArg(imageFilter.Rating.Value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
filter := qb.makeFilter(imageFilter)
|
||||||
|
|
||||||
if oCounter := imageFilter.OCounter; oCounter != nil {
|
query.addFilter(filter)
|
||||||
clause, count := getIntCriterionWhereClause("images.o_counter", *imageFilter.OCounter)
|
|
||||||
query.addWhere(clause)
|
|
||||||
if count == 1 {
|
|
||||||
query.addArg(imageFilter.OCounter.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if Organized := imageFilter.Organized; Organized != nil {
|
|
||||||
var organized string
|
|
||||||
if *Organized == true {
|
|
||||||
organized = "1"
|
|
||||||
} else {
|
|
||||||
organized = "0"
|
|
||||||
}
|
|
||||||
query.addWhere("images.organized = " + organized)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resolutionFilter := imageFilter.Resolution; resolutionFilter != nil {
|
|
||||||
if resolution := resolutionFilter.String(); resolutionFilter.IsValid() {
|
|
||||||
switch resolution {
|
|
||||||
case "VERY_LOW":
|
|
||||||
query.addWhere("MIN(images.height, images.width) < 240")
|
|
||||||
case "LOW":
|
|
||||||
query.addWhere("(MIN(images.height, images.width) >= 240 AND MIN(images.height, images.width) < 360)")
|
|
||||||
case "R360P":
|
|
||||||
query.addWhere("(MIN(images.height, images.width) >= 360 AND MIN(images.height, images.width) < 480)")
|
|
||||||
case "STANDARD":
|
|
||||||
query.addWhere("(MIN(images.height, images.width) >= 480 AND MIN(images.height, images.width) < 540)")
|
|
||||||
case "WEB_HD":
|
|
||||||
query.addWhere("(MIN(images.height, images.width) >= 540 AND MIN(images.height, images.width) < 720)")
|
|
||||||
case "STANDARD_HD":
|
|
||||||
query.addWhere("(MIN(images.height, images.width) >= 720 AND MIN(images.height, images.width) < 1080)")
|
|
||||||
case "FULL_HD":
|
|
||||||
query.addWhere("(MIN(images.height, images.width) >= 1080 AND MIN(images.height, images.width) < 1440)")
|
|
||||||
case "QUAD_HD":
|
|
||||||
query.addWhere("(MIN(images.height, images.width) >= 1440 AND MIN(images.height, images.width) < 1920)")
|
|
||||||
case "VR_HD":
|
|
||||||
query.addWhere("(MIN(images.height, images.width) >= 1920 AND MIN(images.height, images.width) < 2160)")
|
|
||||||
case "FOUR_K":
|
|
||||||
query.addWhere("(MIN(images.height, images.width) >= 2160 AND MIN(images.height, images.width) < 2880)")
|
|
||||||
case "FIVE_K":
|
|
||||||
query.addWhere("(MIN(images.height, images.width) >= 2880 AND MIN(images.height, images.width) < 3384)")
|
|
||||||
case "SIX_K":
|
|
||||||
query.addWhere("(MIN(images.height, images.width) >= 3384 AND MIN(images.height, images.width) < 4320)")
|
|
||||||
case "EIGHT_K":
|
|
||||||
query.addWhere("MIN(images.height, images.width) >= 4320")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if isMissingFilter := imageFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
|
|
||||||
switch *isMissingFilter {
|
|
||||||
case "studio":
|
|
||||||
query.addWhere("images.studio_id IS NULL")
|
|
||||||
case "performers":
|
|
||||||
query.addWhere("performers_join.image_id IS NULL")
|
|
||||||
case "galleries":
|
|
||||||
query.addWhere("galleries_join.image_id IS NULL")
|
|
||||||
case "tags":
|
|
||||||
query.addWhere("tags_join.image_id IS NULL")
|
|
||||||
default:
|
|
||||||
query.addWhere("(images." + *isMissingFilter + " IS NULL OR TRIM(images." + *isMissingFilter + ") = '')")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if tagsFilter := imageFilter.Tags; tagsFilter != nil && len(tagsFilter.Value) > 0 {
|
|
||||||
for _, tagID := range tagsFilter.Value {
|
|
||||||
query.addArg(tagID)
|
|
||||||
}
|
|
||||||
|
|
||||||
query.body += " LEFT JOIN tags on tags_join.tag_id = tags.id"
|
|
||||||
whereClause, havingClause := getMultiCriterionClause("images", "tags", "images_tags", "image_id", "tag_id", tagsFilter)
|
|
||||||
query.addWhere(whereClause)
|
|
||||||
query.addHaving(havingClause)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tagCountFilter := imageFilter.TagCount; tagCountFilter != nil {
|
|
||||||
clause, count := getCountCriterionClause(imageTable, imagesTagsTable, imageIDColumn, *tagCountFilter)
|
|
||||||
|
|
||||||
if count == 1 {
|
|
||||||
query.addArg(tagCountFilter.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
query.addWhere(clause)
|
|
||||||
}
|
|
||||||
|
|
||||||
if galleriesFilter := imageFilter.Galleries; galleriesFilter != nil && len(galleriesFilter.Value) > 0 {
|
|
||||||
for _, galleryID := range galleriesFilter.Value {
|
|
||||||
query.addArg(galleryID)
|
|
||||||
}
|
|
||||||
|
|
||||||
query.body += " LEFT JOIN galleries ON galleries_join.gallery_id = galleries.id"
|
|
||||||
whereClause, havingClause := getMultiCriterionClause("images", "galleries", "galleries_images", "image_id", "gallery_id", galleriesFilter)
|
|
||||||
query.addWhere(whereClause)
|
|
||||||
query.addHaving(havingClause)
|
|
||||||
}
|
|
||||||
|
|
||||||
if performersFilter := imageFilter.Performers; performersFilter != nil && len(performersFilter.Value) > 0 {
|
|
||||||
for _, performerID := range performersFilter.Value {
|
|
||||||
query.addArg(performerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
query.body += " LEFT JOIN performers ON performers_join.performer_id = performers.id"
|
|
||||||
whereClause, havingClause := getMultiCriterionClause("images", "performers", "performers_images", "image_id", "performer_id", performersFilter)
|
|
||||||
query.addWhere(whereClause)
|
|
||||||
query.addHaving(havingClause)
|
|
||||||
}
|
|
||||||
|
|
||||||
if performerCountFilter := imageFilter.PerformerCount; performerCountFilter != nil {
|
|
||||||
clause, count := getCountCriterionClause(imageTable, performersImagesTable, imageIDColumn, *performerCountFilter)
|
|
||||||
|
|
||||||
if count == 1 {
|
|
||||||
query.addArg(performerCountFilter.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
query.addWhere(clause)
|
|
||||||
}
|
|
||||||
|
|
||||||
if studiosFilter := imageFilter.Studios; studiosFilter != nil && len(studiosFilter.Value) > 0 {
|
|
||||||
for _, studioID := range studiosFilter.Value {
|
|
||||||
query.addArg(studioID)
|
|
||||||
}
|
|
||||||
|
|
||||||
whereClause, havingClause := getMultiCriterionClause("images", "studio", "", "", "studio_id", studiosFilter)
|
|
||||||
query.addWhere(whereClause)
|
|
||||||
query.addHaving(havingClause)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleImagePerformerTagsCriterion(&query, imageFilter.PerformerTags)
|
|
||||||
|
|
||||||
query.sortAndPagination = qb.getImageSort(findFilter) + getPagination(findFilter)
|
query.sortAndPagination = qb.getImageSort(findFilter) + getPagination(findFilter)
|
||||||
|
|
||||||
return query
|
return &query, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *imageQueryBuilder) Query(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) ([]*models.Image, int, error) {
|
func (qb *imageQueryBuilder) Query(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) ([]*models.Image, int, error) {
|
||||||
query := qb.makeQuery(imageFilter, findFilter)
|
query, err := qb.makeQuery(imageFilter, findFilter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
idsResult, countResult, err := query.executeFind()
|
idsResult, countResult, err := query.executeFind()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -409,32 +305,131 @@ func (qb *imageQueryBuilder) Query(imageFilter *models.ImageFilterType, findFilt
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (qb *imageQueryBuilder) QueryCount(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) (int, error) {
|
func (qb *imageQueryBuilder) QueryCount(imageFilter *models.ImageFilterType, findFilter *models.FindFilterType) (int, error) {
|
||||||
query := qb.makeQuery(imageFilter, findFilter)
|
query, err := qb.makeQuery(imageFilter, findFilter)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
return query.executeCount()
|
return query.executeCount()
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleImagePerformerTagsCriterion(query *queryBuilder, performerTagsFilter *models.MultiCriterionInput) {
|
func imageIsMissingCriterionHandler(qb *imageQueryBuilder, isMissing *string) criterionHandlerFunc {
|
||||||
if performerTagsFilter != nil && len(performerTagsFilter.Value) > 0 {
|
return func(f *filterBuilder) {
|
||||||
for _, tagID := range performerTagsFilter.Value {
|
if isMissing != nil && *isMissing != "" {
|
||||||
query.addArg(tagID)
|
switch *isMissing {
|
||||||
|
case "studio":
|
||||||
|
f.addWhere("images.studio_id IS NULL")
|
||||||
|
case "performers":
|
||||||
|
qb.performersRepository().join(f, "performers_join", "images.id")
|
||||||
|
f.addWhere("performers_join.image_id IS NULL")
|
||||||
|
case "galleries":
|
||||||
|
qb.galleriesRepository().join(f, "galleries_join", "images.id")
|
||||||
|
f.addWhere("galleries_join.image_id IS NULL")
|
||||||
|
case "tags":
|
||||||
|
qb.tagsRepository().join(f, "tags_join", "images.id")
|
||||||
|
f.addWhere("tags_join.image_id IS NULL")
|
||||||
|
default:
|
||||||
|
f.addWhere("(images." + *isMissing + " IS NULL OR TRIM(images." + *isMissing + ") = '')")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
query.body += " LEFT JOIN performers_tags AS performer_tags_join on performers_join.performer_id = performer_tags_join.performer_id"
|
func (qb *imageQueryBuilder) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder {
|
||||||
|
return multiCriterionHandlerBuilder{
|
||||||
|
primaryTable: imageTable,
|
||||||
|
foreignTable: foreignTable,
|
||||||
|
joinTable: joinTable,
|
||||||
|
primaryFK: imageIDColumn,
|
||||||
|
foreignFK: foreignFK,
|
||||||
|
addJoinsFunc: addJoinsFunc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if performerTagsFilter.Modifier == models.CriterionModifierIncludes {
|
func imageTagsCriterionHandler(qb *imageQueryBuilder, tags *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
// includes any of the provided ids
|
addJoinsFunc := func(f *filterBuilder) {
|
||||||
query.addWhere("performer_tags_join.tag_id IN " + getInBinding(len(performerTagsFilter.Value)))
|
qb.tagsRepository().join(f, "tags_join", "images.id")
|
||||||
} else if performerTagsFilter.Modifier == models.CriterionModifierIncludesAll {
|
f.addJoin(tagTable, "", "tags_join.tag_id = tags.id")
|
||||||
// includes all of the provided ids
|
}
|
||||||
query.addWhere("performer_tags_join.tag_id IN " + getInBinding(len(performerTagsFilter.Value)))
|
h := qb.getMultiCriterionHandlerBuilder(tagTable, imagesTagsTable, tagIDColumn, addJoinsFunc)
|
||||||
query.addHaving(fmt.Sprintf("count(distinct performer_tags_join.tag_id) IS %d", len(performerTagsFilter.Value)))
|
|
||||||
} else if performerTagsFilter.Modifier == models.CriterionModifierExcludes {
|
return h.handler(tags)
|
||||||
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
|
func imageTagCountCriterionHandler(qb *imageQueryBuilder, tagCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||||
performers_images.image_id = images.id AND
|
h := countCriterionHandlerBuilder{
|
||||||
performers_tags.tag_id in %s)`, getInBinding(len(performerTagsFilter.Value))))
|
primaryTable: imageTable,
|
||||||
|
joinTable: imagesTagsTable,
|
||||||
|
primaryFK: imageIDColumn,
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.handler(tagCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageGalleriesCriterionHandler(qb *imageQueryBuilder, galleries *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
|
addJoinsFunc := func(f *filterBuilder) {
|
||||||
|
qb.galleriesRepository().join(f, "galleries_join", "images.id")
|
||||||
|
f.addJoin(galleryTable, "", "galleries_join.gallery_id = galleries.id")
|
||||||
|
}
|
||||||
|
h := qb.getMultiCriterionHandlerBuilder(galleryTable, galleriesImagesTable, galleryIDColumn, addJoinsFunc)
|
||||||
|
|
||||||
|
return h.handler(galleries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func imagePerformersCriterionHandler(qb *imageQueryBuilder, performers *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
|
addJoinsFunc := func(f *filterBuilder) {
|
||||||
|
qb.performersRepository().join(f, "performers_join", "images.id")
|
||||||
|
f.addJoin(performerTable, "", "performers_join.performer_id = performers.id")
|
||||||
|
}
|
||||||
|
h := qb.getMultiCriterionHandlerBuilder(performerTable, performersImagesTable, performerIDColumn, addJoinsFunc)
|
||||||
|
|
||||||
|
return h.handler(performers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func imagePerformerCountCriterionHandler(qb *imageQueryBuilder, performerCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||||
|
h := countCriterionHandlerBuilder{
|
||||||
|
primaryTable: imageTable,
|
||||||
|
joinTable: performersImagesTable,
|
||||||
|
primaryFK: imageIDColumn,
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.handler(performerCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageStudioCriterionHandler(qb *imageQueryBuilder, studios *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
|
addJoinsFunc := func(f *filterBuilder) {
|
||||||
|
f.addJoin(studioTable, "studio", "studio.id = images.studio_id")
|
||||||
|
}
|
||||||
|
h := qb.getMultiCriterionHandlerBuilder("studio", "", studioIDColumn, addJoinsFunc)
|
||||||
|
|
||||||
|
return h.handler(studios)
|
||||||
|
}
|
||||||
|
|
||||||
|
func imagePerformerTagsCriterionHandler(qb *imageQueryBuilder, performerTagsFilter *models.MultiCriterionInput) criterionHandlerFunc {
|
||||||
|
return func(f *filterBuilder) {
|
||||||
|
if performerTagsFilter != nil && len(performerTagsFilter.Value) > 0 {
|
||||||
|
qb.performersRepository().join(f, "performers_join", "images.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_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))), args...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,6 +155,143 @@ func verifyImagePath(t *testing.T, pathCriterion models.StringCriterionInput, ex
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestImageQueryPathOr(t *testing.T) {
|
||||||
|
const image1Idx = 1
|
||||||
|
const image2Idx = 2
|
||||||
|
|
||||||
|
image1Path := getImageStringValue(image1Idx, "Path")
|
||||||
|
image2Path := getImageStringValue(image2Idx, "Path")
|
||||||
|
|
||||||
|
imageFilter := models.ImageFilterType{
|
||||||
|
Path: &models.StringCriterionInput{
|
||||||
|
Value: image1Path,
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
},
|
||||||
|
Or: &models.ImageFilterType{
|
||||||
|
Path: &models.StringCriterionInput{
|
||||||
|
Value: image2Path,
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
sqb := r.Image()
|
||||||
|
|
||||||
|
images := queryImages(t, sqb, &imageFilter, nil)
|
||||||
|
|
||||||
|
assert.Len(t, images, 2)
|
||||||
|
assert.Equal(t, image1Path, images[0].Path)
|
||||||
|
assert.Equal(t, image2Path, images[1].Path)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImageQueryPathAndRating(t *testing.T) {
|
||||||
|
const imageIdx = 1
|
||||||
|
imagePath := getImageStringValue(imageIdx, "Path")
|
||||||
|
imageRating := getRating(imageIdx)
|
||||||
|
|
||||||
|
imageFilter := models.ImageFilterType{
|
||||||
|
Path: &models.StringCriterionInput{
|
||||||
|
Value: imagePath,
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
},
|
||||||
|
And: &models.ImageFilterType{
|
||||||
|
Rating: &models.IntCriterionInput{
|
||||||
|
Value: int(imageRating.Int64),
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
sqb := r.Image()
|
||||||
|
|
||||||
|
images := queryImages(t, sqb, &imageFilter, nil)
|
||||||
|
|
||||||
|
assert.Len(t, images, 1)
|
||||||
|
assert.Equal(t, imagePath, images[0].Path)
|
||||||
|
assert.Equal(t, imageRating.Int64, images[0].Rating.Int64)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImageQueryPathNotRating(t *testing.T) {
|
||||||
|
const imageIdx = 1
|
||||||
|
|
||||||
|
imageRating := getRating(imageIdx)
|
||||||
|
|
||||||
|
pathCriterion := models.StringCriterionInput{
|
||||||
|
Value: "image_.*1_Path",
|
||||||
|
Modifier: models.CriterionModifierMatchesRegex,
|
||||||
|
}
|
||||||
|
|
||||||
|
ratingCriterion := models.IntCriterionInput{
|
||||||
|
Value: int(imageRating.Int64),
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
}
|
||||||
|
|
||||||
|
imageFilter := models.ImageFilterType{
|
||||||
|
Path: &pathCriterion,
|
||||||
|
Not: &models.ImageFilterType{
|
||||||
|
Rating: &ratingCriterion,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
sqb := r.Image()
|
||||||
|
|
||||||
|
images := queryImages(t, sqb, &imageFilter, nil)
|
||||||
|
|
||||||
|
for _, image := range images {
|
||||||
|
verifyString(t, image.Path, pathCriterion)
|
||||||
|
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
||||||
|
verifyInt64(t, image.Rating, ratingCriterion)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImageIllegalQuery(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
const imageIdx = 1
|
||||||
|
subFilter := models.ImageFilterType{
|
||||||
|
Path: &models.StringCriterionInput{
|
||||||
|
Value: getImageStringValue(imageIdx, "Path"),
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
imageFilter := &models.ImageFilterType{
|
||||||
|
And: &subFilter,
|
||||||
|
Or: &subFilter,
|
||||||
|
}
|
||||||
|
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
sqb := r.Image()
|
||||||
|
|
||||||
|
_, _, err := sqb.Query(imageFilter, nil)
|
||||||
|
assert.NotNil(err)
|
||||||
|
|
||||||
|
imageFilter.Or = nil
|
||||||
|
imageFilter.Not = &subFilter
|
||||||
|
_, _, err = sqb.Query(imageFilter, nil)
|
||||||
|
assert.NotNil(err)
|
||||||
|
|
||||||
|
imageFilter.And = nil
|
||||||
|
imageFilter.Or = &subFilter
|
||||||
|
_, _, err = sqb.Query(imageFilter, nil)
|
||||||
|
assert.NotNil(err)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestImageQueryRating(t *testing.T) {
|
func TestImageQueryRating(t *testing.T) {
|
||||||
const rating = 3
|
const rating = 3
|
||||||
ratingCriterion := models.IntCriterionInput{
|
ratingCriterion := models.IntCriterionInput{
|
||||||
@@ -449,6 +586,70 @@ func TestImageQueryIsMissingRating(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestImageQueryGallery(t *testing.T) {
|
||||||
|
withTxn(func(r models.Repository) error {
|
||||||
|
sqb := r.Image()
|
||||||
|
galleryCriterion := models.MultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(galleryIDs[galleryIdxWithImage]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludes,
|
||||||
|
}
|
||||||
|
|
||||||
|
imageFilter := models.ImageFilterType{
|
||||||
|
Galleries: &galleryCriterion,
|
||||||
|
}
|
||||||
|
|
||||||
|
images, _, err := sqb.Query(&imageFilter, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error querying image: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Len(t, images, 1)
|
||||||
|
|
||||||
|
// ensure ids are correct
|
||||||
|
for _, image := range images {
|
||||||
|
assert.True(t, image.ID == imageIDs[imageIdxWithGallery])
|
||||||
|
}
|
||||||
|
|
||||||
|
galleryCriterion = models.MultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(galleryIDs[galleryIdx1WithImage]),
|
||||||
|
strconv.Itoa(galleryIDs[galleryIdx2WithImage]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierIncludesAll,
|
||||||
|
}
|
||||||
|
|
||||||
|
images, _, err = sqb.Query(&imageFilter, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error querying image: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Len(t, images, 1)
|
||||||
|
assert.Equal(t, imageIDs[imageIdxWithTwoGalleries], images[0].ID)
|
||||||
|
|
||||||
|
galleryCriterion = models.MultiCriterionInput{
|
||||||
|
Value: []string{
|
||||||
|
strconv.Itoa(performerIDs[galleryIdx1WithImage]),
|
||||||
|
},
|
||||||
|
Modifier: models.CriterionModifierExcludes,
|
||||||
|
}
|
||||||
|
|
||||||
|
q := getImageStringValue(imageIdxWithTwoGalleries, titleField)
|
||||||
|
findFilter := models.FindFilterType{
|
||||||
|
Q: &q,
|
||||||
|
}
|
||||||
|
|
||||||
|
images, _, err = sqb.Query(&imageFilter, &findFilter)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error querying image: %s", err.Error())
|
||||||
|
}
|
||||||
|
assert.Len(t, images, 0)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestImageQueryPerformers(t *testing.T) {
|
func TestImageQueryPerformers(t *testing.T) {
|
||||||
withTxn(func(r models.Repository) error {
|
withTxn(func(r models.Repository) error {
|
||||||
sqb := r.Image()
|
sqb := r.Image()
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ const (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
imageIdxWithGallery = iota
|
imageIdxWithGallery = iota
|
||||||
|
imageIdx1WithGallery
|
||||||
|
imageIdx2WithGallery
|
||||||
|
imageIdxWithTwoGalleries
|
||||||
imageIdxWithPerformer
|
imageIdxWithPerformer
|
||||||
imageIdx1WithPerformer
|
imageIdx1WithPerformer
|
||||||
imageIdx2WithPerformer
|
imageIdx2WithPerformer
|
||||||
@@ -102,6 +105,9 @@ const (
|
|||||||
const (
|
const (
|
||||||
galleryIdxWithScene = iota
|
galleryIdxWithScene = iota
|
||||||
galleryIdxWithImage
|
galleryIdxWithImage
|
||||||
|
galleryIdx1WithImage
|
||||||
|
galleryIdx2WithImage
|
||||||
|
galleryIdxWithTwoImages
|
||||||
galleryIdxWithPerformer
|
galleryIdxWithPerformer
|
||||||
galleryIdx1WithPerformer
|
galleryIdx1WithPerformer
|
||||||
galleryIdx2WithPerformer
|
galleryIdx2WithPerformer
|
||||||
@@ -230,6 +236,10 @@ var (
|
|||||||
var (
|
var (
|
||||||
imageGalleryLinks = [][2]int{
|
imageGalleryLinks = [][2]int{
|
||||||
{imageIdxWithGallery, galleryIdxWithImage},
|
{imageIdxWithGallery, galleryIdxWithImage},
|
||||||
|
{imageIdx1WithGallery, galleryIdxWithTwoImages},
|
||||||
|
{imageIdx2WithGallery, galleryIdxWithTwoImages},
|
||||||
|
{imageIdxWithTwoGalleries, galleryIdx1WithImage},
|
||||||
|
{imageIdxWithTwoGalleries, galleryIdx2WithImage},
|
||||||
}
|
}
|
||||||
imageStudioLinks = [][2]int{
|
imageStudioLinks = [][2]int{
|
||||||
{imageIdxWithStudio, studioIdxWithImage},
|
{imageIdxWithStudio, studioIdxWithImage},
|
||||||
@@ -513,6 +523,14 @@ func getHeight(index int) sql.NullInt64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getWidth(index int) sql.NullInt64 {
|
||||||
|
height := getHeight(index)
|
||||||
|
return sql.NullInt64{
|
||||||
|
Int64: height.Int64 * 2,
|
||||||
|
Valid: height.Valid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getSceneDate(index int) models.SQLiteDate {
|
func getSceneDate(index int) models.SQLiteDate {
|
||||||
dates := []string{"null", "", "0001-01-01", "2001-02-03"}
|
dates := []string{"null", "", "0001-01-01", "2001-02-03"}
|
||||||
date := dates[index%len(dates)]
|
date := dates[index%len(dates)]
|
||||||
@@ -571,6 +589,7 @@ func createImages(qb models.ImageReaderWriter, n int) error {
|
|||||||
Rating: getRating(i),
|
Rating: getRating(i),
|
||||||
OCounter: getOCounter(i),
|
OCounter: getOCounter(i),
|
||||||
Height: getHeight(i),
|
Height: getHeight(i),
|
||||||
|
Width: getWidth(i),
|
||||||
}
|
}
|
||||||
|
|
||||||
created, err := qb.Create(image)
|
created, err := qb.Create(image)
|
||||||
@@ -599,6 +618,7 @@ func createGalleries(gqb models.GalleryReaderWriter, n int) error {
|
|||||||
Path: models.NullString(getGalleryStringValue(i, pathField)),
|
Path: models.NullString(getGalleryStringValue(i, pathField)),
|
||||||
URL: getGalleryNullStringValue(i, urlField),
|
URL: getGalleryNullStringValue(i, urlField),
|
||||||
Checksum: getGalleryStringValue(i, checksumField),
|
Checksum: getGalleryStringValue(i, checksumField),
|
||||||
|
Rating: getRating(i),
|
||||||
}
|
}
|
||||||
|
|
||||||
created, err := gqb.Create(gallery)
|
created, err := gqb.Create(gallery)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
### ✨ New Features
|
### ✨ New Features
|
||||||
|
* Auto-tagger now tags images and galleries.
|
||||||
* Added rating field to performers and studios.
|
* Added rating field to performers and studios.
|
||||||
* Support serving UI from specific directory location.
|
* Support serving UI from specific directory location.
|
||||||
* Added details, death date, hair color, and weight to Performers.
|
* Added details, death date, hair color, and weight to Performers.
|
||||||
|
|||||||
Reference in New Issue
Block a user