mirror of
https://github.com/stashapp/stash.git
synced 2025-12-07 17:02:38 +01:00
String regex filter criteria and selective autotag (#1082)
* Add regex string filter criterion * Use query interface for auto tagging * Use Query interface for filename parser * Remove query regex interfaces * Add selective auto tag * Use page size 0 as no limit
This commit is contained in:
parent
4fd022a93b
commit
e4d91a0226
24 changed files with 354 additions and 204 deletions
|
|
@ -6,6 +6,7 @@ enum SortDirectionEnum {
|
||||||
input FindFilterType {
|
input FindFilterType {
|
||||||
q: String
|
q: String
|
||||||
page: Int
|
page: Int
|
||||||
|
"""use per_page = 0 to indicate all results. Defaults to 25."""
|
||||||
per_page: Int
|
per_page: Int
|
||||||
sort: String
|
sort: String
|
||||||
direction: SortDirectionEnum
|
direction: SortDirectionEnum
|
||||||
|
|
@ -192,6 +193,10 @@ enum CriterionModifier {
|
||||||
INCLUDES_ALL,
|
INCLUDES_ALL,
|
||||||
INCLUDES,
|
INCLUDES,
|
||||||
EXCLUDES,
|
EXCLUDES,
|
||||||
|
"""MATCHES REGEX"""
|
||||||
|
MATCHES_REGEX,
|
||||||
|
"""NOT MATCHES REGEX"""
|
||||||
|
NOT_MATCHES_REGEX,
|
||||||
}
|
}
|
||||||
|
|
||||||
input StringCriterionInput {
|
input StringCriterionInput {
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@ input CleanMetadataInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
input AutoTagMetadataInput {
|
input AutoTagMetadataInput {
|
||||||
|
"""Paths to tag, null for all files"""
|
||||||
|
paths: [String!]
|
||||||
"""IDs of performers to tag files with, or "*" for all"""
|
"""IDs of performers to tag files with, or "*" for all"""
|
||||||
performers: [String!]
|
performers: [String!]
|
||||||
"""IDs of studios to tag files with, or "*" for all"""
|
"""IDs of studios to tag files with, or "*" for all"""
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ func (r *mutationResolver) MetadataGenerate(ctx context.Context, input models.Ge
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mutationResolver) MetadataAutoTag(ctx context.Context, input models.AutoTagMetadataInput) (string, error) {
|
func (r *mutationResolver) MetadataAutoTag(ctx context.Context, input models.AutoTagMetadataInput) (string, error) {
|
||||||
manager.GetInstance().AutoTag(input.Performers, input.Studios, input.Tags)
|
manager.GetInstance().AutoTag(input)
|
||||||
return "todo", nil
|
return "todo", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,25 @@ func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.Scen
|
||||||
|
|
||||||
func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *models.FindFilterType) (ret *models.FindScenesResultType, err error) {
|
func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *models.FindFilterType) (ret *models.FindScenesResultType, err error) {
|
||||||
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error {
|
||||||
scenes, total, err := repo.Scene().QueryByPathRegex(filter)
|
|
||||||
|
sceneFilter := &models.SceneFilterType{}
|
||||||
|
|
||||||
|
if filter != nil && filter.Q != nil {
|
||||||
|
sceneFilter.Path = &models.StringCriterionInput{
|
||||||
|
Modifier: models.CriterionModifierMatchesRegex,
|
||||||
|
Value: "(?i)" + *filter.Q,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make a copy of the filter if provided, nilling out Q
|
||||||
|
var queryFilter *models.FindFilterType
|
||||||
|
if filter != nil {
|
||||||
|
f := *filter
|
||||||
|
queryFilter = &f
|
||||||
|
queryFilter.Q = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
scenes, total, err := repo.Scene().Query(sceneFilter, queryFilter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -476,9 +476,14 @@ func (p *SceneFilenameParser) Parse(repo models.ReaderRepository) ([]*models.Sce
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
p.Filter.Q = &mapper.regexString
|
sceneFilter := &models.SceneFilterType{
|
||||||
|
Path: &models.StringCriterionInput{
|
||||||
|
Modifier: models.CriterionModifierMatchesRegex,
|
||||||
|
Value: "(?i)" + mapper.regexString,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
scenes, total, err := repo.Scene().QueryByPathRegex(p.Filter)
|
scenes, total, err := repo.Scene().Query(sceneFilter, p.Filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -589,7 +589,7 @@ func (s *singleton) generateScreenshot(sceneId string, at *float64) {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *singleton) AutoTag(performerIds []string, studioIds []string, tagIds []string) {
|
func (s *singleton) AutoTag(input models.AutoTagMetadataInput) {
|
||||||
if s.Status.Status != Idle {
|
if s.Status.Status != Idle {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -599,6 +599,10 @@ func (s *singleton) AutoTag(performerIds []string, studioIds []string, tagIds []
|
||||||
go func() {
|
go func() {
|
||||||
defer s.returnToIdleState()
|
defer s.returnToIdleState()
|
||||||
|
|
||||||
|
performerIds := input.Performers
|
||||||
|
studioIds := input.Studios
|
||||||
|
tagIds := input.Tags
|
||||||
|
|
||||||
// calculate work load
|
// calculate work load
|
||||||
performerCount := len(performerIds)
|
performerCount := len(performerIds)
|
||||||
studioCount := len(studioIds)
|
studioCount := len(studioIds)
|
||||||
|
|
@ -639,13 +643,13 @@ func (s *singleton) AutoTag(performerIds []string, studioIds []string, tagIds []
|
||||||
total := performerCount + studioCount + tagCount
|
total := performerCount + studioCount + tagCount
|
||||||
s.Status.setProgress(0, total)
|
s.Status.setProgress(0, total)
|
||||||
|
|
||||||
s.autoTagPerformers(performerIds)
|
s.autoTagPerformers(input.Paths, performerIds)
|
||||||
s.autoTagStudios(studioIds)
|
s.autoTagStudios(input.Paths, studioIds)
|
||||||
s.autoTagTags(tagIds)
|
s.autoTagTags(input.Paths, tagIds)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *singleton) autoTagPerformers(performerIds []string) {
|
func (s *singleton) autoTagPerformers(paths []string, performerIds []string) {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
for _, performerId := range performerIds {
|
for _, performerId := range performerIds {
|
||||||
var performers []*models.Performer
|
var performers []*models.Performer
|
||||||
|
|
@ -681,7 +685,10 @@ func (s *singleton) autoTagPerformers(performerIds []string) {
|
||||||
for _, performer := range performers {
|
for _, performer := range performers {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
task := AutoTagPerformerTask{
|
task := AutoTagPerformerTask{
|
||||||
|
AutoTagTask: AutoTagTask{
|
||||||
txnManager: s.TxnManager,
|
txnManager: s.TxnManager,
|
||||||
|
paths: paths,
|
||||||
|
},
|
||||||
performer: performer,
|
performer: performer,
|
||||||
}
|
}
|
||||||
go task.Start(&wg)
|
go task.Start(&wg)
|
||||||
|
|
@ -692,7 +699,7 @@ func (s *singleton) autoTagPerformers(performerIds []string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *singleton) autoTagStudios(studioIds []string) {
|
func (s *singleton) autoTagStudios(paths []string, studioIds []string) {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
for _, studioId := range studioIds {
|
for _, studioId := range studioIds {
|
||||||
var studios []*models.Studio
|
var studios []*models.Studio
|
||||||
|
|
@ -727,8 +734,11 @@ func (s *singleton) autoTagStudios(studioIds []string) {
|
||||||
for _, studio := range studios {
|
for _, studio := range studios {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
task := AutoTagStudioTask{
|
task := AutoTagStudioTask{
|
||||||
studio: studio,
|
AutoTagTask: AutoTagTask{
|
||||||
txnManager: s.TxnManager,
|
txnManager: s.TxnManager,
|
||||||
|
paths: paths,
|
||||||
|
},
|
||||||
|
studio: studio,
|
||||||
}
|
}
|
||||||
go task.Start(&wg)
|
go task.Start(&wg)
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
@ -738,7 +748,7 @@ func (s *singleton) autoTagStudios(studioIds []string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *singleton) autoTagTags(tagIds []string) {
|
func (s *singleton) autoTagTags(paths []string, tagIds []string) {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
for _, tagId := range tagIds {
|
for _, tagId := range tagIds {
|
||||||
var tags []*models.Tag
|
var tags []*models.Tag
|
||||||
|
|
@ -772,7 +782,10 @@ func (s *singleton) autoTagTags(tagIds []string) {
|
||||||
for _, tag := range tags {
|
for _, tag := range tags {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
task := AutoTagTagTask{
|
task := AutoTagTagTask{
|
||||||
|
AutoTagTask: AutoTagTask{
|
||||||
txnManager: s.TxnManager,
|
txnManager: s.TxnManager,
|
||||||
|
paths: paths,
|
||||||
|
},
|
||||||
tag: tag,
|
tag: tag,
|
||||||
}
|
}
|
||||||
go task.Start(&wg)
|
go task.Start(&wg)
|
||||||
|
|
|
||||||
|
|
@ -12,18 +12,23 @@ import (
|
||||||
"github.com/stashapp/stash/pkg/scene"
|
"github.com/stashapp/stash/pkg/scene"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AutoTagPerformerTask struct {
|
type AutoTagTask struct {
|
||||||
performer *models.Performer
|
paths []string
|
||||||
txnManager models.TransactionManager
|
txnManager models.TransactionManager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AutoTagPerformerTask struct {
|
||||||
|
AutoTagTask
|
||||||
|
performer *models.Performer
|
||||||
|
}
|
||||||
|
|
||||||
func (t *AutoTagPerformerTask) Start(wg *sync.WaitGroup) {
|
func (t *AutoTagPerformerTask) Start(wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
t.autoTagPerformer()
|
t.autoTagPerformer()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getQueryRegex(name string) string {
|
func (t *AutoTagTask) getQueryRegex(name string) string {
|
||||||
const separatorChars = `.\-_ `
|
const separatorChars = `.\-_ `
|
||||||
// handle path separators
|
// handle path separators
|
||||||
const separator = `[` + separatorChars + `]`
|
const separator = `[` + separatorChars + `]`
|
||||||
|
|
@ -34,12 +39,12 @@ func getQueryRegex(name string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *AutoTagPerformerTask) autoTagPerformer() {
|
func (t *AutoTagPerformerTask) autoTagPerformer() {
|
||||||
regex := getQueryRegex(t.performer.Name.String)
|
regex := t.getQueryRegex(t.performer.Name.String)
|
||||||
|
|
||||||
if err := t.txnManager.WithTxn(context.TODO(), func(r models.Repository) error {
|
if err := t.txnManager.WithTxn(context.TODO(), func(r models.Repository) error {
|
||||||
qb := r.Scene()
|
qb := r.Scene()
|
||||||
const ignoreOrganized = true
|
|
||||||
scenes, err := qb.QueryAllByPathRegex(regex, ignoreOrganized)
|
scenes, err := qb.QueryForAutoTag(regex, t.paths)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error querying scenes with regex '%s': %s", regex, err.Error())
|
return fmt.Errorf("Error querying scenes with regex '%s': %s", regex, err.Error())
|
||||||
|
|
@ -64,8 +69,8 @@ func (t *AutoTagPerformerTask) autoTagPerformer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
type AutoTagStudioTask struct {
|
type AutoTagStudioTask struct {
|
||||||
|
AutoTagTask
|
||||||
studio *models.Studio
|
studio *models.Studio
|
||||||
txnManager models.TransactionManager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *AutoTagStudioTask) Start(wg *sync.WaitGroup) {
|
func (t *AutoTagStudioTask) Start(wg *sync.WaitGroup) {
|
||||||
|
|
@ -75,30 +80,29 @@ func (t *AutoTagStudioTask) Start(wg *sync.WaitGroup) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *AutoTagStudioTask) autoTagStudio() {
|
func (t *AutoTagStudioTask) autoTagStudio() {
|
||||||
regex := getQueryRegex(t.studio.Name.String)
|
regex := t.getQueryRegex(t.studio.Name.String)
|
||||||
|
|
||||||
if err := t.txnManager.WithTxn(context.TODO(), func(r models.Repository) error {
|
if err := t.txnManager.WithTxn(context.TODO(), func(r models.Repository) error {
|
||||||
qb := r.Scene()
|
qb := r.Scene()
|
||||||
const ignoreOrganized = true
|
scenes, err := qb.QueryForAutoTag(regex, t.paths)
|
||||||
scenes, err := qb.QueryAllByPathRegex(regex, ignoreOrganized)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error querying scenes with regex '%s': %s", regex, err.Error())
|
return fmt.Errorf("Error querying scenes with regex '%s': %s", regex, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, scene := range scenes {
|
for _, s := range scenes {
|
||||||
// #306 - don't overwrite studio if already present
|
// #306 - don't overwrite studio if already present
|
||||||
if scene.StudioID.Valid {
|
if s.StudioID.Valid {
|
||||||
// don't modify
|
// don't modify
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof("Adding studio '%s' to scene '%s'", t.studio.Name.String, scene.GetTitle())
|
logger.Infof("Adding studio '%s' to scene '%s'", t.studio.Name.String, s.GetTitle())
|
||||||
|
|
||||||
// set the studio id
|
// set the studio id
|
||||||
studioID := sql.NullInt64{Int64: int64(t.studio.ID), Valid: true}
|
studioID := sql.NullInt64{Int64: int64(t.studio.ID), Valid: true}
|
||||||
scenePartial := models.ScenePartial{
|
scenePartial := models.ScenePartial{
|
||||||
ID: scene.ID,
|
ID: s.ID,
|
||||||
StudioID: &studioID,
|
StudioID: &studioID,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,8 +118,8 @@ func (t *AutoTagStudioTask) autoTagStudio() {
|
||||||
}
|
}
|
||||||
|
|
||||||
type AutoTagTagTask struct {
|
type AutoTagTagTask struct {
|
||||||
|
AutoTagTask
|
||||||
tag *models.Tag
|
tag *models.Tag
|
||||||
txnManager models.TransactionManager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *AutoTagTagTask) Start(wg *sync.WaitGroup) {
|
func (t *AutoTagTagTask) Start(wg *sync.WaitGroup) {
|
||||||
|
|
@ -125,12 +129,11 @@ func (t *AutoTagTagTask) Start(wg *sync.WaitGroup) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *AutoTagTagTask) autoTagTag() {
|
func (t *AutoTagTagTask) autoTagTag() {
|
||||||
regex := getQueryRegex(t.tag.Name)
|
regex := t.getQueryRegex(t.tag.Name)
|
||||||
|
|
||||||
if err := t.txnManager.WithTxn(context.TODO(), func(r models.Repository) error {
|
if err := t.txnManager.WithTxn(context.TODO(), func(r models.Repository) error {
|
||||||
qb := r.Scene()
|
qb := r.Scene()
|
||||||
const ignoreOrganized = true
|
scenes, err := qb.QueryForAutoTag(regex, t.paths)
|
||||||
scenes, err := qb.QueryAllByPathRegex(regex, ignoreOrganized)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error querying scenes with regex '%s': %s", regex, err.Error())
|
return fmt.Errorf("Error querying scenes with regex '%s': %s", regex, err.Error())
|
||||||
|
|
|
||||||
|
|
@ -279,8 +279,10 @@ func TestParsePerformers(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
task := AutoTagPerformerTask{
|
task := AutoTagPerformerTask{
|
||||||
performer: performers[0],
|
AutoTagTask: AutoTagTask{
|
||||||
txnManager: sqlite.NewTransactionManager(),
|
txnManager: sqlite.NewTransactionManager(),
|
||||||
|
},
|
||||||
|
performer: performers[0],
|
||||||
}
|
}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
@ -327,8 +329,10 @@ func TestParseStudios(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
task := AutoTagStudioTask{
|
task := AutoTagStudioTask{
|
||||||
studio: studios[0],
|
AutoTagTask: AutoTagTask{
|
||||||
txnManager: sqlite.NewTransactionManager(),
|
txnManager: sqlite.NewTransactionManager(),
|
||||||
|
},
|
||||||
|
studio: studios[0],
|
||||||
}
|
}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
@ -374,8 +378,10 @@ func TestParseTags(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
task := AutoTagTagTask{
|
task := AutoTagTagTask{
|
||||||
tag: tags[0],
|
AutoTagTask: AutoTagTask{
|
||||||
txnManager: sqlite.NewTransactionManager(),
|
txnManager: sqlite.NewTransactionManager(),
|
||||||
|
},
|
||||||
|
tag: tags[0],
|
||||||
}
|
}
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/image"
|
"github.com/stashapp/stash/pkg/image"
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
"github.com/stashapp/stash/pkg/manager/config"
|
"github.com/stashapp/stash/pkg/manager/config"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CleanTask struct {
|
type CleanTask struct {
|
||||||
|
|
@ -198,28 +198,18 @@ func (t *CleanTask) fileExists(filename string) (bool, error) {
|
||||||
|
|
||||||
func getStashFromPath(pathToCheck string) *models.StashConfig {
|
func getStashFromPath(pathToCheck string) *models.StashConfig {
|
||||||
for _, s := range config.GetStashPaths() {
|
for _, s := range config.GetStashPaths() {
|
||||||
rel, error := filepath.Rel(s.Path, filepath.Dir(pathToCheck))
|
if utils.IsPathInDir(s.Path, filepath.Dir(pathToCheck)) {
|
||||||
|
|
||||||
if error == nil {
|
|
||||||
if !strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStashFromDirPath(pathToCheck string) *models.StashConfig {
|
func getStashFromDirPath(pathToCheck string) *models.StashConfig {
|
||||||
for _, s := range config.GetStashPaths() {
|
for _, s := range config.GetStashPaths() {
|
||||||
rel, error := filepath.Rel(s.Path, pathToCheck)
|
if utils.IsPathInDir(s.Path, pathToCheck) {
|
||||||
|
|
||||||
if error == nil {
|
|
||||||
if !strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,3 +23,35 @@ func (ff FindFilterType) GetDirection() string {
|
||||||
}
|
}
|
||||||
return direction
|
return direction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ff FindFilterType) GetPage() int {
|
||||||
|
const defaultPage = 1
|
||||||
|
if ff.Page == nil || *ff.Page < 1 {
|
||||||
|
return defaultPage
|
||||||
|
}
|
||||||
|
|
||||||
|
return *ff.Page
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ff FindFilterType) GetPageSize() int {
|
||||||
|
const defaultPerPage = 25
|
||||||
|
const minPerPage = 1
|
||||||
|
const maxPerPage = 1000
|
||||||
|
|
||||||
|
if ff.PerPage == nil {
|
||||||
|
return defaultPerPage
|
||||||
|
}
|
||||||
|
|
||||||
|
if *ff.PerPage > 1000 {
|
||||||
|
return maxPerPage
|
||||||
|
} else if *ff.PerPage < 0 {
|
||||||
|
// PerPage == 0 -> no limit
|
||||||
|
return minPerPage
|
||||||
|
}
|
||||||
|
|
||||||
|
return *ff.PerPage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ff FindFilterType) IsGetAll() bool {
|
||||||
|
return ff.PerPage != nil && *ff.PerPage == 0
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -627,13 +627,13 @@ func (_m *SceneReaderWriter) Query(sceneFilter *models.SceneFilterType, findFilt
|
||||||
return r0, r1, r2
|
return r0, r1, r2
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueryAllByPathRegex provides a mock function with given fields: regex, ignoreOrganized
|
// QueryForAutoTag provides a mock function with given fields: regex, pathPrefixes
|
||||||
func (_m *SceneReaderWriter) QueryAllByPathRegex(regex string, ignoreOrganized bool) ([]*models.Scene, error) {
|
func (_m *SceneReaderWriter) QueryForAutoTag(regex string, pathPrefixes []string) ([]*models.Scene, error) {
|
||||||
ret := _m.Called(regex, ignoreOrganized)
|
ret := _m.Called(regex, pathPrefixes)
|
||||||
|
|
||||||
var r0 []*models.Scene
|
var r0 []*models.Scene
|
||||||
if rf, ok := ret.Get(0).(func(string, bool) []*models.Scene); ok {
|
if rf, ok := ret.Get(0).(func(string, []string) []*models.Scene); ok {
|
||||||
r0 = rf(regex, ignoreOrganized)
|
r0 = rf(regex, pathPrefixes)
|
||||||
} else {
|
} else {
|
||||||
if ret.Get(0) != nil {
|
if ret.Get(0) != nil {
|
||||||
r0 = ret.Get(0).([]*models.Scene)
|
r0 = ret.Get(0).([]*models.Scene)
|
||||||
|
|
@ -641,8 +641,8 @@ func (_m *SceneReaderWriter) QueryAllByPathRegex(regex string, ignoreOrganized b
|
||||||
}
|
}
|
||||||
|
|
||||||
var r1 error
|
var r1 error
|
||||||
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
|
if rf, ok := ret.Get(1).(func(string, []string) error); ok {
|
||||||
r1 = rf(regex, ignoreOrganized)
|
r1 = rf(regex, pathPrefixes)
|
||||||
} else {
|
} else {
|
||||||
r1 = ret.Error(1)
|
r1 = ret.Error(1)
|
||||||
}
|
}
|
||||||
|
|
@ -650,36 +650,6 @@ func (_m *SceneReaderWriter) QueryAllByPathRegex(regex string, ignoreOrganized b
|
||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueryByPathRegex provides a mock function with given fields: findFilter
|
|
||||||
func (_m *SceneReaderWriter) QueryByPathRegex(findFilter *models.FindFilterType) ([]*models.Scene, int, error) {
|
|
||||||
ret := _m.Called(findFilter)
|
|
||||||
|
|
||||||
var r0 []*models.Scene
|
|
||||||
if rf, ok := ret.Get(0).(func(*models.FindFilterType) []*models.Scene); ok {
|
|
||||||
r0 = rf(findFilter)
|
|
||||||
} else {
|
|
||||||
if ret.Get(0) != nil {
|
|
||||||
r0 = ret.Get(0).([]*models.Scene)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var r1 int
|
|
||||||
if rf, ok := ret.Get(1).(func(*models.FindFilterType) int); ok {
|
|
||||||
r1 = rf(findFilter)
|
|
||||||
} else {
|
|
||||||
r1 = ret.Get(1).(int)
|
|
||||||
}
|
|
||||||
|
|
||||||
var r2 error
|
|
||||||
if rf, ok := ret.Get(2).(func(*models.FindFilterType) error); ok {
|
|
||||||
r2 = rf(findFilter)
|
|
||||||
} else {
|
|
||||||
r2 = ret.Error(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r0, r1, r2
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetOCounter provides a mock function with given fields: id
|
// ResetOCounter provides a mock function with given fields: id
|
||||||
func (_m *SceneReaderWriter) ResetOCounter(id int) (int, error) {
|
func (_m *SceneReaderWriter) ResetOCounter(id int) (int, error) {
|
||||||
ret := _m.Called(id)
|
ret := _m.Called(id)
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,8 @@ type SceneReader interface {
|
||||||
CountMissingOSHash() (int, error)
|
CountMissingOSHash() (int, error)
|
||||||
Wall(q *string) ([]*Scene, error)
|
Wall(q *string) ([]*Scene, error)
|
||||||
All() ([]*Scene, error)
|
All() ([]*Scene, error)
|
||||||
|
QueryForAutoTag(regex string, pathPrefixes []string) ([]*Scene, error)
|
||||||
Query(sceneFilter *SceneFilterType, findFilter *FindFilterType) ([]*Scene, int, error)
|
Query(sceneFilter *SceneFilterType, findFilter *FindFilterType) ([]*Scene, int, error)
|
||||||
QueryAllByPathRegex(regex string, ignoreOrganized bool) ([]*Scene, error)
|
|
||||||
QueryByPathRegex(findFilter *FindFilterType) ([]*Scene, int, error)
|
|
||||||
GetCover(sceneID int) ([]byte, error)
|
GetCover(sceneID int) ([]byte, error)
|
||||||
GetMovies(sceneID int) ([]MoviesScenes, error)
|
GetMovies(sceneID int) ([]MoviesScenes, error)
|
||||||
GetTagIDs(sceneID int) ([]int, error)
|
GetTagIDs(sceneID int) ([]int, error)
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,13 @@ func TestGalleryQueryPath(t *testing.T) {
|
||||||
pathCriterion.Modifier = models.CriterionModifierNotEquals
|
pathCriterion.Modifier = models.CriterionModifierNotEquals
|
||||||
verifyGalleriesPath(t, r.Gallery(), pathCriterion)
|
verifyGalleriesPath(t, r.Gallery(), pathCriterion)
|
||||||
|
|
||||||
|
pathCriterion.Modifier = models.CriterionModifierMatchesRegex
|
||||||
|
pathCriterion.Value = "gallery.*1_Path"
|
||||||
|
verifyGalleriesPath(t, r.Gallery(), pathCriterion)
|
||||||
|
|
||||||
|
pathCriterion.Modifier = models.CriterionModifierNotMatchesRegex
|
||||||
|
verifyGalleriesPath(t, r.Gallery(), pathCriterion)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,13 +114,20 @@ func TestImageQueryPath(t *testing.T) {
|
||||||
Modifier: models.CriterionModifierEquals,
|
Modifier: models.CriterionModifierEquals,
|
||||||
}
|
}
|
||||||
|
|
||||||
verifyImagePath(t, pathCriterion)
|
verifyImagePath(t, pathCriterion, 1)
|
||||||
|
|
||||||
pathCriterion.Modifier = models.CriterionModifierNotEquals
|
pathCriterion.Modifier = models.CriterionModifierNotEquals
|
||||||
verifyImagePath(t, pathCriterion)
|
verifyImagePath(t, pathCriterion, totalImages-1)
|
||||||
|
|
||||||
|
pathCriterion.Modifier = models.CriterionModifierMatchesRegex
|
||||||
|
pathCriterion.Value = "image_.*1_Path"
|
||||||
|
verifyImagePath(t, pathCriterion, 1) // TODO - 2 if zip path is included
|
||||||
|
|
||||||
|
pathCriterion.Modifier = models.CriterionModifierNotMatchesRegex
|
||||||
|
verifyImagePath(t, pathCriterion, totalImages-1) // TODO - -2 if zip path is included
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyImagePath(t *testing.T, pathCriterion models.StringCriterionInput) {
|
func verifyImagePath(t *testing.T, pathCriterion models.StringCriterionInput, expected int) {
|
||||||
withTxn(func(r models.Repository) error {
|
withTxn(func(r models.Repository) error {
|
||||||
sqb := r.Image()
|
sqb := r.Image()
|
||||||
imageFilter := models.ImageFilterType{
|
imageFilter := models.ImageFilterType{
|
||||||
|
|
@ -132,6 +139,8 @@ func verifyImagePath(t *testing.T, pathCriterion models.StringCriterionInput) {
|
||||||
t.Errorf("Error querying image: %s", err.Error())
|
t.Errorf("Error querying image: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, expected, len(images), "number of returned images")
|
||||||
|
|
||||||
for _, image := range images {
|
for _, image := range images {
|
||||||
verifyString(t, image.Path, pathCriterion)
|
verifyString(t, image.Path, pathCriterion)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
package sqlite
|
package sqlite
|
||||||
|
|
||||||
import "github.com/stashapp/stash/pkg/models"
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
type queryBuilder struct {
|
type queryBuilder struct {
|
||||||
repository *repository
|
repository *repository
|
||||||
|
|
@ -12,9 +16,15 @@ type queryBuilder struct {
|
||||||
args []interface{}
|
args []interface{}
|
||||||
|
|
||||||
sortAndPagination string
|
sortAndPagination string
|
||||||
|
|
||||||
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb queryBuilder) executeFind() ([]int, int, error) {
|
func (qb queryBuilder) executeFind() ([]int, int, error) {
|
||||||
|
if qb.err != nil {
|
||||||
|
return nil, 0, qb.err
|
||||||
|
}
|
||||||
|
|
||||||
return qb.repository.executeFindQuery(qb.body, qb.args, qb.sortAndPagination, qb.whereClauses, qb.havingClauses)
|
return qb.repository.executeFindQuery(qb.body, qb.args, qb.sortAndPagination, qb.whereClauses, qb.havingClauses)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,6 +76,20 @@ func (qb *queryBuilder) handleStringCriterionInput(c *models.StringCriterionInpu
|
||||||
case models.CriterionModifierNotEquals:
|
case models.CriterionModifierNotEquals:
|
||||||
qb.addWhere(column + " NOT LIKE ?")
|
qb.addWhere(column + " NOT LIKE ?")
|
||||||
qb.addArg(c.Value)
|
qb.addArg(c.Value)
|
||||||
|
case models.CriterionModifierMatchesRegex:
|
||||||
|
if _, err := regexp.Compile(c.Value); err != nil {
|
||||||
|
qb.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
qb.addWhere(column + " regexp ?")
|
||||||
|
qb.addArg(c.Value)
|
||||||
|
case models.CriterionModifierNotMatchesRegex:
|
||||||
|
if _, err := regexp.Compile(c.Value); err != nil {
|
||||||
|
qb.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
qb.addWhere(column + " NOT regexp ?")
|
||||||
|
qb.addArg(c.Value)
|
||||||
default:
|
default:
|
||||||
clause, count := getSimpleCriterionClause(modifier, "?")
|
clause, count := getSimpleCriterionClause(modifier, "?")
|
||||||
qb.addWhere(column + " " + clause)
|
qb.addWhere(column + " " + clause)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package sqlite
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
|
@ -289,6 +290,53 @@ func (qb *sceneQueryBuilder) All() ([]*models.Scene, error) {
|
||||||
return qb.queryScenes(selectAll(sceneTable)+qb.getSceneSort(nil), nil)
|
return qb.queryScenes(selectAll(sceneTable)+qb.getSceneSort(nil), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QueryForAutoTag queries for scenes whose paths match the provided regex and
|
||||||
|
// are optionally within the provided path. Excludes organized scenes.
|
||||||
|
// TODO - this should be replaced with Query once it can perform multiple
|
||||||
|
// filters on the same field.
|
||||||
|
func (qb *sceneQueryBuilder) QueryForAutoTag(regex string, pathPrefixes []string) ([]*models.Scene, error) {
|
||||||
|
var args []interface{}
|
||||||
|
body := selectDistinctIDs("scenes") + ` WHERE
|
||||||
|
scenes.path regexp ? AND
|
||||||
|
scenes.organized = 0`
|
||||||
|
|
||||||
|
args = append(args, "(?i)"+regex)
|
||||||
|
|
||||||
|
var pathClauses []string
|
||||||
|
for _, p := range pathPrefixes {
|
||||||
|
pathClauses = append(pathClauses, "scenes.path like ?")
|
||||||
|
|
||||||
|
sep := string(filepath.Separator)
|
||||||
|
if !strings.HasSuffix(p, sep) {
|
||||||
|
p = p + sep
|
||||||
|
}
|
||||||
|
args = append(args, p+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pathClauses) > 0 {
|
||||||
|
body += " AND (" + strings.Join(pathClauses, " OR ") + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
idsResult, err := qb.runIdsQuery(body, args)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var scenes []*models.Scene
|
||||||
|
for _, id := range idsResult {
|
||||||
|
scene, err := qb.Find(id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
scenes = append(scenes, scene)
|
||||||
|
}
|
||||||
|
|
||||||
|
return scenes, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *sceneQueryBuilder) Query(sceneFilter *models.SceneFilterType, findFilter *models.FindFilterType) ([]*models.Scene, int, error) {
|
func (qb *sceneQueryBuilder) Query(sceneFilter *models.SceneFilterType, findFilter *models.FindFilterType) ([]*models.Scene, int, error) {
|
||||||
if sceneFilter == nil {
|
if sceneFilter == nil {
|
||||||
sceneFilter = &models.SceneFilterType{}
|
sceneFilter = &models.SceneFilterType{}
|
||||||
|
|
@ -448,6 +496,7 @@ func (qb *sceneQueryBuilder) Query(sceneFilter *models.SceneFilterType, findFilt
|
||||||
}
|
}
|
||||||
|
|
||||||
query.sortAndPagination = qb.getSceneSort(findFilter) + getPagination(findFilter)
|
query.sortAndPagination = qb.getSceneSort(findFilter) + getPagination(findFilter)
|
||||||
|
|
||||||
idsResult, countResult, err := query.executeFind()
|
idsResult, countResult, err := query.executeFind()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
|
|
@ -501,70 +550,6 @@ func getDurationWhereClause(durationFilter models.IntCriterionInput) (string, []
|
||||||
return clause, args
|
return clause, args
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *sceneQueryBuilder) QueryAllByPathRegex(regex string, ignoreOrganized bool) ([]*models.Scene, error) {
|
|
||||||
var args []interface{}
|
|
||||||
body := selectDistinctIDs("scenes") + " WHERE scenes.path regexp ?"
|
|
||||||
|
|
||||||
if ignoreOrganized {
|
|
||||||
body += " AND scenes.organized = 0"
|
|
||||||
}
|
|
||||||
|
|
||||||
args = append(args, "(?i)"+regex)
|
|
||||||
|
|
||||||
idsResult, err := qb.runIdsQuery(body, args)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var scenes []*models.Scene
|
|
||||||
for _, id := range idsResult {
|
|
||||||
scene, err := qb.Find(id)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
scenes = append(scenes, scene)
|
|
||||||
}
|
|
||||||
|
|
||||||
return scenes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (qb *sceneQueryBuilder) QueryByPathRegex(findFilter *models.FindFilterType) ([]*models.Scene, int, error) {
|
|
||||||
if findFilter == nil {
|
|
||||||
findFilter = &models.FindFilterType{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var whereClauses []string
|
|
||||||
var havingClauses []string
|
|
||||||
var args []interface{}
|
|
||||||
body := selectDistinctIDs("scenes")
|
|
||||||
|
|
||||||
if q := findFilter.Q; q != nil && *q != "" {
|
|
||||||
whereClauses = append(whereClauses, "scenes.path regexp ?")
|
|
||||||
args = append(args, "(?i)"+*q)
|
|
||||||
}
|
|
||||||
|
|
||||||
sortAndPagination := qb.getSceneSort(findFilter) + getPagination(findFilter)
|
|
||||||
idsResult, countResult, err := qb.executeFindQuery(body, args, sortAndPagination, whereClauses, havingClauses)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var scenes []*models.Scene
|
|
||||||
for _, id := range idsResult {
|
|
||||||
scene, err := qb.Find(id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
scenes = append(scenes, scene)
|
|
||||||
}
|
|
||||||
|
|
||||||
return scenes, countResult, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (qb *sceneQueryBuilder) getSceneSort(findFilter *models.FindFilterType) string {
|
func (qb *sceneQueryBuilder) getSceneSort(findFilter *models.FindFilterType) string {
|
||||||
if findFilter == nil {
|
if findFilter == nil {
|
||||||
return " ORDER BY scenes.path, scenes.date ASC "
|
return " ORDER BY scenes.path, scenes.date ASC "
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ package sqlite_test
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -176,6 +177,13 @@ func TestSceneQueryPath(t *testing.T) {
|
||||||
|
|
||||||
pathCriterion.Modifier = models.CriterionModifierNotEquals
|
pathCriterion.Modifier = models.CriterionModifierNotEquals
|
||||||
verifyScenesPath(t, pathCriterion)
|
verifyScenesPath(t, pathCriterion)
|
||||||
|
|
||||||
|
pathCriterion.Modifier = models.CriterionModifierMatchesRegex
|
||||||
|
pathCriterion.Value = "scene_.*1_Path"
|
||||||
|
verifyScenesPath(t, pathCriterion)
|
||||||
|
|
||||||
|
pathCriterion.Modifier = models.CriterionModifierNotMatchesRegex
|
||||||
|
verifyScenesPath(t, pathCriterion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyScenesPath(t *testing.T, pathCriterion models.StringCriterionInput) {
|
func verifyScenesPath(t *testing.T, pathCriterion models.StringCriterionInput) {
|
||||||
|
|
@ -221,6 +229,12 @@ func verifyString(t *testing.T, value string, criterion models.StringCriterionIn
|
||||||
if criterion.Modifier == models.CriterionModifierNotEquals {
|
if criterion.Modifier == models.CriterionModifierNotEquals {
|
||||||
assert.NotEqual(criterion.Value, value)
|
assert.NotEqual(criterion.Value, value)
|
||||||
}
|
}
|
||||||
|
if criterion.Modifier == models.CriterionModifierMatchesRegex {
|
||||||
|
assert.Regexp(regexp.MustCompile(criterion.Value), value)
|
||||||
|
}
|
||||||
|
if criterion.Modifier == models.CriterionModifierNotMatchesRegex {
|
||||||
|
assert.NotRegexp(regexp.MustCompile(criterion.Value), value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSceneQueryRating(t *testing.T) {
|
func TestSceneQueryRating(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const totalScenes = 12
|
const totalScenes = 12
|
||||||
const totalImages = 6
|
const totalImages = 6 // TODO - add one for zip file
|
||||||
const performersNameCase = 6
|
const performersNameCase = 6
|
||||||
const performersNameNoCase = 2
|
const performersNameNoCase = 2
|
||||||
const moviesNameCase = 2
|
const moviesNameCase = 2
|
||||||
|
|
@ -61,6 +61,7 @@ const imageIdxWithTwoPerformers = 2
|
||||||
const imageIdxWithTag = 3
|
const imageIdxWithTag = 3
|
||||||
const imageIdxWithTwoTags = 4
|
const imageIdxWithTwoTags = 4
|
||||||
const imageIdxWithStudio = 5
|
const imageIdxWithStudio = 5
|
||||||
|
const imageIdxInZip = 6
|
||||||
|
|
||||||
const performerIdxWithScene = 0
|
const performerIdxWithScene = 0
|
||||||
const performerIdx1WithScene = 1
|
const performerIdx1WithScene = 1
|
||||||
|
|
@ -110,6 +111,7 @@ const markerIdxWithScene = 0
|
||||||
const pathField = "Path"
|
const pathField = "Path"
|
||||||
const checksumField = "Checksum"
|
const checksumField = "Checksum"
|
||||||
const titleField = "Title"
|
const titleField = "Title"
|
||||||
|
const zipPath = "zipPath.zip"
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
ret := runTests(m)
|
ret := runTests(m)
|
||||||
|
|
@ -318,10 +320,19 @@ func getImageStringValue(index int, field string) string {
|
||||||
return fmt.Sprintf("image_%04d_%s", index, field)
|
return fmt.Sprintf("image_%04d_%s", index, field)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getImagePath(index int) string {
|
||||||
|
// TODO - currently not working
|
||||||
|
// if index == imageIdxInZip {
|
||||||
|
// return image.ZipFilename(zipPath, "image_0001_Path")
|
||||||
|
// }
|
||||||
|
|
||||||
|
return getImageStringValue(index, pathField)
|
||||||
|
}
|
||||||
|
|
||||||
func createImages(qb models.ImageReaderWriter, n int) error {
|
func createImages(qb models.ImageReaderWriter, n int) error {
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
image := models.Image{
|
image := models.Image{
|
||||||
Path: getImageStringValue(i, pathField),
|
Path: getImagePath(i),
|
||||||
Title: sql.NullString{String: getImageStringValue(i, titleField), Valid: true},
|
Title: sql.NullString{String: getImageStringValue(i, titleField), Valid: true},
|
||||||
Checksum: getImageStringValue(i, checksumField),
|
Checksum: getImageStringValue(i, checksumField),
|
||||||
Rating: getRating(i),
|
Rating: getRating(i),
|
||||||
|
|
|
||||||
|
|
@ -32,26 +32,14 @@ func getPagination(findFilter *models.FindFilterType) string {
|
||||||
panic("nil find filter for pagination")
|
panic("nil find filter for pagination")
|
||||||
}
|
}
|
||||||
|
|
||||||
var page int
|
if findFilter.IsGetAll() {
|
||||||
if findFilter.Page == nil || *findFilter.Page < 1 {
|
return " "
|
||||||
page = 1
|
|
||||||
} else {
|
|
||||||
page = *findFilter.Page
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var perPage int
|
return getPaginationSQL(findFilter.GetPage(), findFilter.GetPageSize())
|
||||||
if findFilter.PerPage == nil {
|
|
||||||
perPage = 25
|
|
||||||
} else {
|
|
||||||
perPage = *findFilter.PerPage
|
|
||||||
}
|
|
||||||
|
|
||||||
if perPage > 1000 {
|
|
||||||
perPage = 1000
|
|
||||||
} else if perPage < 1 {
|
|
||||||
perPage = 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getPaginationSQL(page int, perPage int) string {
|
||||||
page = (page - 1) * perPage
|
page = (page - 1) * perPage
|
||||||
return " LIMIT " + strconv.Itoa(perPage) + " OFFSET " + strconv.Itoa(page) + " "
|
return " LIMIT " + strconv.Itoa(perPage) + " OFFSET " + strconv.Itoa(page) + " "
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"os/user"
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/h2non/filetype"
|
"github.com/h2non/filetype"
|
||||||
"github.com/h2non/filetype/types"
|
"github.com/h2non/filetype/types"
|
||||||
|
|
@ -269,3 +270,16 @@ func MatchEntries(dir, pattern string) ([]string, error) {
|
||||||
}
|
}
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsPathInDir returns true if pathToCheck is within dir.
|
||||||
|
func IsPathInDir(dir, pathToCheck string) bool {
|
||||||
|
rel, err := filepath.Rel(dir, pathToCheck)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
if !strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@
|
||||||
* Allow configuration of visible navbar items.
|
* Allow configuration of visible navbar items.
|
||||||
|
|
||||||
### 🎨 Improvements
|
### 🎨 Improvements
|
||||||
|
* Add directory selection to auto-tag task.
|
||||||
|
* Add string matches/not matches regex filter criteria.
|
||||||
* Added configuration option for import file size limit and increased default to 1GB.
|
* Added configuration option for import file size limit and increased default to 1GB.
|
||||||
* Add dry-run option for Clean task.
|
* Add dry-run option for Clean task.
|
||||||
* Refresh UI when changing custom CSS options.
|
* Refresh UI when changing custom CSS options.
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ import { useConfiguration } from "src/core/StashService";
|
||||||
import { Icon, Modal } from "src/components/Shared";
|
import { Icon, Modal } from "src/components/Shared";
|
||||||
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
|
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
|
||||||
|
|
||||||
interface IScanDialogProps {
|
interface IDirectorySelectionDialogProps {
|
||||||
onClose: (paths?: string[]) => void;
|
onClose: (paths?: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ScanDialog: React.FC<IScanDialogProps> = (
|
export const DirectorySelectionDialog: React.FC<IDirectorySelectionDialogProps> = (
|
||||||
props: IScanDialogProps
|
props: IDirectorySelectionDialogProps
|
||||||
) => {
|
) => {
|
||||||
const { data } = useConfiguration();
|
const { data } = useConfiguration();
|
||||||
|
|
||||||
|
|
@ -33,12 +33,12 @@ export const ScanDialog: React.FC<IScanDialogProps> = (
|
||||||
show
|
show
|
||||||
disabled={paths.length === 0}
|
disabled={paths.length === 0}
|
||||||
icon="pencil-alt"
|
icon="pencil-alt"
|
||||||
header="Select folders to scan"
|
header="Select folders"
|
||||||
accept={{
|
accept={{
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
props.onClose(paths);
|
props.onClose(paths);
|
||||||
},
|
},
|
||||||
text: "Scan",
|
text: "Confirm",
|
||||||
}}
|
}}
|
||||||
cancel={{
|
cancel={{
|
||||||
onClick: () => props.onClose(),
|
onClick: () => props.onClose(),
|
||||||
|
|
@ -21,7 +21,7 @@ import { LoadingIndicator, Modal } from "src/components/Shared";
|
||||||
import { downloadFile } from "src/utils";
|
import { downloadFile } from "src/utils";
|
||||||
import { GenerateButton } from "./GenerateButton";
|
import { GenerateButton } from "./GenerateButton";
|
||||||
import { ImportDialog } from "./ImportDialog";
|
import { ImportDialog } from "./ImportDialog";
|
||||||
import { ScanDialog } from "./ScanDialog";
|
import { DirectorySelectionDialog } from "./DirectorySelectionDialog";
|
||||||
|
|
||||||
type Plugin = Pick<GQL.Plugin, "id">;
|
type Plugin = Pick<GQL.Plugin, "id">;
|
||||||
type PluginTask = Pick<GQL.PluginTask, "name" | "description">;
|
type PluginTask = Pick<GQL.PluginTask, "name" | "description">;
|
||||||
|
|
@ -32,6 +32,9 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||||
const [isCleanAlertOpen, setIsCleanAlertOpen] = useState<boolean>(false);
|
const [isCleanAlertOpen, setIsCleanAlertOpen] = useState<boolean>(false);
|
||||||
const [isImportDialogOpen, setIsImportDialogOpen] = useState<boolean>(false);
|
const [isImportDialogOpen, setIsImportDialogOpen] = useState<boolean>(false);
|
||||||
const [isScanDialogOpen, setIsScanDialogOpen] = useState<boolean>(false);
|
const [isScanDialogOpen, setIsScanDialogOpen] = useState<boolean>(false);
|
||||||
|
const [isAutoTagDialogOpen, setIsAutoTagDialogOpen] = useState<boolean>(
|
||||||
|
false
|
||||||
|
);
|
||||||
const [isBackupRunning, setIsBackupRunning] = useState<boolean>(false);
|
const [isBackupRunning, setIsBackupRunning] = useState<boolean>(false);
|
||||||
const [useFileMetadata, setUseFileMetadata] = useState<boolean>(false);
|
const [useFileMetadata, setUseFileMetadata] = useState<boolean>(false);
|
||||||
const [stripFileExtension, setStripFileExtension] = useState<boolean>(false);
|
const [stripFileExtension, setStripFileExtension] = useState<boolean>(false);
|
||||||
|
|
@ -183,7 +186,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ScanDialog onClose={onScanDialogClosed} />;
|
return <DirectorySelectionDialog onClose={onScanDialogClosed} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onScanDialogClosed(paths?: string[]) {
|
function onScanDialogClosed(paths?: string[]) {
|
||||||
|
|
@ -211,18 +214,35 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAutoTagInput() {
|
function renderAutoTagDialog() {
|
||||||
|
if (!isAutoTagDialogOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <DirectorySelectionDialog onClose={onAutoTagDialogClosed} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAutoTagDialogClosed(paths?: string[]) {
|
||||||
|
if (paths) {
|
||||||
|
onAutoTag(paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAutoTagDialogOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAutoTagInput(paths?: string[]) {
|
||||||
const wildcard = ["*"];
|
const wildcard = ["*"];
|
||||||
return {
|
return {
|
||||||
|
paths,
|
||||||
performers: autoTagPerformers ? wildcard : [],
|
performers: autoTagPerformers ? wildcard : [],
|
||||||
studios: autoTagStudios ? wildcard : [],
|
studios: autoTagStudios ? wildcard : [],
|
||||||
tags: autoTagTags ? wildcard : [],
|
tags: autoTagTags ? wildcard : [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onAutoTag() {
|
async function onAutoTag(paths?: string[]) {
|
||||||
try {
|
try {
|
||||||
await mutateMetadataAutoTag(getAutoTagInput());
|
await mutateMetadataAutoTag(getAutoTagInput(paths));
|
||||||
Toast.success({ content: "Started auto tagging" });
|
Toast.success({ content: "Started auto tagging" });
|
||||||
jobStatus.refetch();
|
jobStatus.refetch();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -347,6 +367,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||||
{renderCleanAlert()}
|
{renderCleanAlert()}
|
||||||
{renderImportDialog()}
|
{renderImportDialog()}
|
||||||
{renderScanDialog()}
|
{renderScanDialog()}
|
||||||
|
{renderAutoTagDialog()}
|
||||||
|
|
||||||
<h4>Running Jobs</h4>
|
<h4>Running Jobs</h4>
|
||||||
|
|
||||||
|
|
@ -440,9 +461,21 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Button variant="secondary" type="submit" onClick={() => onAutoTag()}>
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="submit"
|
||||||
|
className="mr-2"
|
||||||
|
onClick={() => onAutoTag()}
|
||||||
|
>
|
||||||
Auto Tag
|
Auto Tag
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="submit"
|
||||||
|
onClick={() => setIsAutoTagDialogOpen(true)}
|
||||||
|
>
|
||||||
|
Selective Auto Tag
|
||||||
|
</Button>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Auto-tag content based on filenames.
|
Auto-tag content based on filenames.
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,16 @@ export abstract class Criterion {
|
||||||
return { value: CriterionModifier.Includes, label: "Includes" };
|
return { value: CriterionModifier.Includes, label: "Includes" };
|
||||||
case CriterionModifier.Excludes:
|
case CriterionModifier.Excludes:
|
||||||
return { value: CriterionModifier.Excludes, label: "Excludes" };
|
return { value: CriterionModifier.Excludes, label: "Excludes" };
|
||||||
|
case CriterionModifier.MatchesRegex:
|
||||||
|
return {
|
||||||
|
value: CriterionModifier.MatchesRegex,
|
||||||
|
label: "Matches Regex",
|
||||||
|
};
|
||||||
|
case CriterionModifier.NotMatchesRegex:
|
||||||
|
return {
|
||||||
|
value: CriterionModifier.NotMatchesRegex,
|
||||||
|
label: "Not Matches Regex",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,6 +205,12 @@ export abstract class Criterion {
|
||||||
case CriterionModifier.Excludes:
|
case CriterionModifier.Excludes:
|
||||||
modifierString = "excludes";
|
modifierString = "excludes";
|
||||||
break;
|
break;
|
||||||
|
case CriterionModifier.MatchesRegex:
|
||||||
|
modifierString = "matches regex";
|
||||||
|
break;
|
||||||
|
case CriterionModifier.NotMatchesRegex:
|
||||||
|
modifierString = "not matches regex";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
modifierString = "";
|
modifierString = "";
|
||||||
}
|
}
|
||||||
|
|
@ -215,18 +231,18 @@ export abstract class Criterion {
|
||||||
return `${this.parameterName}-${this.modifier.toString()}`; // TODO add values?
|
return `${this.parameterName}-${this.modifier.toString()}`; // TODO add values?
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
private static replaceSpecialCharacter(str: string, c: string) {
|
||||||
public set(modifier: CriterionModifier, value: Value) {
|
return str.replaceAll(c, encodeURIComponent(c));
|
||||||
this.modifier = modifier;
|
|
||||||
if (Array.isArray(this.value)) {
|
|
||||||
this.value.push(value);
|
|
||||||
} else {
|
|
||||||
this.value = value;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
public encodeValue(): CriterionValue {
|
public encodeValue(): CriterionValue {
|
||||||
|
// replace certain characters
|
||||||
|
if (typeof this.value === "string") {
|
||||||
|
let ret = this.value;
|
||||||
|
ret = Criterion.replaceSpecialCharacter(ret, "&");
|
||||||
|
ret = Criterion.replaceSpecialCharacter(ret, "+");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
return this.value;
|
return this.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -257,6 +273,8 @@ export class StringCriterion extends Criterion {
|
||||||
StringCriterion.getModifierOption(CriterionModifier.Excludes),
|
StringCriterion.getModifierOption(CriterionModifier.Excludes),
|
||||||
StringCriterion.getModifierOption(CriterionModifier.IsNull),
|
StringCriterion.getModifierOption(CriterionModifier.IsNull),
|
||||||
StringCriterion.getModifierOption(CriterionModifier.NotNull),
|
StringCriterion.getModifierOption(CriterionModifier.NotNull),
|
||||||
|
StringCriterion.getModifierOption(CriterionModifier.MatchesRegex),
|
||||||
|
StringCriterion.getModifierOption(CriterionModifier.NotMatchesRegex),
|
||||||
];
|
];
|
||||||
public options: string[] | undefined;
|
public options: string[] | undefined;
|
||||||
public value: string = "";
|
public value: string = "";
|
||||||
|
|
@ -286,6 +304,8 @@ export class MandatoryStringCriterion extends StringCriterion {
|
||||||
StringCriterion.getModifierOption(CriterionModifier.NotEquals),
|
StringCriterion.getModifierOption(CriterionModifier.NotEquals),
|
||||||
StringCriterion.getModifierOption(CriterionModifier.Includes),
|
StringCriterion.getModifierOption(CriterionModifier.Includes),
|
||||||
StringCriterion.getModifierOption(CriterionModifier.Excludes),
|
StringCriterion.getModifierOption(CriterionModifier.Excludes),
|
||||||
|
StringCriterion.getModifierOption(CriterionModifier.MatchesRegex),
|
||||||
|
StringCriterion.getModifierOption(CriterionModifier.NotMatchesRegex),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue