Merge branch 'develop' into v0.13.1-to-develop

This commit is contained in:
WithoutPants 2022-03-16 16:57:52 +11:00 committed by GitHub
commit 984eba83bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 754 additions and 314 deletions

View file

@ -221,10 +221,14 @@ func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source models.Scr
return nil, fmt.Errorf("%w: sceneID is not an integer: '%s'", ErrInput, *input.SceneID)
}
c, err = r.scraperCache().ScrapeID(ctx, *source.ScraperID, sceneID, models.ScrapeContentTypeScene)
content = []models.ScrapedContent{c}
if c != nil {
content = []models.ScrapedContent{c}
}
case input.SceneInput != nil:
c, err = r.scraperCache().ScrapeFragment(ctx, *source.ScraperID, scraper.Input{Scene: input.SceneInput})
content = []models.ScrapedContent{c}
if c != nil {
content = []models.ScrapedContent{c}
}
case input.Query != nil:
content, err = r.scraperCache().ScrapeName(ctx, *source.ScraperID, *input.Query, models.ScrapeContentTypeScene)
default:

View file

@ -12,7 +12,7 @@ func marshalScrapedScenes(content []models.ScrapedContent) ([]*models.ScrapedSce
var ret []*models.ScrapedScene
for _, c := range content {
if c == nil {
ret = append(ret, nil)
// graphql schema requires scenes to be non-nil
continue
}
@ -35,7 +35,7 @@ func marshalScrapedPerformers(content []models.ScrapedContent) ([]*models.Scrape
var ret []*models.ScrapedPerformer
for _, c := range content {
if c == nil {
ret = append(ret, nil)
// graphql schema requires performers to be non-nil
continue
}
@ -58,7 +58,7 @@ func marshalScrapedGalleries(content []models.ScrapedContent) ([]*models.Scraped
var ret []*models.ScrapedGallery
for _, c := range content {
if c == nil {
ret = append(ret, nil)
// graphql schema requires galleries to be non-nil
continue
}
@ -81,7 +81,7 @@ func marshalScrapedMovies(content []models.ScrapedContent) ([]*models.ScrapedMov
var ret []*models.ScrapedMovie
for _, c := range content {
if c == nil {
ret = append(ret, nil)
// graphql schema requires movies to be non-nil
continue
}

View file

@ -206,13 +206,15 @@ func Start(uiBox embed.FS, loginUIBox embed.FS) {
}
if ext == ".html" || ext == "" {
themeColor := c.GetThemeColor()
data, err := uiBox.ReadFile(uiRootDir + "/index.html")
if err != nil {
panic(err)
}
prefix := getProxyPrefix(r.Header)
baseURLIndex := strings.ReplaceAll(string(data), "/%BASE_URL%", prefix)
baseURLIndex := strings.ReplaceAll(string(data), "%COLOR%", themeColor)
baseURLIndex = strings.ReplaceAll(baseURLIndex, "/%BASE_URL%", prefix)
baseURLIndex = strings.Replace(baseURLIndex, "base href=\"/\"", fmt.Sprintf("base href=\"%s\"", prefix+"/"), 1)
_, _ = w.Write([]byte(baseURLIndex))
} else {

View file

@ -2,21 +2,23 @@ package autotag
import (
"github.com/stashapp/stash/pkg/gallery"
"github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/pkg/models"
)
func getGalleryFileTagger(s *models.Gallery) tagger {
func getGalleryFileTagger(s *models.Gallery, cache *match.Cache) tagger {
return tagger{
ID: s.ID,
Type: "gallery",
Name: s.GetTitle(),
Path: s.Path.String,
ID: s.ID,
Type: "gallery",
Name: s.GetTitle(),
Path: s.Path.String,
cache: cache,
}
}
// 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)
func GalleryPerformers(s *models.Gallery, rw models.GalleryReaderWriter, performerReader models.PerformerReader, cache *match.Cache) error {
t := getGalleryFileTagger(s, cache)
return t.tagPerformers(performerReader, func(subjectID, otherID int) (bool, error) {
return gallery.AddPerformer(rw, subjectID, otherID)
@ -26,13 +28,13 @@ func GalleryPerformers(s *models.Gallery, rw models.GalleryReaderWriter, perform
// 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 {
func GalleryStudios(s *models.Gallery, rw models.GalleryReaderWriter, studioReader models.StudioReader, cache *match.Cache) error {
if s.StudioID.Valid {
// don't modify
return nil
}
t := getGalleryFileTagger(s)
t := getGalleryFileTagger(s, cache)
return t.tagStudios(studioReader, func(subjectID, otherID int) (bool, error) {
return addGalleryStudio(rw, subjectID, otherID)
@ -40,8 +42,8 @@ func GalleryStudios(s *models.Gallery, rw models.GalleryReaderWriter, studioRead
}
// 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)
func GalleryTags(s *models.Gallery, rw models.GalleryReaderWriter, tagReader models.TagReader, cache *match.Cache) error {
t := getGalleryFileTagger(s, cache)
return t.tagTags(tagReader, func(subjectID, otherID int) (bool, error) {
return gallery.AddTag(rw, subjectID, otherID)

View file

@ -37,6 +37,7 @@ func TestGalleryPerformers(t *testing.T) {
mockPerformerReader := &mocks.PerformerReaderWriter{}
mockGalleryReader := &mocks.GalleryReaderWriter{}
mockPerformerReader.On("Query", mock.Anything, mock.Anything).Return(nil, 0, nil)
mockPerformerReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Performer{&performer, &reversedPerformer}, nil).Once()
if test.Matches {
@ -48,7 +49,7 @@ func TestGalleryPerformers(t *testing.T) {
ID: galleryID,
Path: models.NullString(test.Path),
}
err := GalleryPerformers(&gallery, mockGalleryReader, mockPerformerReader)
err := GalleryPerformers(&gallery, mockGalleryReader, mockPerformerReader, nil)
assert.Nil(err)
mockPerformerReader.AssertExpectations(t)
@ -92,7 +93,7 @@ func TestGalleryStudios(t *testing.T) {
ID: galleryID,
Path: models.NullString(test.Path),
}
err := GalleryStudios(&gallery, mockGalleryReader, mockStudioReader)
err := GalleryStudios(&gallery, mockGalleryReader, mockStudioReader, nil)
assert.Nil(err)
mockStudioReader.AssertExpectations(t)
@ -103,6 +104,7 @@ func TestGalleryStudios(t *testing.T) {
mockStudioReader := &mocks.StudioReaderWriter{}
mockGalleryReader := &mocks.GalleryReaderWriter{}
mockStudioReader.On("Query", mock.Anything, mock.Anything).Return(nil, 0, nil)
mockStudioReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Studio{&studio, &reversedStudio}, nil).Once()
mockStudioReader.On("GetAliases", mock.Anything).Return([]string{}, nil).Maybe()
@ -117,6 +119,7 @@ func TestGalleryStudios(t *testing.T) {
mockStudioReader := &mocks.StudioReaderWriter{}
mockGalleryReader := &mocks.GalleryReaderWriter{}
mockStudioReader.On("Query", mock.Anything, mock.Anything).Return(nil, 0, nil)
mockStudioReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Studio{&studio, &reversedStudio}, nil).Once()
mockStudioReader.On("GetAliases", studioID).Return([]string{
studioName,
@ -159,7 +162,7 @@ func TestGalleryTags(t *testing.T) {
ID: galleryID,
Path: models.NullString(test.Path),
}
err := GalleryTags(&gallery, mockGalleryReader, mockTagReader)
err := GalleryTags(&gallery, mockGalleryReader, mockTagReader, nil)
assert.Nil(err)
mockTagReader.AssertExpectations(t)
@ -170,6 +173,7 @@ func TestGalleryTags(t *testing.T) {
mockTagReader := &mocks.TagReaderWriter{}
mockGalleryReader := &mocks.GalleryReaderWriter{}
mockTagReader.On("Query", mock.Anything, mock.Anything).Return(nil, 0, nil)
mockTagReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Tag{&tag, &reversedTag}, nil).Once()
mockTagReader.On("GetAliases", mock.Anything).Return([]string{}, nil).Maybe()
@ -183,6 +187,7 @@ func TestGalleryTags(t *testing.T) {
mockTagReader := &mocks.TagReaderWriter{}
mockGalleryReader := &mocks.GalleryReaderWriter{}
mockTagReader.On("Query", mock.Anything, mock.Anything).Return(nil, 0, nil)
mockTagReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Tag{&tag, &reversedTag}, nil).Once()
mockTagReader.On("GetAliases", tagID).Return([]string{
tagName,

View file

@ -2,21 +2,23 @@ package autotag
import (
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/pkg/models"
)
func getImageFileTagger(s *models.Image) tagger {
func getImageFileTagger(s *models.Image, cache *match.Cache) tagger {
return tagger{
ID: s.ID,
Type: "image",
Name: s.GetTitle(),
Path: s.Path,
ID: s.ID,
Type: "image",
Name: s.GetTitle(),
Path: s.Path,
cache: cache,
}
}
// 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)
func ImagePerformers(s *models.Image, rw models.ImageReaderWriter, performerReader models.PerformerReader, cache *match.Cache) error {
t := getImageFileTagger(s, cache)
return t.tagPerformers(performerReader, func(subjectID, otherID int) (bool, error) {
return image.AddPerformer(rw, subjectID, otherID)
@ -26,13 +28,13 @@ func ImagePerformers(s *models.Image, rw models.ImageReaderWriter, performerRead
// 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 {
func ImageStudios(s *models.Image, rw models.ImageReaderWriter, studioReader models.StudioReader, cache *match.Cache) error {
if s.StudioID.Valid {
// don't modify
return nil
}
t := getImageFileTagger(s)
t := getImageFileTagger(s, cache)
return t.tagStudios(studioReader, func(subjectID, otherID int) (bool, error) {
return addImageStudio(rw, subjectID, otherID)
@ -40,8 +42,8 @@ func ImageStudios(s *models.Image, rw models.ImageReaderWriter, studioReader mod
}
// 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)
func ImageTags(s *models.Image, rw models.ImageReaderWriter, tagReader models.TagReader, cache *match.Cache) error {
t := getImageFileTagger(s, cache)
return t.tagTags(tagReader, func(subjectID, otherID int) (bool, error) {
return image.AddTag(rw, subjectID, otherID)

View file

@ -37,6 +37,7 @@ func TestImagePerformers(t *testing.T) {
mockPerformerReader := &mocks.PerformerReaderWriter{}
mockImageReader := &mocks.ImageReaderWriter{}
mockPerformerReader.On("Query", mock.Anything, mock.Anything).Return(nil, 0, nil)
mockPerformerReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Performer{&performer, &reversedPerformer}, nil).Once()
if test.Matches {
@ -48,7 +49,7 @@ func TestImagePerformers(t *testing.T) {
ID: imageID,
Path: test.Path,
}
err := ImagePerformers(&image, mockImageReader, mockPerformerReader)
err := ImagePerformers(&image, mockImageReader, mockPerformerReader, nil)
assert.Nil(err)
mockPerformerReader.AssertExpectations(t)
@ -92,7 +93,7 @@ func TestImageStudios(t *testing.T) {
ID: imageID,
Path: test.Path,
}
err := ImageStudios(&image, mockImageReader, mockStudioReader)
err := ImageStudios(&image, mockImageReader, mockStudioReader, nil)
assert.Nil(err)
mockStudioReader.AssertExpectations(t)
@ -103,6 +104,7 @@ func TestImageStudios(t *testing.T) {
mockStudioReader := &mocks.StudioReaderWriter{}
mockImageReader := &mocks.ImageReaderWriter{}
mockStudioReader.On("Query", mock.Anything, mock.Anything).Return(nil, 0, nil)
mockStudioReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Studio{&studio, &reversedStudio}, nil).Once()
mockStudioReader.On("GetAliases", mock.Anything).Return([]string{}, nil).Maybe()
@ -117,6 +119,7 @@ func TestImageStudios(t *testing.T) {
mockStudioReader := &mocks.StudioReaderWriter{}
mockImageReader := &mocks.ImageReaderWriter{}
mockStudioReader.On("Query", mock.Anything, mock.Anything).Return(nil, 0, nil)
mockStudioReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Studio{&studio, &reversedStudio}, nil).Once()
mockStudioReader.On("GetAliases", studioID).Return([]string{
studioName,
@ -159,7 +162,7 @@ func TestImageTags(t *testing.T) {
ID: imageID,
Path: test.Path,
}
err := ImageTags(&image, mockImageReader, mockTagReader)
err := ImageTags(&image, mockImageReader, mockTagReader, nil)
assert.Nil(err)
mockTagReader.AssertExpectations(t)
@ -170,6 +173,7 @@ func TestImageTags(t *testing.T) {
mockTagReader := &mocks.TagReaderWriter{}
mockImageReader := &mocks.ImageReaderWriter{}
mockTagReader.On("Query", mock.Anything, mock.Anything).Return(nil, 0, nil)
mockTagReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Tag{&tag, &reversedTag}, nil).Once()
mockTagReader.On("GetAliases", mock.Anything).Return([]string{}, nil).Maybe()
@ -184,6 +188,7 @@ func TestImageTags(t *testing.T) {
mockTagReader := &mocks.TagReaderWriter{}
mockImageReader := &mocks.ImageReaderWriter{}
mockTagReader.On("Query", mock.Anything, mock.Anything).Return(nil, 0, nil)
mockTagReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Tag{&tag, &reversedTag}, nil).Once()
mockTagReader.On("GetAliases", tagID).Return([]string{
tagName,

View file

@ -361,7 +361,7 @@ func TestParsePerformerScenes(t *testing.T) {
for _, p := range performers {
if err := withTxn(func(r models.Repository) error {
return PerformerScenes(p, nil, r.Scene())
return PerformerScenes(p, nil, r.Scene(), nil)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@ -413,7 +413,7 @@ func TestParseStudioScenes(t *testing.T) {
return err
}
return StudioScenes(s, nil, aliases, r.Scene())
return StudioScenes(s, nil, aliases, r.Scene(), nil)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@ -469,7 +469,7 @@ func TestParseTagScenes(t *testing.T) {
return err
}
return TagScenes(s, nil, aliases, r.Scene())
return TagScenes(s, nil, aliases, r.Scene(), nil)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@ -516,7 +516,7 @@ func TestParsePerformerImages(t *testing.T) {
for _, p := range performers {
if err := withTxn(func(r models.Repository) error {
return PerformerImages(p, nil, r.Image())
return PerformerImages(p, nil, r.Image(), nil)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@ -568,7 +568,7 @@ func TestParseStudioImages(t *testing.T) {
return err
}
return StudioImages(s, nil, aliases, r.Image())
return StudioImages(s, nil, aliases, r.Image(), nil)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@ -624,7 +624,7 @@ func TestParseTagImages(t *testing.T) {
return err
}
return TagImages(s, nil, aliases, r.Image())
return TagImages(s, nil, aliases, r.Image(), nil)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@ -671,7 +671,7 @@ func TestParsePerformerGalleries(t *testing.T) {
for _, p := range performers {
if err := withTxn(func(r models.Repository) error {
return PerformerGalleries(p, nil, r.Gallery())
return PerformerGalleries(p, nil, r.Gallery(), nil)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@ -723,7 +723,7 @@ func TestParseStudioGalleries(t *testing.T) {
return err
}
return StudioGalleries(s, nil, aliases, r.Gallery())
return StudioGalleries(s, nil, aliases, r.Gallery(), nil)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}
@ -779,7 +779,7 @@ func TestParseTagGalleries(t *testing.T) {
return err
}
return TagGalleries(s, nil, aliases, r.Gallery())
return TagGalleries(s, nil, aliases, r.Gallery(), nil)
}); err != nil {
t.Errorf("Error auto-tagging performers: %s", err)
}

View file

@ -3,21 +3,23 @@ package autotag
import (
"github.com/stashapp/stash/pkg/gallery"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scene"
)
func getPerformerTagger(p *models.Performer) tagger {
func getPerformerTagger(p *models.Performer, cache *match.Cache) tagger {
return tagger{
ID: p.ID,
Type: "performer",
Name: p.Name.String,
ID: p.ID,
Type: "performer",
Name: p.Name.String,
cache: cache,
}
}
// PerformerScenes searches for scenes whose path matches the provided performer name and tags the scene with the performer.
func PerformerScenes(p *models.Performer, paths []string, rw models.SceneReaderWriter) error {
t := getPerformerTagger(p)
func PerformerScenes(p *models.Performer, paths []string, rw models.SceneReaderWriter, cache *match.Cache) error {
t := getPerformerTagger(p, cache)
return t.tagScenes(paths, rw, func(subjectID, otherID int) (bool, error) {
return scene.AddPerformer(rw, otherID, subjectID)
@ -25,8 +27,8 @@ func PerformerScenes(p *models.Performer, paths []string, rw models.SceneReaderW
}
// 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)
func PerformerImages(p *models.Performer, paths []string, rw models.ImageReaderWriter, cache *match.Cache) error {
t := getPerformerTagger(p, cache)
return t.tagImages(paths, rw, func(subjectID, otherID int) (bool, error) {
return image.AddPerformer(rw, otherID, subjectID)
@ -34,8 +36,8 @@ func PerformerImages(p *models.Performer, paths []string, rw models.ImageReaderW
}
// 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)
func PerformerGalleries(p *models.Performer, paths []string, rw models.GalleryReaderWriter, cache *match.Cache) error {
t := getPerformerTagger(p, cache)
return t.tagGalleries(paths, rw, func(subjectID, otherID int) (bool, error) {
return gallery.AddPerformer(rw, otherID, subjectID)

View file

@ -21,15 +21,15 @@ func TestPerformerScenes(t *testing.T) {
performerNames := []test{
{
"performer name",
`(?i)(?:^|_|[^\w\d])performer[.\-_ ]*name(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])performer[.\-_ ]*name(?:$|_|[^\p{L}\d])`,
},
{
"performer + name",
`(?i)(?:^|_|[^\w\d])performer[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])performer[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\p{L}\d])`,
},
{
`performer + name\`,
`(?i)(?:^|_|[^\w\d])performer[.\-_ ]*\+[.\-_ ]*name\\(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])performer[.\-_ ]*\+[.\-_ ]*name\\(?:$|_|[^\p{L}\d])`,
},
}
@ -81,7 +81,7 @@ func testPerformerScenes(t *testing.T, performerName, expectedRegex string) {
mockSceneReader.On("UpdatePerformers", sceneID, []int{performerID}).Return(nil).Once()
}
err := PerformerScenes(&performer, nil, mockSceneReader)
err := PerformerScenes(&performer, nil, mockSceneReader, nil)
assert := assert.New(t)
@ -100,11 +100,11 @@ func TestPerformerImages(t *testing.T) {
performerNames := []test{
{
"performer name",
`(?i)(?:^|_|[^\w\d])performer[.\-_ ]*name(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])performer[.\-_ ]*name(?:$|_|[^\p{L}\d])`,
},
{
"performer + name",
`(?i)(?:^|_|[^\w\d])performer[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])performer[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\p{L}\d])`,
},
}
@ -156,7 +156,7 @@ func testPerformerImages(t *testing.T, performerName, expectedRegex string) {
mockImageReader.On("UpdatePerformers", imageID, []int{performerID}).Return(nil).Once()
}
err := PerformerImages(&performer, nil, mockImageReader)
err := PerformerImages(&performer, nil, mockImageReader, nil)
assert := assert.New(t)
@ -175,11 +175,11 @@ func TestPerformerGalleries(t *testing.T) {
performerNames := []test{
{
"performer name",
`(?i)(?:^|_|[^\w\d])performer[.\-_ ]*name(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])performer[.\-_ ]*name(?:$|_|[^\p{L}\d])`,
},
{
"performer + name",
`(?i)(?:^|_|[^\w\d])performer[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])performer[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\p{L}\d])`,
},
}
@ -230,7 +230,7 @@ func testPerformerGalleries(t *testing.T, performerName, expectedRegex string) {
mockGalleryReader.On("UpdatePerformers", galleryID, []int{performerID}).Return(nil).Once()
}
err := PerformerGalleries(&performer, nil, mockGalleryReader)
err := PerformerGalleries(&performer, nil, mockGalleryReader, nil)
assert := assert.New(t)

View file

@ -1,22 +1,24 @@
package autotag
import (
"github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scene"
)
func getSceneFileTagger(s *models.Scene) tagger {
func getSceneFileTagger(s *models.Scene, cache *match.Cache) tagger {
return tagger{
ID: s.ID,
Type: "scene",
Name: s.GetTitle(),
Path: s.Path,
ID: s.ID,
Type: "scene",
Name: s.GetTitle(),
Path: s.Path,
cache: cache,
}
}
// ScenePerformers tags the provided scene with performers whose name matches the scene's path.
func ScenePerformers(s *models.Scene, rw models.SceneReaderWriter, performerReader models.PerformerReader) error {
t := getSceneFileTagger(s)
func ScenePerformers(s *models.Scene, rw models.SceneReaderWriter, performerReader models.PerformerReader, cache *match.Cache) error {
t := getSceneFileTagger(s, cache)
return t.tagPerformers(performerReader, func(subjectID, otherID int) (bool, error) {
return scene.AddPerformer(rw, subjectID, otherID)
@ -26,13 +28,13 @@ func ScenePerformers(s *models.Scene, rw models.SceneReaderWriter, performerRead
// SceneStudios tags the provided scene with the first studio whose name matches the scene's path.
//
// Scenes will not be tagged if studio is already set.
func SceneStudios(s *models.Scene, rw models.SceneReaderWriter, studioReader models.StudioReader) error {
func SceneStudios(s *models.Scene, rw models.SceneReaderWriter, studioReader models.StudioReader, cache *match.Cache) error {
if s.StudioID.Valid {
// don't modify
return nil
}
t := getSceneFileTagger(s)
t := getSceneFileTagger(s, cache)
return t.tagStudios(studioReader, func(subjectID, otherID int) (bool, error) {
return addSceneStudio(rw, subjectID, otherID)
@ -40,8 +42,8 @@ func SceneStudios(s *models.Scene, rw models.SceneReaderWriter, studioReader mod
}
// SceneTags tags the provided scene with tags whose name matches the scene's path.
func SceneTags(s *models.Scene, rw models.SceneReaderWriter, tagReader models.TagReader) error {
t := getSceneFileTagger(s)
func SceneTags(s *models.Scene, rw models.SceneReaderWriter, tagReader models.TagReader, cache *match.Cache) error {
t := getSceneFileTagger(s, cache)
return t.tagTags(tagReader, func(subjectID, otherID int) (bool, error) {
return scene.AddTag(rw, subjectID, otherID)

View file

@ -172,6 +172,7 @@ func TestScenePerformers(t *testing.T) {
mockPerformerReader := &mocks.PerformerReaderWriter{}
mockSceneReader := &mocks.SceneReaderWriter{}
mockPerformerReader.On("Query", mock.Anything, mock.Anything).Return(nil, 0, nil)
mockPerformerReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Performer{&performer, &reversedPerformer}, nil).Once()
if test.Matches {
@ -183,7 +184,7 @@ func TestScenePerformers(t *testing.T) {
ID: sceneID,
Path: test.Path,
}
err := ScenePerformers(&scene, mockSceneReader, mockPerformerReader)
err := ScenePerformers(&scene, mockSceneReader, mockPerformerReader, nil)
assert.Nil(err)
mockPerformerReader.AssertExpectations(t)
@ -227,7 +228,7 @@ func TestSceneStudios(t *testing.T) {
ID: sceneID,
Path: test.Path,
}
err := SceneStudios(&scene, mockSceneReader, mockStudioReader)
err := SceneStudios(&scene, mockSceneReader, mockStudioReader, nil)
assert.Nil(err)
mockStudioReader.AssertExpectations(t)
@ -238,6 +239,7 @@ func TestSceneStudios(t *testing.T) {
mockStudioReader := &mocks.StudioReaderWriter{}
mockSceneReader := &mocks.SceneReaderWriter{}
mockStudioReader.On("Query", mock.Anything, mock.Anything).Return(nil, 0, nil)
mockStudioReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Studio{&studio, &reversedStudio}, nil).Once()
mockStudioReader.On("GetAliases", mock.Anything).Return([]string{}, nil).Maybe()
@ -252,6 +254,7 @@ func TestSceneStudios(t *testing.T) {
mockStudioReader := &mocks.StudioReaderWriter{}
mockSceneReader := &mocks.SceneReaderWriter{}
mockStudioReader.On("Query", mock.Anything, mock.Anything).Return(nil, 0, nil)
mockStudioReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Studio{&studio, &reversedStudio}, nil).Once()
mockStudioReader.On("GetAliases", studioID).Return([]string{
studioName,
@ -294,7 +297,7 @@ func TestSceneTags(t *testing.T) {
ID: sceneID,
Path: test.Path,
}
err := SceneTags(&scene, mockSceneReader, mockTagReader)
err := SceneTags(&scene, mockSceneReader, mockTagReader, nil)
assert.Nil(err)
mockTagReader.AssertExpectations(t)
@ -305,6 +308,7 @@ func TestSceneTags(t *testing.T) {
mockTagReader := &mocks.TagReaderWriter{}
mockSceneReader := &mocks.SceneReaderWriter{}
mockTagReader.On("Query", mock.Anything, mock.Anything).Return(nil, 0, nil)
mockTagReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Tag{&tag, &reversedTag}, nil).Once()
mockTagReader.On("GetAliases", mock.Anything).Return([]string{}, nil).Maybe()
@ -319,6 +323,7 @@ func TestSceneTags(t *testing.T) {
mockTagReader := &mocks.TagReaderWriter{}
mockSceneReader := &mocks.SceneReaderWriter{}
mockTagReader.On("Query", mock.Anything, mock.Anything).Return(nil, 0, nil)
mockTagReader.On("QueryForAutoTag", mock.Anything).Return([]*models.Tag{&tag, &reversedTag}, nil).Once()
mockTagReader.On("GetAliases", tagID).Return([]string{
tagName,

View file

@ -3,6 +3,7 @@ package autotag
import (
"database/sql"
"github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/pkg/models"
)
@ -78,11 +79,12 @@ func addGalleryStudio(galleryWriter models.GalleryReaderWriter, galleryID, studi
return true, nil
}
func getStudioTagger(p *models.Studio, aliases []string) []tagger {
func getStudioTagger(p *models.Studio, aliases []string, cache *match.Cache) []tagger {
ret := []tagger{{
ID: p.ID,
Type: "studio",
Name: p.Name.String,
ID: p.ID,
Type: "studio",
Name: p.Name.String,
cache: cache,
}}
for _, a := range aliases {
@ -97,8 +99,8 @@ func getStudioTagger(p *models.Studio, aliases []string) []tagger {
}
// StudioScenes searches for scenes whose path matches the provided studio name and tags the scene with the studio, if studio is not already set on the scene.
func StudioScenes(p *models.Studio, paths []string, aliases []string, rw models.SceneReaderWriter) error {
t := getStudioTagger(p, aliases)
func StudioScenes(p *models.Studio, paths []string, aliases []string, rw models.SceneReaderWriter, cache *match.Cache) error {
t := getStudioTagger(p, aliases, cache)
for _, tt := range t {
if err := tt.tagScenes(paths, rw, func(subjectID, otherID int) (bool, error) {
@ -112,8 +114,8 @@ func StudioScenes(p *models.Studio, paths []string, aliases []string, rw models.
}
// 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, aliases []string, rw models.ImageReaderWriter) error {
t := getStudioTagger(p, aliases)
func StudioImages(p *models.Studio, paths []string, aliases []string, rw models.ImageReaderWriter, cache *match.Cache) error {
t := getStudioTagger(p, aliases, cache)
for _, tt := range t {
if err := tt.tagImages(paths, rw, func(subjectID, otherID int) (bool, error) {
@ -127,8 +129,8 @@ func StudioImages(p *models.Studio, paths []string, aliases []string, rw models.
}
// 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, aliases []string, rw models.GalleryReaderWriter) error {
t := getStudioTagger(p, aliases)
func StudioGalleries(p *models.Studio, paths []string, aliases []string, rw models.GalleryReaderWriter, cache *match.Cache) error {
t := getStudioTagger(p, aliases, cache)
for _, tt := range t {
if err := tt.tagGalleries(paths, rw, func(subjectID, otherID int) (bool, error) {

View file

@ -20,39 +20,39 @@ type testStudioCase struct {
var testStudioCases = []testStudioCase{
{
"studio name",
`(?i)(?:^|_|[^\w\d])studio[.\-_ ]*name(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])studio[.\-_ ]*name(?:$|_|[^\p{L}\d])`,
"",
"",
},
{
"studio + name",
`(?i)(?:^|_|[^\w\d])studio[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])studio[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\p{L}\d])`,
"",
"",
},
{
`studio + name\`,
`(?i)(?:^|_|[^\w\d])studio[.\-_ ]*\+[.\-_ ]*name\\(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])studio[.\-_ ]*\+[.\-_ ]*name\\(?:$|_|[^\p{L}\d])`,
"",
"",
},
{
"studio name",
`(?i)(?:^|_|[^\w\d])studio[.\-_ ]*name(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])studio[.\-_ ]*name(?:$|_|[^\p{L}\d])`,
"alias name",
`(?i)(?:^|_|[^\w\d])alias[.\-_ ]*name(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])alias[.\-_ ]*name(?:$|_|[^\p{L}\d])`,
},
{
"studio + name",
`(?i)(?:^|_|[^\w\d])studio[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])studio[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\p{L}\d])`,
"alias + name",
`(?i)(?:^|_|[^\w\d])alias[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])alias[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\p{L}\d])`,
},
{
`studio + name\`,
`(?i)(?:^|_|[^\w\d])studio[.\-_ ]*\+[.\-_ ]*name\\(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])studio[.\-_ ]*\+[.\-_ ]*name\\(?:$|_|[^\p{L}\d])`,
`alias + name\`,
`(?i)(?:^|_|[^\w\d])alias[.\-_ ]*\+[.\-_ ]*name\\(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])alias[.\-_ ]*\+[.\-_ ]*name\\(?:$|_|[^\p{L}\d])`,
},
}
@ -142,7 +142,7 @@ func testStudioScenes(t *testing.T, tc testStudioCase) {
}).Return(nil, nil).Once()
}
err := StudioScenes(&studio, nil, aliases, mockSceneReader)
err := StudioScenes(&studio, nil, aliases, mockSceneReader, nil)
assert := assert.New(t)
@ -234,7 +234,7 @@ func testStudioImages(t *testing.T, tc testStudioCase) {
}).Return(nil, nil).Once()
}
err := StudioImages(&studio, nil, aliases, mockImageReader)
err := StudioImages(&studio, nil, aliases, mockImageReader, nil)
assert := assert.New(t)
@ -324,7 +324,7 @@ func testStudioGalleries(t *testing.T, tc testStudioCase) {
}).Return(nil, nil).Once()
}
err := StudioGalleries(&studio, nil, aliases, mockGalleryReader)
err := StudioGalleries(&studio, nil, aliases, mockGalleryReader, nil)
assert := assert.New(t)

View file

@ -3,22 +3,25 @@ package autotag
import (
"github.com/stashapp/stash/pkg/gallery"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scene"
)
func getTagTaggers(p *models.Tag, aliases []string) []tagger {
func getTagTaggers(p *models.Tag, aliases []string, cache *match.Cache) []tagger {
ret := []tagger{{
ID: p.ID,
Type: "tag",
Name: p.Name,
ID: p.ID,
Type: "tag",
Name: p.Name,
cache: cache,
}}
for _, a := range aliases {
ret = append(ret, tagger{
ID: p.ID,
Type: "tag",
Name: a,
ID: p.ID,
Type: "tag",
Name: a,
cache: cache,
})
}
@ -26,8 +29,8 @@ func getTagTaggers(p *models.Tag, aliases []string) []tagger {
}
// TagScenes searches for scenes whose path matches the provided tag name and tags the scene with the tag.
func TagScenes(p *models.Tag, paths []string, aliases []string, rw models.SceneReaderWriter) error {
t := getTagTaggers(p, aliases)
func TagScenes(p *models.Tag, paths []string, aliases []string, rw models.SceneReaderWriter, cache *match.Cache) error {
t := getTagTaggers(p, aliases, cache)
for _, tt := range t {
if err := tt.tagScenes(paths, rw, func(subjectID, otherID int) (bool, error) {
@ -40,8 +43,8 @@ func TagScenes(p *models.Tag, paths []string, aliases []string, rw models.SceneR
}
// 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, aliases []string, rw models.ImageReaderWriter) error {
t := getTagTaggers(p, aliases)
func TagImages(p *models.Tag, paths []string, aliases []string, rw models.ImageReaderWriter, cache *match.Cache) error {
t := getTagTaggers(p, aliases, cache)
for _, tt := range t {
if err := tt.tagImages(paths, rw, func(subjectID, otherID int) (bool, error) {
@ -54,8 +57,8 @@ func TagImages(p *models.Tag, paths []string, aliases []string, rw models.ImageR
}
// 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, aliases []string, rw models.GalleryReaderWriter) error {
t := getTagTaggers(p, aliases)
func TagGalleries(p *models.Tag, paths []string, aliases []string, rw models.GalleryReaderWriter, cache *match.Cache) error {
t := getTagTaggers(p, aliases, cache)
for _, tt := range t {
if err := tt.tagGalleries(paths, rw, func(subjectID, otherID int) (bool, error) {

View file

@ -20,39 +20,39 @@ type testTagCase struct {
var testTagCases = []testTagCase{
{
"tag name",
`(?i)(?:^|_|[^\w\d])tag[.\-_ ]*name(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])tag[.\-_ ]*name(?:$|_|[^\p{L}\d])`,
"",
"",
},
{
"tag + name",
`(?i)(?:^|_|[^\w\d])tag[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])tag[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\p{L}\d])`,
"",
"",
},
{
`tag + name\`,
`(?i)(?:^|_|[^\w\d])tag[.\-_ ]*\+[.\-_ ]*name\\(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])tag[.\-_ ]*\+[.\-_ ]*name\\(?:$|_|[^\p{L}\d])`,
"",
"",
},
{
"tag name",
`(?i)(?:^|_|[^\w\d])tag[.\-_ ]*name(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])tag[.\-_ ]*name(?:$|_|[^\p{L}\d])`,
"alias name",
`(?i)(?:^|_|[^\w\d])alias[.\-_ ]*name(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])alias[.\-_ ]*name(?:$|_|[^\p{L}\d])`,
},
{
"tag + name",
`(?i)(?:^|_|[^\w\d])tag[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])tag[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\p{L}\d])`,
"alias + name",
`(?i)(?:^|_|[^\w\d])alias[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])alias[.\-_ ]*\+[.\-_ ]*name(?:$|_|[^\p{L}\d])`,
},
{
`tag + name\`,
`(?i)(?:^|_|[^\w\d])tag[.\-_ ]*\+[.\-_ ]*name\\(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])tag[.\-_ ]*\+[.\-_ ]*name\\(?:$|_|[^\p{L}\d])`,
`alias + name\`,
`(?i)(?:^|_|[^\w\d])alias[.\-_ ]*\+[.\-_ ]*name\\(?:$|_|[^\w\d])`,
`(?i)(?:^|_|[^\p{L}\d])alias[.\-_ ]*\+[.\-_ ]*name\\(?:$|_|[^\p{L}\d])`,
},
}
@ -137,7 +137,7 @@ func testTagScenes(t *testing.T, tc testTagCase) {
mockSceneReader.On("UpdateTags", sceneID, []int{tagID}).Return(nil).Once()
}
err := TagScenes(&tag, nil, aliases, mockSceneReader)
err := TagScenes(&tag, nil, aliases, mockSceneReader, nil)
assert := assert.New(t)
@ -225,7 +225,7 @@ func testTagImages(t *testing.T, tc testTagCase) {
mockImageReader.On("UpdateTags", imageID, []int{tagID}).Return(nil).Once()
}
err := TagImages(&tag, nil, aliases, mockImageReader)
err := TagImages(&tag, nil, aliases, mockImageReader, nil)
assert := assert.New(t)
@ -312,7 +312,7 @@ func testTagGalleries(t *testing.T, tc testTagCase) {
mockGalleryReader.On("UpdateTags", galleryID, []int{tagID}).Return(nil).Once()
}
err := TagGalleries(&tag, nil, aliases, mockGalleryReader)
err := TagGalleries(&tag, nil, aliases, mockGalleryReader, nil)
assert := assert.New(t)

View file

@ -26,6 +26,8 @@ type tagger struct {
Type string
Name string
Path string
cache *match.Cache
}
type addLinkFunc func(subjectID, otherID int) (bool, error)
@ -39,7 +41,7 @@ func (t *tagger) addLog(otherType, otherName string) {
}
func (t *tagger) tagPerformers(performerReader models.PerformerReader, addFunc addLinkFunc) error {
others, err := match.PathToPerformers(t.Path, performerReader)
others, err := match.PathToPerformers(t.Path, performerReader, t.cache)
if err != nil {
return err
}
@ -60,7 +62,7 @@ func (t *tagger) tagPerformers(performerReader models.PerformerReader, addFunc a
}
func (t *tagger) tagStudios(studioReader models.StudioReader, addFunc addLinkFunc) error {
studio, err := match.PathToStudio(t.Path, studioReader)
studio, err := match.PathToStudio(t.Path, studioReader, t.cache)
if err != nil {
return err
}
@ -81,7 +83,7 @@ func (t *tagger) tagStudios(studioReader models.StudioReader, addFunc addLinkFun
}
func (t *tagger) tagTags(tagReader models.TagReader, addFunc addLinkFunc) error {
others, err := match.PathToTags(t.Path, tagReader)
others, err := match.PathToTags(t.Path, tagReader, t.cache)
if err != nil {
return err
}

View file

@ -1,15 +1,10 @@
package database
import (
"regexp"
"strconv"
"strings"
)
func regexFn(re, s string) (bool, error) {
return regexp.MatchString(re, s)
}
func durationToTinyIntFn(str string) (int64, error) {
splits := strings.Split(str, ":")

42
pkg/database/regex.go Normal file
View file

@ -0,0 +1,42 @@
package database
import (
"regexp"
lru "github.com/hashicorp/golang-lru"
)
// size of the regex LRU cache in elements.
// A small number number was chosen because it's most likely use is for a
// single query - this function gets called for every row in the (filtered)
// results. It's likely to only need no more than 1 or 2 in any given query.
// After that point, it's just sitting in the cache and is unlikely to be used
// again.
const regexCacheSize = 10
var regexCache *lru.Cache
func init() {
regexCache, _ = lru.New(regexCacheSize)
}
// regexFn is registered as an SQLite function as "regexp"
// It uses an LRU cache to cache recent regex patterns to reduce CPU load over
// identical patterns.
func regexFn(re, s string) (bool, error) {
entry, ok := regexCache.Get(re)
var compiled *regexp.Regexp
if !ok {
var err error
compiled, err = regexp.Compile(re)
if err != nil {
return false, err
}
regexCache.Add(re, compiled)
} else {
compiled = entry.(*regexp.Regexp)
}
return compiled.MatchString(s), nil
}

View file

@ -63,7 +63,8 @@ func (t *SceneIdentifier) scrapeScene(ctx context.Context, scene *models.Scene)
// scrape using the source
scraped, err := source.Scraper.ScrapeScene(ctx, scene.ID)
if err != nil {
return nil, fmt.Errorf("error scraping from %v: %v", source.Scraper, err)
logger.Errorf("error scraping from %v: %v", source.Scraper, err)
continue
}
// if results were found then return

View file

@ -85,12 +85,12 @@ func TestSceneIdentifier_Identify(t *testing.T) {
{
"error scraping",
errID1,
true,
false,
},
{
"error scraping from second",
errID2,
true,
false,
},
{
"found in first scraper",

View file

@ -151,6 +151,9 @@ const (
HandyKey = "handy_key"
FunscriptOffset = "funscript_offset"
ThemeColor = "theme_color"
DefaultThemeColor = "#202b33"
// Security
dangerousAllowPublicWithoutAuth = "dangerous_allow_public_without_auth"
dangerousAllowPublicWithoutAuthDefault = "false"
@ -619,6 +622,10 @@ func (i *Instance) GetPort() int {
return ret
}
func (i *Instance) GetThemeColor() string {
return i.getString(ThemeColor)
}
func (i *Instance) GetExternalHost() string {
return i.getString(ExternalHost)
}
@ -1175,6 +1182,8 @@ func (i *Instance) setDefaultValues(write bool) error {
i.main.SetDefault(PreviewAudio, previewAudioDefault)
i.main.SetDefault(SoundOnPreview, false)
i.main.SetDefault(ThemeColor, DefaultThemeColor)
i.main.SetDefault(WriteImageThumbnails, writeImageThumbnailsDefault)
i.main.SetDefault(Database, defaultDatabaseFilePath)

View file

@ -7,11 +7,13 @@ import (
"strconv"
"strings"
"sync"
"time"
"github.com/stashapp/stash/pkg/autotag"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/job"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scene"
)
@ -19,9 +21,13 @@ import (
type autoTagJob struct {
txnManager models.TransactionManager
input models.AutoTagMetadataInput
cache match.Cache
}
func (j *autoTagJob) Execute(ctx context.Context, progress *job.Progress) {
begin := time.Now()
input := j.input
if j.isFileBasedAutoTag(input) {
// doing file-based auto-tag
@ -30,6 +36,8 @@ func (j *autoTagJob) Execute(ctx context.Context, progress *job.Progress) {
// doing specific performer/studio/tag auto-tag
j.autoTagSpecific(ctx, progress)
}
logger.Infof("Finished autotag after %s", time.Since(begin).String())
}
func (j *autoTagJob) isFileBasedAutoTag(input models.AutoTagMetadataInput) bool {
@ -50,6 +58,7 @@ func (j *autoTagJob) autoTagFiles(ctx context.Context, progress *job.Progress, p
ctx: ctx,
progress: progress,
txnManager: j.txnManager,
cache: &j.cache,
}
t.process()
@ -105,8 +114,6 @@ func (j *autoTagJob) autoTagSpecific(ctx context.Context, progress *job.Progress
j.autoTagPerformers(ctx, progress, input.Paths, performerIds)
j.autoTagStudios(ctx, progress, input.Paths, studioIds)
j.autoTagTags(ctx, progress, input.Paths, tagIds)
logger.Info("Finished autotag")
}
func (j *autoTagJob) autoTagPerformers(ctx context.Context, progress *job.Progress, paths []string, performerIds []string) {
@ -150,13 +157,13 @@ func (j *autoTagJob) autoTagPerformers(ctx context.Context, progress *job.Progre
}
if err := j.txnManager.WithTxn(context.TODO(), func(r models.Repository) error {
if err := autotag.PerformerScenes(performer, paths, r.Scene()); err != nil {
if err := autotag.PerformerScenes(performer, paths, r.Scene(), &j.cache); err != nil {
return err
}
if err := autotag.PerformerImages(performer, paths, r.Image()); err != nil {
if err := autotag.PerformerImages(performer, paths, r.Image(), &j.cache); err != nil {
return err
}
if err := autotag.PerformerGalleries(performer, paths, r.Gallery()); err != nil {
if err := autotag.PerformerGalleries(performer, paths, r.Gallery(), &j.cache); err != nil {
return err
}
@ -222,13 +229,13 @@ func (j *autoTagJob) autoTagStudios(ctx context.Context, progress *job.Progress,
return err
}
if err := autotag.StudioScenes(studio, paths, aliases, r.Scene()); err != nil {
if err := autotag.StudioScenes(studio, paths, aliases, r.Scene(), &j.cache); err != nil {
return err
}
if err := autotag.StudioImages(studio, paths, aliases, r.Image()); err != nil {
if err := autotag.StudioImages(studio, paths, aliases, r.Image(), &j.cache); err != nil {
return err
}
if err := autotag.StudioGalleries(studio, paths, aliases, r.Gallery()); err != nil {
if err := autotag.StudioGalleries(studio, paths, aliases, r.Gallery(), &j.cache); err != nil {
return err
}
@ -288,13 +295,13 @@ func (j *autoTagJob) autoTagTags(ctx context.Context, progress *job.Progress, pa
return err
}
if err := autotag.TagScenes(tag, paths, aliases, r.Scene()); err != nil {
if err := autotag.TagScenes(tag, paths, aliases, r.Scene(), &j.cache); err != nil {
return err
}
if err := autotag.TagImages(tag, paths, aliases, r.Image()); err != nil {
if err := autotag.TagImages(tag, paths, aliases, r.Image(), &j.cache); err != nil {
return err
}
if err := autotag.TagGalleries(tag, paths, aliases, r.Gallery()); err != nil {
if err := autotag.TagGalleries(tag, paths, aliases, r.Gallery(), &j.cache); err != nil {
return err
}
@ -323,6 +330,7 @@ type autoTagFilesTask struct {
ctx context.Context
progress *job.Progress
txnManager models.TransactionManager
cache *match.Cache
}
func (t *autoTagFilesTask) makeSceneFilter() *models.SceneFilterType {
@ -469,6 +477,7 @@ func (t *autoTagFilesTask) processScenes(r models.ReaderRepository) error {
performers: t.performers,
studios: t.studios,
tags: t.tags,
cache: t.cache,
}
var wg sync.WaitGroup
@ -483,6 +492,10 @@ func (t *autoTagFilesTask) processScenes(r models.ReaderRepository) error {
more = false
} else {
*findFilter.Page++
if *findFilter.Page%10 == 1 {
logger.Infof("Processed %d scenes...", (*findFilter.Page-1)*batchSize)
}
}
}
@ -517,6 +530,7 @@ func (t *autoTagFilesTask) processImages(r models.ReaderRepository) error {
performers: t.performers,
studios: t.studios,
tags: t.tags,
cache: t.cache,
}
var wg sync.WaitGroup
@ -531,6 +545,10 @@ func (t *autoTagFilesTask) processImages(r models.ReaderRepository) error {
more = false
} else {
*findFilter.Page++
if *findFilter.Page%10 == 1 {
logger.Infof("Processed %d images...", (*findFilter.Page-1)*batchSize)
}
}
}
@ -565,6 +583,7 @@ func (t *autoTagFilesTask) processGalleries(r models.ReaderRepository) error {
performers: t.performers,
studios: t.studios,
tags: t.tags,
cache: t.cache,
}
var wg sync.WaitGroup
@ -579,6 +598,10 @@ func (t *autoTagFilesTask) processGalleries(r models.ReaderRepository) error {
more = false
} else {
*findFilter.Page++
if *findFilter.Page%10 == 1 {
logger.Infof("Processed %d galleries...", (*findFilter.Page-1)*batchSize)
}
}
}
@ -596,14 +619,17 @@ func (t *autoTagFilesTask) process() {
logger.Infof("Starting autotag of %d files", total)
logger.Info("Autotagging scenes...")
if err := t.processScenes(r); err != nil {
return err
}
logger.Info("Autotagging images...")
if err := t.processImages(r); err != nil {
return err
}
logger.Info("Autotagging galleries...")
if err := t.processGalleries(r); err != nil {
return err
}
@ -616,8 +642,6 @@ func (t *autoTagFilesTask) process() {
}); err != nil {
logger.Error(err.Error())
}
logger.Info("Finished autotag")
}
type autoTagSceneTask struct {
@ -627,23 +651,25 @@ type autoTagSceneTask struct {
performers bool
studios bool
tags bool
cache *match.Cache
}
func (t *autoTagSceneTask) 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.ScenePerformers(t.scene, r.Scene(), r.Performer()); err != nil {
if err := autotag.ScenePerformers(t.scene, r.Scene(), r.Performer(), t.cache); err != nil {
return fmt.Errorf("error tagging scene performers for %s: %v", t.scene.Path, err)
}
}
if t.studios {
if err := autotag.SceneStudios(t.scene, r.Scene(), r.Studio()); err != nil {
if err := autotag.SceneStudios(t.scene, r.Scene(), r.Studio(), t.cache); err != nil {
return fmt.Errorf("error tagging scene studio for %s: %v", t.scene.Path, err)
}
}
if t.tags {
if err := autotag.SceneTags(t.scene, r.Scene(), r.Tag()); err != nil {
if err := autotag.SceneTags(t.scene, r.Scene(), r.Tag(), t.cache); err != nil {
return fmt.Errorf("error tagging scene tags for %s: %v", t.scene.Path, err)
}
}
@ -661,23 +687,25 @@ type autoTagImageTask struct {
performers bool
studios bool
tags bool
cache *match.Cache
}
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 {
if err := autotag.ImagePerformers(t.image, r.Image(), r.Performer(), t.cache); err != nil {
return fmt.Errorf("error tagging image performers for %s: %v", t.image.Path, err)
}
}
if t.studios {
if err := autotag.ImageStudios(t.image, r.Image(), r.Studio()); err != nil {
if err := autotag.ImageStudios(t.image, r.Image(), r.Studio(), t.cache); err != nil {
return fmt.Errorf("error tagging image studio for %s: %v", t.image.Path, err)
}
}
if t.tags {
if err := autotag.ImageTags(t.image, r.Image(), r.Tag()); err != nil {
if err := autotag.ImageTags(t.image, r.Image(), r.Tag(), t.cache); err != nil {
return fmt.Errorf("error tagging image tags for %s: %v", t.image.Path, err)
}
}
@ -695,23 +723,25 @@ type autoTagGalleryTask struct {
performers bool
studios bool
tags bool
cache *match.Cache
}
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 {
if err := autotag.GalleryPerformers(t.gallery, r.Gallery(), r.Performer(), t.cache); err != nil {
return fmt.Errorf("error tagging gallery performers for %s: %v", t.gallery.Path.String, err)
}
}
if t.studios {
if err := autotag.GalleryStudios(t.gallery, r.Gallery(), r.Studio()); err != nil {
if err := autotag.GalleryStudios(t.gallery, r.Gallery(), r.Studio(), t.cache); err != nil {
return fmt.Errorf("error tagging gallery studio for %s: %v", t.gallery.Path.String, err)
}
}
if t.tags {
if err := autotag.GalleryTags(t.gallery, r.Gallery(), r.Tag()); err != nil {
if err := autotag.GalleryTags(t.gallery, r.Gallery(), r.Tag(), t.cache); err != nil {
return fmt.Errorf("error tagging gallery tags for %s: %v", t.gallery.Path.String, err)
}
}

View file

@ -239,6 +239,11 @@ func (s scraperSource) ScrapeScene(ctx context.Context, sceneID int) (*models.Sc
return nil, err
}
// don't try to convert nil return value
if content == nil {
return nil, nil
}
if scene, ok := content.(models.ScrapedScene); ok {
return &scene, nil
}

120
pkg/match/cache.go Normal file
View file

@ -0,0 +1,120 @@
package match
import "github.com/stashapp/stash/pkg/models"
const singleFirstCharacterRegex = `^[\p{L}][.\-_ ]`
// Cache is used to cache queries that should not change across an autotag process.
type Cache struct {
singleCharPerformers []*models.Performer
singleCharStudios []*models.Studio
singleCharTags []*models.Tag
}
// getSingleLetterPerformers returns all performers with names that start with single character words.
// The autotag query splits the words into two-character words to query
// against. This means that performers with single-letter words in their names could potentially
// be missed.
// This query is expensive, so it's queried once and cached, if the cache if provided.
func getSingleLetterPerformers(c *Cache, reader models.PerformerReader) ([]*models.Performer, error) {
if c == nil {
c = &Cache{}
}
if c.singleCharPerformers == nil {
pp := -1
performers, _, err := reader.Query(&models.PerformerFilterType{
Name: &models.StringCriterionInput{
Value: singleFirstCharacterRegex,
Modifier: models.CriterionModifierMatchesRegex,
},
}, &models.FindFilterType{
PerPage: &pp,
})
if err != nil {
return nil, err
}
if len(performers) == 0 {
// make singleWordPerformers not nil
c.singleCharPerformers = make([]*models.Performer, 0)
} else {
c.singleCharPerformers = performers
}
}
return c.singleCharPerformers, nil
}
// getSingleLetterStudios returns all studios with names that start with single character words.
// See getSingleLetterPerformers for details.
func getSingleLetterStudios(c *Cache, reader models.StudioReader) ([]*models.Studio, error) {
if c == nil {
c = &Cache{}
}
if c.singleCharStudios == nil {
pp := -1
studios, _, err := reader.Query(&models.StudioFilterType{
Name: &models.StringCriterionInput{
Value: singleFirstCharacterRegex,
Modifier: models.CriterionModifierMatchesRegex,
},
}, &models.FindFilterType{
PerPage: &pp,
})
if err != nil {
return nil, err
}
if len(studios) == 0 {
// make singleWordStudios not nil
c.singleCharStudios = make([]*models.Studio, 0)
} else {
c.singleCharStudios = studios
}
}
return c.singleCharStudios, nil
}
// getSingleLetterTags returns all tags with names that start with single character words.
// See getSingleLetterPerformers for details.
func getSingleLetterTags(c *Cache, reader models.TagReader) ([]*models.Tag, error) {
if c == nil {
c = &Cache{}
}
if c.singleCharTags == nil {
pp := -1
tags, _, err := reader.Query(&models.TagFilterType{
Name: &models.StringCriterionInput{
Value: singleFirstCharacterRegex,
Modifier: models.CriterionModifierMatchesRegex,
},
Or: &models.TagFilterType{
Aliases: &models.StringCriterionInput{
Value: singleFirstCharacterRegex,
Modifier: models.CriterionModifierMatchesRegex,
},
},
}, &models.FindFilterType{
PerPage: &pp,
})
if err != nil {
return nil, err
}
if len(tags) == 0 {
// make singleWordTags not nil
c.singleCharTags = make([]*models.Tag, 0)
} else {
c.singleCharTags = tags
}
}
return c.singleCharTags, nil
}

View file

@ -15,12 +15,15 @@ import (
)
const (
separatorChars = `.\-_ `
separatorChars = `.\-_ `
separatorPattern = `(?:_|[^\p{L}\w\d])+`
reNotLetterWordUnicode = `[^\p{L}\w\d]`
reNotLetterWord = `[^\w\d]`
)
var separatorRE = regexp.MustCompile(separatorPattern)
func getPathQueryRegex(name string) string {
// escape specific regex characters
name = regexp.QuoteMeta(name)
@ -30,13 +33,7 @@ func getPathQueryRegex(name string) string {
ret := strings.ReplaceAll(name, " ", separator+"*")
// \p{L} is specifically omitted here because of the performance hit when
// including it. It does mean that paths where the name is bounded by
// unicode letters will be returned. However, the results should be tested
// by nameMatchesPath which does include \p{L}. The improvement in query
// performance should be outweigh the performance hit of testing any extra
// results.
ret = `(?:^|_|[^\w\d])` + ret + `(?:$|_|[^\w\d])`
ret = `(?:^|_|[^\p{L}\d])` + ret + `(?:$|_|[^\p{L}\d])`
return ret
}
@ -50,9 +47,7 @@ func getPathWords(path string) []string {
}
// handle path separators
const separator = `(?:_|[^\p{L}\w\d])+`
re := regexp.MustCompile(separator)
retStr = re.ReplaceAllString(retStr, " ")
retStr = separatorRE.ReplaceAllString(retStr, " ")
words := strings.Split(retStr, " ")
@ -127,10 +122,24 @@ func regexpMatchesPath(r *regexp.Regexp, path string) int {
return found[len(found)-1][0]
}
func PathToPerformers(path string, performerReader models.PerformerReader) ([]*models.Performer, error) {
words := getPathWords(path)
func getPerformers(words []string, performerReader models.PerformerReader, cache *Cache) ([]*models.Performer, error) {
performers, err := performerReader.QueryForAutoTag(words)
if err != nil {
return nil, err
}
swPerformers, err := getSingleLetterPerformers(cache, performerReader)
if err != nil {
return nil, err
}
return append(performers, swPerformers...), nil
}
func PathToPerformers(path string, reader models.PerformerReader, cache *Cache) ([]*models.Performer, error) {
words := getPathWords(path)
performers, err := getPerformers(words, reader, cache)
if err != nil {
return nil, err
}
@ -146,12 +155,26 @@ func PathToPerformers(path string, performerReader models.PerformerReader) ([]*m
return ret, nil
}
func getStudios(words []string, reader models.StudioReader, cache *Cache) ([]*models.Studio, error) {
studios, err := reader.QueryForAutoTag(words)
if err != nil {
return nil, err
}
swStudios, err := getSingleLetterStudios(cache, reader)
if err != nil {
return nil, err
}
return append(studios, swStudios...), nil
}
// PathToStudio returns the Studio that matches the given path.
// Where multiple matching studios are found, the one that matches the latest
// position in the path is returned.
func PathToStudio(path string, reader models.StudioReader) (*models.Studio, error) {
func PathToStudio(path string, reader models.StudioReader, cache *Cache) (*models.Studio, error) {
words := getPathWords(path)
candidates, err := reader.QueryForAutoTag(words)
candidates, err := getStudios(words, reader, cache)
if err != nil {
return nil, err
@ -183,9 +206,23 @@ func PathToStudio(path string, reader models.StudioReader) (*models.Studio, erro
return ret, nil
}
func PathToTags(path string, tagReader models.TagReader) ([]*models.Tag, error) {
func getTags(words []string, reader models.TagReader, cache *Cache) ([]*models.Tag, error) {
tags, err := reader.QueryForAutoTag(words)
if err != nil {
return nil, err
}
swTags, err := getSingleLetterTags(cache, reader)
if err != nil {
return nil, err
}
return append(tags, swTags...), nil
}
func PathToTags(path string, reader models.TagReader, cache *Cache) ([]*models.Tag, error) {
words := getPathWords(path)
tags, err := tagReader.QueryForAutoTag(words)
tags, err := getTags(words, reader, cache)
if err != nil {
return nil, err
@ -199,7 +236,7 @@ func PathToTags(path string, tagReader models.TagReader) ([]*models.Tag, error)
}
if !matches {
aliases, err := tagReader.GetAliases(t.ID)
aliases, err := reader.GetAliases(t.ID)
if err != nil {
return nil, err
}

View file

@ -22,7 +22,7 @@ type autotagScraper struct {
}
func autotagMatchPerformers(path string, performerReader models.PerformerReader) ([]*models.ScrapedPerformer, error) {
p, err := match.PathToPerformers(path, performerReader)
p, err := match.PathToPerformers(path, performerReader, nil)
if err != nil {
return nil, fmt.Errorf("error matching performers: %w", err)
}
@ -46,7 +46,7 @@ func autotagMatchPerformers(path string, performerReader models.PerformerReader)
}
func autotagMatchStudio(path string, studioReader models.StudioReader) (*models.ScrapedStudio, error) {
studio, err := match.PathToStudio(path, studioReader)
studio, err := match.PathToStudio(path, studioReader, nil)
if err != nil {
return nil, fmt.Errorf("error matching studios: %w", err)
}
@ -63,7 +63,7 @@ func autotagMatchStudio(path string, studioReader models.StudioReader) (*models.
}
func autotagMatchTags(path string, tagReader models.TagReader) ([]*models.ScrapedTag, error) {
t, err := match.PathToTags(path, tagReader)
t, err := match.PathToTags(path, tagReader, nil)
if err != nil {
return nil, fmt.Errorf("error matching tags: %w", err)
}

View file

@ -273,10 +273,16 @@ func (c Cache) ScrapeID(ctx context.Context, scraperID string, id int, ty models
return nil, fmt.Errorf("scraper %s: unable to load scene id %v: %w", scraperID, id, err)
}
ret, err = ss.viaScene(ctx, c.client, scene)
// don't assign nil concrete pointer to ret interface, otherwise nil
// detection is harder
scraped, err := ss.viaScene(ctx, c.client, scene)
if err != nil {
return nil, fmt.Errorf("scraper %s: %w", scraperID, err)
}
if scraped != nil {
ret = scraped
}
case models.ScrapeContentTypeGallery:
gs, ok := s.(galleryScraper)
if !ok {
@ -288,10 +294,16 @@ func (c Cache) ScrapeID(ctx context.Context, scraperID string, id int, ty models
return nil, fmt.Errorf("scraper %s: unable to load gallery id %v: %w", scraperID, id, err)
}
ret, err = gs.viaGallery(ctx, c.client, gallery)
// don't assign nil concrete pointer to ret interface, otherwise nil
// detection is harder
scraped, err := gs.viaGallery(ctx, c.client, gallery)
if err != nil {
return nil, fmt.Errorf("scraper %s: %w", scraperID, err)
}
if scraped != nil {
ret = scraped
}
}
return c.postScrape(ctx, ret)

View file

@ -258,7 +258,10 @@ func (q *jsonQuery) runQuery(selector string) ([]string, error) {
value := gjson.Get(q.doc, selector)
if !value.Exists() {
return nil, fmt.Errorf("could not find json path '%s' in json object", selector)
// many possible reasons why the selector may not be in the json object
// and not all are errors.
// Just return nil
return nil, nil
}
var ret []string

View file

@ -97,4 +97,22 @@ jsonScrapers:
verifyField(t, "Some girls are so damn hot that they can get you bent out of shape, and you will not even be mad at them for doing so. Well, tawny blonde Mia Malkova can bend her body into any shape she pleases, and thats sure to satisfy all of the horny cocks and wet pussies out there. This girl has acrobatic and contortionist abilities that could even twist a pretzel into a new knot, which can be very helpful in the ... arrow_drop_down Some girls are so damn hot that they can get you bent out of shape, and you will not even be mad at them for doing so. Well, tawny blonde Mia Malkova can bend her body into any shape she pleases, and thats sure to satisfy all of the horny cocks and wet pussies out there. This girl has acrobatic and contortionist abilities that could even twist a pretzel into a new knot, which can be very helpful in the VR Porn movies trust us. Ankles behind her neck and feet over her back so she can kiss her toes, turned, twisted and gyrating, she can fuck any which way she wants (and that ass!), will surely make you fall in love with this hot Virtual Reality Porn slut, as she is one of the finest of them all. Talking about perfection, maybe its all the acrobatic work that keeps it in such gorgeous shape? Who cares really, because you just want to take a big bite out of it and never let go. But its not all about the body. Mias also got a great smile, which might not sound kinky, but believe us, it is a smile that will heat up your innards and drop your pants. Is it her golden skin, her innocent pink lips or that heart-shaped face? There is just too much good stuff going on with Mia Malkova, which is maybe why these past few years have heaped awards upon awards on this Southern California native. Mia came to VR Bangers for her first VR Porn video, so you know shes only going for top-notch scenes with top-game performers, men, and women. Better hit up that yoga studio if you ever dream of being able to bang a flexible and talented chick like lady Malkova. arrow_drop_up", scrapedPerformer.Details, "Details")
verifyField(t, "Blonde", scrapedPerformer.HairColor, "HairColor")
verifyField(t, "57", scrapedPerformer.Weight, "Weight")
notFoundJson := `
{
"data": null
}`
q = &jsonQuery{
doc: notFoundJson,
}
scrapedPerformer, err = performerScraper.scrapePerformer(context.Background(), q)
if err != nil {
t.Fatalf("Error scraping performer: %s", err.Error())
}
if scrapedPerformer != nil {
t.Errorf("expected nil scraped performer when not found, got %v", scrapedPerformer)
}
}

View file

@ -761,7 +761,7 @@ func (r mappedResults) setKey(index int, key string, value string) mappedResults
}
func (s mappedScraper) scrapePerformer(ctx context.Context, q mappedQuery) (*models.ScrapedPerformer, error) {
var ret models.ScrapedPerformer
var ret *models.ScrapedPerformer
performerMap := s.Performer
if performerMap == nil {
@ -772,7 +772,8 @@ func (s mappedScraper) scrapePerformer(ctx context.Context, q mappedQuery) (*mod
results := performerMap.process(ctx, q, s.Common)
if len(results) > 0 {
results[0].apply(&ret)
ret = &models.ScrapedPerformer{}
results[0].apply(ret)
// now apply the tags
if performerTagsMap != nil {
@ -787,7 +788,7 @@ func (s mappedScraper) scrapePerformer(ctx context.Context, q mappedQuery) (*mod
}
}
return &ret, nil
return ret, nil
}
func (s mappedScraper) scrapePerformers(ctx context.Context, q mappedQuery) ([]*models.ScrapedPerformer, error) {
@ -903,7 +904,7 @@ func (s mappedScraper) scrapeScenes(ctx context.Context, q mappedQuery) ([]*mode
}
func (s mappedScraper) scrapeScene(ctx context.Context, q mappedQuery) (*models.ScrapedScene, error) {
var ret models.ScrapedScene
var ret *models.ScrapedScene
sceneScraperConfig := s.Scene
sceneMap := sceneScraperConfig.mappedConfig
@ -914,15 +915,14 @@ func (s mappedScraper) scrapeScene(ctx context.Context, q mappedQuery) (*models.
logger.Debug(`Processing scene:`)
results := sceneMap.process(ctx, q, s.Common)
if len(results) > 0 {
ss := s.processScene(ctx, q, results[0])
ret = *ss
ret = s.processScene(ctx, q, results[0])
}
return &ret, nil
return ret, nil
}
func (s mappedScraper) scrapeGallery(ctx context.Context, q mappedQuery) (*models.ScrapedGallery, error) {
var ret models.ScrapedGallery
var ret *models.ScrapedGallery
galleryScraperConfig := s.Gallery
galleryMap := galleryScraperConfig.mappedConfig
@ -937,7 +937,9 @@ func (s mappedScraper) scrapeGallery(ctx context.Context, q mappedQuery) (*model
logger.Debug(`Processing gallery:`)
results := galleryMap.process(ctx, q, s.Common)
if len(results) > 0 {
results[0].apply(&ret)
ret = &models.ScrapedGallery{}
results[0].apply(ret)
// now apply the performers and tags
if galleryPerformersMap != nil {
@ -974,11 +976,11 @@ func (s mappedScraper) scrapeGallery(ctx context.Context, q mappedQuery) (*model
}
}
return &ret, nil
return ret, nil
}
func (s mappedScraper) scrapeMovie(ctx context.Context, q mappedQuery) (*models.ScrapedMovie, error) {
var ret models.ScrapedMovie
var ret *models.ScrapedMovie
movieScraperConfig := s.Movie
movieMap := movieScraperConfig.mappedConfig
@ -990,7 +992,8 @@ func (s mappedScraper) scrapeMovie(ctx context.Context, q mappedQuery) (*models.
results := movieMap.process(ctx, q, s.Common)
if len(results) > 0 {
results[0].apply(&ret)
ret = &models.ScrapedMovie{}
results[0].apply(ret)
if movieStudioMap != nil {
logger.Debug(`Processing movie studio:`)
@ -1004,5 +1007,5 @@ func (s mappedScraper) scrapeMovie(ctx context.Context, q mappedQuery) (*models.
}
}
return &ret, nil
return ret, nil
}

View file

@ -173,21 +173,21 @@ func (s *scriptScraper) scrapeByURL(ctx context.Context, url string, ty models.S
func (s *scriptScraper) scrape(ctx context.Context, input string, ty models.ScrapeContentType) (models.ScrapedContent, error) {
switch ty {
case models.ScrapeContentTypePerformer:
var performer models.ScrapedPerformer
var performer *models.ScrapedPerformer
err := s.runScraperScript(input, &performer)
return &performer, err
return performer, err
case models.ScrapeContentTypeGallery:
var gallery models.ScrapedGallery
var gallery *models.ScrapedGallery
err := s.runScraperScript(input, &gallery)
return &gallery, err
return gallery, err
case models.ScrapeContentTypeScene:
var scene models.ScrapedScene
var scene *models.ScrapedScene
err := s.runScraperScript(input, &scene)
return &scene, err
return scene, err
case models.ScrapeContentTypeMovie:
var movie models.ScrapedMovie
var movie *models.ScrapedMovie
err := s.runScraperScript(input, &movie)
return &movie, err
return movie, err
}
return nil, ErrNotSupported
@ -200,11 +200,11 @@ func (s *scriptScraper) scrapeSceneByScene(ctx context.Context, scene *models.Sc
return nil, err
}
var ret models.ScrapedScene
var ret *models.ScrapedScene
err = s.runScraperScript(string(inString), &ret)
return &ret, err
return ret, err
}
func (s *scriptScraper) scrapeGalleryByGallery(ctx context.Context, gallery *models.Gallery) (*models.ScrapedGallery, error) {
@ -214,11 +214,11 @@ func (s *scriptScraper) scrapeGalleryByGallery(ctx context.Context, gallery *mod
return nil, err
}
var ret models.ScrapedGallery
var ret *models.ScrapedGallery
err = s.runScraperScript(string(inString), &ret)
return &ret, err
return ret, err
}
func findPythonExecutable() (string, error) {

View file

@ -486,16 +486,13 @@ func galleryAverageResolutionCriterionHandler(qb *galleryQueryBuilder, resolutio
}
func (qb *galleryQueryBuilder) getGallerySort(findFilter *models.FindFilterType) string {
var sort string
var direction string
if findFilter == nil {
sort = "path"
direction = "ASC"
} else {
sort = findFilter.GetSort("path")
direction = findFilter.GetDirection()
if findFilter == nil || findFilter.Sort == nil || *findFilter.Sort == "" {
return ""
}
sort := findFilter.GetSort("path")
direction := findFilter.GetDirection()
switch sort {
case "images_count":
return getCountSort(galleryTable, galleriesImagesTable, galleryIDColumn, direction)

View file

@ -517,8 +517,8 @@ INNER JOIN (` + valuesClause + `) t ON t.column2 = pt.tag_id
}
func (qb *imageQueryBuilder) getImageSort(findFilter *models.FindFilterType) string {
if findFilter == nil {
return " ORDER BY images.path ASC "
if findFilter == nil || findFilter.Sort == nil || *findFilter.Sort == "" {
return ""
}
sort := findFilter.GetSort("title")
direction := findFilter.GetDirection()

View file

@ -21,12 +21,6 @@ WHERE performers_tags.tag_id = ?
GROUP BY performers_tags.performer_id
`
// KNOWN ISSUE: using \p{L} to find single unicode character names results in
// very slow queries.
// Suggested solution will be to cache single-character names and not include it
// in the autotag query.
const singleFirstCharacterRegex = `^[\w][.\-_ ]`
type performerQueryBuilder struct {
repository
}
@ -189,9 +183,6 @@ func (qb *performerQueryBuilder) QueryForAutoTag(words []string) ([]*models.Perf
var whereClauses []string
var args []interface{}
whereClauses = append(whereClauses, "name regexp ?")
args = append(args, singleFirstCharacterRegex)
for _, w := range words {
whereClauses = append(whereClauses, "name like ?")
args = append(args, w+"%")

View file

@ -760,8 +760,7 @@ func (qb *sceneQueryBuilder) getDefaultSceneSort() string {
}
func (qb *sceneQueryBuilder) setSceneSort(query *queryBuilder, findFilter *models.FindFilterType) {
if findFilter == nil {
query.sortAndPagination += qb.getDefaultSceneSort()
if findFilter == nil || findFilter.Sort == nil || *findFilter.Sort == "" {
return
}
sort := findFilter.GetSort("title")

View file

@ -144,10 +144,6 @@ func (qb *studioQueryBuilder) QueryForAutoTag(words []string) ([]*models.Studio,
var whereClauses []string
var args []interface{}
// always include names that begin with a single character
whereClauses = append(whereClauses, "studios.name regexp ? OR COALESCE(studio_aliases.alias, '') regexp ?")
args = append(args, singleFirstCharacterRegex, singleFirstCharacterRegex)
for _, w := range words {
ww := w + "%"
whereClauses = append(whereClauses, "studios.name like ?")

View file

@ -235,10 +235,6 @@ func (qb *tagQueryBuilder) QueryForAutoTag(words []string) ([]*models.Tag, error
var whereClauses []string
var args []interface{}
// always include names that begin with a single character
whereClauses = append(whereClauses, "tags.name regexp ? OR COALESCE(tag_aliases.alias, '') regexp ?")
args = append(args, singleFirstCharacterRegex, singleFirstCharacterRegex)
for _, w := range words {
ww := w + "%"
whereClauses = append(whereClauses, "tags.name like ?")

View file

@ -9,7 +9,7 @@
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1"
/>
<meta name="theme-color" content="#202b33" />
<meta name="theme-color" content="%COLOR%" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/

View file

@ -17,6 +17,7 @@ import V0110 from "./versions/v0110.md";
import V0120 from "./versions/v0120.md";
import V0130 from "./versions/v0130.md";
import V0131 from "./versions/v0131.md";
import V0140 from "./versions/v0140.md";
import { MarkdownPage } from "../Shared/MarkdownPage";
// to avoid use of explicit any
@ -55,9 +56,9 @@ const Changelog: React.FC = () => {
// after new release:
// add entry to releases, using the current* fields
// then update the current fields.
const currentVersion = stashVersion || "v0.13.1";
const currentVersion = stashVersion || "v0.14.0";
const currentDate = buildDate;
const currentPage = V0131;
const currentPage = V0140;
const releases: IStashRelease[] = [
{
@ -66,11 +67,15 @@ const Changelog: React.FC = () => {
page: currentPage,
defaultOpen: true,
},
{
version: "v0.13.1",
date: "2022-03-16",
page: V0131,
},
{
version: "v0.13.0",
date: "2022-03-08",
page: V0130,
defaultOpen: true,
},
{
version: "v0.12.0",

View file

@ -0,0 +1,9 @@
### 🎨 Improvements
* Allow customisation of UI theme color using `theme_color` property in `config.yml` ([#2365](https://github.com/stashapp/stash/pull/2365))
* Improved autotag performance. ([#2368](https://github.com/stashapp/stash/pull/2368))
### 🐛 Bug fixes
* Removed warnings and incorrect error message in json scrapers. ([#2375](https://github.com/stashapp/stash/pull/2375))
* Ensure identify continues using other scrapers if a scrape returns no results. ([#2375](https://github.com/stashapp/stash/pull/2375))
* Continue trying to identify scene if scraper fails. ([#2375](https://github.com/stashapp/stash/pull/2375))
* Fix auto-tag not using case-insensitive matching. ([#2378](https://github.com/stashapp/stash/pull/2378))

View file

@ -78,7 +78,7 @@ export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> = ({
const otherOperations = [
{
text: "Remove from Gallery",
text: intl.formatMessage({ id: "actions.remove_from_gallery" }),
onClick: removeImages,
isDisplayed: showWhenSelected,
postRefetch: true,

View file

@ -25,7 +25,6 @@ interface IMenuItem {
hotkey: string;
userCreatable?: boolean;
}
const messages = defineMessages({
scenes: {
id: "scenes",
@ -231,7 +230,7 @@ export const MainNavbar: React.FC = () => {
<Button
className="minimal logout-button d-flex align-items-center"
href="/logout"
title="Log out"
title={intl.formatMessage({ id: "actions.logout" })}
>
<Icon icon="sign-out-alt" />
</Button>
@ -250,7 +249,10 @@ export const MainNavbar: React.FC = () => {
target="_blank"
onClick={handleDismiss}
>
<Button className="minimal donate" title="Donate">
<Button
className="minimal donate"
title={intl.formatMessage({ id: "donate" })}
>
<Icon icon="heart" />
<span className="d-none d-sm-inline">
{intl.formatMessage(messages.donate)}
@ -268,7 +270,7 @@ export const MainNavbar: React.FC = () => {
<Button
className="nav-utility minimal"
onClick={() => openManual()}
title="Help"
title={intl.formatMessage({ id: "help" })}
>
<Icon icon="question-circle" />
</Button>

View file

@ -102,7 +102,10 @@ export const EditMoviesDialog: React.FC<IListOperationProps> = (
<Modal
show
icon="pencil-alt"
header="Edit Movies"
header={intl.formatMessage(
{ id: "actions.edit_entity" },
{ entityType: intl.formatMessage({ id: "movies" }) }
)}
accept={{
onClick: onSave,
text: intl.formatMessage({ id: "actions.apply" }),

View file

@ -195,33 +195,33 @@ export const MovieScrapeDialog: React.FC<IMovieScrapeDialogProps> = (
return (
<>
<ScrapedInputGroupRow
title="Name"
title={intl.formatMessage({ id: "name" })}
result={name}
onChange={(value) => setName(value)}
/>
<ScrapedInputGroupRow
title="Aliases"
title={intl.formatMessage({ id: "aliases" })}
result={aliases}
onChange={(value) => setAliases(value)}
/>
<ScrapedInputGroupRow
title="Duration"
title={intl.formatMessage({ id: "duration" })}
result={duration}
onChange={(value) => setDuration(value)}
/>
<ScrapedInputGroupRow
title="Date"
title={intl.formatMessage({ id: "date" })}
placeholder="YYYY-MM-DD"
result={date}
onChange={(value) => setDate(value)}
/>
<ScrapedInputGroupRow
title="Director"
title={intl.formatMessage({ id: "director" })}
result={director}
onChange={(value) => setDirector(value)}
/>
<ScrapedTextAreaRow
title="Synopsis"
title={intl.formatMessage({ id: "synopsis" })}
result={synopsis}
onChange={(value) => setSynopsis(value)}
/>

View file

@ -198,7 +198,10 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
<Modal
show
icon="pencil-alt"
header="Edit Performers"
header={intl.formatMessage(
{ id: "actions.edit_entity" },
{ entityType: intl.formatMessage({ id: "performers" }) }
)}
accept={{
onClick: onSave,
text: intl.formatMessage({ id: "actions.apply" }),

View file

@ -390,7 +390,11 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
<LoadingIndicator message="Encoding image..." />
) : (
<Button variant="link" onClick={() => showLightbox()}>
<img className="performer" src={activeImage} alt="Performer" />
<img
className="performer"
src={activeImage}
alt={intl.formatMessage({ id: "performer" })}
/>
</Button>
)}
</div>

View file

@ -1,4 +1,5 @@
import React, { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { LoadingIndicator } from "src/components/Shared";
import { PerformerEditPanel } from "./PerformerEditPanel";
@ -7,6 +8,7 @@ const PerformerCreate: React.FC = () => {
const [imageEncoding, setImageEncoding] = useState<boolean>(false);
const activeImage = imagePreview ?? "";
const intl = useIntl();
const onImageChange = (image?: string | null) => setImagePreview(image);
const onImageEncoding = (isEncoding = false) => setImageEncoding(isEncoding);
@ -16,7 +18,13 @@ const PerformerCreate: React.FC = () => {
return <LoadingIndicator message="Encoding image..." />;
}
if (activeImage) {
return <img className="performer" src={activeImage} alt="Performer" />;
return (
<img
className="performer"
src={activeImage}
alt={intl.formatMessage({ id: "performer" })}
/>
);
}
}
@ -26,7 +34,12 @@ const PerformerCreate: React.FC = () => {
{renderPerformerImage()}
</div>
<div className="col-md-8">
<h2>Create Performer</h2>
<h2>
<FormattedMessage
id="actions.create_entity"
values={{ entityType: intl.formatMessage({ id: "performer" }) }}
/>
</h2>
<PerformerEditPanel
performer={{}}
isVisible

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import { Button, Form, Col, Row, Badge, Dropdown } from "react-bootstrap";
import { FormattedMessage } from "react-intl";
import { FormattedMessage, useIntl } from "react-intl";
import Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql";
import * as yup from "yup";
@ -84,6 +84,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
const [createTag] = useTagCreate();
const intl = useIntl();
const genderOptions = [""].concat(genderStrings);
@ -775,7 +776,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
<Button
variant="danger"
className="mr-2 py-0"
title="Delete StashID"
title={intl.formatMessage({ id: "actions.delete_stashid" })}
onClick={() => removeStashID(stashID)}
>
<Icon icon="trash-alt" />
@ -815,7 +816,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
<Prompt
when={formik.dirty}
message="Unsaved changes. Are you sure you want to leave?"
message={intl.formatMessage({ id: "dialogs.unsaved_changes" })}
/>
{renderButtons("mb-3")}
@ -827,7 +828,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
<Col xs={fieldXS} xl={fieldXL}>
<Form.Control
className="text-input"
placeholder="Name"
placeholder={intl.formatMessage({ id: "name" })}
{...formik.getFieldProps("name")}
isInvalid={!!formik.errors.name}
/>
@ -845,7 +846,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
<Form.Control
as="textarea"
className="text-input"
placeholder="Alias"
placeholder={intl.formatMessage({ id: "aliases" })}
{...formik.getFieldProps("aliases")}
/>
</Col>
@ -889,7 +890,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
<Form.Control
as="textarea"
className="text-input"
placeholder="Tattoos"
placeholder={intl.formatMessage({ id: "tattoos" })}
{...formik.getFieldProps("tattoos")}
/>
</Col>
@ -903,7 +904,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
<Form.Control
as="textarea"
className="text-input"
placeholder="Piercings"
placeholder={intl.formatMessage({ id: "piercings" })}
{...formik.getFieldProps("piercings")}
/>
</Col>
@ -934,7 +935,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
<Form.Control
as="textarea"
className="text-input"
placeholder="Details"
placeholder={intl.formatMessage({ id: "details" })}
{...formik.getFieldProps("details")}
/>
</Col>

View file

@ -1,5 +1,6 @@
import React from "react";
import { Button } from "react-bootstrap";
import { useIntl } from "react-intl";
import { Icon } from "src/components/Shared";
import { SceneDataFragment } from "src/core/generated-graphql";
import { TextUtils } from "src/utils";
@ -13,6 +14,7 @@ export const ExternalPlayerButton: React.FC<IExternalPlayerButtonProps> = ({
}) => {
const isAndroid = /(android)/i.test(navigator.userAgent);
const isAppleDevice = /(ipod|iphone|ipad)/i.test(navigator.userAgent);
const intl = useIntl();
const { paths, path, title } = scene;
@ -44,7 +46,7 @@ export const ExternalPlayerButton: React.FC<IExternalPlayerButtonProps> = ({
<Button
className="minimal px-0 px-sm-2 pt-2"
variant="secondary"
title="Open in external player"
title={intl.formatMessage({ id: "actions.open_in_external_player" })}
>
<a href={url}>
<Icon icon="external-link-alt" color="white" />

View file

@ -348,7 +348,7 @@ const ScenePage: React.FC<IProps> = ({ scene, refetch }) => {
variant="secondary"
id="operation-menu"
className="minimal"
title="Operations"
title={intl.formatMessage({ id: "operations" })}
>
<Icon icon="ellipsis-v" />
</Dropdown.Toggle>

View file

@ -288,14 +288,14 @@ export const SettingModal = <T extends {}>(props: ISettingModal<T>) => {
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={() => close()}>
Cancel
<FormattedMessage id="actions.cancel" />
</Button>
<Button
type="submit"
variant="primary"
onClick={() => close(currentValue)}
>
Confirm
<FormattedMessage id="actions.confirm" />
</Button>
</Modal.Footer>
</Form>

View file

@ -57,10 +57,18 @@ export const SettingsServicesPanel: React.FC = () => {
try {
if (enableDisable) {
await enableDLNA(input);
Toast.success({ content: "Enabled DLNA temporarily" });
Toast.success({
content: intl.formatMessage({
id: "config.dlna.enabled_dlna_temporarily",
}),
});
} else {
await disableDLNA(input);
Toast.success({ content: "Disabled DLNA temporarily" });
Toast.success({
content: intl.formatMessage({
id: "config.dlna.disabled_dlna_temporarily",
}),
});
}
} catch (e) {
Toast.error(e);
@ -86,7 +94,11 @@ export const SettingsServicesPanel: React.FC = () => {
try {
await addTempDLANIP(input);
Toast.success({ content: "Allowed IP temporarily" });
Toast.success({
content: intl.formatMessage({
id: "config.dlna.allowed_ip_temporarily",
}),
});
} catch (e) {
Toast.error(e);
} finally {
@ -106,7 +118,9 @@ export const SettingsServicesPanel: React.FC = () => {
try {
await removeTempDLNAIP(input);
Toast.success({ content: "Disallowed IP" });
Toast.success({
content: intl.formatMessage({ id: "config.dlna.disallowed_ip" }),
});
} catch (e) {
Toast.error(e);
} finally {
@ -184,7 +198,11 @@ export const SettingsServicesPanel: React.FC = () => {
} else {
await disableDLNA(input);
}
Toast.success({ content: "Successfully cancelled temporary behaviour" });
Toast.success({
content: intl.formatMessage({
id: "config.dlna.successfully_cancelled_temporary_behaviour",
}),
});
} catch (e) {
Toast.error(e);
} finally {

View file

@ -1,5 +1,5 @@
import React, { useEffect, useState, useMemo } from "react";
import { FormattedMessage } from "react-intl";
import { FormattedMessage, useIntl } from "react-intl";
import { Button, InputGroup, Form } from "react-bootstrap";
import { debounce } from "lodash";
import { Icon, LoadingIndicator } from "src/components/Shared";
@ -22,6 +22,7 @@ export const FolderSelect: React.FC<IProps> = ({
currentDirectory
);
const { data, error, loading } = useDirectory(debouncedDirectory);
const intl = useIntl();
const selectableDirectories: string[] = currentDirectory
? data?.directory.directories ?? defaultDirectories ?? []
@ -62,7 +63,7 @@ export const FolderSelect: React.FC<IProps> = ({
currentDirectory && data?.directory?.parent ? (
<li className="folder-list-parent folder-list-item">
<Button variant="link" onClick={() => goUp()}>
<FormattedMessage defaultMessage="Up a directory" id="up-dir" />
<FormattedMessage id="setup.folder.up_dir" />
</Button>
</li>
) : null;
@ -71,7 +72,7 @@ export const FolderSelect: React.FC<IProps> = ({
<>
<InputGroup>
<Form.Control
placeholder="File path"
placeholder={intl.formatMessage({ id: "setup.folder.file_path" })}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setDebounced(e.currentTarget.value);
}}

View file

@ -25,6 +25,7 @@ import { useToast } from "src/hooks";
import { TextUtils } from "src/utils";
import { SelectComponents } from "react-select/src/components";
import { ConfigurationContext } from "src/hooks/Config";
import { useIntl } from "react-intl";
export type ValidTypes =
| GQL.SlimPerformerDataFragment
@ -402,6 +403,7 @@ export const PerformerSelect: React.FC<IFilterProps> = (props) => {
const [createPerformer] = usePerformerCreate();
const { configuration } = React.useContext(ConfigurationContext);
const intl = useIntl();
const defaultCreatable =
!configuration?.interface.disableDropdownCreate.performer ?? true;
@ -426,7 +428,13 @@ export const PerformerSelect: React.FC<IFilterProps> = (props) => {
type="performers"
isLoading={loading}
items={performers}
placeholder={props.noSelectionString ?? "Select performer..."}
placeholder={
props.noSelectionString ??
intl.formatMessage(
{ id: "actions.select_entity" },
{ entityType: intl.formatMessage({ id: "performer" }) }
)
}
/>
);
};
@ -440,6 +448,7 @@ export const StudioSelect: React.FC<
const [allAliases, setAllAliases] = useState<string[]>([]);
const { data, loading } = useAllStudiosForFilter();
const [createStudio] = useStudioCreate();
const intl = useIntl();
const { configuration } = React.useContext(ConfigurationContext);
const defaultCreatable =
@ -550,7 +559,13 @@ export const StudioSelect: React.FC<
type="studios"
isLoading={loading}
items={studios}
placeholder={props.noSelectionString ?? "Select studio..."}
placeholder={
props.noSelectionString ??
intl.formatMessage(
{ id: "actions.select_entity" },
{ entityType: intl.formatMessage({ id: "studio" }) }
)
}
creatable={props.creatable ?? defaultCreatable}
onCreate={onCreate}
/>
@ -560,6 +575,7 @@ export const StudioSelect: React.FC<
export const MovieSelect: React.FC<IFilterProps> = (props) => {
const { data, loading } = useAllMoviesForFilter();
const items = data?.allMovies ?? [];
const intl = useIntl();
return (
<FilterSelectComponent
@ -568,7 +584,13 @@ export const MovieSelect: React.FC<IFilterProps> = (props) => {
type="movies"
isLoading={loading}
items={items}
placeholder={props.noSelectionString ?? "Select movie..."}
placeholder={
props.noSelectionString ??
intl.formatMessage(
{ id: "actions.select_entity" },
{ entityType: intl.formatMessage({ id: "movie" }) }
)
}
/>
);
};
@ -580,7 +602,13 @@ export const TagSelect: React.FC<IFilterProps & { excludeIds?: string[] }> = (
const [allAliases, setAllAliases] = useState<string[]>([]);
const { data, loading } = useAllTagsForFilter();
const [createTag] = useTagCreate();
const placeholder = props.noSelectionString ?? "Select tags...";
const intl = useIntl();
const placeholder =
props.noSelectionString ??
intl.formatMessage(
{ id: "actions.select_entity" },
{ entityType: intl.formatMessage({ id: "tags" }) }
);
const { configuration } = React.useContext(ConfigurationContext);
const defaultCreatable =

View file

@ -4,6 +4,7 @@ import * as GQL from "src/core/generated-graphql";
import StudioModal from "./StudioModal";
import PerformerModal from "../PerformerModal";
import { TaggerStateContext } from "../context";
import { useIntl } from "react-intl";
type PerformerModalCallback = (toCreate?: GQL.PerformerCreateInput) => void;
type StudioModalCallback = (toCreate?: GQL.StudioCreateInput) => void;
@ -43,6 +44,8 @@ export const SceneTaggerModals: React.FC = ({ children }) => {
StudioModalCallback | undefined
>();
const intl = useIntl();
function handlePerformerSave(toCreate: GQL.PerformerCreateInput) {
if (performerCallback) {
performerCallback(toCreate);
@ -110,7 +113,10 @@ export const SceneTaggerModals: React.FC = ({ children }) => {
performer={performerToCreate}
onSave={handlePerformerSave}
icon="tags"
header="Create Performer"
header={intl.formatMessage(
{ id: "actions.create_entity" },
{ entityType: intl.formatMessage({ id: "performer" }) }
)}
endpoint={endpoint}
create
/>
@ -122,7 +128,10 @@ export const SceneTaggerModals: React.FC = ({ children }) => {
studio={studioToCreate}
handleStudioCreate={handleStudioSave}
icon="tags"
header="Create Studio"
header={intl.formatMessage(
{ id: "actions.create_entity" },
{ entityType: intl.formatMessage({ id: "studio" }) }
)}
/>
)}
{children}

View file

@ -135,7 +135,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
<Col xs={fieldXS} xl={fieldXL}>
<Form.Control
className="text-input"
placeholder="Name"
placeholder={intl.formatMessage({ id: "name" })}
{...formik.getFieldProps("name")}
isInvalid={!!formik.errors.name}
/>

View file

@ -120,6 +120,7 @@ These options are typically not exposed in the UI and must be changed manually i
| `custom_served_folders` | A map of URLs to file system folders. See below. |
| `custom_ui_location` | The file system folder where the UI files will be served from, instead of using the embedded UI. Empty to disable. Stash must be restarted to take effect. |
| `max_upload_size` | Maximum file upload size for import files. Defaults to 1GB. |
| `theme_color` | Sets the `theme-color` property in the UI. |
### Custom served folders

View file

@ -592,7 +592,9 @@ export const LightboxComponent: React.FC<IProps> = ({
<Button
ref={overlayTarget}
variant="link"
title="Options"
title={intl.formatMessage({
id: "dialogs.lightbox.options",
})}
onClick={() => setShowOptions(!showOptions)}
>
<Icon icon="cog" />

View file

@ -858,7 +858,10 @@
"next_step": "Wenn du bereit bist ein neues System anzulegen, klicke Weiter.",
"unable_to_locate_specified_config": "Wenn du das hier liest, konnte Stash die Konfigurationsdatei, welche spezifiziert wurde, nicht finden. Dieser Wizard wird dich deshalb durch den Prozess führen, eine neue Konfiguration anzulegen."
},
"welcome_to_stash": "Willkommen zu Stash"
"welcome_to_stash": "Willkommen zu Stash",
"folder": {
"up_dir": "Ein Verzeichnis hoch"
}
},
"stash_id": "Stash-ID",
"stash_ids": "Stash IDs",
@ -900,7 +903,6 @@
"total": "Gesamt",
"true": "Wahr",
"twitter": "Twitter",
"up-dir": "Ein Verzeichnis hoch",
"updated_at": "Aktualisiert am",
"url": "URL",
"videos": "Videos",

View file

@ -31,6 +31,7 @@
"download": "Download",
"download_backup": "Download Backup",
"edit": "Edit",
"edit_entity": "Edit {entityType}",
"export": "Export…",
"export_all": "Export all…",
"find": "Find",
@ -83,6 +84,7 @@
"selective_auto_tag": "Selective Auto Tag",
"selective_clean": "Selective Clean",
"selective_scan": "Selective Scan",
"select_entity": "Select {entityType}",
"set_as_default": "Set as default",
"set_back_image": "Back image…",
"set_front_image": "Front image…",
@ -102,7 +104,11 @@
"use_default": "Use default",
"view_random": "View Random",
"continue": "Continue",
"submit": "Submit"
"submit": "Submit",
"logout": "Log out",
"remove_from_gallery": "Remove from Gallery",
"delete_stashid": "Delete StashID",
"open_in_external_player": "Open in external player"
},
"actions_name": "Actions",
"age": "Age",
@ -201,7 +207,12 @@
"recent_ip_addresses": "Recent IP addresses",
"server_display_name": "Server Display Name",
"server_display_name_desc": "Display name for the DLNA server. Defaults to {server_name} if empty.",
"until_restart": "until restart"
"until_restart": "until restart",
"allowed_ip_temporarily": "Allowed IP temporarily",
"disabled_dlna_temporarily": "Disabled DLNA temporarily",
"disallowed_ip": "Disallowed IP",
"enabled_dlna_temporarily": "Enabled DLNA temporarily",
"successfully_cancelled_temporary_behaviour": "Successfully cancelled temporary behaviour"
},
"general": {
"auth": {
@ -868,7 +879,11 @@
"next_step": "When you're ready to proceed with setting up a new system, click Next.",
"unable_to_locate_specified_config": "If you're reading this, then Stash couldn't find the configuration file specified at the command line or the environment. This wizard will guide you through the process of setting up a new configuration."
},
"welcome_to_stash": "Welcome to Stash"
"welcome_to_stash": "Welcome to Stash",
"folder": {
"file_path": "File path",
"up_dir": "Up a directory"
}
},
"stash_id": "Stash ID",
"stash_ids": "Stash IDs",
@ -910,7 +925,6 @@
"total": "Total",
"true": "True",
"twitter": "Twitter",
"up-dir": "Up a directory",
"updated_at": "Updated At",
"url": "URL",
"videos": "Videos",

View file

@ -833,7 +833,10 @@
"next_step": "Cuando estés preparado para la creación de un nuevo entorno pulsa Siguiente.",
"unable_to_locate_specified_config": "Si estás leyendo esto es que Stash no ha podido encontrar el fichero de configuración especificado en la línea de comandos o en el entorno en el que está instalado. Este asistente te guiará durante el proceso de creación de una nueva configuración."
},
"welcome_to_stash": "Bienvenido a Stash"
"welcome_to_stash": "Bienvenido a Stash",
"folder": {
"up_dir": "Ascender en el árbol de directorios"
}
},
"stash_id": "Identificador único Stash",
"stash_ids": "Stash IDs",
@ -875,7 +878,6 @@
"total": "Total",
"true": "Sí",
"twitter": "Twitter",
"up-dir": "Ascender en el árbol de directorios",
"updated_at": "Fecha de modificación",
"url": "URL",
"weight": "Peso",

View file

@ -817,7 +817,10 @@
"next_step": "Kun olet valmis etenemään järjestelmän luontiin, paina Seuraava.",
"unable_to_locate_specified_config": "Mikäli luet tätä, Stash ei löydä konfiguraatiotiedostoa, joka on määritelty joko komentorivillä tai muualla. Tämä velho auttaa sinua uuden konfiguraation luomisessa."
},
"welcome_to_stash": "Tervetuloa Stashiin"
"welcome_to_stash": "Tervetuloa Stashiin",
"folder": {
"up_dir": "Ylös"
}
},
"stash_id": "Stash ID",
"stash_ids": "Stash ID:t",
@ -859,7 +862,6 @@
"total": "Yhteensä",
"true": "On",
"twitter": "Twitter",
"up-dir": "Ylös",
"updated_at": "Päivitetty",
"url": "URL",
"videos": "Videot",

View file

@ -851,7 +851,10 @@
"next_step": "Lorsque vous êtes prêt à procéder à la configuration d'un nouveau système, cliquez sur Suivant.",
"unable_to_locate_specified_config": "Si vous lisez ceci, alors Stash n'a pas pu trouver le fichier de configuration spécifié sur la ligne de commande ou l'environnement. Cet assistant vous guidera tout au long du processus de configuration d'une nouvelle configuration."
},
"welcome_to_stash": "Bienvenue sur Stash"
"welcome_to_stash": "Bienvenue sur Stash",
"folder": {
"up_dir": "Remonter d'un répertoire"
}
},
"stash_id": "Stash ID",
"stash_ids": "Stash IDs",
@ -893,7 +896,6 @@
"total": "Total",
"true": "Vrai",
"twitter": "Twitter",
"up-dir": "Remonter d'un répertoire",
"updated_at": "Date de modification",
"url": "URL",
"videos": "Vidéos",

View file

@ -901,7 +901,10 @@
"next_step": "Quando siete pronti a procedere all'impostazione di un nuovo sistema, cliccate Prossimo.",
"unable_to_locate_specified_config": "Se state leggendo questo, allora Stash non ha potuto trovare il file di configurazione specificato nella linea di comando o ambiente. Questa procedura guidata vi guiderà attraverso il processo di impostazione di una nuova configurazione."
},
"welcome_to_stash": "Benvenuti su Stash"
"welcome_to_stash": "Benvenuti su Stash",
"folder": {
"up_dir": "Sali una cartella"
}
},
"stash_id": "ID Stash",
"stash_ids": "ID Stash",
@ -949,7 +952,6 @@
"total": "Totale",
"true": "Vero",
"twitter": "Twitter",
"up-dir": "Sali una cartella",
"updated_at": "Aggiornato Al",
"url": "URL",
"videos": "Video",

View file

@ -901,7 +901,10 @@
"next_step": "新しいシステムの構築準備が整ったら、次へをクリックしてください。",
"unable_to_locate_specified_config": "このメッセージをお読みいただいている場合、Stashはコマンドラインまたは環境変数で指定された構成ファイルを見つけることができませんでした。 このウィザードで、新しい構成をセットアップするプロセスをご案内します。"
},
"welcome_to_stash": "Stashへようこそ"
"welcome_to_stash": "Stashへようこそ",
"folder": {
"up_dir": "上の階層へ"
}
},
"stash_id": "Stash ID",
"stash_ids": "Stash ID",
@ -949,7 +952,6 @@
"total": "合計",
"true": "有効",
"twitter": "Twitter",
"up-dir": "上の階層へ",
"updated_at": "更新日:",
"url": "URL",
"videos": "動画",

View file

@ -854,7 +854,10 @@
"next_step": "Wanneer u klaar bent om door te gaan met het instellen van een nieuw systeem, klikt u op Volgende.",
"unable_to_locate_specified_config": "Als je dit leest, kan Stash het opgegeven configuratiebestand op de opdrachtregel of in de omgeving niet vinden. Deze wizard leidt u door het proces van het opzetten van een nieuwe configuratiebestand."
},
"welcome_to_stash": "Welkom bij Stash"
"welcome_to_stash": "Welkom bij Stash",
"folder": {
"up_dir": "Een directory omhoog"
}
},
"stash_id": "Stash ID",
"stash_ids": "Stash IDs",
@ -896,7 +899,6 @@
"total": "Totaal",
"true": "Waar",
"twitter": "Twitter",
"up-dir": "Een directory omhoog",
"updated_at": "Bijgewerkt op",
"url": "URL",
"videos": "Video's",

View file

@ -901,7 +901,10 @@
"next_step": "Quando estiver pronto para prosseguir com a criação do novo sistema, clique Próximo.",
"unable_to_locate_specified_config": "Se está lendo isto, então o Stash não pôde encontrar o arquivo de configuração especificado na linha de comando ou no ambiente. Este assistente irá te guiar durante o processo de criação de uma nova configuração."
},
"welcome_to_stash": "Bem-vindo ao Stash"
"welcome_to_stash": "Bem-vindo ao Stash",
"folder": {
"up_dir": "Subir um diretório"
}
},
"stash_id": "Stash ID",
"stash_ids": "Stash IDs",
@ -949,7 +952,6 @@
"total": "Total",
"true": "Verdadeiro",
"twitter": "Twitter",
"up-dir": "Subir um diretório",
"updated_at": "Atualizado em",
"url": "URL",
"videos": "Vídeos",

View file

@ -901,7 +901,10 @@
"next_step": "När du är redo att fortsätta med uppstarten av ett nytt system tryck på Nästa.",
"unable_to_locate_specified_config": "Om du läser detta så kunde Stash inte hitta konfigurationsfilen som specifierades via kommandoraden eller miljön. Denna hjälp kommer guida dig genom processen av att ställa in en ny konfiguration."
},
"welcome_to_stash": "Välkommen till Stash"
"welcome_to_stash": "Välkommen till Stash",
"folder": {
"up_dir": "Upp en mapp"
}
},
"stash_id": "Stash ID",
"stash_ids": "Stash ID:er",
@ -949,7 +952,6 @@
"total": "Total",
"true": "Sant",
"twitter": "Twitter",
"up-dir": "Upp en mapp",
"updated_at": "Uppdaterad vid",
"url": "URL",
"videos": "Videor",

View file

@ -845,7 +845,10 @@
"next_step": "Yeni bir sistem oluşturmak için hazır olduğunuzda Sonraki düğmesine basın.",
"unable_to_locate_specified_config": "Eğer bunu okuyorsanız, Stash yapılandırma dosyasını bulamamış demektir. Bu sihirbaz yeni bir yapılandırma sırasında size yol gösterecektir."
},
"welcome_to_stash": "Stash uygulamasına hoşgeldiniz"
"welcome_to_stash": "Stash uygulamasına hoşgeldiniz",
"folder": {
"up_dir": "Bir dizin üste çık"
}
},
"stash_id": "Stash Kimliği (ID)",
"stash_ids": "Stash Kimlikleri (ID)",
@ -887,7 +890,6 @@
"total": "Toplam",
"true": "Doğru",
"twitter": "Twitter",
"up-dir": "Bir dizin üste çık",
"updated_at": "Güncellenme Zamanı",
"url": "Internet Adresi (URL)",
"videos": "Videolar",

View file

@ -854,7 +854,10 @@
"next_step": "当你准备好建立一个新系统时,点击“下一个”。",
"unable_to_locate_specified_config": "如果你看到这就意味着Stash无法用命令行或者环境变量找到配置文件。这个向导将指引你去建立一个新的配置。"
},
"welcome_to_stash": "欢迎使用Stash"
"welcome_to_stash": "欢迎使用Stash",
"folder": {
"up_dir": "上级目录"
}
},
"stash_id": "Stash 号",
"stash_ids": "Stash号",
@ -896,7 +899,6 @@
"total": "总共",
"true": "真",
"twitter": "推特",
"up-dir": "上级目录",
"updated_at": "更新时间",
"url": "链接",
"videos": "视频",

View file

@ -102,7 +102,12 @@
"temp_disable": "暫時關閉…",
"temp_enable": "暫時啟用…",
"use_default": "使用預設選項",
"view_random": "隨機開啟"
"view_random": "隨機開啟",
"logout": "登出",
"remove_from_gallery": "自圖庫中移除",
"delete_stashid": "刪除 StashID",
"edit_entity": "編輯{entityType}",
"open_in_external_player": "透過外部播放器開啟"
},
"actions_name": "動作",
"age": "年齡",
@ -201,7 +206,12 @@
"recent_ip_addresses": "最近的 IP 位址",
"server_display_name": "伺服器顯示名稱",
"server_display_name_desc": "DLNA 伺服器的顯示名稱。如果為空,則預設為 {server_name}。",
"until_restart": "直到重啟"
"until_restart": "直到重啟",
"allowed_ip_temporarily": "已暫時允許 IP 位址",
"disabled_dlna_temporarily": "已暫時關閉 DLNA 伺服器",
"disallowed_ip": "已禁止的 IP 位址",
"enabled_dlna_temporarily": "已暫時開啟 DLNA 伺服器",
"successfully_cancelled_temporary_behaviour": "已關閉暫時啟用伺服器的功能"
},
"general": {
"auth": {
@ -901,7 +911,11 @@
"next_step": "當您準備繼續設定時,點擊「下一步」。",
"unable_to_locate_specified_config": "如果看到此畫面的話,則代表 Stash 無法找到您在命令列所提供的設定檔路徑。本安裝畫面將帶您建立新的設定檔案。"
},
"welcome_to_stash": "歡迎使用 Stash"
"welcome_to_stash": "歡迎使用 Stash",
"folder": {
"up_dir": "往上一層",
"file_path": "檔案路徑"
}
},
"stash_id": "Stash ID",
"stash_ids": "Stash IDs",
@ -949,7 +963,6 @@
"total": "總計",
"true": "是",
"twitter": "Twitter",
"up-dir": "往上一層",
"updated_at": "更新於",
"url": "連結",
"videos": "影片",