mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +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 {
|
||||
AND: GalleryFilterType
|
||||
OR: GalleryFilterType
|
||||
NOT: GalleryFilterType
|
||||
|
||||
"""Filter by path"""
|
||||
path: StringCriterionInput
|
||||
"""Filter to only include galleries missing this property"""
|
||||
@@ -219,6 +223,10 @@ input TagFilterType {
|
||||
}
|
||||
|
||||
input ImageFilterType {
|
||||
AND: ImageFilterType
|
||||
OR: ImageFilterType
|
||||
NOT: ImageFilterType
|
||||
|
||||
"""Filter by path"""
|
||||
path: StringCriterionInput
|
||||
"""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 existingStudioSceneName = testName + ".dontChangeStudio.mp4"
|
||||
const existingStudioImageName = testName + ".dontChangeStudio.mp4"
|
||||
const existingStudioGalleryName = testName + ".dontChangeStudio.mp4"
|
||||
|
||||
var existingStudioID int
|
||||
|
||||
@@ -109,7 +111,7 @@ func createTag(qb models.TagWriter) error {
|
||||
|
||||
func createScenes(sqb models.SceneReaderWriter) error {
|
||||
// create the scenes
|
||||
scenePatterns, falseScenePatterns := generateScenePaths(testName)
|
||||
scenePatterns, falseScenePatterns := generateTestPaths(testName, sceneExt)
|
||||
|
||||
for _, fn := range scenePatterns {
|
||||
err := createScene(sqb, makeScene(fn, true))
|
||||
@@ -169,6 +171,130 @@ func createScene(sqb models.SceneWriter, scene *models.Scene) error {
|
||||
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 {
|
||||
t := sqlite.NewTransactionManager()
|
||||
return t.WithTxn(context.TODO(), f)
|
||||
@@ -204,6 +330,16 @@ func populateDB() error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = createImages(r.Image())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = createGalleries(r.Gallery())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
@@ -212,7 +348,7 @@ func populateDB() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestParsePerformers(t *testing.T) {
|
||||
func TestParsePerformerScenes(t *testing.T) {
|
||||
var performers []*models.Performer
|
||||
if err := withTxn(func(r models.Repository) 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
|
||||
if err := withTxn(func(r models.Repository) 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
|
||||
if err := withTxn(func(r models.Repository) error {
|
||||
var err error
|
||||
@@ -356,3 +492,293 @@ func TestParseTags(t *testing.T) {
|
||||
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
|
||||
|
||||
import (
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"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)
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
var scenes []*models.Scene
|
||||
matchingPaths, falsePaths := generateScenePaths(performerName)
|
||||
matchingPaths, falsePaths := generateTestPaths(performerName, "mp4")
|
||||
for i, p := range append(matchingPaths, falsePaths...) {
|
||||
scenes = append(scenes, &models.Scene{
|
||||
ID: i + 1,
|
||||
@@ -79,3 +79,147 @@ func testPerformerScenes(t *testing.T, performerName, expectedRegex string) {
|
||||
assert.Nil(err)
|
||||
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"
|
||||
)
|
||||
|
||||
func pathsFilter(paths []string) *models.SceneFilterType {
|
||||
func scenePathsFilter(paths []string) *models.SceneFilterType {
|
||||
if paths == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -52,7 +52,7 @@ func getMatchingScenes(name string, paths []string, sceneReader models.SceneRead
|
||||
Organized: &organized,
|
||||
}
|
||||
|
||||
filter.And = pathsFilter(paths)
|
||||
filter.And = scenePathsFilter(paths)
|
||||
|
||||
pp := models.PerPageAll
|
||||
scenes, _, err := sceneReader.Query(&filter, &models.FindFilterType{
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
const sceneExt = "mp4"
|
||||
|
||||
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
|
||||
ret = append(ret, fmt.Sprintf("%s%saaa.mp4", name, separator))
|
||||
ret = append(ret, fmt.Sprintf("aaa%s%s.mp4", separator, name))
|
||||
ret = append(ret, fmt.Sprintf("aaa%s%s%sbbb.mp4", separator, name, separator))
|
||||
ret = append(ret, fmt.Sprintf("dir/%s%saaa.mp4", name, separator))
|
||||
ret = append(ret, fmt.Sprintf("dir\\%s%saaa.mp4", name, separator))
|
||||
ret = append(ret, fmt.Sprintf("%s%saaa/dir/bbb.mp4", name, separator))
|
||||
ret = append(ret, fmt.Sprintf("%s%saaa\\dir\\bbb.mp4", name, separator))
|
||||
ret = append(ret, fmt.Sprintf("dir/%s%s/aaa.mp4", name, separator))
|
||||
ret = append(ret, fmt.Sprintf("dir\\%s%s\\aaa.mp4", name, separator))
|
||||
ret = append(ret, fmt.Sprintf("%s%saaa.%s", name, separator, ext))
|
||||
ret = append(ret, fmt.Sprintf("aaa%s%s.%s", separator, name, ext))
|
||||
ret = append(ret, fmt.Sprintf("aaa%s%s%sbbb.%s", separator, name, separator, ext))
|
||||
ret = append(ret, fmt.Sprintf("dir/%s%saaa.%s", name, separator, ext))
|
||||
ret = append(ret, fmt.Sprintf("dir\\%s%saaa.%s", name, separator, ext))
|
||||
ret = append(ret, fmt.Sprintf("%s%saaa/dir/bbb.%s", name, separator, ext))
|
||||
ret = append(ret, fmt.Sprintf("%s%saaa\\dir\\bbb.%s", name, separator, ext))
|
||||
ret = append(ret, fmt.Sprintf("dir/%s%s/aaa.%s", name, separator, ext))
|
||||
ret = append(ret, fmt.Sprintf("dir\\%s%s\\aaa.%s", name, separator, ext))
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func generateSplitNamePatterns(name, separator string) []string {
|
||||
func generateSplitNamePatterns(name, separator, ext string) []string {
|
||||
var ret []string
|
||||
splitted := strings.Split(name, " ")
|
||||
// only do this for names that are split into two
|
||||
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
|
||||
}
|
||||
|
||||
func generateFalseNamePatterns(name string, separator string) []string {
|
||||
func generateFalseNamePatterns(name string, separator, ext string) []string {
|
||||
splitted := strings.Split(name, " ")
|
||||
|
||||
var ret []string
|
||||
// only do this for names that are split into two
|
||||
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
|
||||
}
|
||||
|
||||
func generateScenePaths(testName string) (scenePatterns []string, falseScenePatterns []string) {
|
||||
func generateTestPaths(testName, ext string) (scenePatterns []string, falseScenePatterns []string) {
|
||||
separators := append(testSeparators, testEndSeparators...)
|
||||
|
||||
for _, separator := range separators {
|
||||
scenePatterns = append(scenePatterns, generateNamePatterns(testName, separator)...)
|
||||
scenePatterns = append(scenePatterns, generateNamePatterns(strings.ToLower(testName), separator)...)
|
||||
scenePatterns = append(scenePatterns, generateNamePatterns(strings.ReplaceAll(testName, " ", ""), separator)...)
|
||||
falseScenePatterns = append(falseScenePatterns, generateFalseNamePatterns(testName, separator)...)
|
||||
scenePatterns = append(scenePatterns, generateNamePatterns(testName, separator, ext)...)
|
||||
scenePatterns = append(scenePatterns, generateNamePatterns(strings.ToLower(testName), separator, ext)...)
|
||||
scenePatterns = append(scenePatterns, generateNamePatterns(strings.ReplaceAll(testName, " ", ""), separator, ext)...)
|
||||
falseScenePatterns = append(falseScenePatterns, generateFalseNamePatterns(testName, separator, ext)...)
|
||||
}
|
||||
|
||||
// add test cases for intra-name separators
|
||||
for _, separator := range testSeparators {
|
||||
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
|
||||
falseScenePatterns = append(falseScenePatterns, fmt.Sprintf("aaa%s.mp4", testName))
|
||||
falseScenePatterns = append(falseScenePatterns, fmt.Sprintf("%saaa.mp4", testName))
|
||||
falseScenePatterns = append(falseScenePatterns, fmt.Sprintf("aaa%s.%s", testName, ext))
|
||||
falseScenePatterns = append(falseScenePatterns, fmt.Sprintf("%saaa.%s", testName, ext))
|
||||
|
||||
// add path separator false scenarios
|
||||
falseScenePatterns = append(falseScenePatterns, generateFalseNamePatterns(testName, "/")...)
|
||||
falseScenePatterns = append(falseScenePatterns, generateFalseNamePatterns(testName, "\\")...)
|
||||
falseScenePatterns = append(falseScenePatterns, generateFalseNamePatterns(testName, "/", ext)...)
|
||||
falseScenePatterns = append(falseScenePatterns, generateFalseNamePatterns(testName, "\\", ext)...)
|
||||
|
||||
// split patterns only valid for ._- and whitespace
|
||||
for _, separator := range testSeparators {
|
||||
scenePatterns = append(scenePatterns, generateSplitNamePatterns(testName, separator)...)
|
||||
scenePatterns = append(scenePatterns, generateSplitNamePatterns(testName, separator, ext)...)
|
||||
}
|
||||
|
||||
// false patterns for other separators
|
||||
for _, separator := range testEndSeparators {
|
||||
falseScenePatterns = append(falseScenePatterns, generateSplitNamePatterns(testName, separator)...)
|
||||
falseScenePatterns = append(falseScenePatterns, generateSplitNamePatterns(testName, separator, ext)...)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type pathTestTable struct {
|
||||
ScenePath string
|
||||
Path string
|
||||
Matches bool
|
||||
}
|
||||
|
||||
func generateTestTable(testName string) []pathTestTable {
|
||||
func generateTestTable(testName, ext string) []pathTestTable {
|
||||
var ret []pathTestTable
|
||||
|
||||
var scenePatterns []string
|
||||
@@ -116,14 +118,14 @@ func generateTestTable(testName string) []pathTestTable {
|
||||
separators := append(testSeparators, testEndSeparators...)
|
||||
|
||||
for _, separator := range separators {
|
||||
scenePatterns = append(scenePatterns, generateNamePatterns(testName, separator)...)
|
||||
scenePatterns = append(scenePatterns, generateNamePatterns(strings.ToLower(testName), separator)...)
|
||||
falseScenePatterns = append(falseScenePatterns, generateFalseNamePatterns(testName, separator)...)
|
||||
scenePatterns = append(scenePatterns, generateNamePatterns(testName, separator, ext)...)
|
||||
scenePatterns = append(scenePatterns, generateNamePatterns(strings.ToLower(testName), separator, ext)...)
|
||||
falseScenePatterns = append(falseScenePatterns, generateFalseNamePatterns(testName, separator, ext)...)
|
||||
}
|
||||
|
||||
for _, p := range scenePatterns {
|
||||
t := pathTestTable{
|
||||
ScenePath: p,
|
||||
Path: p,
|
||||
Matches: true,
|
||||
}
|
||||
|
||||
@@ -132,7 +134,7 @@ func generateTestTable(testName string) []pathTestTable {
|
||||
|
||||
for _, p := range falseScenePatterns {
|
||||
t := pathTestTable{
|
||||
ScenePath: p,
|
||||
Path: p,
|
||||
Matches: false,
|
||||
}
|
||||
|
||||
@@ -158,7 +160,7 @@ func TestScenePerformers(t *testing.T) {
|
||||
Name: models.NullString(reversedPerformerName),
|
||||
}
|
||||
|
||||
testTables := generateTestTable(performerName)
|
||||
testTables := generateTestTable(performerName, sceneExt)
|
||||
|
||||
assert := assert.New(t)
|
||||
|
||||
@@ -175,7 +177,7 @@ func TestScenePerformers(t *testing.T) {
|
||||
|
||||
scene := models.Scene{
|
||||
ID: sceneID,
|
||||
Path: test.ScenePath,
|
||||
Path: test.Path,
|
||||
}
|
||||
err := ScenePerformers(&scene, mockSceneReader, mockPerformerReader)
|
||||
|
||||
@@ -201,7 +203,7 @@ func TestSceneStudios(t *testing.T) {
|
||||
Name: models.NullString(reversedStudioName),
|
||||
}
|
||||
|
||||
testTables := generateTestTable(studioName)
|
||||
testTables := generateTestTable(studioName, sceneExt)
|
||||
|
||||
assert := assert.New(t)
|
||||
|
||||
@@ -222,7 +224,7 @@ func TestSceneStudios(t *testing.T) {
|
||||
|
||||
scene := models.Scene{
|
||||
ID: sceneID,
|
||||
Path: test.ScenePath,
|
||||
Path: test.Path,
|
||||
}
|
||||
err := SceneStudios(&scene, mockSceneReader, mockStudioReader)
|
||||
|
||||
@@ -248,7 +250,7 @@ func TestSceneTags(t *testing.T) {
|
||||
Name: reversedTagName,
|
||||
}
|
||||
|
||||
testTables := generateTestTable(tagName)
|
||||
testTables := generateTestTable(tagName, sceneExt)
|
||||
|
||||
assert := assert.New(t)
|
||||
|
||||
@@ -265,7 +267,7 @@ func TestSceneTags(t *testing.T) {
|
||||
|
||||
scene := models.Scene{
|
||||
ID: sceneID,
|
||||
Path: test.ScenePath,
|
||||
Path: test.Path,
|
||||
}
|
||||
err := SceneTags(&scene, mockSceneReader, mockTagReader)
|
||||
|
||||
|
||||
@@ -48,6 +48,54 @@ func addSceneStudio(sceneWriter models.SceneReaderWriter, sceneID, studioID int)
|
||||
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 {
|
||||
return tagger{
|
||||
ID: p.ID,
|
||||
@@ -64,3 +112,21 @@ func StudioScenes(p *models.Studio, paths []string, rw models.SceneReaderWriter)
|
||||
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
|
||||
|
||||
var scenes []*models.Scene
|
||||
matchingPaths, falsePaths := generateScenePaths(studioName)
|
||||
matchingPaths, falsePaths := generateTestPaths(studioName, sceneExt)
|
||||
for i, p := range append(matchingPaths, falsePaths...) {
|
||||
scenes = append(scenes, &models.Scene{
|
||||
ID: i + 1,
|
||||
@@ -83,3 +83,155 @@ func testStudioScenes(t *testing.T, studioName, expectedRegex string) {
|
||||
assert.Nil(err)
|
||||
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
|
||||
|
||||
import (
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"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)
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
var scenes []*models.Scene
|
||||
matchingPaths, falsePaths := generateScenePaths(tagName)
|
||||
matchingPaths, falsePaths := generateTestPaths(tagName, "mp4")
|
||||
for i, p := range append(matchingPaths, falsePaths...) {
|
||||
scenes = append(scenes, &models.Scene{
|
||||
ID: i + 1,
|
||||
@@ -79,3 +79,147 @@ func testTagScenes(t *testing.T, tagName, expectedRegex string) {
|
||||
assert.Nil(err)
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
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) {
|
||||
return qb.Update(models.ImagePartial{
|
||||
@@ -8,3 +11,43 @@ func UpdateFileModTime(qb models.ImageWriter, id int, modTime models.NullSQLiteT
|
||||
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"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -650,7 +648,7 @@ func (s *singleton) AutoTag(input models.AutoTagMetadataInput) {
|
||||
|
||||
if s.isFileBasedAutoTag(input) {
|
||||
// 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 {
|
||||
// doing specific performer/studio/tag auto-tag
|
||||
s.autoTagSpecific(input)
|
||||
@@ -658,90 +656,17 @@ func (s *singleton) AutoTag(input models.AutoTagMetadataInput) {
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *singleton) autoTagScenes(paths []string, performers, studios, tags bool) {
|
||||
if err := s.TxnManager.WithReadTxn(context.TODO(), func(r models.ReaderRepository) error {
|
||||
ret := &models.SceneFilterType{}
|
||||
or := ret
|
||||
sep := string(filepath.Separator)
|
||||
|
||||
for _, p := range 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
|
||||
|
||||
// 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,
|
||||
func (s *singleton) autoTagFiles(paths []string, performers, studios, tags bool) {
|
||||
t := autoTagFilesTask{
|
||||
paths: paths,
|
||||
performers: performers,
|
||||
studios: studios,
|
||||
tags: tags,
|
||||
txnManager: s.TxnManager,
|
||||
status: &s.Status,
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -838,7 +763,17 @@ func (s *singleton) autoTagPerformers(paths []string, performerIds []string) {
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
return fmt.Errorf("error auto-tagging tag '%s': %s", tag.Name, err.Error())
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/stashapp/stash/pkg/autotag"
|
||||
@@ -9,6 +11,317 @@ import (
|
||||
"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 {
|
||||
txnManager models.TransactionManager
|
||||
scene *models.Scene
|
||||
@@ -42,3 +355,71 @@ func (t *autoTagSceneTask) Start(wg *sync.WaitGroup) {
|
||||
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 (
|
||||
"database/sql"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Gallery struct {
|
||||
@@ -39,6 +40,20 @@ type GalleryPartial struct {
|
||||
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
|
||||
|
||||
type Galleries []*Gallery
|
||||
|
||||
@@ -2,6 +2,7 @@ package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Image stores the metadata for a single image.
|
||||
@@ -40,6 +41,16 @@ type ImagePartial struct {
|
||||
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.
|
||||
type ImageFileType struct {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
galleryFilter = &models.GalleryFilterType{}
|
||||
}
|
||||
@@ -169,15 +231,7 @@ func (qb *galleryQueryBuilder) makeQuery(galleryFilter *models.GalleryFilterType
|
||||
|
||||
query := qb.newQuery()
|
||||
|
||||
query.body = selectDistinctIDs("galleries")
|
||||
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
|
||||
`
|
||||
query.body = selectDistinctIDs(galleryTable)
|
||||
|
||||
if q := findFilter.Q; q != nil && *q != "" {
|
||||
searchColumns := []string{"galleries.title", "galleries.path", "galleries.checksum"}
|
||||
@@ -186,110 +240,23 @@ func (qb *galleryQueryBuilder) makeQuery(galleryFilter *models.GalleryFilterType
|
||||
query.addArg(thisArgs...)
|
||||
}
|
||||
|
||||
if zipFilter := galleryFilter.IsZip; zipFilter != nil {
|
||||
var favStr string
|
||||
if *zipFilter == true {
|
||||
favStr = "1"
|
||||
} else {
|
||||
favStr = "0"
|
||||
}
|
||||
query.addWhere("galleries.zip = " + favStr)
|
||||
if err := qb.validateFilter(galleryFilter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filter := qb.makeFilter(galleryFilter)
|
||||
|
||||
query.handleStringCriterionInput(galleryFilter.Path, "galleries.path")
|
||||
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.addFilter(filter)
|
||||
|
||||
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) {
|
||||
query := qb.makeQuery(galleryFilter, findFilter)
|
||||
query, err := qb.makeQuery(galleryFilter, findFilter)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
idsResult, countResult, err := query.executeFind()
|
||||
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) {
|
||||
query := qb.makeQuery(galleryFilter, findFilter)
|
||||
query, err := qb.makeQuery(galleryFilter, findFilter)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return query.executeCount()
|
||||
}
|
||||
|
||||
func (qb *galleryQueryBuilder) handleAverageResolutionFilter(query *queryBuilder, resolutionFilter *models.ResolutionEnum) {
|
||||
if resolutionFilter == nil {
|
||||
return
|
||||
func galleryIsMissingCriterionHandler(qb *galleryQueryBuilder, isMissing *string) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
if isMissing != nil && *isMissing != "" {
|
||||
switch *isMissing {
|
||||
case "scenes":
|
||||
f.addJoin("scenes_galleries", "scenes_join", "scenes_join.gallery_id = galleries.id")
|
||||
f.addWhere("scenes_join.gallery_id IS NULL")
|
||||
case "studio":
|
||||
f.addWhere("galleries.studio_id IS NULL")
|
||||
case "performers":
|
||||
qb.performersRepository().join(f, "performers_join", "galleries.id")
|
||||
f.addWhere("performers_join.gallery_id IS NULL")
|
||||
case "date":
|
||||
f.addWhere("galleries.date IS \"\" OR galleries.date IS \"0001-01-01\"")
|
||||
case "tags":
|
||||
qb.tagsRepository().join(f, "tags_join", "galleries.id")
|
||||
f.addWhere("tags_join.gallery_id IS NULL")
|
||||
default:
|
||||
f.addWhere("(galleries." + *isMissing + " IS NULL OR TRIM(galleries." + *isMissing + ") = '')")
|
||||
}
|
||||
|
||||
if resolution := resolutionFilter.String(); resolutionFilter.IsValid() {
|
||||
var low int
|
||||
var high int
|
||||
|
||||
switch resolution {
|
||||
case "VERY_LOW":
|
||||
high = 240
|
||||
case "LOW":
|
||||
low = 240
|
||||
high = 360
|
||||
case "R360P":
|
||||
low = 360
|
||||
high = 480
|
||||
case "STANDARD":
|
||||
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) {
|
||||
if performerTagsFilter != nil && len(performerTagsFilter.Value) > 0 {
|
||||
for _, tagID := range performerTagsFilter.Value {
|
||||
query.addArg(tagID)
|
||||
func (qb *galleryQueryBuilder) getMultiCriterionHandlerBuilder(foreignTable, joinTable, foreignFK string, addJoinsFunc func(f *filterBuilder)) multiCriterionHandlerBuilder {
|
||||
return multiCriterionHandlerBuilder{
|
||||
primaryTable: galleryTable,
|
||||
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,
|
||||
}
|
||||
|
||||
query.body += " LEFT JOIN performers_tags AS performer_tags_join on performers_join.performer_id = performer_tags_join.performer_id"
|
||||
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
|
||||
query.addWhere("performer_tags_join.tag_id IN " + getInBinding(len(performerTagsFilter.Value)))
|
||||
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
|
||||
query.addWhere("performer_tags_join.tag_id IN " + getInBinding(len(performerTagsFilter.Value)))
|
||||
query.addHaving(fmt.Sprintf("count(distinct performer_tags_join.tag_id) IS %d", len(performerTagsFilter.Value)))
|
||||
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 {
|
||||
query.addWhere(fmt.Sprintf(`not exists
|
||||
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))))
|
||||
performers_tags.tag_id in %s)`, getInBinding(len(performerTagsFilter.Value))), args...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
min := resolution.GetMinResolution()
|
||||
max := resolution.GetMaxResolution()
|
||||
|
||||
const widthHeight = "avg(MIN(images.width, images.height))"
|
||||
|
||||
if min > 0 {
|
||||
f.addHaving(widthHeight + " >= " + strconv.Itoa(min))
|
||||
}
|
||||
|
||||
if max > 0 {
|
||||
f.addHaving(widthHeight + " < " + strconv.Itoa(max))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,6 +442,8 @@ func (qb *galleryQueryBuilder) getGallerySort(findFilter *models.FindFilterType)
|
||||
}
|
||||
|
||||
switch sort {
|
||||
case "images_count":
|
||||
return getCountSort(galleryTable, galleriesImagesTable, galleryIDColumn, direction)
|
||||
case "tag_count":
|
||||
return getCountSort(galleryTable, galleriesTagsTable, galleryIDColumn, direction)
|
||||
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) {
|
||||
const sceneIdx = 1
|
||||
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) {
|
||||
const imageCount = 0
|
||||
imageCountCriterion := models.IntCriterionInput{
|
||||
|
||||
@@ -12,35 +12,6 @@ const imageIDColumn = "image_id"
|
||||
const performersImagesTable = "performers_images"
|
||||
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) + `
|
||||
LEFT JOIN galleries_images as galleries_join on galleries_join.image_id = images.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)
|
||||
}
|
||||
|
||||
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 {
|
||||
imageFilter = &models.ImageFilterType{}
|
||||
}
|
||||
@@ -227,12 +260,6 @@ func (qb *imageQueryBuilder) makeQuery(imageFilter *models.ImageFilterType, find
|
||||
query := qb.newQuery()
|
||||
|
||||
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 != "" {
|
||||
searchColumns := []string{"images.title", "images.path", "images.checksum"}
|
||||
@@ -241,154 +268,23 @@ func (qb *imageQueryBuilder) makeQuery(imageFilter *models.ImageFilterType, find
|
||||
query.addArg(thisArgs...)
|
||||
}
|
||||
|
||||
query.handleStringCriterionInput(imageFilter.Path, "images.path")
|
||||
if err := qb.validateFilter(imageFilter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filter := qb.makeFilter(imageFilter)
|
||||
|
||||
if rating := imageFilter.Rating; rating != nil {
|
||||
clause, count := getIntCriterionWhereClause("images.rating", *imageFilter.Rating)
|
||||
query.addWhere(clause)
|
||||
if count == 1 {
|
||||
query.addArg(imageFilter.Rating.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if oCounter := imageFilter.OCounter; oCounter != nil {
|
||||
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.addFilter(filter)
|
||||
|
||||
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) {
|
||||
query := qb.makeQuery(imageFilter, findFilter)
|
||||
query, err := qb.makeQuery(imageFilter, findFilter)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
idsResult, countResult, err := query.executeFind()
|
||||
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) {
|
||||
query := qb.makeQuery(imageFilter, findFilter)
|
||||
query, err := qb.makeQuery(imageFilter, findFilter)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return query.executeCount()
|
||||
}
|
||||
|
||||
func handleImagePerformerTagsCriterion(query *queryBuilder, performerTagsFilter *models.MultiCriterionInput) {
|
||||
if performerTagsFilter != nil && len(performerTagsFilter.Value) > 0 {
|
||||
for _, tagID := range performerTagsFilter.Value {
|
||||
query.addArg(tagID)
|
||||
func imageIsMissingCriterionHandler(qb *imageQueryBuilder, isMissing *string) criterionHandlerFunc {
|
||||
return func(f *filterBuilder) {
|
||||
if isMissing != nil && *isMissing != "" {
|
||||
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 + ") = '')")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
func imageTagsCriterionHandler(qb *imageQueryBuilder, tags *models.MultiCriterionInput) criterionHandlerFunc {
|
||||
addJoinsFunc := func(f *filterBuilder) {
|
||||
qb.tagsRepository().join(f, "tags_join", "images.id")
|
||||
f.addJoin(tagTable, "", "tags_join.tag_id = tags.id")
|
||||
}
|
||||
h := qb.getMultiCriterionHandlerBuilder(tagTable, imagesTagsTable, tagIDColumn, addJoinsFunc)
|
||||
|
||||
return h.handler(tags)
|
||||
}
|
||||
|
||||
func imageTagCountCriterionHandler(qb *imageQueryBuilder, tagCount *models.IntCriterionInput) criterionHandlerFunc {
|
||||
h := countCriterionHandlerBuilder{
|
||||
primaryTable: imageTable,
|
||||
joinTable: imagesTagsTable,
|
||||
primaryFK: imageIDColumn,
|
||||
}
|
||||
|
||||
query.body += " LEFT JOIN performers_tags AS performer_tags_join on performers_join.performer_id = performer_tags_join.performer_id"
|
||||
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
|
||||
query.addWhere("performer_tags_join.tag_id IN " + getInBinding(len(performerTagsFilter.Value)))
|
||||
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
|
||||
query.addWhere("performer_tags_join.tag_id IN " + getInBinding(len(performerTagsFilter.Value)))
|
||||
query.addHaving(fmt.Sprintf("count(distinct performer_tags_join.tag_id) IS %d", len(performerTagsFilter.Value)))
|
||||
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 {
|
||||
query.addWhere(fmt.Sprintf(`not exists
|
||||
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))))
|
||||
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) {
|
||||
const rating = 3
|
||||
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) {
|
||||
withTxn(func(r models.Repository) error {
|
||||
sqb := r.Image()
|
||||
|
||||
@@ -48,6 +48,9 @@ const (
|
||||
|
||||
const (
|
||||
imageIdxWithGallery = iota
|
||||
imageIdx1WithGallery
|
||||
imageIdx2WithGallery
|
||||
imageIdxWithTwoGalleries
|
||||
imageIdxWithPerformer
|
||||
imageIdx1WithPerformer
|
||||
imageIdx2WithPerformer
|
||||
@@ -102,6 +105,9 @@ const (
|
||||
const (
|
||||
galleryIdxWithScene = iota
|
||||
galleryIdxWithImage
|
||||
galleryIdx1WithImage
|
||||
galleryIdx2WithImage
|
||||
galleryIdxWithTwoImages
|
||||
galleryIdxWithPerformer
|
||||
galleryIdx1WithPerformer
|
||||
galleryIdx2WithPerformer
|
||||
@@ -230,6 +236,10 @@ var (
|
||||
var (
|
||||
imageGalleryLinks = [][2]int{
|
||||
{imageIdxWithGallery, galleryIdxWithImage},
|
||||
{imageIdx1WithGallery, galleryIdxWithTwoImages},
|
||||
{imageIdx2WithGallery, galleryIdxWithTwoImages},
|
||||
{imageIdxWithTwoGalleries, galleryIdx1WithImage},
|
||||
{imageIdxWithTwoGalleries, galleryIdx2WithImage},
|
||||
}
|
||||
imageStudioLinks = [][2]int{
|
||||
{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 {
|
||||
dates := []string{"null", "", "0001-01-01", "2001-02-03"}
|
||||
date := dates[index%len(dates)]
|
||||
@@ -571,6 +589,7 @@ func createImages(qb models.ImageReaderWriter, n int) error {
|
||||
Rating: getRating(i),
|
||||
OCounter: getOCounter(i),
|
||||
Height: getHeight(i),
|
||||
Width: getWidth(i),
|
||||
}
|
||||
|
||||
created, err := qb.Create(image)
|
||||
@@ -599,6 +618,7 @@ func createGalleries(gqb models.GalleryReaderWriter, n int) error {
|
||||
Path: models.NullString(getGalleryStringValue(i, pathField)),
|
||||
URL: getGalleryNullStringValue(i, urlField),
|
||||
Checksum: getGalleryStringValue(i, checksumField),
|
||||
Rating: getRating(i),
|
||||
}
|
||||
|
||||
created, err := gqb.Create(gallery)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
### ✨ New Features
|
||||
* Auto-tagger now tags images and galleries.
|
||||
* Added rating field to performers and studios.
|
||||
* Support serving UI from specific directory location.
|
||||
* Added details, death date, hair color, and weight to Performers.
|
||||
|
||||
Reference in New Issue
Block a user