[Files Refactor] Performance tuning (#2819)

* Load scene relationships on demand
* Load image relationships on demand
* Load gallery relationships on demand
* Add dataloaden
* Use dataloaders
* Use where in for other find many functions
This commit is contained in:
WithoutPants
2022-08-12 12:21:46 +10:00
parent 9b31b20fed
commit 00608c167a
317 changed files with 28002 additions and 14875 deletions

View File

@@ -162,6 +162,10 @@ generate-frontend:
generate-backend: touch-ui
go generate -mod=vendor ./cmd/stash
.PHONY: generate-dataloaders
generate-dataloaders:
go generate -mod=vendor ./internal/api/loaders
# Regenerates stash-box client files
.PHONY: generate-stash-box-client
generate-stash-box-client:

10
go.mod
View File

@@ -36,11 +36,11 @@ require (
github.com/vektra/mockery/v2 v2.10.0
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb
golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9
golang.org/x/sys v0.0.0-20220329152356-43be30ef3008
golang.org/x/net v0.0.0-20220722155237-a158d28d115b
golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/text v0.3.7
golang.org/x/tools v0.1.10 // indirect
golang.org/x/tools v0.1.12 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/yaml.v2 v2.4.0
)
@@ -56,6 +56,7 @@ require (
github.com/lucasb-eyer/go-colorful v1.2.0
github.com/spf13/cast v1.4.1
github.com/vearutop/statigz v1.1.6
github.com/vektah/dataloaden v0.3.0
github.com/vektah/gqlparser/v2 v2.4.1
gopkg.in/guregu/null.v4 v4.0.0
)
@@ -100,8 +101,7 @@ require (
github.com/tidwall/match v1.1.1 // indirect
github.com/urfave/cli/v2 v2.4.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)

22
go.sum
View File

@@ -753,6 +753,8 @@ github.com/urfave/cli/v2 v2.4.0 h1:m2pxjjDFgDxSPtO8WSdbndj17Wu2y8vOT86wE/tjr+I=
github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg=
github.com/vearutop/statigz v1.1.6 h1:si1zvulh/6P4S/SjFticuKQ8/EgQISglaRuycj8PWso=
github.com/vearutop/statigz v1.1.6/go.mod h1:czAv7iXgPv/s+xsgXpVEhhD0NSOQ4wZPgmM/n7LANDI=
github.com/vektah/dataloaden v0.3.0 h1:ZfVN2QD6swgvp+tDqdH/OIT/wu3Dhu0cus0k5gIZS84=
github.com/vektah/dataloaden v0.3.0/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U=
github.com/vektah/gqlparser/v2 v2.4.0/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
github.com/vektah/gqlparser/v2 v2.4.1 h1:QOyEn8DAPMUMARGMeshKDkDgNmVoEaEGiDB0uWxcSlQ=
github.com/vektah/gqlparser/v2 v2.4.1/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
@@ -771,6 +773,7 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
@@ -863,8 +866,9 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -920,8 +924,8 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9 h1:0qxwC5n+ttVOINCBeRHO0nq9X7uy8SDsPoi5OaCdIEI=
golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -954,6 +958,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1047,8 +1052,10 @@ golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220329152356-43be30ef3008 h1:pq9pwoi2rjLWvmiVser/lIOgiyA3fli4M+RfGVMA7nE=
golang.org/x/sys v0.0.0-20220329152356-43be30ef3008/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 h1:v1W7bwXHsnLLloWYTVEdvGvA7BHMeBYsPcF0GLDxIRs=
golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1078,6 +1085,7 @@ golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
@@ -1135,14 +1143,14 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=

View File

@@ -0,0 +1,187 @@
//go:generate go run -mod=vendor github.com/vektah/dataloaden SceneLoader int *github.com/stashapp/stash/pkg/models.Scene
//go:generate go run -mod=vendor github.com/vektah/dataloaden GalleryLoader int *github.com/stashapp/stash/pkg/models.Gallery
//go:generate go run -mod=vendor github.com/vektah/dataloaden ImageLoader int *github.com/stashapp/stash/pkg/models.Image
//go:generate go run -mod=vendor github.com/vektah/dataloaden PerformerLoader int *github.com/stashapp/stash/pkg/models.Performer
//go:generate go run -mod=vendor github.com/vektah/dataloaden StudioLoader int *github.com/stashapp/stash/pkg/models.Studio
//go:generate go run -mod=vendor github.com/vektah/dataloaden TagLoader int *github.com/stashapp/stash/pkg/models.Tag
//go:generate go run -mod=vendor github.com/vektah/dataloaden MovieLoader int *github.com/stashapp/stash/pkg/models.Movie
package loaders
import (
"context"
"net/http"
"time"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/txn"
)
type contextKey struct{ name string }
var (
loadersCtxKey = &contextKey{"loaders"}
)
const (
wait = 1 * time.Millisecond
maxBatch = 100
)
type Loaders struct {
SceneByID *SceneLoader
GalleryByID *GalleryLoader
ImageByID *ImageLoader
PerformerByID *PerformerLoader
StudioByID *StudioLoader
TagByID *TagLoader
MovieByID *MovieLoader
}
type Middleware struct {
DatabaseProvider txn.DatabaseProvider
Repository manager.Repository
}
func (m Middleware) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ldrs := Loaders{
SceneByID: &SceneLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchScenes(ctx),
},
GalleryByID: &GalleryLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchGalleries(ctx),
},
ImageByID: &ImageLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchImages(ctx),
},
PerformerByID: &PerformerLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchPerformers(ctx),
},
StudioByID: &StudioLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchStudios(ctx),
},
TagByID: &TagLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchTags(ctx),
},
MovieByID: &MovieLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchMovies(ctx),
},
}
newCtx := context.WithValue(r.Context(), loadersCtxKey, ldrs)
next.ServeHTTP(w, r.WithContext(newCtx))
})
}
func From(ctx context.Context) Loaders {
return ctx.Value(loadersCtxKey).(Loaders)
}
func toErrorSlice(err error) []error {
if err != nil {
return []error{err}
}
return nil
}
func (m Middleware) withTxn(ctx context.Context, fn func(ctx context.Context) error) error {
return txn.WithDatabase(ctx, m.DatabaseProvider, fn)
}
func (m Middleware) fetchScenes(ctx context.Context) func(keys []int) ([]*models.Scene, []error) {
return func(keys []int) (ret []*models.Scene, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Scene.FindMany(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchImages(ctx context.Context) func(keys []int) ([]*models.Image, []error) {
return func(keys []int) (ret []*models.Image, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Image.FindMany(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchGalleries(ctx context.Context) func(keys []int) ([]*models.Gallery, []error) {
return func(keys []int) (ret []*models.Gallery, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Gallery.FindMany(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchPerformers(ctx context.Context) func(keys []int) ([]*models.Performer, []error) {
return func(keys []int) (ret []*models.Performer, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Performer.FindMany(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchStudios(ctx context.Context) func(keys []int) ([]*models.Studio, []error) {
return func(keys []int) (ret []*models.Studio, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Studio.FindMany(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchTags(ctx context.Context) func(keys []int) ([]*models.Tag, []error) {
return func(keys []int) (ret []*models.Tag, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Tag.FindMany(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchMovies(ctx context.Context) func(keys []int) ([]*models.Movie, []error) {
return func(keys []int) (ret []*models.Movie, errs []error) {
err := m.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Movie.FindMany(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}

View File

@@ -0,0 +1,224 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
package loaders
import (
"sync"
"time"
"github.com/stashapp/stash/pkg/models"
)
// GalleryLoaderConfig captures the config to create a new GalleryLoader
type GalleryLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([]*models.Gallery, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
MaxBatch int
}
// NewGalleryLoader creates a new GalleryLoader given a fetch, wait, and maxBatch
func NewGalleryLoader(config GalleryLoaderConfig) *GalleryLoader {
return &GalleryLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// GalleryLoader batches and caches requests
type GalleryLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([]*models.Gallery, []error)
// how long to done before sending a batch
wait time.Duration
// this will limit the maximum number of keys to send in one batch, 0 = no limit
maxBatch int
// INTERNAL
// lazily created cache
cache map[int]*models.Gallery
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *galleryLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type galleryLoaderBatch struct {
keys []int
data []*models.Gallery
error []error
closing bool
done chan struct{}
}
// Load a Gallery by key, batching and caching will be applied automatically
func (l *GalleryLoader) Load(key int) (*models.Gallery, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a Gallery.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *GalleryLoader) LoadThunk(key int) func() (*models.Gallery, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() (*models.Gallery, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &galleryLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() (*models.Gallery, error) {
<-batch.done
var data *models.Gallery
if pos < len(batch.data) {
data = batch.data[pos]
}
var err error
// its convenient to be able to return a single error for everything
if len(batch.error) == 1 {
err = batch.error[0]
} else if batch.error != nil {
err = batch.error[pos]
}
if err == nil {
l.mu.Lock()
l.unsafeSet(key, data)
l.mu.Unlock()
}
return data, err
}
}
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *GalleryLoader) LoadAll(keys []int) ([]*models.Gallery, []error) {
results := make([]func() (*models.Gallery, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
gallerys := make([]*models.Gallery, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
gallerys[i], errors[i] = thunk()
}
return gallerys, errors
}
// LoadAllThunk returns a function that when called will block waiting for a Gallerys.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *GalleryLoader) LoadAllThunk(keys []int) func() ([]*models.Gallery, []error) {
results := make([]func() (*models.Gallery, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([]*models.Gallery, []error) {
gallerys := make([]*models.Gallery, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
gallerys[i], errors[i] = thunk()
}
return gallerys, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *GalleryLoader) Prime(key int, value *models.Gallery) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
// and end up with the whole cache pointing to the same value.
cpy := *value
l.unsafeSet(key, &cpy)
}
l.mu.Unlock()
return !found
}
// Clear the value at key from the cache, if it exists
func (l *GalleryLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *GalleryLoader) unsafeSet(key int, value *models.Gallery) {
if l.cache == nil {
l.cache = map[int]*models.Gallery{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *galleryLoaderBatch) keyIndex(l *GalleryLoader, key int) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
}
}
pos := len(b.keys)
b.keys = append(b.keys, key)
if pos == 0 {
go b.startTimer(l)
}
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
if !b.closing {
b.closing = true
l.batch = nil
go b.end(l)
}
}
return pos
}
func (b *galleryLoaderBatch) startTimer(l *GalleryLoader) {
time.Sleep(l.wait)
l.mu.Lock()
// we must have hit a batch limit and are already finalizing this batch
if b.closing {
l.mu.Unlock()
return
}
l.batch = nil
l.mu.Unlock()
b.end(l)
}
func (b *galleryLoaderBatch) end(l *GalleryLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -0,0 +1,224 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
package loaders
import (
"sync"
"time"
"github.com/stashapp/stash/pkg/models"
)
// ImageLoaderConfig captures the config to create a new ImageLoader
type ImageLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([]*models.Image, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
MaxBatch int
}
// NewImageLoader creates a new ImageLoader given a fetch, wait, and maxBatch
func NewImageLoader(config ImageLoaderConfig) *ImageLoader {
return &ImageLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// ImageLoader batches and caches requests
type ImageLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([]*models.Image, []error)
// how long to done before sending a batch
wait time.Duration
// this will limit the maximum number of keys to send in one batch, 0 = no limit
maxBatch int
// INTERNAL
// lazily created cache
cache map[int]*models.Image
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *imageLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type imageLoaderBatch struct {
keys []int
data []*models.Image
error []error
closing bool
done chan struct{}
}
// Load a Image by key, batching and caching will be applied automatically
func (l *ImageLoader) Load(key int) (*models.Image, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a Image.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *ImageLoader) LoadThunk(key int) func() (*models.Image, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() (*models.Image, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &imageLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() (*models.Image, error) {
<-batch.done
var data *models.Image
if pos < len(batch.data) {
data = batch.data[pos]
}
var err error
// its convenient to be able to return a single error for everything
if len(batch.error) == 1 {
err = batch.error[0]
} else if batch.error != nil {
err = batch.error[pos]
}
if err == nil {
l.mu.Lock()
l.unsafeSet(key, data)
l.mu.Unlock()
}
return data, err
}
}
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *ImageLoader) LoadAll(keys []int) ([]*models.Image, []error) {
results := make([]func() (*models.Image, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
images := make([]*models.Image, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
images[i], errors[i] = thunk()
}
return images, errors
}
// LoadAllThunk returns a function that when called will block waiting for a Images.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *ImageLoader) LoadAllThunk(keys []int) func() ([]*models.Image, []error) {
results := make([]func() (*models.Image, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([]*models.Image, []error) {
images := make([]*models.Image, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
images[i], errors[i] = thunk()
}
return images, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *ImageLoader) Prime(key int, value *models.Image) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
// and end up with the whole cache pointing to the same value.
cpy := *value
l.unsafeSet(key, &cpy)
}
l.mu.Unlock()
return !found
}
// Clear the value at key from the cache, if it exists
func (l *ImageLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *ImageLoader) unsafeSet(key int, value *models.Image) {
if l.cache == nil {
l.cache = map[int]*models.Image{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *imageLoaderBatch) keyIndex(l *ImageLoader, key int) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
}
}
pos := len(b.keys)
b.keys = append(b.keys, key)
if pos == 0 {
go b.startTimer(l)
}
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
if !b.closing {
b.closing = true
l.batch = nil
go b.end(l)
}
}
return pos
}
func (b *imageLoaderBatch) startTimer(l *ImageLoader) {
time.Sleep(l.wait)
l.mu.Lock()
// we must have hit a batch limit and are already finalizing this batch
if b.closing {
l.mu.Unlock()
return
}
l.batch = nil
l.mu.Unlock()
b.end(l)
}
func (b *imageLoaderBatch) end(l *ImageLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -0,0 +1,224 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
package loaders
import (
"sync"
"time"
"github.com/stashapp/stash/pkg/models"
)
// MovieLoaderConfig captures the config to create a new MovieLoader
type MovieLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([]*models.Movie, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
MaxBatch int
}
// NewMovieLoader creates a new MovieLoader given a fetch, wait, and maxBatch
func NewMovieLoader(config MovieLoaderConfig) *MovieLoader {
return &MovieLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// MovieLoader batches and caches requests
type MovieLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([]*models.Movie, []error)
// how long to done before sending a batch
wait time.Duration
// this will limit the maximum number of keys to send in one batch, 0 = no limit
maxBatch int
// INTERNAL
// lazily created cache
cache map[int]*models.Movie
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *movieLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type movieLoaderBatch struct {
keys []int
data []*models.Movie
error []error
closing bool
done chan struct{}
}
// Load a Movie by key, batching and caching will be applied automatically
func (l *MovieLoader) Load(key int) (*models.Movie, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a Movie.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *MovieLoader) LoadThunk(key int) func() (*models.Movie, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() (*models.Movie, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &movieLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() (*models.Movie, error) {
<-batch.done
var data *models.Movie
if pos < len(batch.data) {
data = batch.data[pos]
}
var err error
// its convenient to be able to return a single error for everything
if len(batch.error) == 1 {
err = batch.error[0]
} else if batch.error != nil {
err = batch.error[pos]
}
if err == nil {
l.mu.Lock()
l.unsafeSet(key, data)
l.mu.Unlock()
}
return data, err
}
}
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *MovieLoader) LoadAll(keys []int) ([]*models.Movie, []error) {
results := make([]func() (*models.Movie, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
movies := make([]*models.Movie, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
movies[i], errors[i] = thunk()
}
return movies, errors
}
// LoadAllThunk returns a function that when called will block waiting for a Movies.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *MovieLoader) LoadAllThunk(keys []int) func() ([]*models.Movie, []error) {
results := make([]func() (*models.Movie, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([]*models.Movie, []error) {
movies := make([]*models.Movie, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
movies[i], errors[i] = thunk()
}
return movies, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *MovieLoader) Prime(key int, value *models.Movie) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
// and end up with the whole cache pointing to the same value.
cpy := *value
l.unsafeSet(key, &cpy)
}
l.mu.Unlock()
return !found
}
// Clear the value at key from the cache, if it exists
func (l *MovieLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *MovieLoader) unsafeSet(key int, value *models.Movie) {
if l.cache == nil {
l.cache = map[int]*models.Movie{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *movieLoaderBatch) keyIndex(l *MovieLoader, key int) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
}
}
pos := len(b.keys)
b.keys = append(b.keys, key)
if pos == 0 {
go b.startTimer(l)
}
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
if !b.closing {
b.closing = true
l.batch = nil
go b.end(l)
}
}
return pos
}
func (b *movieLoaderBatch) startTimer(l *MovieLoader) {
time.Sleep(l.wait)
l.mu.Lock()
// we must have hit a batch limit and are already finalizing this batch
if b.closing {
l.mu.Unlock()
return
}
l.batch = nil
l.mu.Unlock()
b.end(l)
}
func (b *movieLoaderBatch) end(l *MovieLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -0,0 +1,224 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
package loaders
import (
"sync"
"time"
"github.com/stashapp/stash/pkg/models"
)
// PerformerLoaderConfig captures the config to create a new PerformerLoader
type PerformerLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([]*models.Performer, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
MaxBatch int
}
// NewPerformerLoader creates a new PerformerLoader given a fetch, wait, and maxBatch
func NewPerformerLoader(config PerformerLoaderConfig) *PerformerLoader {
return &PerformerLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// PerformerLoader batches and caches requests
type PerformerLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([]*models.Performer, []error)
// how long to done before sending a batch
wait time.Duration
// this will limit the maximum number of keys to send in one batch, 0 = no limit
maxBatch int
// INTERNAL
// lazily created cache
cache map[int]*models.Performer
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *performerLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type performerLoaderBatch struct {
keys []int
data []*models.Performer
error []error
closing bool
done chan struct{}
}
// Load a Performer by key, batching and caching will be applied automatically
func (l *PerformerLoader) Load(key int) (*models.Performer, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a Performer.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *PerformerLoader) LoadThunk(key int) func() (*models.Performer, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() (*models.Performer, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &performerLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() (*models.Performer, error) {
<-batch.done
var data *models.Performer
if pos < len(batch.data) {
data = batch.data[pos]
}
var err error
// its convenient to be able to return a single error for everything
if len(batch.error) == 1 {
err = batch.error[0]
} else if batch.error != nil {
err = batch.error[pos]
}
if err == nil {
l.mu.Lock()
l.unsafeSet(key, data)
l.mu.Unlock()
}
return data, err
}
}
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *PerformerLoader) LoadAll(keys []int) ([]*models.Performer, []error) {
results := make([]func() (*models.Performer, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
performers := make([]*models.Performer, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
performers[i], errors[i] = thunk()
}
return performers, errors
}
// LoadAllThunk returns a function that when called will block waiting for a Performers.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *PerformerLoader) LoadAllThunk(keys []int) func() ([]*models.Performer, []error) {
results := make([]func() (*models.Performer, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([]*models.Performer, []error) {
performers := make([]*models.Performer, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
performers[i], errors[i] = thunk()
}
return performers, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *PerformerLoader) Prime(key int, value *models.Performer) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
// and end up with the whole cache pointing to the same value.
cpy := *value
l.unsafeSet(key, &cpy)
}
l.mu.Unlock()
return !found
}
// Clear the value at key from the cache, if it exists
func (l *PerformerLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *PerformerLoader) unsafeSet(key int, value *models.Performer) {
if l.cache == nil {
l.cache = map[int]*models.Performer{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *performerLoaderBatch) keyIndex(l *PerformerLoader, key int) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
}
}
pos := len(b.keys)
b.keys = append(b.keys, key)
if pos == 0 {
go b.startTimer(l)
}
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
if !b.closing {
b.closing = true
l.batch = nil
go b.end(l)
}
}
return pos
}
func (b *performerLoaderBatch) startTimer(l *PerformerLoader) {
time.Sleep(l.wait)
l.mu.Lock()
// we must have hit a batch limit and are already finalizing this batch
if b.closing {
l.mu.Unlock()
return
}
l.batch = nil
l.mu.Unlock()
b.end(l)
}
func (b *performerLoaderBatch) end(l *PerformerLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -0,0 +1,224 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
package loaders
import (
"sync"
"time"
"github.com/stashapp/stash/pkg/models"
)
// SceneLoaderConfig captures the config to create a new SceneLoader
type SceneLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([]*models.Scene, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
MaxBatch int
}
// NewSceneLoader creates a new SceneLoader given a fetch, wait, and maxBatch
func NewSceneLoader(config SceneLoaderConfig) *SceneLoader {
return &SceneLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// SceneLoader batches and caches requests
type SceneLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([]*models.Scene, []error)
// how long to done before sending a batch
wait time.Duration
// this will limit the maximum number of keys to send in one batch, 0 = no limit
maxBatch int
// INTERNAL
// lazily created cache
cache map[int]*models.Scene
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *sceneLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type sceneLoaderBatch struct {
keys []int
data []*models.Scene
error []error
closing bool
done chan struct{}
}
// Load a Scene by key, batching and caching will be applied automatically
func (l *SceneLoader) Load(key int) (*models.Scene, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a Scene.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *SceneLoader) LoadThunk(key int) func() (*models.Scene, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() (*models.Scene, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &sceneLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() (*models.Scene, error) {
<-batch.done
var data *models.Scene
if pos < len(batch.data) {
data = batch.data[pos]
}
var err error
// its convenient to be able to return a single error for everything
if len(batch.error) == 1 {
err = batch.error[0]
} else if batch.error != nil {
err = batch.error[pos]
}
if err == nil {
l.mu.Lock()
l.unsafeSet(key, data)
l.mu.Unlock()
}
return data, err
}
}
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *SceneLoader) LoadAll(keys []int) ([]*models.Scene, []error) {
results := make([]func() (*models.Scene, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
scenes := make([]*models.Scene, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
scenes[i], errors[i] = thunk()
}
return scenes, errors
}
// LoadAllThunk returns a function that when called will block waiting for a Scenes.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *SceneLoader) LoadAllThunk(keys []int) func() ([]*models.Scene, []error) {
results := make([]func() (*models.Scene, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([]*models.Scene, []error) {
scenes := make([]*models.Scene, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
scenes[i], errors[i] = thunk()
}
return scenes, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *SceneLoader) Prime(key int, value *models.Scene) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
// and end up with the whole cache pointing to the same value.
cpy := *value
l.unsafeSet(key, &cpy)
}
l.mu.Unlock()
return !found
}
// Clear the value at key from the cache, if it exists
func (l *SceneLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *SceneLoader) unsafeSet(key int, value *models.Scene) {
if l.cache == nil {
l.cache = map[int]*models.Scene{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *sceneLoaderBatch) keyIndex(l *SceneLoader, key int) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
}
}
pos := len(b.keys)
b.keys = append(b.keys, key)
if pos == 0 {
go b.startTimer(l)
}
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
if !b.closing {
b.closing = true
l.batch = nil
go b.end(l)
}
}
return pos
}
func (b *sceneLoaderBatch) startTimer(l *SceneLoader) {
time.Sleep(l.wait)
l.mu.Lock()
// we must have hit a batch limit and are already finalizing this batch
if b.closing {
l.mu.Unlock()
return
}
l.batch = nil
l.mu.Unlock()
b.end(l)
}
func (b *sceneLoaderBatch) end(l *SceneLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -0,0 +1,224 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
package loaders
import (
"sync"
"time"
"github.com/stashapp/stash/pkg/models"
)
// StudioLoaderConfig captures the config to create a new StudioLoader
type StudioLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([]*models.Studio, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
MaxBatch int
}
// NewStudioLoader creates a new StudioLoader given a fetch, wait, and maxBatch
func NewStudioLoader(config StudioLoaderConfig) *StudioLoader {
return &StudioLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// StudioLoader batches and caches requests
type StudioLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([]*models.Studio, []error)
// how long to done before sending a batch
wait time.Duration
// this will limit the maximum number of keys to send in one batch, 0 = no limit
maxBatch int
// INTERNAL
// lazily created cache
cache map[int]*models.Studio
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *studioLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type studioLoaderBatch struct {
keys []int
data []*models.Studio
error []error
closing bool
done chan struct{}
}
// Load a Studio by key, batching and caching will be applied automatically
func (l *StudioLoader) Load(key int) (*models.Studio, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a Studio.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *StudioLoader) LoadThunk(key int) func() (*models.Studio, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() (*models.Studio, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &studioLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() (*models.Studio, error) {
<-batch.done
var data *models.Studio
if pos < len(batch.data) {
data = batch.data[pos]
}
var err error
// its convenient to be able to return a single error for everything
if len(batch.error) == 1 {
err = batch.error[0]
} else if batch.error != nil {
err = batch.error[pos]
}
if err == nil {
l.mu.Lock()
l.unsafeSet(key, data)
l.mu.Unlock()
}
return data, err
}
}
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *StudioLoader) LoadAll(keys []int) ([]*models.Studio, []error) {
results := make([]func() (*models.Studio, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
studios := make([]*models.Studio, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
studios[i], errors[i] = thunk()
}
return studios, errors
}
// LoadAllThunk returns a function that when called will block waiting for a Studios.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *StudioLoader) LoadAllThunk(keys []int) func() ([]*models.Studio, []error) {
results := make([]func() (*models.Studio, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([]*models.Studio, []error) {
studios := make([]*models.Studio, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
studios[i], errors[i] = thunk()
}
return studios, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *StudioLoader) Prime(key int, value *models.Studio) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
// and end up with the whole cache pointing to the same value.
cpy := *value
l.unsafeSet(key, &cpy)
}
l.mu.Unlock()
return !found
}
// Clear the value at key from the cache, if it exists
func (l *StudioLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *StudioLoader) unsafeSet(key int, value *models.Studio) {
if l.cache == nil {
l.cache = map[int]*models.Studio{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *studioLoaderBatch) keyIndex(l *StudioLoader, key int) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
}
}
pos := len(b.keys)
b.keys = append(b.keys, key)
if pos == 0 {
go b.startTimer(l)
}
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
if !b.closing {
b.closing = true
l.batch = nil
go b.end(l)
}
}
return pos
}
func (b *studioLoaderBatch) startTimer(l *StudioLoader) {
time.Sleep(l.wait)
l.mu.Lock()
// we must have hit a batch limit and are already finalizing this batch
if b.closing {
l.mu.Unlock()
return
}
l.batch = nil
l.mu.Unlock()
b.end(l)
}
func (b *studioLoaderBatch) end(l *StudioLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -0,0 +1,224 @@
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
package loaders
import (
"sync"
"time"
"github.com/stashapp/stash/pkg/models"
)
// TagLoaderConfig captures the config to create a new TagLoader
type TagLoaderConfig struct {
// Fetch is a method that provides the data for the loader
Fetch func(keys []int) ([]*models.Tag, []error)
// Wait is how long wait before sending a batch
Wait time.Duration
// MaxBatch will limit the maximum number of keys to send in one batch, 0 = not limit
MaxBatch int
}
// NewTagLoader creates a new TagLoader given a fetch, wait, and maxBatch
func NewTagLoader(config TagLoaderConfig) *TagLoader {
return &TagLoader{
fetch: config.Fetch,
wait: config.Wait,
maxBatch: config.MaxBatch,
}
}
// TagLoader batches and caches requests
type TagLoader struct {
// this method provides the data for the loader
fetch func(keys []int) ([]*models.Tag, []error)
// how long to done before sending a batch
wait time.Duration
// this will limit the maximum number of keys to send in one batch, 0 = no limit
maxBatch int
// INTERNAL
// lazily created cache
cache map[int]*models.Tag
// the current batch. keys will continue to be collected until timeout is hit,
// then everything will be sent to the fetch method and out to the listeners
batch *tagLoaderBatch
// mutex to prevent races
mu sync.Mutex
}
type tagLoaderBatch struct {
keys []int
data []*models.Tag
error []error
closing bool
done chan struct{}
}
// Load a Tag by key, batching and caching will be applied automatically
func (l *TagLoader) Load(key int) (*models.Tag, error) {
return l.LoadThunk(key)()
}
// LoadThunk returns a function that when called will block waiting for a Tag.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *TagLoader) LoadThunk(key int) func() (*models.Tag, error) {
l.mu.Lock()
if it, ok := l.cache[key]; ok {
l.mu.Unlock()
return func() (*models.Tag, error) {
return it, nil
}
}
if l.batch == nil {
l.batch = &tagLoaderBatch{done: make(chan struct{})}
}
batch := l.batch
pos := batch.keyIndex(l, key)
l.mu.Unlock()
return func() (*models.Tag, error) {
<-batch.done
var data *models.Tag
if pos < len(batch.data) {
data = batch.data[pos]
}
var err error
// its convenient to be able to return a single error for everything
if len(batch.error) == 1 {
err = batch.error[0]
} else if batch.error != nil {
err = batch.error[pos]
}
if err == nil {
l.mu.Lock()
l.unsafeSet(key, data)
l.mu.Unlock()
}
return data, err
}
}
// LoadAll fetches many keys at once. It will be broken into appropriate sized
// sub batches depending on how the loader is configured
func (l *TagLoader) LoadAll(keys []int) ([]*models.Tag, []error) {
results := make([]func() (*models.Tag, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
tags := make([]*models.Tag, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
tags[i], errors[i] = thunk()
}
return tags, errors
}
// LoadAllThunk returns a function that when called will block waiting for a Tags.
// This method should be used if you want one goroutine to make requests to many
// different data loaders without blocking until the thunk is called.
func (l *TagLoader) LoadAllThunk(keys []int) func() ([]*models.Tag, []error) {
results := make([]func() (*models.Tag, error), len(keys))
for i, key := range keys {
results[i] = l.LoadThunk(key)
}
return func() ([]*models.Tag, []error) {
tags := make([]*models.Tag, len(keys))
errors := make([]error, len(keys))
for i, thunk := range results {
tags[i], errors[i] = thunk()
}
return tags, errors
}
}
// Prime the cache with the provided key and value. If the key already exists, no change is made
// and false is returned.
// (To forcefully prime the cache, clear the key first with loader.clear(key).prime(key, value).)
func (l *TagLoader) Prime(key int, value *models.Tag) bool {
l.mu.Lock()
var found bool
if _, found = l.cache[key]; !found {
// make a copy when writing to the cache, its easy to pass a pointer in from a loop var
// and end up with the whole cache pointing to the same value.
cpy := *value
l.unsafeSet(key, &cpy)
}
l.mu.Unlock()
return !found
}
// Clear the value at key from the cache, if it exists
func (l *TagLoader) Clear(key int) {
l.mu.Lock()
delete(l.cache, key)
l.mu.Unlock()
}
func (l *TagLoader) unsafeSet(key int, value *models.Tag) {
if l.cache == nil {
l.cache = map[int]*models.Tag{}
}
l.cache[key] = value
}
// keyIndex will return the location of the key in the batch, if its not found
// it will add the key to the batch
func (b *tagLoaderBatch) keyIndex(l *TagLoader, key int) int {
for i, existingKey := range b.keys {
if key == existingKey {
return i
}
}
pos := len(b.keys)
b.keys = append(b.keys, key)
if pos == 0 {
go b.startTimer(l)
}
if l.maxBatch != 0 && pos >= l.maxBatch-1 {
if !b.closing {
b.closing = true
l.batch = nil
go b.end(l)
}
}
return pos
}
func (b *tagLoaderBatch) startTimer(l *TagLoader) {
time.Sleep(l.wait)
l.mu.Lock()
// we must have hit a batch limit and are already finalizing this batch
if b.closing {
l.mu.Unlock()
return
}
l.batch = nil
l.mu.Unlock()
b.end(l)
}
func (b *tagLoaderBatch) end(l *TagLoader) {
b.data, b.error = l.fetch(b.keys)
close(b.done)
}

View File

@@ -245,3 +245,13 @@ func (r *queryResolver) SceneMarkerTags(ctx context.Context, scene_id string) ([
return result, nil
}
func firstError(errs []error) error {
for _, e := range errs {
if e != nil {
return e
}
}
return nil
}

View File

@@ -5,6 +5,8 @@ import (
"strconv"
"time"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/pkg/file"
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/models"
@@ -145,15 +147,17 @@ func (r *galleryResolver) Date(ctx context.Context, obj *models.Gallery) (*strin
}
func (r *galleryResolver) Scenes(ctx context.Context, obj *models.Gallery) (ret []*models.Scene, err error) {
if !obj.SceneIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Scene.FindMany(ctx, obj.SceneIDs)
return err
return obj.LoadSceneIDs(ctx, r.repository.Gallery)
}); err != nil {
return nil, err
}
}
return ret, nil
var errs []error
ret, errs = loaders.From(ctx).SceneByID.LoadAll(obj.SceneIDs.List())
return ret, firstError(errs)
}
func (r *galleryResolver) Studio(ctx context.Context, obj *models.Gallery) (ret *models.Studio, err error) {
@@ -161,39 +165,35 @@ func (r *galleryResolver) Studio(ctx context.Context, obj *models.Gallery) (ret
return nil, nil
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Studio.Find(ctx, *obj.StudioID)
return err
}); err != nil {
return nil, err
}
return ret, nil
return loaders.From(ctx).StudioByID.Load(*obj.StudioID)
}
func (r *galleryResolver) Tags(ctx context.Context, obj *models.Gallery) (ret []*models.Tag, err error) {
if !obj.TagIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Tag.FindMany(ctx, obj.TagIDs)
return err
return obj.LoadTagIDs(ctx, r.repository.Gallery)
}); err != nil {
return nil, err
}
}
return ret, nil
var errs []error
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.TagIDs.List())
return ret, firstError(errs)
}
func (r *galleryResolver) Performers(ctx context.Context, obj *models.Gallery) (ret []*models.Performer, err error) {
if !obj.PerformerIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Performer.FindMany(ctx, obj.PerformerIDs)
return err
return obj.LoadPerformerIDs(ctx, r.repository.Gallery)
}); err != nil {
return nil, err
}
}
return ret, nil
var errs []error
ret, errs = loaders.From(ctx).PerformerByID.LoadAll(obj.PerformerIDs.List())
return ret, firstError(errs)
}
func (r *galleryResolver) ImageCount(ctx context.Context, obj *models.Gallery) (ret int, err error) {

View File

@@ -5,6 +5,7 @@ import (
"strconv"
"time"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/pkg/models"
)
@@ -74,15 +75,17 @@ func (r *imageResolver) Paths(ctx context.Context, obj *models.Image) (*ImagePat
}
func (r *imageResolver) Galleries(ctx context.Context, obj *models.Image) (ret []*models.Gallery, err error) {
if !obj.GalleryIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Gallery.FindMany(ctx, obj.GalleryIDs)
return err
return obj.LoadGalleryIDs(ctx, r.repository.Image)
}); err != nil {
return nil, err
}
}
return ret, nil
var errs []error
ret, errs = loaders.From(ctx).GalleryByID.LoadAll(obj.GalleryIDs.List())
return ret, firstError(errs)
}
func (r *imageResolver) Studio(ctx context.Context, obj *models.Image) (ret *models.Studio, err error) {
@@ -90,34 +93,33 @@ func (r *imageResolver) Studio(ctx context.Context, obj *models.Image) (ret *mod
return nil, nil
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Studio.Find(ctx, *obj.StudioID)
return err
}); err != nil {
return nil, err
}
return ret, nil
return loaders.From(ctx).StudioByID.Load(*obj.StudioID)
}
func (r *imageResolver) Tags(ctx context.Context, obj *models.Image) (ret []*models.Tag, err error) {
if !obj.TagIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.FindMany(ctx, obj.TagIDs)
return err
return obj.LoadTagIDs(ctx, r.repository.Image)
}); err != nil {
return nil, err
}
}
return ret, nil
var errs []error
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.TagIDs.List())
return ret, firstError(errs)
}
func (r *imageResolver) Performers(ctx context.Context, obj *models.Image) (ret []*models.Performer, err error) {
if !obj.PerformerIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Performer.FindMany(ctx, obj.PerformerIDs)
return err
return obj.LoadPerformerIDs(ctx, r.repository.Image)
}); err != nil {
return nil, err
}
}
return ret, nil
var errs []error
ret, errs = loaders.From(ctx).PerformerByID.LoadAll(obj.PerformerIDs.List())
return ret, firstError(errs)
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"time"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
@@ -56,14 +57,7 @@ func (r *movieResolver) Rating(ctx context.Context, obj *models.Movie) (*int, er
func (r *movieResolver) Studio(ctx context.Context, obj *models.Movie) (ret *models.Studio, err error) {
if obj.StudioID.Valid {
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Studio.Find(ctx, int(obj.StudioID.Int64))
return err
}); err != nil {
return nil, err
}
return ret, nil
return loaders.From(ctx).StudioByID.Load(int(obj.StudioID.Int64))
}
return nil, nil

View File

@@ -199,15 +199,17 @@ func (r *performerResolver) Scenes(ctx context.Context, obj *models.Performer) (
return ret, nil
}
func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer) (ret []*models.StashID, err error) {
func (r *performerResolver) StashIds(ctx context.Context, obj *models.Performer) ([]*models.StashID, error) {
var ret []models.StashID
if err := r.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Performer.GetStashIDs(ctx, obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
return stashIDsSliceToPtrSlice(ret), nil
}
func (r *performerResolver) Rating(ctx context.Context, obj *models.Performer) (*int, error) {

View File

@@ -6,6 +6,7 @@ import (
"strconv"
"time"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/pkg/file"
@@ -163,14 +164,17 @@ func (r *sceneResolver) Captions(ctx context.Context, obj *models.Scene) (ret []
}
func (r *sceneResolver) Galleries(ctx context.Context, obj *models.Scene) (ret []*models.Gallery, err error) {
if !obj.GalleryIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Gallery.FindMany(ctx, obj.GalleryIDs)
return err
return obj.LoadGalleryIDs(ctx, r.repository.Scene)
}); err != nil {
return nil, err
}
}
return ret, nil
var errs []error
ret, errs = loaders.From(ctx).GalleryByID.LoadAll(obj.GalleryIDs.List())
return ret, firstError(errs)
}
func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (ret *models.Studio, err error) {
@@ -178,24 +182,26 @@ func (r *sceneResolver) Studio(ctx context.Context, obj *models.Scene) (ret *mod
return nil, nil
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Studio.Find(ctx, *obj.StudioID)
return err
}); err != nil {
return nil, err
}
return ret, nil
return loaders.From(ctx).StudioByID.Load(*obj.StudioID)
}
func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*SceneMovie, err error) {
if !obj.Movies.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
mqb := r.repository.Movie
qb := r.repository.Scene
for _, sm := range obj.Movies {
movie, err := mqb.Find(ctx, sm.MovieID)
return obj.LoadMovies(ctx, qb)
}); err != nil {
return nil, err
}
}
loader := loaders.From(ctx).MovieByID
for _, sm := range obj.Movies.List() {
movie, err := loader.Load(sm.MovieID)
if err != nil {
return err
return nil, err
}
sceneIdx := sm.SceneIndex
@@ -207,33 +213,55 @@ func (r *sceneResolver) Movies(ctx context.Context, obj *models.Scene) (ret []*S
ret = append(ret, sceneMovie)
}
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (r *sceneResolver) Tags(ctx context.Context, obj *models.Scene) (ret []*models.Tag, err error) {
if !obj.TagIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.FindMany(ctx, obj.TagIDs)
return err
return obj.LoadTagIDs(ctx, r.repository.Scene)
}); err != nil {
return nil, err
}
}
return ret, nil
var errs []error
ret, errs = loaders.From(ctx).TagByID.LoadAll(obj.TagIDs.List())
return ret, firstError(errs)
}
func (r *sceneResolver) Performers(ctx context.Context, obj *models.Scene) (ret []*models.Performer, err error) {
if !obj.PerformerIDs.Loaded() {
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Performer.FindMany(ctx, obj.PerformerIDs)
return err
return obj.LoadPerformerIDs(ctx, r.repository.Scene)
}); err != nil {
return nil, err
}
}
var errs []error
ret, errs = loaders.From(ctx).PerformerByID.LoadAll(obj.PerformerIDs.List())
return ret, firstError(errs)
}
func stashIDsSliceToPtrSlice(v []models.StashID) []*models.StashID {
ret := make([]*models.StashID, len(v))
for i, vv := range v {
c := vv
ret[i] = &c
}
return ret
}
func (r *sceneResolver) StashIds(ctx context.Context, obj *models.Scene) (ret []*models.StashID, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
return obj.LoadStashIDs(ctx, r.repository.Scene)
}); err != nil {
return nil, err
}
return ret, nil
return stashIDsSliceToPtrSlice(obj.StashIDs.List()), nil
}
func (r *sceneResolver) Phash(ctx context.Context, obj *models.Scene) (*string, error) {

View File

@@ -4,6 +4,7 @@ import (
"context"
"time"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/api/urlbuilders"
"github.com/stashapp/stash/pkg/gallery"
"github.com/stashapp/stash/pkg/image"
@@ -97,14 +98,7 @@ func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (
return nil, nil
}
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Studio.Find(ctx, int(obj.ParentID.Int64))
return err
}); err != nil {
return nil, err
}
return ret, nil
return loaders.From(ctx).StudioByID.Load(int(obj.ParentID.Int64))
}
func (r *studioResolver) ChildStudios(ctx context.Context, obj *models.Studio) (ret []*models.Studio, err error) {
@@ -118,15 +112,17 @@ func (r *studioResolver) ChildStudios(ctx context.Context, obj *models.Studio) (
return ret, nil
}
func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) (ret []*models.StashID, err error) {
func (r *studioResolver) StashIds(ctx context.Context, obj *models.Studio) ([]*models.StashID, error) {
var ret []models.StashID
if err := r.withTxn(ctx, func(ctx context.Context) error {
var err error
ret, err = r.repository.Studio.GetStashIDs(ctx, obj.ID)
return err
}); err != nil {
return nil, err
}
return ret, nil
return stashIDsSliceToPtrSlice(ret), nil
}
func (r *studioResolver) Rating(ctx context.Context, obj *models.Studio) (*int, error) {

View File

@@ -36,9 +36,25 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
}
// Populate a new performer from the input
performerIDs, err := stringslice.StringSliceToIntSlice(input.PerformerIds)
if err != nil {
return nil, fmt.Errorf("converting performer ids: %w", err)
}
tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
sceneIDs, err := stringslice.StringSliceToIntSlice(input.SceneIds)
if err != nil {
return nil, fmt.Errorf("converting scene ids: %w", err)
}
currentTime := time.Now()
newGallery := models.Gallery{
Title: input.Title,
PerformerIDs: models.NewRelatedIDs(performerIDs),
TagIDs: models.NewRelatedIDs(tagIDs),
SceneIDs: models.NewRelatedIDs(sceneIDs),
CreatedAt: currentTime,
UpdatedAt: currentTime,
}
@@ -60,20 +76,6 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
newGallery.StudioID = &studioID
}
var err error
newGallery.PerformerIDs, err = stringslice.StringSliceToIntSlice(input.PerformerIds)
if err != nil {
return nil, fmt.Errorf("converting performer ids: %w", err)
}
newGallery.TagIDs, err = stringslice.StringSliceToIntSlice(input.TagIds)
if err != nil {
return nil, fmt.Errorf("converting tag ids: %w", err)
}
newGallery.SceneIDs, err = stringslice.StringSliceToIntSlice(input.SceneIds)
if err != nil {
return nil, fmt.Errorf("converting scene ids: %w", err)
}
// Start the transaction and save the gallery
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Gallery

View File

@@ -26,6 +26,16 @@ func (r *mutationResolver) getPerformer(ctx context.Context, id int) (ret *model
return ret, nil
}
func stashIDPtrSliceToSlice(v []*models.StashID) []models.StashID {
ret := make([]models.StashID, len(v))
for i, vv := range v {
c := vv
ret[i] = *c
}
return ret
}
func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerCreateInput) (*models.Performer, error) {
// generate checksum from performer name rather than image
checksum := md5.FromString(input.Name)
@@ -152,7 +162,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input PerformerC
// Save the stash_ids
if input.StashIds != nil {
stashIDJoins := input.StashIds
stashIDJoins := stashIDPtrSliceToSlice(input.StashIds)
if err := qb.UpdateStashIDs(ctx, performer.ID, stashIDJoins); err != nil {
return err
}
@@ -275,7 +285,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input PerformerU
// Save the stash_ids
if translator.hasField("stash_ids") {
stashIDJoins := input.StashIds
stashIDJoins := stashIDPtrSliceToSlice(input.StashIds)
if err := qb.UpdateStashIDs(ctx, performerID, stashIDJoins); err != nil {
return err
}

View File

@@ -57,6 +57,11 @@ func (r *mutationResolver) SubmitStashBoxSceneDraft(ctx context.Context, input S
if err != nil {
return err
}
if err := scene.LoadStashIDs(ctx, qb); err != nil {
return err
}
filepath := manager.GetInstance().Paths.Scene.GetScreenshotPath(scene.GetHash(config.GetInstance().GetVideoFileNamingAlgorithm()))
res, err = client.SubmitSceneDraft(ctx, scene, boxes[input.StashBoxIndex].Endpoint, filepath)

View File

@@ -90,7 +90,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input StudioCreateI
// Save the stash_ids
if input.StashIds != nil {
stashIDJoins := input.StashIds
stashIDJoins := stashIDPtrSliceToSlice(input.StashIds)
if err := qb.UpdateStashIDs(ctx, s.ID, stashIDJoins); err != nil {
return err
}
@@ -182,7 +182,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input StudioUpdateI
// Save the stash_ids
if translator.hasField("stash_ids") {
stashIDJoins := input.StashIds
stashIDJoins := stashIDPtrSliceToSlice(input.StashIds)
if err := qb.UpdateStashIDs(ctx, studioID, stashIDJoins); err != nil {
return err
}

View File

@@ -26,6 +26,7 @@ import (
"github.com/go-chi/httplog"
"github.com/rs/cors"
"github.com/stashapp/stash/internal/api/loaders"
"github.com/stashapp/stash/internal/manager"
"github.com/stashapp/stash/internal/manager/config"
"github.com/stashapp/stash/pkg/fsutil"
@@ -74,6 +75,14 @@ func Start() error {
}
txnManager := manager.GetInstance().Repository
dataloaders := loaders.Middleware{
DatabaseProvider: txnManager,
Repository: txnManager,
}
r.Use(dataloaders.Middleware)
pluginCache := manager.GetInstance().PluginCache
sceneService := manager.GetInstance().SceneService
imageService := manager.GetInstance().ImageService

View File

@@ -6,8 +6,19 @@ import (
"github.com/stashapp/stash/pkg/gallery"
"github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
)
type GalleryPerformerUpdater interface {
models.PerformerIDLoader
gallery.PartialUpdater
}
type GalleryTagUpdater interface {
models.TagIDLoader
gallery.PartialUpdater
}
func getGalleryFileTagger(s *models.Gallery, cache *match.Cache) tagger {
var path string
if s.Path() != "" {
@@ -28,11 +39,24 @@ func getGalleryFileTagger(s *models.Gallery, cache *match.Cache) tagger {
}
// GalleryPerformers tags the provided gallery with performers whose name matches the gallery's path.
func GalleryPerformers(ctx context.Context, s *models.Gallery, rw gallery.PartialUpdater, performerReader match.PerformerAutoTagQueryer, cache *match.Cache) error {
func GalleryPerformers(ctx context.Context, s *models.Gallery, rw GalleryPerformerUpdater, performerReader match.PerformerAutoTagQueryer, cache *match.Cache) error {
t := getGalleryFileTagger(s, cache)
return t.tagPerformers(ctx, performerReader, func(subjectID, otherID int) (bool, error) {
return gallery.AddPerformer(ctx, rw, s, otherID)
if err := s.LoadPerformerIDs(ctx, rw); err != nil {
return false, err
}
existing := s.PerformerIDs.List()
if intslice.IntInclude(existing, otherID) {
return false, nil
}
if err := gallery.AddPerformer(ctx, rw, s, otherID); err != nil {
return false, err
}
return true, nil
})
}
@@ -53,10 +77,23 @@ func GalleryStudios(ctx context.Context, s *models.Gallery, rw GalleryFinderUpda
}
// GalleryTags tags the provided gallery with tags whose name matches the gallery's path.
func GalleryTags(ctx context.Context, s *models.Gallery, rw gallery.PartialUpdater, tagReader match.TagAutoTagQueryer, cache *match.Cache) error {
func GalleryTags(ctx context.Context, s *models.Gallery, rw GalleryTagUpdater, tagReader match.TagAutoTagQueryer, cache *match.Cache) error {
t := getGalleryFileTagger(s, cache)
return t.tagTags(ctx, tagReader, func(subjectID, otherID int) (bool, error) {
return gallery.AddTag(ctx, rw, s, otherID)
if err := s.LoadTagIDs(ctx, rw); err != nil {
return false, err
}
existing := s.TagIDs.List()
if intslice.IntInclude(existing, otherID) {
return false, nil
}
if err := gallery.AddTag(ctx, rw, s, otherID); err != nil {
return false, err
}
return true, nil
})
}

View File

@@ -60,6 +60,7 @@ func TestGalleryPerformers(t *testing.T) {
Path: test.Path,
},
},
PerformerIDs: models.NewRelatedIDs([]int{}),
}
err := GalleryPerformers(testCtx, &gallery, mockGalleryReader, mockPerformerReader, nil)
@@ -183,6 +184,7 @@ func TestGalleryTags(t *testing.T) {
Path: test.Path,
},
},
TagIDs: models.NewRelatedIDs([]int{}),
}
err := GalleryTags(testCtx, &gallery, mockGalleryReader, mockTagReader, nil)

View File

@@ -6,8 +6,19 @@ import (
"github.com/stashapp/stash/pkg/image"
"github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
)
type ImagePerformerUpdater interface {
models.PerformerIDLoader
image.PartialUpdater
}
type ImageTagUpdater interface {
models.TagIDLoader
image.PartialUpdater
}
func getImageFileTagger(s *models.Image, cache *match.Cache) tagger {
return tagger{
ID: s.ID,
@@ -19,11 +30,24 @@ func getImageFileTagger(s *models.Image, cache *match.Cache) tagger {
}
// ImagePerformers tags the provided image with performers whose name matches the image's path.
func ImagePerformers(ctx context.Context, s *models.Image, rw image.PartialUpdater, performerReader match.PerformerAutoTagQueryer, cache *match.Cache) error {
func ImagePerformers(ctx context.Context, s *models.Image, rw ImagePerformerUpdater, performerReader match.PerformerAutoTagQueryer, cache *match.Cache) error {
t := getImageFileTagger(s, cache)
return t.tagPerformers(ctx, performerReader, func(subjectID, otherID int) (bool, error) {
return image.AddPerformer(ctx, rw, s, otherID)
if err := s.LoadPerformerIDs(ctx, rw); err != nil {
return false, err
}
existing := s.PerformerIDs.List()
if intslice.IntInclude(existing, otherID) {
return false, nil
}
if err := image.AddPerformer(ctx, rw, s, otherID); err != nil {
return false, err
}
return true, nil
})
}
@@ -44,10 +68,23 @@ func ImageStudios(ctx context.Context, s *models.Image, rw ImageFinderUpdater, s
}
// ImageTags tags the provided image with tags whose name matches the image's path.
func ImageTags(ctx context.Context, s *models.Image, rw image.PartialUpdater, tagReader match.TagAutoTagQueryer, cache *match.Cache) error {
func ImageTags(ctx context.Context, s *models.Image, rw ImageTagUpdater, tagReader match.TagAutoTagQueryer, cache *match.Cache) error {
t := getImageFileTagger(s, cache)
return t.tagTags(ctx, tagReader, func(subjectID, otherID int) (bool, error) {
return image.AddTag(ctx, rw, s, otherID)
if err := s.LoadTagIDs(ctx, rw); err != nil {
return false, err
}
existing := s.TagIDs.List()
if intslice.IntInclude(existing, otherID) {
return false, nil
}
if err := image.AddTag(ctx, rw, s, otherID); err != nil {
return false, err
}
return true, nil
})
}

View File

@@ -61,6 +61,7 @@ func TestImagePerformers(t *testing.T) {
image := models.Image{
ID: imageID,
Files: []*file.ImageFile{makeImageFile(test.Path)},
PerformerIDs: models.NewRelatedIDs([]int{}),
}
err := ImagePerformers(testCtx, &image, mockImageReader, mockPerformerReader, nil)
@@ -176,6 +177,7 @@ func TestImageTags(t *testing.T) {
image := models.Image{
ID: imageID,
Files: []*file.ImageFile{makeImageFile(test.Path)},
TagIDs: models.NewRelatedIDs([]int{}),
}
err := ImageTags(testCtx, &image, mockImageReader, mockTagReader, nil)

View File

@@ -8,20 +8,24 @@ import (
"github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scene"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
)
type SceneQueryPerformerUpdater interface {
scene.Queryer
models.PerformerIDLoader
scene.PartialUpdater
}
type ImageQueryPerformerUpdater interface {
image.Queryer
models.PerformerIDLoader
image.PartialUpdater
}
type GalleryQueryPerformerUpdater interface {
gallery.Queryer
models.PerformerIDLoader
gallery.PartialUpdater
}
@@ -39,7 +43,20 @@ func PerformerScenes(ctx context.Context, p *models.Performer, paths []string, r
t := getPerformerTagger(p, cache)
return t.tagScenes(ctx, paths, rw, func(o *models.Scene) (bool, error) {
return scene.AddPerformer(ctx, rw, o, p.ID)
if err := o.LoadPerformerIDs(ctx, rw); err != nil {
return false, err
}
existing := o.PerformerIDs.List()
if intslice.IntInclude(existing, p.ID) {
return false, nil
}
if err := scene.AddPerformer(ctx, rw, o, p.ID); err != nil {
return false, err
}
return true, nil
})
}
@@ -47,8 +64,21 @@ func PerformerScenes(ctx context.Context, p *models.Performer, paths []string, r
func PerformerImages(ctx context.Context, p *models.Performer, paths []string, rw ImageQueryPerformerUpdater, cache *match.Cache) error {
t := getPerformerTagger(p, cache)
return t.tagImages(ctx, paths, rw, func(i *models.Image) (bool, error) {
return image.AddPerformer(ctx, rw, i, p.ID)
return t.tagImages(ctx, paths, rw, func(o *models.Image) (bool, error) {
if err := o.LoadPerformerIDs(ctx, rw); err != nil {
return false, err
}
existing := o.PerformerIDs.List()
if intslice.IntInclude(existing, p.ID) {
return false, nil
}
if err := image.AddPerformer(ctx, rw, o, p.ID); err != nil {
return false, err
}
return true, nil
})
}
@@ -57,6 +87,19 @@ func PerformerGalleries(ctx context.Context, p *models.Performer, paths []string
t := getPerformerTagger(p, cache)
return t.tagGalleries(ctx, paths, rw, func(o *models.Gallery) (bool, error) {
return gallery.AddPerformer(ctx, rw, o, p.ID)
if err := o.LoadPerformerIDs(ctx, rw); err != nil {
return false, err
}
existing := o.PerformerIDs.List()
if intslice.IntInclude(existing, p.ID) {
return false, nil
}
if err := gallery.AddPerformer(ctx, rw, o, p.ID); err != nil {
return false, err
}
return true, nil
})
}

View File

@@ -56,6 +56,7 @@ func testPerformerScenes(t *testing.T, performerName, expectedRegex string) {
},
},
},
PerformerIDs: models.NewRelatedIDs([]int{}),
})
}
@@ -135,6 +136,7 @@ func testPerformerImages(t *testing.T, performerName, expectedRegex string) {
images = append(images, &models.Image{
ID: i + 1,
Files: []*file.ImageFile{makeImageFile(p)},
PerformerIDs: models.NewRelatedIDs([]int{}),
})
}
@@ -219,6 +221,7 @@ func testPerformerGalleries(t *testing.T, performerName, expectedRegex string) {
Path: v,
},
},
PerformerIDs: models.NewRelatedIDs([]int{}),
})
}

View File

@@ -6,8 +6,19 @@ import (
"github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scene"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
)
type ScenePerformerUpdater interface {
models.PerformerIDLoader
scene.PartialUpdater
}
type SceneTagUpdater interface {
models.TagIDLoader
scene.PartialUpdater
}
func getSceneFileTagger(s *models.Scene, cache *match.Cache) tagger {
return tagger{
ID: s.ID,
@@ -19,11 +30,24 @@ func getSceneFileTagger(s *models.Scene, cache *match.Cache) tagger {
}
// ScenePerformers tags the provided scene with performers whose name matches the scene's path.
func ScenePerformers(ctx context.Context, s *models.Scene, rw scene.PartialUpdater, performerReader match.PerformerAutoTagQueryer, cache *match.Cache) error {
func ScenePerformers(ctx context.Context, s *models.Scene, rw ScenePerformerUpdater, performerReader match.PerformerAutoTagQueryer, cache *match.Cache) error {
t := getSceneFileTagger(s, cache)
return t.tagPerformers(ctx, performerReader, func(subjectID, otherID int) (bool, error) {
return scene.AddPerformer(ctx, rw, s, otherID)
if err := s.LoadPerformerIDs(ctx, rw); err != nil {
return false, err
}
existing := s.PerformerIDs.List()
if intslice.IntInclude(existing, otherID) {
return false, nil
}
if err := scene.AddPerformer(ctx, rw, s, otherID); err != nil {
return false, err
}
return true, nil
})
}
@@ -44,10 +68,23 @@ func SceneStudios(ctx context.Context, s *models.Scene, rw SceneFinderUpdater, s
}
// SceneTags tags the provided scene with tags whose name matches the scene's path.
func SceneTags(ctx context.Context, s *models.Scene, rw scene.PartialUpdater, tagReader match.TagAutoTagQueryer, cache *match.Cache) error {
func SceneTags(ctx context.Context, s *models.Scene, rw SceneTagUpdater, tagReader match.TagAutoTagQueryer, cache *match.Cache) error {
t := getSceneFileTagger(s, cache)
return t.tagTags(ctx, tagReader, func(subjectID, otherID int) (bool, error) {
return scene.AddTag(ctx, rw, s, otherID)
if err := s.LoadTagIDs(ctx, rw); err != nil {
return false, err
}
existing := s.TagIDs.List()
if intslice.IntInclude(existing, otherID) {
return false, nil
}
if err := scene.AddTag(ctx, rw, s, otherID); err != nil {
return false, err
}
return true, nil
})
}

View File

@@ -186,6 +186,7 @@ func TestScenePerformers(t *testing.T) {
},
},
},
PerformerIDs: models.NewRelatedIDs([]int{}),
}
if test.Matches {
@@ -325,6 +326,7 @@ func TestSceneTags(t *testing.T) {
},
},
},
TagIDs: models.NewRelatedIDs([]int{}),
}
err := SceneTags(testCtx, &scene, mockSceneReader, mockTagReader, nil)

View File

@@ -8,20 +8,24 @@ import (
"github.com/stashapp/stash/pkg/match"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/scene"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
)
type SceneQueryTagUpdater interface {
scene.Queryer
models.TagIDLoader
scene.PartialUpdater
}
type ImageQueryTagUpdater interface {
image.Queryer
models.TagIDLoader
image.PartialUpdater
}
type GalleryQueryTagUpdater interface {
gallery.Queryer
models.TagIDLoader
gallery.PartialUpdater
}
@@ -51,7 +55,20 @@ func TagScenes(ctx context.Context, p *models.Tag, paths []string, aliases []str
for _, tt := range t {
if err := tt.tagScenes(ctx, paths, rw, func(o *models.Scene) (bool, error) {
return scene.AddTag(ctx, rw, o, p.ID)
if err := o.LoadTagIDs(ctx, rw); err != nil {
return false, err
}
existing := o.TagIDs.List()
if intslice.IntInclude(existing, p.ID) {
return false, nil
}
if err := scene.AddTag(ctx, rw, o, p.ID); err != nil {
return false, err
}
return true, nil
}); err != nil {
return err
}
@@ -64,8 +81,21 @@ func TagImages(ctx context.Context, p *models.Tag, paths []string, aliases []str
t := getTagTaggers(p, aliases, cache)
for _, tt := range t {
if err := tt.tagImages(ctx, paths, rw, func(i *models.Image) (bool, error) {
return image.AddTag(ctx, rw, i, p.ID)
if err := tt.tagImages(ctx, paths, rw, func(o *models.Image) (bool, error) {
if err := o.LoadTagIDs(ctx, rw); err != nil {
return false, err
}
existing := o.TagIDs.List()
if intslice.IntInclude(existing, p.ID) {
return false, nil
}
if err := image.AddTag(ctx, rw, o, p.ID); err != nil {
return false, err
}
return true, nil
}); err != nil {
return err
}
@@ -79,7 +109,20 @@ func TagGalleries(ctx context.Context, p *models.Tag, paths []string, aliases []
for _, tt := range t {
if err := tt.tagGalleries(ctx, paths, rw, func(o *models.Gallery) (bool, error) {
return gallery.AddTag(ctx, rw, o, p.ID)
if err := o.LoadTagIDs(ctx, rw); err != nil {
return false, err
}
existing := o.TagIDs.List()
if intslice.IntInclude(existing, p.ID) {
return false, nil
}
if err := gallery.AddTag(ctx, rw, o, p.ID); err != nil {
return false, err
}
return true, nil
}); err != nil {
return err
}

View File

@@ -96,6 +96,7 @@ func testTagScenes(t *testing.T, tc testTagCase) {
},
},
},
TagIDs: models.NewRelatedIDs([]int{}),
})
}
@@ -188,6 +189,7 @@ func testTagImages(t *testing.T, tc testTagCase) {
images = append(images, &models.Image{
ID: i + 1,
Files: []*file.ImageFile{makeImageFile(p)},
TagIDs: models.NewRelatedIDs([]int{}),
})
}
@@ -286,6 +288,7 @@ func testTagGalleries(t *testing.T, tc testTagCase) {
Path: v,
},
},
TagIDs: models.NewRelatedIDs([]int{}),
})
}

View File

@@ -193,6 +193,17 @@ func (t *SceneIdentifier) getSceneUpdater(ctx context.Context, s *models.Scene,
func (t *SceneIdentifier) modifyScene(ctx context.Context, txnManager txn.Manager, s *models.Scene, result *scrapeResult) error {
var updater *scene.UpdateSet
if err := txn.WithTxn(ctx, txnManager, func(ctx context.Context) error {
// load scene relationships
if err := s.LoadPerformerIDs(ctx, t.SceneReaderUpdater); err != nil {
return err
}
if err := s.LoadTagIDs(ctx, t.SceneReaderUpdater); err != nil {
return err
}
if err := s.LoadStashIDs(ctx, t.SceneReaderUpdater); err != nil {
return err
}
var err error
updater, err = t.getSceneUpdater(ctx, s, result)
if err != nil {
@@ -205,8 +216,7 @@ func (t *SceneIdentifier) modifyScene(ctx context.Context, txnManager txn.Manage
return nil
}
_, err = updater.Update(ctx, t.SceneReaderUpdater, t.ScreenshotSetter)
if err != nil {
if _, err := updater.Update(ctx, t.SceneReaderUpdater, t.ScreenshotSetter); err != nil {
return fmt.Errorf("error updating scene: %w", err)
}

View File

@@ -129,6 +129,9 @@ func TestSceneIdentifier_Identify(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
scene := &models.Scene{
ID: tt.sceneID,
PerformerIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
}
if err := identifier.Identify(testCtx, &mocks.TxnManager{}, scene); (err != nil) != tt.wantErr {
t.Errorf("SceneIdentifier.Identify() error = %v, wantErr %v", err, tt.wantErr)
@@ -155,7 +158,11 @@ func TestSceneIdentifier_modifyScene(t *testing.T) {
{
"empty update",
args{
&models.Scene{},
&models.Scene{
PerformerIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
},
&scrapeResult{
result: &scraper.ScrapedScene{},
},

View File

@@ -13,7 +13,7 @@ import (
type PerformerCreator interface {
Create(ctx context.Context, newPerformer models.Performer) (*models.Performer, error)
UpdateStashIDs(ctx context.Context, performerID int, stashIDs []*models.StashID) error
UpdateStashIDs(ctx context.Context, performerID int, stashIDs []models.StashID) error
}
func getPerformerID(ctx context.Context, endpoint string, w PerformerCreator, p *models.ScrapedPerformer, createMissing bool) (*int, error) {
@@ -39,7 +39,7 @@ func createMissingPerformer(ctx context.Context, endpoint string, w PerformerCre
}
if endpoint != "" && p.RemoteSiteID != nil {
if err := w.UpdateStashIDs(ctx, created.ID, []*models.StashID{
if err := w.UpdateStashIDs(ctx, created.ID, []models.StashID{
{
Endpoint: endpoint,
StashID: *p.RemoteSiteID,

View File

@@ -141,13 +141,13 @@ func Test_createMissingPerformer(t *testing.T) {
return p.Name.String == invalidName
})).Return(nil, errors.New("error creating performer"))
mockPerformerReaderWriter.On("UpdateStashIDs", testCtx, performerID, []*models.StashID{
mockPerformerReaderWriter.On("UpdateStashIDs", testCtx, performerID, []models.StashID{
{
Endpoint: invalidEndpoint,
StashID: remoteSiteID,
},
}).Return(errors.New("error updating stash ids"))
mockPerformerReaderWriter.On("UpdateStashIDs", testCtx, performerID, []*models.StashID{
mockPerformerReaderWriter.On("UpdateStashIDs", testCtx, performerID, []models.StashID{
{
Endpoint: validEndpoint,
StashID: remoteSiteID,

View File

@@ -18,6 +18,9 @@ import (
type SceneReaderUpdater interface {
GetCover(ctx context.Context, sceneID int) ([]byte, error)
scene.Updater
models.PerformerIDLoader
models.TagIDLoader
models.StashIDLoader
}
type TagCreator interface {
@@ -82,7 +85,7 @@ func (g sceneRelationships) performers(ctx context.Context, ignoreMale bool) ([]
endpoint := g.result.source.RemoteSite
var performerIDs []int
originalPerformerIDs := g.scene.PerformerIDs
originalPerformerIDs := g.scene.PerformerIDs.List()
if strategy == FieldStrategyMerge {
// add to existing
@@ -129,7 +132,7 @@ func (g sceneRelationships) tags(ctx context.Context) ([]int, error) {
}
var tagIDs []int
originalTagIDs := target.TagIDs
originalTagIDs := target.TagIDs.List()
if strategy == FieldStrategyMerge {
// add to existing
@@ -186,7 +189,7 @@ func (g sceneRelationships) stashIDs(ctx context.Context) ([]models.StashID, err
}
var stashIDs []models.StashID
originalStashIDs := target.StashIDs
originalStashIDs := target.StashIDs.List()
if strategy == FieldStrategyMerge {
// add to existing

View File

@@ -158,13 +158,16 @@ func Test_sceneRelationships_performers(t *testing.T) {
emptyScene := &models.Scene{
ID: sceneID,
PerformerIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
}
sceneWithPerformer := &models.Scene{
ID: sceneWithPerformerID,
PerformerIDs: []int{
PerformerIDs: models.NewRelatedIDs([]int{
existingPerformerID,
},
}),
}
tr := sceneRelationships{
@@ -174,7 +177,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
tests := []struct {
name string
sceneID *models.Scene
scene *models.Scene
fieldOptions *FieldOptions
scraped []*models.ScrapedPerformer
ignoreMale bool
@@ -301,7 +304,7 @@ func Test_sceneRelationships_performers(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tr.scene = tt.sceneID
tr.scene = tt.scene
tr.fieldOptions["performers"] = tt.fieldOptions
tr.result = &scrapeResult{
result: &scraper.ScrapedScene{
@@ -342,13 +345,18 @@ func Test_sceneRelationships_tags(t *testing.T) {
emptyScene := &models.Scene{
ID: sceneID,
TagIDs: models.NewRelatedIDs([]int{}),
PerformerIDs: models.NewRelatedIDs([]int{}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
}
sceneWithTag := &models.Scene{
ID: sceneWithTagID,
TagIDs: []int{
TagIDs: models.NewRelatedIDs([]int{
existingID,
},
}),
PerformerIDs: models.NewRelatedIDs([]int{}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
}
mockSceneReaderWriter := &mocks.SceneReaderWriter{}
@@ -531,12 +539,12 @@ func Test_sceneRelationships_stashIDs(t *testing.T) {
sceneWithStashIDs := &models.Scene{
ID: sceneWithStashID,
StashIDs: []models.StashID{
StashIDs: models.NewRelatedStashIDs([]models.StashID{
{
StashID: remoteSiteID,
Endpoint: existingEndpoint,
},
},
}),
}
mockSceneReaderWriter := &mocks.SceneReaderWriter{}

View File

@@ -12,7 +12,7 @@ import (
type StudioCreator interface {
Create(ctx context.Context, newStudio models.Studio) (*models.Studio, error)
UpdateStashIDs(ctx context.Context, studioID int, stashIDs []*models.StashID) error
UpdateStashIDs(ctx context.Context, studioID int, stashIDs []models.StashID) error
}
func createMissingStudio(ctx context.Context, endpoint string, w StudioCreator, studio *models.ScrapedStudio) (*int, error) {
@@ -22,7 +22,7 @@ func createMissingStudio(ctx context.Context, endpoint string, w StudioCreator,
}
if endpoint != "" && studio.RemoteSiteID != nil {
if err := w.UpdateStashIDs(ctx, created.ID, []*models.StashID{
if err := w.UpdateStashIDs(ctx, created.ID, []models.StashID{
{
Endpoint: endpoint,
StashID: *studio.RemoteSiteID,

View File

@@ -30,13 +30,13 @@ func Test_createMissingStudio(t *testing.T) {
return p.Name.String == invalidName
})).Return(nil, errors.New("error creating performer"))
mockStudioReaderWriter.On("UpdateStashIDs", testCtx, createdID, []*models.StashID{
mockStudioReaderWriter.On("UpdateStashIDs", testCtx, createdID, []models.StashID{
{
Endpoint: invalidEndpoint,
StashID: remoteSiteID,
},
}).Return(errors.New("error updating stash ids"))
mockStudioReaderWriter.On("UpdateStashIDs", testCtx, createdID, []*models.StashID{
mockStudioReaderWriter.On("UpdateStashIDs", testCtx, createdID, []models.StashID{
{
Endpoint: validEndpoint,
StashID: remoteSiteID,

View File

@@ -403,6 +403,10 @@ func exportScene(ctx context.Context, wg *sync.WaitGroup, jobChan <-chan *models
for s := range jobChan {
sceneHash := s.GetHash(t.fileNamingAlgorithm)
if err := s.LoadRelationships(ctx, sceneReader); err != nil {
logger.Errorf("[scenes] <%s> error loading scene relationships: %v", sceneHash, err)
}
newSceneJSON, err := scene.ToBasicJSON(ctx, sceneReader, s)
if err != nil {
logger.Errorf("[scenes] <%s> error getting scene JSON: %s", sceneHash, err.Error())

View File

@@ -166,7 +166,7 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) {
_, err := r.Performer.Update(ctx, partial)
if !t.refresh {
err = r.Performer.UpdateStashIDs(ctx, t.performer.ID, []*models.StashID{
err = r.Performer.UpdateStashIDs(ctx, t.performer.ID, []models.StashID{
{
Endpoint: t.box.Endpoint,
StashID: *performer.RemoteSiteID,
@@ -231,7 +231,7 @@ func (t *StashBoxPerformerTagTask) stashBoxPerformerTag(ctx context.Context) {
return err
}
err = r.Performer.UpdateStashIDs(ctx, createdPerformer.ID, []*models.StashID{
err = r.Performer.UpdateStashIDs(ctx, createdPerformer.ID, []models.StashID{
{
Endpoint: t.box.Endpoint,
StashID: *performer.RemoteSiteID,

View File

@@ -91,8 +91,12 @@ func (s *Service) destroyFolderImages(ctx context.Context, i *models.Gallery, fi
}
for _, img := range imgs {
if err := img.LoadGalleryIDs(ctx, s.ImageFinder); err != nil {
return nil, err
}
// only destroy images that are not attached to other galleries
if len(img.GalleryIDs) > 1 {
if len(img.GalleryIDs.List()) > 1 {
continue
}

View File

@@ -14,7 +14,7 @@ import (
)
type Importer struct {
ReaderWriter FinderCreatorUpdater
ReaderWriter FullCreatorUpdater
StudioWriter studio.NameFinderCreator
PerformerWriter performer.NameFinderCreator
TagWriter tag.NameFinderCreator
@@ -24,6 +24,11 @@ type Importer struct {
gallery models.Gallery
}
type FullCreatorUpdater interface {
FinderCreatorUpdater
Update(ctx context.Context, updatedGallery *models.Gallery) error
}
func (i *Importer) PreImport(ctx context.Context) error {
i.gallery = i.galleryJSONToGallery(i.Input)
@@ -43,7 +48,10 @@ func (i *Importer) PreImport(ctx context.Context) error {
}
func (i *Importer) galleryJSONToGallery(galleryJSON jsonschema.Gallery) models.Gallery {
newGallery := models.Gallery{}
newGallery := models.Gallery{
PerformerIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}),
}
if galleryJSON.Title != "" {
newGallery.Title = galleryJSON.Title
@@ -149,7 +157,7 @@ func (i *Importer) populatePerformers(ctx context.Context) error {
}
for _, p := range performers {
i.gallery.PerformerIDs = append(i.gallery.PerformerIDs, p.ID)
i.gallery.PerformerIDs.Add(p.ID)
}
}
@@ -207,7 +215,7 @@ func (i *Importer) populateTags(ctx context.Context) error {
}
for _, t := range tags {
i.gallery.TagIDs = append(i.gallery.TagIDs, t.ID)
i.gallery.TagIDs.Add(t.ID)
}
}

View File

@@ -11,7 +11,6 @@ import (
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/plugin"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
)
// const mutexType = "gallery"
@@ -20,16 +19,17 @@ type FinderCreatorUpdater interface {
FindByFileID(ctx context.Context, fileID file.ID) ([]*models.Gallery, error)
FindByFingerprints(ctx context.Context, fp []file.Fingerprint) ([]*models.Gallery, error)
Create(ctx context.Context, newGallery *models.Gallery, fileIDs []file.ID) error
Update(ctx context.Context, updatedGallery *models.Gallery) error
AddFileID(ctx context.Context, id int, fileID file.ID) error
}
type SceneFinderUpdater interface {
FindByPath(ctx context.Context, p string) ([]*models.Scene, error)
Update(ctx context.Context, updatedScene *models.Scene) error
AddGalleryIDs(ctx context.Context, sceneID int, galleryIDs []int) error
}
type ScanHandler struct {
CreatorUpdater FinderCreatorUpdater
CreatorUpdater FullCreatorUpdater
SceneFinderUpdater SceneFinderUpdater
PluginCache *plugin.Cache
@@ -97,8 +97,8 @@ func (h *ScanHandler) associateExisting(ctx context.Context, existing []*models.
i.Files = append(i.Files, f)
}
if err := h.CreatorUpdater.Update(ctx, i); err != nil {
return fmt.Errorf("updating gallery: %w", err)
if err := h.CreatorUpdater.AddFileID(ctx, i.ID, f.Base().ID); err != nil {
return fmt.Errorf("adding file to gallery: %w", err)
}
}
@@ -122,15 +122,10 @@ func (h *ScanHandler) associateScene(ctx context.Context, existing []*models.Gal
for _, scene := range scenes {
// found related Scene
newIDs := intslice.IntAppendUniques(scene.GalleryIDs, galleryIDs)
if len(newIDs) > len(scene.GalleryIDs) {
logger.Infof("associate: Gallery %s is related to scene: %s", f.Base().Path, scene.GetTitle())
scene.GalleryIDs = newIDs
if err := h.SceneFinderUpdater.Update(ctx, scene); err != nil {
if err := h.SceneFinderUpdater.AddGalleryIDs(ctx, scene.ID, galleryIDs); err != nil {
return err
}
}
}
return nil
}

View File

@@ -20,6 +20,7 @@ type Repository interface {
type ImageFinder interface {
FindByFolderID(ctx context.Context, folder file.FolderID) ([]*models.Image, error)
FindByZipFileID(ctx context.Context, zipFileID file.ID) ([]*models.Image, error)
models.GalleryIDLoader
}
type ImageService interface {

View File

@@ -26,36 +26,22 @@ func AddImage(ctx context.Context, qb ImageUpdater, galleryID int, imageID int)
return qb.UpdateImages(ctx, galleryID, imageIDs)
}
func AddPerformer(ctx context.Context, qb PartialUpdater, o *models.Gallery, performerID int) (bool, error) {
if !intslice.IntInclude(o.PerformerIDs, performerID) {
if _, err := qb.UpdatePartial(ctx, o.ID, models.GalleryPartial{
func AddPerformer(ctx context.Context, qb PartialUpdater, o *models.Gallery, performerID int) error {
_, err := qb.UpdatePartial(ctx, o.ID, models.GalleryPartial{
PerformerIDs: &models.UpdateIDs{
IDs: []int{performerID},
Mode: models.RelationshipUpdateModeAdd,
},
}); err != nil {
return false, err
}
return true, nil
}
return false, nil
})
return err
}
func AddTag(ctx context.Context, qb PartialUpdater, o *models.Gallery, tagID int) (bool, error) {
if !intslice.IntInclude(o.TagIDs, tagID) {
if _, err := qb.UpdatePartial(ctx, o.ID, models.GalleryPartial{
func AddTag(ctx context.Context, qb PartialUpdater, o *models.Gallery, tagID int) error {
_, err := qb.UpdatePartial(ctx, o.ID, models.GalleryPartial{
TagIDs: &models.UpdateIDs{
IDs: []int{tagID},
Mode: models.RelationshipUpdateModeAdd,
},
}); err != nil {
return false, err
}
return true, nil
}
return false, nil
})
return err
}

View File

@@ -17,8 +17,13 @@ type GalleryChecksumsFinder interface {
FindByChecksums(ctx context.Context, checksums []string) ([]*models.Gallery, error)
}
type FullCreatorUpdater interface {
FinderCreatorUpdater
Update(ctx context.Context, updatedImage *models.Image) error
}
type Importer struct {
ReaderWriter FinderCreatorUpdater
ReaderWriter FullCreatorUpdater
StudioWriter studio.NameFinderCreator
GalleryWriter GalleryChecksumsFinder
PerformerWriter performer.NameFinderCreator
@@ -57,6 +62,9 @@ func (i *Importer) imageJSONToImage(imageJSON jsonschema.Image) models.Image {
newImage := models.Image{
// Checksum: imageJSON.Checksum,
// Path: i.Path,
PerformerIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}),
GalleryIDs: models.NewRelatedIDs([]int{}),
}
if imageJSON.Title != "" {
@@ -145,7 +153,7 @@ func (i *Importer) populateGalleries(ctx context.Context) error {
continue
}
} else {
i.image.GalleryIDs = append(i.image.GalleryIDs, gallery[0].ID)
i.image.GalleryIDs.Add(gallery[0].ID)
}
}
@@ -190,7 +198,7 @@ func (i *Importer) populatePerformers(ctx context.Context) error {
}
for _, p := range performers {
i.image.PerformerIDs = append(i.image.PerformerIDs, p.ID)
i.image.PerformerIDs.Add(p.ID)
}
}
@@ -222,7 +230,7 @@ func (i *Importer) populateTags(ctx context.Context) error {
}
for _, t := range tags {
i.image.TagIDs = append(i.image.TagIDs, t.ID)
i.image.TagIDs.Add(t.ID)
}
}

View File

@@ -24,7 +24,8 @@ type FinderCreatorUpdater interface {
FindByFileID(ctx context.Context, fileID file.ID) ([]*models.Image, error)
FindByFingerprints(ctx context.Context, fp []file.Fingerprint) ([]*models.Image, error)
Create(ctx context.Context, newImage *models.ImageCreateInput) error
Update(ctx context.Context, updatedImage *models.Image) error
AddFileID(ctx context.Context, id int, fileID file.ID) error
models.GalleryIDLoader
}
type GalleryFinderCreator interface {
@@ -97,6 +98,7 @@ func (h *ScanHandler) Handle(ctx context.Context, f file.File) error {
newImage := &models.Image{
CreatedAt: now,
UpdatedAt: now,
GalleryIDs: models.NewRelatedIDs([]int{}),
}
// if the file is in a zip, then associate it with the gallery
@@ -107,7 +109,7 @@ func (h *ScanHandler) Handle(ctx context.Context, f file.File) error {
}
for _, gg := range g {
newImage.GalleryIDs = append(newImage.GalleryIDs, gg.ID)
newImage.GalleryIDs.Add(gg.ID)
}
} else if h.ScanConfig.GetCreateGalleriesFromFolders() {
if err := h.associateFolderBasedGallery(ctx, newImage, imageFile); err != nil {
@@ -162,8 +164,8 @@ func (h *ScanHandler) associateExisting(ctx context.Context, existing []*models.
}
}
if err := h.CreatorUpdater.Update(ctx, i); err != nil {
return fmt.Errorf("updating image: %w", err)
if err := h.CreatorUpdater.AddFileID(ctx, i.ID, f.ID); err != nil {
return fmt.Errorf("adding file to image: %w", err)
}
}
}
@@ -210,8 +212,12 @@ func (h *ScanHandler) associateFolderBasedGallery(ctx context.Context, newImage
return err
}
if g != nil && !intslice.IntInclude(newImage.GalleryIDs, g.ID) {
newImage.GalleryIDs = append(newImage.GalleryIDs, g.ID)
if err := newImage.LoadGalleryIDs(ctx, h.CreatorUpdater); err != nil {
return err
}
if g != nil && !intslice.IntInclude(newImage.GalleryIDs.List(), g.ID) {
newImage.GalleryIDs.Add(g.ID)
logger.Infof("Adding %s to folder-based gallery %s", f.Base().Path, g.Path())
}

View File

@@ -4,43 +4,29 @@ import (
"context"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
)
type PartialUpdater interface {
UpdatePartial(ctx context.Context, id int, partial models.ImagePartial) (*models.Image, error)
}
func AddPerformer(ctx context.Context, qb PartialUpdater, i *models.Image, performerID int) (bool, error) {
if !intslice.IntInclude(i.PerformerIDs, performerID) {
if _, err := qb.UpdatePartial(ctx, i.ID, models.ImagePartial{
func AddPerformer(ctx context.Context, qb PartialUpdater, i *models.Image, performerID int) error {
_, err := qb.UpdatePartial(ctx, i.ID, models.ImagePartial{
PerformerIDs: &models.UpdateIDs{
IDs: []int{performerID},
Mode: models.RelationshipUpdateModeAdd,
},
}); err != nil {
return false, err
}
})
return true, nil
}
return false, nil
return err
}
func AddTag(ctx context.Context, qb PartialUpdater, i *models.Image, tagID int) (bool, error) {
if !intslice.IntInclude(i.TagIDs, tagID) {
if _, err := qb.UpdatePartial(ctx, i.ID, models.ImagePartial{
func AddTag(ctx context.Context, qb PartialUpdater, i *models.Image, tagID int) error {
_, err := qb.UpdatePartial(ctx, i.ID, models.ImagePartial{
TagIDs: &models.UpdateIDs{
IDs: []int{tagID},
Mode: models.RelationshipUpdateModeAdd,
},
}); err != nil {
return false, err
}
return true, nil
}
return false, nil
})
return err
}

View File

@@ -74,14 +74,23 @@ type GalleryDestroyInput struct {
DeleteGenerated *bool `json:"delete_generated"`
}
type GalleryFinder interface {
FindMany(ctx context.Context, ids []int) ([]*Gallery, error)
}
type GalleryReader interface {
Find(ctx context.Context, id int) (*Gallery, error)
FindMany(ctx context.Context, ids []int) ([]*Gallery, error)
GalleryFinder
FindByChecksum(ctx context.Context, checksum string) ([]*Gallery, error)
FindByChecksums(ctx context.Context, checksums []string) ([]*Gallery, error)
FindByPath(ctx context.Context, path string) ([]*Gallery, error)
FindBySceneID(ctx context.Context, sceneID int) ([]*Gallery, error)
FindByImageID(ctx context.Context, imageID int) ([]*Gallery, error)
SceneIDLoader
PerformerIDLoader
TagIDLoader
Count(ctx context.Context) (int, error)
All(ctx context.Context) ([]*Gallery, error)
Query(ctx context.Context, galleryFilter *GalleryFilterType, findFilter *FindFilterType) ([]*Gallery, int, error)

View File

@@ -102,6 +102,10 @@ type ImageReader interface {
All(ctx context.Context) ([]*Image, error)
Query(ctx context.Context, options ImageQueryOptions) (*ImageQueryResult, error)
QueryCount(ctx context.Context, imageFilter *ImageFilterType, findFilter *FindFilterType) (int, error)
GalleryIDLoader
PerformerIDLoader
TagIDLoader
}
type ImageWriter interface {

View File

@@ -36,7 +36,7 @@ type Performer struct {
DeathDate string `json:"death_date,omitempty"`
HairColor string `json:"hair_color,omitempty"`
Weight int `json:"weight,omitempty"`
StashIDs []*models.StashID `json:"stash_ids,omitempty"`
StashIDs []models.StashID `json:"stash_ids,omitempty"`
IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"`
}

View File

@@ -19,7 +19,7 @@ type Studio struct {
Rating int `json:"rating,omitempty"`
Details string `json:"details,omitempty"`
Aliases []string `json:"aliases,omitempty"`
StashIDs []*models.StashID `json:"stash_ids,omitempty"`
StashIDs []models.StashID `json:"stash_ids,omitempty"`
IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"`
}

View File

@@ -272,6 +272,75 @@ func (_m *GalleryReaderWriter) GetImageIDs(ctx context.Context, galleryID int) (
return r0, r1
}
// GetPerformerIDs provides a mock function with given fields: ctx, relatedID
func (_m *GalleryReaderWriter) GetPerformerIDs(ctx context.Context, relatedID int) ([]int, error) {
ret := _m.Called(ctx, relatedID)
var r0 []int
if rf, ok := ret.Get(0).(func(context.Context, int) []int); ok {
r0 = rf(ctx, relatedID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]int)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, relatedID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSceneIDs provides a mock function with given fields: ctx, relatedID
func (_m *GalleryReaderWriter) GetSceneIDs(ctx context.Context, relatedID int) ([]int, error) {
ret := _m.Called(ctx, relatedID)
var r0 []int
if rf, ok := ret.Get(0).(func(context.Context, int) []int); ok {
r0 = rf(ctx, relatedID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]int)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, relatedID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTagIDs provides a mock function with given fields: ctx, relatedID
func (_m *GalleryReaderWriter) GetTagIDs(ctx context.Context, relatedID int) ([]int, error) {
ret := _m.Called(ctx, relatedID)
var r0 []int
if rf, ok := ret.Get(0).(func(context.Context, int) []int); ok {
r0 = rf(ctx, relatedID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]int)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, relatedID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Query provides a mock function with given fields: ctx, galleryFilter, findFilter
func (_m *GalleryReaderWriter) Query(ctx context.Context, galleryFilter *models.GalleryFilterType, findFilter *models.FindFilterType) ([]*models.Gallery, int, error) {
ret := _m.Called(ctx, galleryFilter, findFilter)

View File

@@ -197,29 +197,6 @@ func (_m *ImageReaderWriter) FindByGalleryID(ctx context.Context, galleryID int)
return r0, r1
}
// FindByPath provides a mock function with given fields: ctx, path
func (_m *ImageReaderWriter) FindByPath(ctx context.Context, path string) ([]*models.Image, error) {
ret := _m.Called(ctx, path)
var r0 []*models.Image
if rf, ok := ret.Get(0).(func(context.Context, string) []*models.Image); ok {
r0 = rf(ctx, path)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.Image)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, path)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// FindMany provides a mock function with given fields: ctx, ids
func (_m *ImageReaderWriter) FindMany(ctx context.Context, ids []int) ([]*models.Image, error) {
ret := _m.Called(ctx, ids)
@@ -243,6 +220,75 @@ func (_m *ImageReaderWriter) FindMany(ctx context.Context, ids []int) ([]*models
return r0, r1
}
// GetGalleryIDs provides a mock function with given fields: ctx, relatedID
func (_m *ImageReaderWriter) GetGalleryIDs(ctx context.Context, relatedID int) ([]int, error) {
ret := _m.Called(ctx, relatedID)
var r0 []int
if rf, ok := ret.Get(0).(func(context.Context, int) []int); ok {
r0 = rf(ctx, relatedID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]int)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, relatedID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPerformerIDs provides a mock function with given fields: ctx, relatedID
func (_m *ImageReaderWriter) GetPerformerIDs(ctx context.Context, relatedID int) ([]int, error) {
ret := _m.Called(ctx, relatedID)
var r0 []int
if rf, ok := ret.Get(0).(func(context.Context, int) []int); ok {
r0 = rf(ctx, relatedID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]int)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, relatedID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTagIDs provides a mock function with given fields: ctx, relatedID
func (_m *ImageReaderWriter) GetTagIDs(ctx context.Context, relatedID int) ([]int, error) {
ret := _m.Called(ctx, relatedID)
var r0 []int
if rf, ok := ret.Get(0).(func(context.Context, int) []int); ok {
r0 = rf(ctx, relatedID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]int)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, relatedID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// IncrementOCounter provides a mock function with given fields: ctx, id
func (_m *ImageReaderWriter) IncrementOCounter(ctx context.Context, id int) (int, error) {
ret := _m.Called(ctx, id)

View File

@@ -360,22 +360,22 @@ func (_m *PerformerReaderWriter) GetImage(ctx context.Context, performerID int)
return r0, r1
}
// GetStashIDs provides a mock function with given fields: ctx, performerID
func (_m *PerformerReaderWriter) GetStashIDs(ctx context.Context, performerID int) ([]*models.StashID, error) {
ret := _m.Called(ctx, performerID)
// GetStashIDs provides a mock function with given fields: ctx, relatedID
func (_m *PerformerReaderWriter) GetStashIDs(ctx context.Context, relatedID int) ([]models.StashID, error) {
ret := _m.Called(ctx, relatedID)
var r0 []*models.StashID
if rf, ok := ret.Get(0).(func(context.Context, int) []*models.StashID); ok {
r0 = rf(ctx, performerID)
var r0 []models.StashID
if rf, ok := ret.Get(0).(func(context.Context, int) []models.StashID); ok {
r0 = rf(ctx, relatedID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.StashID)
r0 = ret.Get(0).([]models.StashID)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, performerID)
r1 = rf(ctx, relatedID)
} else {
r1 = ret.Error(1)
}
@@ -520,11 +520,11 @@ func (_m *PerformerReaderWriter) UpdateImage(ctx context.Context, performerID in
}
// UpdateStashIDs provides a mock function with given fields: ctx, performerID, stashIDs
func (_m *PerformerReaderWriter) UpdateStashIDs(ctx context.Context, performerID int, stashIDs []*models.StashID) error {
func (_m *PerformerReaderWriter) UpdateStashIDs(ctx context.Context, performerID int, stashIDs []models.StashID) error {
ret := _m.Called(ctx, performerID, stashIDs)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int, []*models.StashID) error); ok {
if rf, ok := ret.Get(0).(func(context.Context, int, []models.StashID) error); ok {
r0 = rf(ctx, performerID, stashIDs)
} else {
r0 = ret.Error(0)

View File

@@ -500,6 +500,121 @@ func (_m *SceneReaderWriter) GetCover(ctx context.Context, sceneID int) ([]byte,
return r0, r1
}
// GetGalleryIDs provides a mock function with given fields: ctx, relatedID
func (_m *SceneReaderWriter) GetGalleryIDs(ctx context.Context, relatedID int) ([]int, error) {
ret := _m.Called(ctx, relatedID)
var r0 []int
if rf, ok := ret.Get(0).(func(context.Context, int) []int); ok {
r0 = rf(ctx, relatedID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]int)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, relatedID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMovies provides a mock function with given fields: ctx, id
func (_m *SceneReaderWriter) GetMovies(ctx context.Context, id int) ([]models.MoviesScenes, error) {
ret := _m.Called(ctx, id)
var r0 []models.MoviesScenes
if rf, ok := ret.Get(0).(func(context.Context, int) []models.MoviesScenes); ok {
r0 = rf(ctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]models.MoviesScenes)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetPerformerIDs provides a mock function with given fields: ctx, relatedID
func (_m *SceneReaderWriter) GetPerformerIDs(ctx context.Context, relatedID int) ([]int, error) {
ret := _m.Called(ctx, relatedID)
var r0 []int
if rf, ok := ret.Get(0).(func(context.Context, int) []int); ok {
r0 = rf(ctx, relatedID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]int)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, relatedID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetStashIDs provides a mock function with given fields: ctx, relatedID
func (_m *SceneReaderWriter) GetStashIDs(ctx context.Context, relatedID int) ([]models.StashID, error) {
ret := _m.Called(ctx, relatedID)
var r0 []models.StashID
if rf, ok := ret.Get(0).(func(context.Context, int) []models.StashID); ok {
r0 = rf(ctx, relatedID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]models.StashID)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, relatedID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetTagIDs provides a mock function with given fields: ctx, relatedID
func (_m *SceneReaderWriter) GetTagIDs(ctx context.Context, relatedID int) ([]int, error) {
ret := _m.Called(ctx, relatedID)
var r0 []int
if rf, ok := ret.Get(0).(func(context.Context, int) []int); ok {
r0 = rf(ctx, relatedID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]int)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, relatedID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// IncrementOCounter provides a mock function with given fields: ctx, id
func (_m *SceneReaderWriter) IncrementOCounter(ctx context.Context, id int) (int, error) {
ret := _m.Called(ctx, id)

View File

@@ -270,22 +270,22 @@ func (_m *StudioReaderWriter) GetImage(ctx context.Context, studioID int) ([]byt
return r0, r1
}
// GetStashIDs provides a mock function with given fields: ctx, studioID
func (_m *StudioReaderWriter) GetStashIDs(ctx context.Context, studioID int) ([]*models.StashID, error) {
ret := _m.Called(ctx, studioID)
// GetStashIDs provides a mock function with given fields: ctx, relatedID
func (_m *StudioReaderWriter) GetStashIDs(ctx context.Context, relatedID int) ([]models.StashID, error) {
ret := _m.Called(ctx, relatedID)
var r0 []*models.StashID
if rf, ok := ret.Get(0).(func(context.Context, int) []*models.StashID); ok {
r0 = rf(ctx, studioID)
var r0 []models.StashID
if rf, ok := ret.Get(0).(func(context.Context, int) []models.StashID); ok {
r0 = rf(ctx, relatedID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.StashID)
r0 = ret.Get(0).([]models.StashID)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, studioID)
r1 = rf(ctx, relatedID)
} else {
r1 = ret.Error(1)
}
@@ -442,11 +442,11 @@ func (_m *StudioReaderWriter) UpdateImage(ctx context.Context, studioID int, ima
}
// UpdateStashIDs provides a mock function with given fields: ctx, studioID, stashIDs
func (_m *StudioReaderWriter) UpdateStashIDs(ctx context.Context, studioID int, stashIDs []*models.StashID) error {
func (_m *StudioReaderWriter) UpdateStashIDs(ctx context.Context, studioID int, stashIDs []models.StashID) error {
ret := _m.Called(ctx, studioID, stashIDs)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int, []*models.StashID) error); ok {
if rf, ok := ret.Get(0).(func(context.Context, int, []models.StashID) error); ok {
r0 = rf(ctx, studioID, stashIDs)
} else {
r0 = ret.Error(0)

View File

@@ -1,6 +1,7 @@
package models
import (
"context"
"path/filepath"
"time"
@@ -35,9 +36,27 @@ type Gallery struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
SceneIDs []int `json:"scene_ids"`
TagIDs []int `json:"tag_ids"`
PerformerIDs []int `json:"performer_ids"`
SceneIDs RelatedIDs `json:"scene_ids"`
TagIDs RelatedIDs `json:"tag_ids"`
PerformerIDs RelatedIDs `json:"performer_ids"`
}
func (g *Gallery) LoadSceneIDs(ctx context.Context, l SceneIDLoader) error {
return g.SceneIDs.load(func() ([]int, error) {
return l.GetSceneIDs(ctx, g.ID)
})
}
func (g *Gallery) LoadPerformerIDs(ctx context.Context, l PerformerIDLoader) error {
return g.PerformerIDs.load(func() ([]int, error) {
return l.GetPerformerIDs(ctx, g.ID)
})
}
func (g *Gallery) LoadTagIDs(ctx context.Context, l TagIDLoader) error {
return g.TagIDs.load(func() ([]int, error) {
return l.GetTagIDs(ctx, g.ID)
})
}
func (g Gallery) PrimaryFile() file.File {

View File

@@ -1,6 +1,7 @@
package models
import (
"context"
"time"
"github.com/stashapp/stash/pkg/file"
@@ -22,9 +23,27 @@ type Image struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
GalleryIDs []int `json:"gallery_ids"`
TagIDs []int `json:"tag_ids"`
PerformerIDs []int `json:"performer_ids"`
GalleryIDs RelatedIDs `json:"gallery_ids"`
TagIDs RelatedIDs `json:"tag_ids"`
PerformerIDs RelatedIDs `json:"performer_ids"`
}
func (i *Image) LoadGalleryIDs(ctx context.Context, l GalleryIDLoader) error {
return i.GalleryIDs.load(func() ([]int, error) {
return l.GetGalleryIDs(ctx, i.ID)
})
}
func (i *Image) LoadPerformerIDs(ctx context.Context, l PerformerIDLoader) error {
return i.PerformerIDs.load(func() ([]int, error) {
return l.GetPerformerIDs(ctx, i.ID)
})
}
func (i *Image) LoadTagIDs(ctx context.Context, l TagIDLoader) error {
return i.TagIDs.load(func() ([]int, error) {
return l.GetTagIDs(ctx, i.ID)
})
}
func (i Image) PrimaryFile() *file.ImageFile {

View File

@@ -1,6 +1,7 @@
package models
import (
"context"
"path/filepath"
"strconv"
"time"
@@ -26,11 +27,65 @@ type Scene struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
GalleryIDs []int `json:"gallery_ids"`
TagIDs []int `json:"tag_ids"`
PerformerIDs []int `json:"performer_ids"`
Movies []MoviesScenes `json:"movies"`
StashIDs []StashID `json:"stash_ids"`
GalleryIDs RelatedIDs `json:"gallery_ids"`
TagIDs RelatedIDs `json:"tag_ids"`
PerformerIDs RelatedIDs `json:"performer_ids"`
Movies RelatedMovies `json:"movies"`
StashIDs RelatedStashIDs `json:"stash_ids"`
}
func (s *Scene) LoadGalleryIDs(ctx context.Context, l GalleryIDLoader) error {
return s.GalleryIDs.load(func() ([]int, error) {
return l.GetGalleryIDs(ctx, s.ID)
})
}
func (s *Scene) LoadPerformerIDs(ctx context.Context, l PerformerIDLoader) error {
return s.PerformerIDs.load(func() ([]int, error) {
return l.GetPerformerIDs(ctx, s.ID)
})
}
func (s *Scene) LoadTagIDs(ctx context.Context, l TagIDLoader) error {
return s.TagIDs.load(func() ([]int, error) {
return l.GetTagIDs(ctx, s.ID)
})
}
func (s *Scene) LoadMovies(ctx context.Context, l SceneMovieLoader) error {
return s.Movies.load(func() ([]MoviesScenes, error) {
return l.GetMovies(ctx, s.ID)
})
}
func (s *Scene) LoadStashIDs(ctx context.Context, l StashIDLoader) error {
return s.StashIDs.load(func() ([]StashID, error) {
return l.GetStashIDs(ctx, s.ID)
})
}
func (s *Scene) LoadRelationships(ctx context.Context, l SceneReader) error {
if err := s.LoadGalleryIDs(ctx, l); err != nil {
return err
}
if err := s.LoadPerformerIDs(ctx, l); err != nil {
return err
}
if err := s.LoadTagIDs(ctx, l); err != nil {
return err
}
if err := s.LoadMovies(ctx, l); err != nil {
return err
}
if err := s.LoadStashIDs(ctx, l); err != nil {
return err
}
return nil
}
func (s Scene) PrimaryFile() *file.VideoFile {

View File

@@ -125,9 +125,13 @@ type PerformerFilterType struct {
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
}
type PerformerFinder interface {
FindMany(ctx context.Context, ids []int) ([]*Performer, error)
}
type PerformerReader interface {
Find(ctx context.Context, id int) (*Performer, error)
FindMany(ctx context.Context, ids []int) ([]*Performer, error)
PerformerFinder
FindBySceneID(ctx context.Context, sceneID int) ([]*Performer, error)
FindNamesBySceneID(ctx context.Context, sceneID int) ([]*Performer, error)
FindByImageID(ctx context.Context, imageID int) ([]*Performer, error)
@@ -143,7 +147,7 @@ type PerformerReader interface {
QueryForAutoTag(ctx context.Context, words []string) ([]*Performer, error)
Query(ctx context.Context, performerFilter *PerformerFilterType, findFilter *FindFilterType) ([]*Performer, int, error)
GetImage(ctx context.Context, performerID int) ([]byte, error)
GetStashIDs(ctx context.Context, performerID int) ([]*StashID, error)
StashIDLoader
GetTagIDs(ctx context.Context, performerID int) ([]int, error)
}
@@ -154,7 +158,7 @@ type PerformerWriter interface {
Destroy(ctx context.Context, id int) error
UpdateImage(ctx context.Context, performerID int, image []byte) error
DestroyImage(ctx context.Context, performerID int) error
UpdateStashIDs(ctx context.Context, performerID int, stashIDs []*StashID) error
UpdateStashIDs(ctx context.Context, performerID int, stashIDs []StashID) error
UpdateTags(ctx context.Context, performerID int, tagIDs []int) error
}

191
pkg/models/relationships.go Normal file
View File

@@ -0,0 +1,191 @@
package models
import "context"
type SceneIDLoader interface {
GetSceneIDs(ctx context.Context, relatedID int) ([]int, error)
}
type GalleryIDLoader interface {
GetGalleryIDs(ctx context.Context, relatedID int) ([]int, error)
}
type PerformerIDLoader interface {
GetPerformerIDs(ctx context.Context, relatedID int) ([]int, error)
}
type TagIDLoader interface {
GetTagIDs(ctx context.Context, relatedID int) ([]int, error)
}
type SceneMovieLoader interface {
GetMovies(ctx context.Context, id int) ([]MoviesScenes, error)
}
type StashIDLoader interface {
GetStashIDs(ctx context.Context, relatedID int) ([]StashID, error)
}
// RelatedIDs represents a list of related IDs.
// TODO - this can be made generic
type RelatedIDs struct {
list []int
}
// NewRelatedIDs returns a loaded RelatedIDs object with the provided IDs.
// Loaded will return true when called on the returned object if the provided slice is not nil.
func NewRelatedIDs(ids []int) RelatedIDs {
return RelatedIDs{
list: ids,
}
}
// Loaded returns true if the related IDs have been loaded.
func (r RelatedIDs) Loaded() bool {
return r.list != nil
}
func (r RelatedIDs) mustLoaded() {
if !r.Loaded() {
panic("list has not been loaded")
}
}
// List returns the related IDs. Panics if the relationship has not been loaded.
func (r RelatedIDs) List() []int {
r.mustLoaded()
return r.list
}
// Add adds the provided ids to the list. Panics if the relationship has not been loaded.
func (r *RelatedIDs) Add(ids ...int) {
r.mustLoaded()
r.list = append(r.list, ids...)
}
func (r *RelatedIDs) load(fn func() ([]int, error)) error {
if r.Loaded() {
return nil
}
ids, err := fn()
if err != nil {
return err
}
if ids == nil {
ids = []int{}
}
r.list = ids
return nil
}
// RelatedMovies represents a list of related Movies.
type RelatedMovies struct {
list []MoviesScenes
}
// NewRelatedMovies returns a loaded RelatedMovies object with the provided movies.
// Loaded will return true when called on the returned object if the provided slice is not nil.
func NewRelatedMovies(list []MoviesScenes) RelatedMovies {
return RelatedMovies{
list: list,
}
}
// Loaded returns true if the relationship has been loaded.
func (r RelatedMovies) Loaded() bool {
return r.list != nil
}
func (r RelatedMovies) mustLoaded() {
if !r.Loaded() {
panic("list has not been loaded")
}
}
// List returns the related Movies. Panics if the relationship has not been loaded.
func (r RelatedMovies) List() []MoviesScenes {
r.mustLoaded()
return r.list
}
// Add adds the provided ids to the list. Panics if the relationship has not been loaded.
func (r *RelatedMovies) Add(movies ...MoviesScenes) {
r.mustLoaded()
r.list = append(r.list, movies...)
}
func (r *RelatedMovies) load(fn func() ([]MoviesScenes, error)) error {
if r.Loaded() {
return nil
}
ids, err := fn()
if err != nil {
return err
}
if ids == nil {
ids = []MoviesScenes{}
}
r.list = ids
return nil
}
type RelatedStashIDs struct {
list []StashID
}
// NewRelatedStashIDs returns a RelatedStashIDs object with the provided ids.
// Loaded will return true when called on the returned object if the provided slice is not nil.
func NewRelatedStashIDs(list []StashID) RelatedStashIDs {
return RelatedStashIDs{
list: list,
}
}
func (r RelatedStashIDs) mustLoaded() {
if !r.Loaded() {
panic("list has not been loaded")
}
}
// Loaded returns true if the relationship has been loaded.
func (r RelatedStashIDs) Loaded() bool {
return r.list != nil
}
// List returns the related Stash IDs. Panics if the relationship has not been loaded.
func (r RelatedStashIDs) List() []StashID {
r.mustLoaded()
return r.list
}
func (r *RelatedStashIDs) load(fn func() ([]StashID, error)) error {
if r.Loaded() {
return nil
}
ids, err := fn()
if err != nil {
return err
}
if ids == nil {
ids = []StashID{}
}
r.list = ids
return nil
}

View File

@@ -133,6 +133,13 @@ type SceneReader interface {
FindByPerformerID(ctx context.Context, performerID int) ([]*Scene, error)
FindByGalleryID(ctx context.Context, performerID int) ([]*Scene, error)
FindDuplicates(ctx context.Context, distance int) ([][]*Scene, error)
GalleryIDLoader
PerformerIDLoader
TagIDLoader
SceneMovieLoader
StashIDLoader
CountByPerformerID(ctx context.Context, performerID int) (int, error)
// FindByStudioID(studioID int) ([]*Scene, error)
FindByMovieID(ctx context.Context, movieID int) ([]*Scene, error)

View File

@@ -30,9 +30,13 @@ type StudioFilterType struct {
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
}
type StudioFinder interface {
FindMany(ctx context.Context, ids []int) ([]*Studio, error)
}
type StudioReader interface {
Find(ctx context.Context, id int) (*Studio, error)
FindMany(ctx context.Context, ids []int) ([]*Studio, error)
StudioFinder
FindChildren(ctx context.Context, id int) ([]*Studio, error)
FindByName(ctx context.Context, name string, nocase bool) (*Studio, error)
FindByStashID(ctx context.Context, stashID StashID) ([]*Studio, error)
@@ -44,7 +48,7 @@ type StudioReader interface {
Query(ctx context.Context, studioFilter *StudioFilterType, findFilter *FindFilterType) ([]*Studio, int, error)
GetImage(ctx context.Context, studioID int) ([]byte, error)
HasImage(ctx context.Context, studioID int) (bool, error)
GetStashIDs(ctx context.Context, studioID int) ([]*StashID, error)
StashIDLoader
GetAliases(ctx context.Context, studioID int) ([]string, error)
}
@@ -55,7 +59,7 @@ type StudioWriter interface {
Destroy(ctx context.Context, id int) error
UpdateImage(ctx context.Context, studioID int, image []byte) error
DestroyImage(ctx context.Context, studioID int) error
UpdateStashIDs(ctx context.Context, studioID int, stashIDs []*StashID) error
UpdateStashIDs(ctx context.Context, studioID int, stashIDs []StashID) error
UpdateAliases(ctx context.Context, studioID int, aliases []string) error
}

View File

@@ -34,9 +34,13 @@ type TagFilterType struct {
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
}
type TagFinder interface {
FindMany(ctx context.Context, ids []int) ([]*Tag, error)
}
type TagReader interface {
Find(ctx context.Context, id int) (*Tag, error)
FindMany(ctx context.Context, ids []int) ([]*Tag, error)
TagFinder
FindBySceneID(ctx context.Context, sceneID int) ([]*Tag, error)
FindByPerformerID(ctx context.Context, performerID int) ([]*Tag, error)
FindBySceneMarkerID(ctx context.Context, sceneMarkerID int) ([]*Tag, error)

View File

@@ -12,7 +12,7 @@ import (
type ImageStashIDGetter interface {
GetImage(ctx context.Context, performerID int) ([]byte, error)
GetStashIDs(ctx context.Context, performerID int) ([]*models.StashID, error)
models.StashIDLoader
}
// ToJSON converts a Performer object into its JSON equivalent.
@@ -100,9 +100,9 @@ func ToJSON(ctx context.Context, reader ImageStashIDGetter, performer *models.Pe
}
stashIDs, _ := reader.GetStashIDs(ctx, performer.ID)
var ret []*models.StashID
var ret []models.StashID
for _, stashID := range stashIDs {
newJoin := &models.StashID{
newJoin := models.StashID{
StashID: stashID.StashID,
Endpoint: stashID.Endpoint,
}

View File

@@ -50,8 +50,8 @@ var stashID = models.StashID{
StashID: "StashID",
Endpoint: "Endpoint",
}
var stashIDs = []*models.StashID{
&stashID,
var stashIDs = []models.StashID{
stashID,
}
const image = "aW1hZ2VCeXRlcw=="
@@ -155,8 +155,8 @@ func createFullJSONPerformer(name string, image string) *jsonschema.Performer {
DeathDate: deathDate.String,
HairColor: hairColor,
Weight: weight,
StashIDs: []*models.StashID{
&stashID,
StashIDs: []models.StashID{
stashID,
},
IgnoreAutoTag: autoTagIgnored,
}

View File

@@ -19,7 +19,7 @@ type NameFinderCreatorUpdater interface {
UpdateFull(ctx context.Context, updatedPerformer models.Performer) (*models.Performer, error)
UpdateTags(ctx context.Context, performerID int, tagIDs []int) error
UpdateImage(ctx context.Context, performerID int, image []byte) error
UpdateStashIDs(ctx context.Context, performerID int, stashIDs []*models.StashID) error
UpdateStashIDs(ctx context.Context, performerID int, stashIDs []models.StashID) error
}
type Importer struct {

View File

@@ -80,7 +80,7 @@ func ToBasicJSON(ctx context.Context, reader CoverGetter, scene *models.Scene) (
}
var ret []models.StashID
for _, stashID := range scene.StashIDs {
for _, stashID := range scene.StashIDs.List() {
newJoin := models.StashID{
StashID: stashID.StashID,
Endpoint: stashID.Endpoint,
@@ -219,7 +219,7 @@ type MovieFinder interface {
// GetSceneMoviesJSON returns a slice of SceneMovie JSON representation objects
// corresponding to the provided scene's scene movie relationships.
func GetSceneMoviesJSON(ctx context.Context, movieReader MovieFinder, scene *models.Scene) ([]jsonschema.SceneMovie, error) {
sceneMovies := scene.Movies
sceneMovies := scene.Movies.List()
var results []jsonschema.SceneMovie
for _, sceneMovie := range sceneMovies {
@@ -246,7 +246,7 @@ func GetSceneMoviesJSON(ctx context.Context, movieReader MovieFinder, scene *mod
func GetDependentMovieIDs(ctx context.Context, scene *models.Scene) ([]int, error) {
var ret []int
m := scene.Movies
m := scene.Movies.List()
for _, mm := range m {
ret = append(ret, mm.MovieID)
}

View File

@@ -18,6 +18,7 @@ import (
type FullCreatorUpdater interface {
CreatorUpdater
Update(ctx context.Context, updatedScene *models.Scene) error
Updater
}
@@ -78,6 +79,11 @@ func (i *Importer) sceneJSONToScene(sceneJSON jsonschema.Scene) models.Scene {
Title: sceneJSON.Title,
Details: sceneJSON.Details,
URL: sceneJSON.URL,
PerformerIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}),
GalleryIDs: models.NewRelatedIDs([]int{}),
Movies: models.NewRelatedMovies([]models.MoviesScenes{}),
StashIDs: models.NewRelatedStashIDs(sceneJSON.StashIDs),
}
// if sceneJSON.Checksum != "" {
@@ -141,8 +147,6 @@ func (i *Importer) sceneJSONToScene(sceneJSON jsonschema.Scene) models.Scene {
// }
// }
newScene.StashIDs = append(newScene.StashIDs, i.Input.StashIDs...)
return newScene
}
@@ -214,7 +218,7 @@ func (i *Importer) populateGalleries(ctx context.Context) error {
}
for _, o := range galleries {
i.scene.GalleryIDs = append(i.scene.GalleryIDs, o.ID)
i.scene.GalleryIDs.Add(o.ID)
}
}
@@ -259,7 +263,7 @@ func (i *Importer) populatePerformers(ctx context.Context) error {
}
for _, p := range performers {
i.scene.PerformerIDs = append(i.scene.PerformerIDs, p.ID)
i.scene.PerformerIDs.Add(p.ID)
}
}
@@ -317,7 +321,7 @@ func (i *Importer) populateMovies(ctx context.Context) error {
toAdd.SceneIndex = &index
}
i.scene.Movies = append(i.scene.Movies, toAdd)
i.scene.Movies.Add(toAdd)
}
}
@@ -344,7 +348,7 @@ func (i *Importer) populateTags(ctx context.Context) error {
}
for _, p := range tags {
i.scene.TagIDs = append(i.scene.TagIDs, p.ID)
i.scene.TagIDs.Add(p.ID)
}
}

View File

@@ -22,8 +22,8 @@ type CreatorUpdater interface {
FindByFileID(ctx context.Context, fileID file.ID) ([]*models.Scene, error)
FindByFingerprints(ctx context.Context, fp []file.Fingerprint) ([]*models.Scene, error)
Create(ctx context.Context, newScene *models.Scene, fileIDs []file.ID) error
Update(ctx context.Context, updatedScene *models.Scene) error
UpdatePartial(ctx context.Context, id int, updatedScene models.ScenePartial) (*models.Scene, error)
AddFileID(ctx context.Context, id int, fileID file.ID) error
}
type ScanGenerator interface {
@@ -118,7 +118,7 @@ func (h *ScanHandler) associateExisting(ctx context.Context, existing []*models.
for _, s := range existing {
found := false
for _, sf := range s.Files {
if sf.ID == f.Base().ID {
if sf.ID == f.ID {
found = true
break
}
@@ -127,10 +127,10 @@ func (h *ScanHandler) associateExisting(ctx context.Context, existing []*models.
if !found {
logger.Infof("Adding %s to scene %s", f.Path, s.GetTitle())
s.Files = append(s.Files, f)
}
if err := h.CreatorUpdater.Update(ctx, s); err != nil {
return fmt.Errorf("updating scene: %w", err)
if err := h.CreatorUpdater.AddFileID(ctx, s.ID, f.ID); err != nil {
return fmt.Errorf("adding file to scene: %w", err)
}
}
}

View File

@@ -7,7 +7,6 @@ import (
"time"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
"github.com/stashapp/stash/pkg/utils"
)
@@ -87,53 +86,32 @@ func (u UpdateSet) UpdateInput() models.SceneUpdateInput {
return ret
}
func AddPerformer(ctx context.Context, qb PartialUpdater, o *models.Scene, performerID int) (bool, error) {
if !intslice.IntInclude(o.PerformerIDs, performerID) {
if _, err := qb.UpdatePartial(ctx, o.ID, models.ScenePartial{
func AddPerformer(ctx context.Context, qb PartialUpdater, o *models.Scene, performerID int) error {
_, err := qb.UpdatePartial(ctx, o.ID, models.ScenePartial{
PerformerIDs: &models.UpdateIDs{
IDs: []int{performerID},
Mode: models.RelationshipUpdateModeAdd,
},
}); err != nil {
return false, err
}
return true, nil
}
return false, nil
})
return err
}
func AddTag(ctx context.Context, qb PartialUpdater, o *models.Scene, tagID int) (bool, error) {
if !intslice.IntInclude(o.TagIDs, tagID) {
if _, err := qb.UpdatePartial(ctx, o.ID, models.ScenePartial{
func AddTag(ctx context.Context, qb PartialUpdater, o *models.Scene, tagID int) error {
_, err := qb.UpdatePartial(ctx, o.ID, models.ScenePartial{
TagIDs: &models.UpdateIDs{
IDs: []int{tagID},
Mode: models.RelationshipUpdateModeAdd,
},
}); err != nil {
return false, err
}
return true, nil
}
return false, nil
})
return err
}
func AddGallery(ctx context.Context, qb PartialUpdater, o *models.Scene, galleryID int) (bool, error) {
if !intslice.IntInclude(o.GalleryIDs, galleryID) {
if _, err := qb.UpdatePartial(ctx, o.ID, models.ScenePartial{
func AddGallery(ctx context.Context, qb PartialUpdater, o *models.Scene, galleryID int) error {
_, err := qb.UpdatePartial(ctx, o.ID, models.ScenePartial{
TagIDs: &models.UpdateIDs{
IDs: []int{galleryID},
Mode: models.RelationshipUpdateModeAdd,
},
}); err != nil {
return false, err
}
return true, nil
}
return false, nil
})
return err
}

View File

@@ -32,20 +32,21 @@ import (
type SceneReader interface {
Find(ctx context.Context, id int) (*models.Scene, error)
models.StashIDLoader
}
type PerformerReader interface {
match.PerformerFinder
Find(ctx context.Context, id int) (*models.Performer, error)
FindBySceneID(ctx context.Context, sceneID int) ([]*models.Performer, error)
GetStashIDs(ctx context.Context, performerID int) ([]*models.StashID, error)
models.StashIDLoader
GetImage(ctx context.Context, performerID int) ([]byte, error)
}
type StudioReader interface {
match.StudioFinder
studio.Finder
GetStashIDs(ctx context.Context, studioID int) ([]*models.StashID, error)
models.StashIDLoader
}
type TagFinder interface {
tag.Queryer
@@ -227,7 +228,11 @@ func (c Client) SubmitStashBoxFingerprints(ctx context.Context, sceneIDs []strin
continue
}
stashIDs := scene.StashIDs
if err := scene.LoadStashIDs(ctx, qb); err != nil {
return err
}
stashIDs := scene.StashIDs.List()
sceneStashID := ""
for _, stashID := range stashIDs {
if stashID.Endpoint == endpoint {
@@ -827,8 +832,9 @@ func (c Client) SubmitSceneDraft(ctx context.Context, scene *models.Scene, endpo
}
for _, stashID := range stashIDs {
c := stashID
if stashID.Endpoint == endpoint {
performerDraft.ID = &stashID.StashID
performerDraft.ID = &c.StashID
break
}
}
@@ -855,7 +861,7 @@ func (c Client) SubmitSceneDraft(ctx context.Context, scene *models.Scene, endpo
}
}
stashIDs := scene.StashIDs
stashIDs := scene.StashIDs.List()
var stashID *string
for _, v := range stashIDs {
if v.Endpoint == endpoint {
@@ -952,8 +958,9 @@ func (c Client) SubmitPerformerDraft(ctx context.Context, performer *models.Perf
}
var stashID *string
for _, v := range stashIDs {
c := v
if v.Endpoint == endpoint {
stashID = &v.StashID
stashID = &c.StashID
break
}
}

View File

@@ -133,15 +133,21 @@ func (qb *GalleryStore) Create(ctx context.Context, newObject *models.Gallery, f
}
}
if err := galleriesPerformersTableMgr.insertJoins(ctx, id, newObject.PerformerIDs); err != nil {
if newObject.PerformerIDs.Loaded() {
if err := galleriesPerformersTableMgr.insertJoins(ctx, id, newObject.PerformerIDs.List()); err != nil {
return err
}
if err := galleriesTagsTableMgr.insertJoins(ctx, id, newObject.TagIDs); err != nil {
}
if newObject.TagIDs.Loaded() {
if err := galleriesTagsTableMgr.insertJoins(ctx, id, newObject.TagIDs.List()); err != nil {
return err
}
if err := galleriesScenesTableMgr.insertJoins(ctx, id, newObject.SceneIDs); err != nil {
}
if newObject.SceneIDs.Loaded() {
if err := galleriesScenesTableMgr.insertJoins(ctx, id, newObject.SceneIDs.List()); err != nil {
return err
}
}
updated, err := qb.Find(ctx, id)
if err != nil {
@@ -161,15 +167,21 @@ func (qb *GalleryStore) Update(ctx context.Context, updatedObject *models.Galler
return err
}
if err := galleriesPerformersTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.PerformerIDs); err != nil {
if updatedObject.PerformerIDs.Loaded() {
if err := galleriesPerformersTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.PerformerIDs.List()); err != nil {
return err
}
if err := galleriesTagsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.TagIDs); err != nil {
}
if updatedObject.TagIDs.Loaded() {
if err := galleriesTagsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.TagIDs.List()); err != nil {
return err
}
if err := galleriesScenesTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.SceneIDs); err != nil {
}
if updatedObject.SceneIDs.Loaded() {
if err := galleriesScenesTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.SceneIDs.List()); err != nil {
return err
}
}
fileIDs := make([]file.ID, len(updatedObject.Files))
for i, f := range updatedObject.Files {
@@ -249,16 +261,18 @@ func (qb *GalleryStore) getMany(ctx context.Context, q *goqu.SelectDataset) ([]*
s := f.resolve()
if err := qb.resolveRelationships(ctx, s); err != nil {
return err
}
ret = append(ret, s)
return nil
}); err != nil {
return nil, err
}
for _, s := range ret {
if err := qb.resolveRelationships(ctx, s); err != nil {
return nil, err
}
}
return ret, nil
}
@@ -281,24 +295,6 @@ func (qb *GalleryStore) resolveRelationships(ctx context.Context, s *models.Gall
s.FolderPath = folder.Path
}
// performers
s.PerformerIDs, err = qb.performersRepository().getIDs(ctx, s.ID)
if err != nil {
return fmt.Errorf("resolving gallery performers: %w", err)
}
// tags
s.TagIDs, err = qb.tagsRepository().getIDs(ctx, s.ID)
if err != nil {
return fmt.Errorf("resolving gallery tags: %w", err)
}
// scenes
s.SceneIDs, err = qb.scenesRepository().getIDs(ctx, s.ID)
if err != nil {
return fmt.Errorf("resolving gallery scenes: %w", err)
}
return nil
}
@@ -989,6 +985,11 @@ func (qb *GalleryStore) filesRepository() *filesRepository {
}
}
func (qb *GalleryStore) AddFileID(ctx context.Context, id int, fileID file.ID) error {
const firstPrimary = false
return galleriesFilesTableMgr.insertJoins(ctx, id, firstPrimary, []file.ID{fileID})
}
func (qb *GalleryStore) performersRepository() *joinRepository {
return &joinRepository{
repository: repository{
@@ -1000,6 +1001,10 @@ func (qb *GalleryStore) performersRepository() *joinRepository {
}
}
func (qb *GalleryStore) GetPerformerIDs(ctx context.Context, id int) ([]int, error) {
return qb.performersRepository().getIDs(ctx, id)
}
func (qb *GalleryStore) tagsRepository() *joinRepository {
return &joinRepository{
repository: repository{
@@ -1011,6 +1016,10 @@ func (qb *GalleryStore) tagsRepository() *joinRepository {
}
}
func (qb *GalleryStore) GetTagIDs(ctx context.Context, id int) ([]int, error) {
return qb.tagsRepository().getIDs(ctx, id)
}
func (qb *GalleryStore) imagesRepository() *joinRepository {
return &joinRepository{
repository: repository{
@@ -1041,3 +1050,7 @@ func (qb *GalleryStore) scenesRepository() *joinRepository {
fkColumn: sceneIDColumn,
}
}
func (qb *GalleryStore) GetSceneIDs(ctx context.Context, id int) ([]int, error) {
return qb.scenesRepository().getIDs(ctx, id)
}

View File

@@ -17,6 +17,26 @@ import (
var invalidID = -1
func loadGalleryRelationships(ctx context.Context, expected models.Gallery, actual *models.Gallery) error {
if expected.SceneIDs.Loaded() {
if err := actual.LoadSceneIDs(ctx, db.Gallery); err != nil {
return err
}
}
if expected.TagIDs.Loaded() {
if err := actual.LoadTagIDs(ctx, db.Gallery); err != nil {
return err
}
}
if expected.PerformerIDs.Loaded() {
if err := actual.LoadPerformerIDs(ctx, db.Gallery); err != nil {
return err
}
}
return nil
}
func Test_galleryQueryBuilder_Create(t *testing.T) {
var (
title = "title"
@@ -48,9 +68,9 @@ func Test_galleryQueryBuilder_Create(t *testing.T) {
StudioID: &studioIDs[studioIdxWithScene],
CreatedAt: createdAt,
UpdatedAt: updatedAt,
SceneIDs: []int{sceneIDs[sceneIdx1WithPerformer], sceneIDs[sceneIdx1WithStudio]},
TagIDs: []int{tagIDs[tagIdx1WithScene], tagIDs[tagIdx1WithDupName]},
PerformerIDs: []int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]},
SceneIDs: models.NewRelatedIDs([]int{sceneIDs[sceneIdx1WithPerformer], sceneIDs[sceneIdx1WithStudio]}),
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithScene], tagIDs[tagIdx1WithDupName]}),
PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]}),
Files: []file.File{},
},
false,
@@ -70,9 +90,9 @@ func Test_galleryQueryBuilder_Create(t *testing.T) {
},
CreatedAt: createdAt,
UpdatedAt: updatedAt,
SceneIDs: []int{sceneIDs[sceneIdx1WithPerformer], sceneIDs[sceneIdx1WithStudio]},
TagIDs: []int{tagIDs[tagIdx1WithScene], tagIDs[tagIdx1WithDupName]},
PerformerIDs: []int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]},
SceneIDs: models.NewRelatedIDs([]int{sceneIDs[sceneIdx1WithPerformer], sceneIDs[sceneIdx1WithStudio]}),
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithScene], tagIDs[tagIdx1WithDupName]}),
PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]}),
},
false,
},
@@ -86,21 +106,21 @@ func Test_galleryQueryBuilder_Create(t *testing.T) {
{
"invalid scene id",
models.Gallery{
SceneIDs: []int{invalidID},
SceneIDs: models.NewRelatedIDs([]int{invalidID}),
},
true,
},
{
"invalid tag id",
models.Gallery{
TagIDs: []int{invalidID},
TagIDs: models.NewRelatedIDs([]int{invalidID}),
},
true,
},
{
"invalid performer id",
models.Gallery{
PerformerIDs: []int{invalidID},
PerformerIDs: models.NewRelatedIDs([]int{invalidID}),
},
true,
},
@@ -132,6 +152,12 @@ func Test_galleryQueryBuilder_Create(t *testing.T) {
copy := tt.newObject
copy.ID = s.ID
// load relationships
if err := loadGalleryRelationships(ctx, copy, &s); err != nil {
t.Errorf("loadGalleryRelationships() error = %v", err)
return
}
assert.Equal(copy, s)
// ensure can find the scene
@@ -144,6 +170,12 @@ func Test_galleryQueryBuilder_Create(t *testing.T) {
return
}
// load relationships
if err := loadGalleryRelationships(ctx, copy, found); err != nil {
t.Errorf("loadGalleryRelationships() error = %v", err)
return
}
assert.Equal(copy, *found)
return
@@ -190,9 +222,9 @@ func Test_galleryQueryBuilder_Update(t *testing.T) {
},
CreatedAt: createdAt,
UpdatedAt: updatedAt,
SceneIDs: []int{sceneIDs[sceneIdx1WithPerformer], sceneIDs[sceneIdx1WithStudio]},
TagIDs: []int{tagIDs[tagIdx1WithScene], tagIDs[tagIdx1WithDupName]},
PerformerIDs: []int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]},
SceneIDs: models.NewRelatedIDs([]int{sceneIDs[sceneIdx1WithPerformer], sceneIDs[sceneIdx1WithStudio]}),
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithScene], tagIDs[tagIdx1WithDupName]}),
PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]}),
},
false,
},
@@ -203,9 +235,9 @@ func Test_galleryQueryBuilder_Update(t *testing.T) {
Files: []file.File{
makeGalleryFileWithID(galleryIdxWithImage),
},
SceneIDs: []int{},
TagIDs: []int{},
PerformerIDs: []int{},
SceneIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}),
PerformerIDs: models.NewRelatedIDs([]int{}),
Organized: true,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
@@ -219,9 +251,9 @@ func Test_galleryQueryBuilder_Update(t *testing.T) {
Files: []file.File{
makeGalleryFileWithID(galleryIdxWithScene),
},
SceneIDs: []int{},
TagIDs: []int{},
PerformerIDs: []int{},
SceneIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}),
PerformerIDs: models.NewRelatedIDs([]int{}),
Organized: true,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
@@ -235,9 +267,9 @@ func Test_galleryQueryBuilder_Update(t *testing.T) {
Files: []file.File{
makeGalleryFileWithID(galleryIdxWithTag),
},
SceneIDs: []int{},
TagIDs: []int{},
PerformerIDs: []int{},
SceneIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}),
PerformerIDs: models.NewRelatedIDs([]int{}),
Organized: true,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
@@ -251,9 +283,9 @@ func Test_galleryQueryBuilder_Update(t *testing.T) {
Files: []file.File{
makeGalleryFileWithID(galleryIdxWithPerformer),
},
SceneIDs: []int{},
TagIDs: []int{},
PerformerIDs: []int{},
SceneIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}),
PerformerIDs: models.NewRelatedIDs([]int{}),
Organized: true,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
@@ -282,7 +314,7 @@ func Test_galleryQueryBuilder_Update(t *testing.T) {
makeGalleryFileWithID(galleryIdxWithImage),
},
Organized: true,
SceneIDs: []int{invalidID},
SceneIDs: models.NewRelatedIDs([]int{invalidID}),
CreatedAt: createdAt,
UpdatedAt: updatedAt,
},
@@ -296,7 +328,7 @@ func Test_galleryQueryBuilder_Update(t *testing.T) {
makeGalleryFileWithID(galleryIdxWithImage),
},
Organized: true,
TagIDs: []int{invalidID},
TagIDs: models.NewRelatedIDs([]int{invalidID}),
CreatedAt: createdAt,
UpdatedAt: updatedAt,
},
@@ -310,7 +342,7 @@ func Test_galleryQueryBuilder_Update(t *testing.T) {
makeGalleryFileWithID(galleryIdxWithImage),
},
Organized: true,
PerformerIDs: []int{invalidID},
PerformerIDs: models.NewRelatedIDs([]int{invalidID}),
CreatedAt: createdAt,
UpdatedAt: updatedAt,
},
@@ -339,6 +371,12 @@ func Test_galleryQueryBuilder_Update(t *testing.T) {
return
}
// load relationships
if err := loadGalleryRelationships(ctx, copy, s); err != nil {
t.Errorf("loadGalleryRelationships() error = %v", err)
return
}
assert.Equal(copy, *s)
return
@@ -426,9 +464,9 @@ func Test_galleryQueryBuilder_UpdatePartial(t *testing.T) {
},
CreatedAt: createdAt,
UpdatedAt: updatedAt,
SceneIDs: []int{sceneIDs[sceneIdxWithGallery]},
TagIDs: []int{tagIDs[tagIdx1WithGallery], tagIDs[tagIdx1WithDupName]},
PerformerIDs: []int{performerIDs[performerIdx1WithGallery], performerIDs[performerIdx1WithDupName]},
SceneIDs: models.NewRelatedIDs([]int{sceneIDs[sceneIdxWithGallery]}),
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithGallery], tagIDs[tagIdx1WithDupName]}),
PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithGallery], performerIDs[performerIdx1WithDupName]}),
},
false,
},
@@ -441,9 +479,9 @@ func Test_galleryQueryBuilder_UpdatePartial(t *testing.T) {
Files: []file.File{
makeGalleryFile(galleryIdxWithImage),
},
SceneIDs: []int{},
TagIDs: []int{},
PerformerIDs: []int{},
SceneIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}),
PerformerIDs: models.NewRelatedIDs([]int{}),
},
false,
},
@@ -472,6 +510,11 @@ func Test_galleryQueryBuilder_UpdatePartial(t *testing.T) {
}
clearGalleryFileIDs(got)
// load relationships
if err := loadGalleryRelationships(ctx, tt.want, got); err != nil {
t.Errorf("loadGalleryRelationships() error = %v", err)
return
}
assert.Equal(tt.want, *got)
s, err := qb.Find(ctx, tt.id)
@@ -480,6 +523,11 @@ func Test_galleryQueryBuilder_UpdatePartial(t *testing.T) {
}
clearGalleryFileIDs(s)
// load relationships
if err := loadGalleryRelationships(ctx, tt.want, s); err != nil {
t.Errorf("loadGalleryRelationships() error = %v", err)
return
}
assert.Equal(tt.want, *s)
})
}
@@ -503,10 +551,10 @@ func Test_galleryQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Gallery{
SceneIDs: append(indexesToIDs(sceneIDs, sceneGalleries.reverseLookup(galleryIdx1WithImage)),
SceneIDs: models.NewRelatedIDs(append(indexesToIDs(sceneIDs, sceneGalleries.reverseLookup(galleryIdx1WithImage)),
sceneIDs[sceneIdx1WithStudio],
sceneIDs[sceneIdx1WithPerformer],
),
)),
},
false,
},
@@ -520,10 +568,10 @@ func Test_galleryQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Gallery{
TagIDs: append(indexesToIDs(tagIDs, galleryTags[galleryIdxWithTwoTags]),
TagIDs: models.NewRelatedIDs(append(indexesToIDs(tagIDs, galleryTags[galleryIdxWithTwoTags]),
tagIDs[tagIdx1WithDupName],
tagIDs[tagIdx1WithImage],
),
)),
},
false,
},
@@ -537,10 +585,10 @@ func Test_galleryQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Gallery{
PerformerIDs: append(indexesToIDs(performerIDs, galleryPerformers[galleryIdxWithTwoPerformers]),
PerformerIDs: models.NewRelatedIDs(append(indexesToIDs(performerIDs, galleryPerformers[galleryIdxWithTwoPerformers]),
performerIDs[performerIdx1WithDupName],
performerIDs[performerIdx1WithImage],
),
)),
},
false,
},
@@ -554,9 +602,9 @@ func Test_galleryQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Gallery{
SceneIDs: append(indexesToIDs(sceneIDs, sceneGalleries.reverseLookup(galleryIdxWithScene)),
SceneIDs: models.NewRelatedIDs(append(indexesToIDs(sceneIDs, sceneGalleries.reverseLookup(galleryIdxWithScene)),
sceneIDs[sceneIdx1WithPerformer],
),
)),
},
false,
},
@@ -570,9 +618,9 @@ func Test_galleryQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Gallery{
TagIDs: append(indexesToIDs(tagIDs, galleryTags[galleryIdxWithTwoTags]),
TagIDs: models.NewRelatedIDs(append(indexesToIDs(tagIDs, galleryTags[galleryIdxWithTwoTags]),
tagIDs[tagIdx1WithScene],
),
)),
},
false,
},
@@ -586,9 +634,9 @@ func Test_galleryQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Gallery{
PerformerIDs: append(indexesToIDs(performerIDs, galleryPerformers[galleryIdxWithTwoPerformers]),
PerformerIDs: models.NewRelatedIDs(append(indexesToIDs(performerIDs, galleryPerformers[galleryIdxWithTwoPerformers]),
performerIDs[performerIdx1WithScene],
),
)),
},
false,
},
@@ -638,7 +686,7 @@ func Test_galleryQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Gallery{
SceneIDs: []int{},
SceneIDs: models.NewRelatedIDs([]int{}),
},
false,
},
@@ -652,7 +700,7 @@ func Test_galleryQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Gallery{
TagIDs: []int{tagIDs[tagIdx2WithGallery]},
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx2WithGallery]}),
},
false,
},
@@ -666,7 +714,7 @@ func Test_galleryQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Gallery{
PerformerIDs: []int{performerIDs[performerIdx2WithGallery]},
PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx2WithGallery]}),
},
false,
},
@@ -680,7 +728,7 @@ func Test_galleryQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Gallery{
SceneIDs: []int{sceneIDs[sceneIdxWithGallery]},
SceneIDs: models.NewRelatedIDs([]int{sceneIDs[sceneIdxWithGallery]}),
},
false,
},
@@ -694,7 +742,7 @@ func Test_galleryQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Gallery{
TagIDs: indexesToIDs(tagIDs, galleryTags[galleryIdxWithTwoTags]),
TagIDs: models.NewRelatedIDs(indexesToIDs(tagIDs, galleryTags[galleryIdxWithTwoTags])),
},
false,
},
@@ -708,7 +756,7 @@ func Test_galleryQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Gallery{
PerformerIDs: indexesToIDs(performerIDs, galleryPerformers[galleryIdxWithTwoPerformers]),
PerformerIDs: models.NewRelatedIDs(indexesToIDs(performerIDs, galleryPerformers[galleryIdxWithTwoPerformers])),
},
false,
},
@@ -735,6 +783,16 @@ func Test_galleryQueryBuilder_UpdatePartialRelationships(t *testing.T) {
t.Errorf("galleryQueryBuilder.Find() error = %v", err)
}
// load relationships
if err := loadGalleryRelationships(ctx, tt.want, got); err != nil {
t.Errorf("loadGalleryRelationships() error = %v", err)
return
}
if err := loadGalleryRelationships(ctx, tt.want, s); err != nil {
t.Errorf("loadGalleryRelationships() error = %v", err)
return
}
// only compare fields that were in the partial
if tt.partial.PerformerIDs != nil {
assert.Equal(tt.want.PerformerIDs, got.PerformerIDs)
@@ -851,12 +909,33 @@ func Test_galleryQueryBuilder_Find(t *testing.T) {
if got != nil {
clearGalleryFileIDs(got)
// load relationships
if err := loadGalleryRelationships(ctx, *tt.want, got); err != nil {
t.Errorf("loadGalleryRelationships() error = %v", err)
return
}
}
assert.Equal(tt.want, got)
})
}
}
func postFindGalleries(ctx context.Context, want []*models.Gallery, got []*models.Gallery) error {
for i, s := range got {
clearGalleryFileIDs(s)
// load relationships
if i < len(want) {
if err := loadGalleryRelationships(ctx, *want[i], s); err != nil {
return err
}
}
}
return nil
}
func Test_galleryQueryBuilder_FindMany(t *testing.T) {
tests := []struct {
name string
@@ -893,8 +972,9 @@ func Test_galleryQueryBuilder_FindMany(t *testing.T) {
return
}
for _, f := range got {
clearGalleryFileIDs(f)
if err := postFindGalleries(ctx, tt.want, got); err != nil {
t.Errorf("loadGalleryRelationships() error = %v", err)
return
}
assert.Equal(tt.want, got)
@@ -950,8 +1030,9 @@ func Test_galleryQueryBuilder_FindByChecksum(t *testing.T) {
return
}
for _, f := range got {
clearGalleryFileIDs(f)
if err := postFindGalleries(ctx, tt.want, got); err != nil {
t.Errorf("loadGalleryRelationships() error = %v", err)
return
}
assert.Equal(tt.want, got)
@@ -1012,8 +1093,9 @@ func Test_galleryQueryBuilder_FindByChecksums(t *testing.T) {
return
}
for _, f := range got {
clearGalleryFileIDs(f)
if err := postFindGalleries(ctx, tt.want, got); err != nil {
t.Errorf("loadGalleryRelationships() error = %v", err)
return
}
assert.Equal(tt.want, got)
@@ -1069,8 +1151,9 @@ func Test_galleryQueryBuilder_FindByPath(t *testing.T) {
return
}
for _, f := range got {
clearGalleryFileIDs(f)
if err := postFindGalleries(ctx, tt.want, got); err != nil {
t.Errorf("loadGalleryRelationships() error = %v", err)
return
}
assert.Equal(tt.want, got)
@@ -1110,8 +1193,9 @@ func Test_galleryQueryBuilder_FindBySceneID(t *testing.T) {
return
}
for _, f := range got {
clearGalleryFileIDs(f)
if err := postFindGalleries(ctx, tt.want, got); err != nil {
t.Errorf("loadGalleryRelationships() error = %v", err)
return
}
assert.Equal(tt.want, got)
@@ -1154,8 +1238,9 @@ func Test_galleryQueryBuilder_FindByImageID(t *testing.T) {
return
}
for _, f := range got {
clearGalleryFileIDs(f)
if err := postFindGalleries(ctx, tt.want, got); err != nil {
t.Errorf("loadGalleryRelationships() error = %v", err)
return
}
assert.Equal(tt.want, got)
@@ -2143,7 +2228,11 @@ func verifyGalleriesTagCount(t *testing.T, tagCountCriterion models.IntCriterion
assert.Greater(t, len(galleries), 0)
for _, gallery := range galleries {
verifyInt(t, len(gallery.TagIDs), tagCountCriterion)
if err := gallery.LoadTagIDs(ctx, sqb); err != nil {
t.Errorf("gallery.LoadTagIDs() error = %v", err)
return nil
}
verifyInt(t, len(gallery.TagIDs.List()), tagCountCriterion)
}
return nil
@@ -2180,7 +2269,12 @@ func verifyGalleriesPerformerCount(t *testing.T, performerCountCriterion models.
assert.Greater(t, len(galleries), 0)
for _, gallery := range galleries {
verifyInt(t, len(gallery.PerformerIDs), performerCountCriterion)
if err := gallery.LoadPerformerIDs(ctx, sqb); err != nil {
t.Errorf("gallery.LoadPerformerIDs() error = %v", err)
return nil
}
verifyInt(t, len(gallery.PerformerIDs.List()), performerCountCriterion)
}
return nil

View File

@@ -116,18 +116,19 @@ func (qb *ImageStore) Create(ctx context.Context, newObject *models.ImageCreateI
}
}
if len(newObject.GalleryIDs) > 0 {
if err := imageGalleriesTableMgr.insertJoins(ctx, id, newObject.GalleryIDs); err != nil {
if newObject.PerformerIDs.Loaded() {
if err := imagesPerformersTableMgr.insertJoins(ctx, id, newObject.PerformerIDs.List()); err != nil {
return err
}
}
if len(newObject.PerformerIDs) > 0 {
if err := imagesPerformersTableMgr.insertJoins(ctx, id, newObject.PerformerIDs); err != nil {
if newObject.TagIDs.Loaded() {
if err := imagesTagsTableMgr.insertJoins(ctx, id, newObject.TagIDs.List()); err != nil {
return err
}
}
if len(newObject.TagIDs) > 0 {
if err := imagesTagsTableMgr.insertJoins(ctx, id, newObject.TagIDs); err != nil {
if newObject.GalleryIDs.Loaded() {
if err := imageGalleriesTableMgr.insertJoins(ctx, id, newObject.GalleryIDs.List()); err != nil {
return err
}
}
@@ -184,15 +185,23 @@ func (qb *ImageStore) Update(ctx context.Context, updatedObject *models.Image) e
return err
}
if err := imageGalleriesTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.GalleryIDs); err != nil {
if updatedObject.PerformerIDs.Loaded() {
if err := imagesPerformersTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.PerformerIDs.List()); err != nil {
return err
}
if err := imagesPerformersTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.PerformerIDs); err != nil {
}
if updatedObject.TagIDs.Loaded() {
if err := imagesTagsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.TagIDs.List()); err != nil {
return err
}
if err := imagesTagsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.TagIDs); err != nil {
}
if updatedObject.GalleryIDs.Loaded() {
if err := imageGalleriesTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.GalleryIDs.List()); err != nil {
return err
}
}
fileIDs := make([]file.ID, len(updatedObject.Files))
for i, f := range updatedObject.Files {
@@ -265,16 +274,18 @@ func (qb *ImageStore) getMany(ctx context.Context, q *goqu.SelectDataset) ([]*mo
i := f.resolve()
if err := qb.resolveRelationships(ctx, i); err != nil {
return err
}
ret = append(ret, i)
return nil
}); err != nil {
return nil, err
}
for _, i := range ret {
if err := qb.resolveRelationships(ctx, i); err != nil {
return nil, err
}
}
return ret, nil
}
@@ -287,24 +298,6 @@ func (qb *ImageStore) resolveRelationships(ctx context.Context, i *models.Image)
return fmt.Errorf("resolving image files: %w", err)
}
// performers
i.PerformerIDs, err = qb.performersRepository().getIDs(ctx, i.ID)
if err != nil {
return fmt.Errorf("resolving image performers: %w", err)
}
// tags
i.TagIDs, err = qb.tagsRepository().getIDs(ctx, i.ID)
if err != nil {
return fmt.Errorf("resolving image tags: %w", err)
}
// galleries
i.GalleryIDs, err = qb.galleriesRepository().getIDs(ctx, i.ID)
if err != nil {
return fmt.Errorf("resolving image galleries: %w", err)
}
return nil
}
@@ -1004,9 +997,14 @@ func (qb *ImageStore) filesRepository() *filesRepository {
}
}
// func (qb *imageQueryBuilder) GetGalleryIDs(ctx context.Context, imageID int) ([]int, error) {
// return qb.galleriesRepository().getIDs(ctx, imageID)
// }
func (qb *ImageStore) AddFileID(ctx context.Context, id int, fileID file.ID) error {
const firstPrimary = false
return imagesFilesTableMgr.insertJoins(ctx, id, firstPrimary, []file.ID{fileID})
}
func (qb *ImageStore) GetGalleryIDs(ctx context.Context, imageID int) ([]int, error) {
return qb.galleriesRepository().getIDs(ctx, imageID)
}
// func (qb *imageQueryBuilder) UpdateGalleries(ctx context.Context, imageID int, galleryIDs []int) error {
// // Delete the existing joins and then create new ones

View File

@@ -15,6 +15,26 @@ import (
"github.com/stretchr/testify/assert"
)
func loadImageRelationships(ctx context.Context, expected models.Image, actual *models.Image) error {
if expected.GalleryIDs.Loaded() {
if err := actual.LoadGalleryIDs(ctx, db.Image); err != nil {
return err
}
}
if expected.TagIDs.Loaded() {
if err := actual.LoadTagIDs(ctx, db.Image); err != nil {
return err
}
}
if expected.PerformerIDs.Loaded() {
if err := actual.LoadPerformerIDs(ctx, db.Image); err != nil {
return err
}
}
return nil
}
func Test_imageQueryBuilder_Create(t *testing.T) {
var (
title = "title"
@@ -41,9 +61,9 @@ func Test_imageQueryBuilder_Create(t *testing.T) {
StudioID: &studioIDs[studioIdxWithImage],
CreatedAt: createdAt,
UpdatedAt: updatedAt,
GalleryIDs: []int{galleryIDs[galleryIdxWithImage]},
TagIDs: []int{tagIDs[tagIdx1WithImage], tagIDs[tagIdx1WithDupName]},
PerformerIDs: []int{performerIDs[performerIdx1WithImage], performerIDs[performerIdx1WithDupName]},
GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithImage]}),
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithImage], tagIDs[tagIdx1WithDupName]}),
PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithImage], performerIDs[performerIdx1WithDupName]}),
Files: []*file.ImageFile{},
},
false,
@@ -61,9 +81,9 @@ func Test_imageQueryBuilder_Create(t *testing.T) {
},
CreatedAt: createdAt,
UpdatedAt: updatedAt,
GalleryIDs: []int{galleryIDs[galleryIdxWithImage]},
TagIDs: []int{tagIDs[tagIdx1WithImage], tagIDs[tagIdx1WithDupName]},
PerformerIDs: []int{performerIDs[performerIdx1WithImage], performerIDs[performerIdx1WithDupName]},
GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithImage]}),
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithImage], tagIDs[tagIdx1WithDupName]}),
PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithImage], performerIDs[performerIdx1WithDupName]}),
},
false,
},
@@ -77,21 +97,21 @@ func Test_imageQueryBuilder_Create(t *testing.T) {
{
"invalid gallery id",
models.Image{
GalleryIDs: []int{invalidID},
GalleryIDs: models.NewRelatedIDs([]int{invalidID}),
},
true,
},
{
"invalid tag id",
models.Image{
TagIDs: []int{invalidID},
TagIDs: models.NewRelatedIDs([]int{invalidID}),
},
true,
},
{
"invalid performer id",
models.Image{
PerformerIDs: []int{invalidID},
PerformerIDs: models.NewRelatedIDs([]int{invalidID}),
},
true,
},
@@ -126,6 +146,12 @@ func Test_imageQueryBuilder_Create(t *testing.T) {
copy := tt.newObject
copy.ID = s.ID
// load relationships
if err := loadImageRelationships(ctx, copy, &s); err != nil {
t.Errorf("loadImageRelationships() error = %v", err)
return
}
assert.Equal(copy, s)
// ensure can find the image
@@ -134,6 +160,12 @@ func Test_imageQueryBuilder_Create(t *testing.T) {
t.Errorf("imageQueryBuilder.Find() error = %v", err)
}
// load relationships
if err := loadImageRelationships(ctx, copy, found); err != nil {
t.Errorf("loadImageRelationships() error = %v", err)
return
}
assert.Equal(copy, *found)
return
@@ -181,9 +213,9 @@ func Test_imageQueryBuilder_Update(t *testing.T) {
},
CreatedAt: createdAt,
UpdatedAt: updatedAt,
GalleryIDs: []int{galleryIDs[galleryIdxWithImage]},
TagIDs: []int{tagIDs[tagIdx1WithImage], tagIDs[tagIdx1WithDupName]},
PerformerIDs: []int{performerIDs[performerIdx1WithImage], performerIDs[performerIdx1WithDupName]},
GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithImage]}),
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithImage], tagIDs[tagIdx1WithDupName]}),
PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithImage], performerIDs[performerIdx1WithDupName]}),
},
false,
},
@@ -194,9 +226,9 @@ func Test_imageQueryBuilder_Update(t *testing.T) {
Files: []*file.ImageFile{
makeImageFileWithID(imageIdxWithGallery),
},
GalleryIDs: []int{},
TagIDs: []int{},
PerformerIDs: []int{},
GalleryIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}),
PerformerIDs: models.NewRelatedIDs([]int{}),
Organized: true,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
@@ -210,9 +242,9 @@ func Test_imageQueryBuilder_Update(t *testing.T) {
Files: []*file.ImageFile{
makeImageFileWithID(imageIdxWithGallery),
},
GalleryIDs: []int{},
TagIDs: []int{},
PerformerIDs: []int{},
GalleryIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}),
PerformerIDs: models.NewRelatedIDs([]int{}),
Organized: true,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
@@ -226,9 +258,9 @@ func Test_imageQueryBuilder_Update(t *testing.T) {
Files: []*file.ImageFile{
makeImageFileWithID(imageIdxWithTag),
},
GalleryIDs: []int{},
TagIDs: []int{},
PerformerIDs: []int{},
GalleryIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}),
PerformerIDs: models.NewRelatedIDs([]int{}),
Organized: true,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
@@ -242,9 +274,9 @@ func Test_imageQueryBuilder_Update(t *testing.T) {
Files: []*file.ImageFile{
makeImageFileWithID(imageIdxWithPerformer),
},
GalleryIDs: []int{},
TagIDs: []int{},
PerformerIDs: []int{},
GalleryIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}),
PerformerIDs: models.NewRelatedIDs([]int{}),
Organized: true,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
@@ -273,7 +305,7 @@ func Test_imageQueryBuilder_Update(t *testing.T) {
makeImageFileWithID(imageIdxWithGallery),
},
Organized: true,
GalleryIDs: []int{invalidID},
GalleryIDs: models.NewRelatedIDs([]int{invalidID}),
CreatedAt: createdAt,
UpdatedAt: updatedAt,
},
@@ -287,7 +319,7 @@ func Test_imageQueryBuilder_Update(t *testing.T) {
makeImageFileWithID(imageIdxWithGallery),
},
Organized: true,
TagIDs: []int{invalidID},
TagIDs: models.NewRelatedIDs([]int{invalidID}),
CreatedAt: createdAt,
UpdatedAt: updatedAt,
},
@@ -301,7 +333,7 @@ func Test_imageQueryBuilder_Update(t *testing.T) {
makeImageFileWithID(imageIdxWithGallery),
},
Organized: true,
PerformerIDs: []int{invalidID},
PerformerIDs: models.NewRelatedIDs([]int{invalidID}),
CreatedAt: createdAt,
UpdatedAt: updatedAt,
},
@@ -329,6 +361,12 @@ func Test_imageQueryBuilder_Update(t *testing.T) {
t.Errorf("imageQueryBuilder.Find() error = %v", err)
}
// load relationships
if err := loadImageRelationships(ctx, copy, s); err != nil {
t.Errorf("loadImageRelationships() error = %v", err)
return
}
assert.Equal(copy, *s)
return
@@ -400,9 +438,9 @@ func Test_imageQueryBuilder_UpdatePartial(t *testing.T) {
},
CreatedAt: createdAt,
UpdatedAt: updatedAt,
GalleryIDs: []int{galleryIDs[galleryIdxWithImage]},
TagIDs: []int{tagIDs[tagIdx1WithImage], tagIDs[tagIdx1WithDupName]},
PerformerIDs: []int{performerIDs[performerIdx1WithImage], performerIDs[performerIdx1WithDupName]},
GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithImage]}),
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithImage], tagIDs[tagIdx1WithDupName]}),
PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithImage], performerIDs[performerIdx1WithDupName]}),
},
false,
},
@@ -416,9 +454,9 @@ func Test_imageQueryBuilder_UpdatePartial(t *testing.T) {
Files: []*file.ImageFile{
makeImageFile(imageIdx1WithGallery),
},
GalleryIDs: []int{},
TagIDs: []int{},
PerformerIDs: []int{},
GalleryIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}),
PerformerIDs: models.NewRelatedIDs([]int{}),
},
false,
},
@@ -447,6 +485,12 @@ func Test_imageQueryBuilder_UpdatePartial(t *testing.T) {
}
clearImageFileIDs(got)
// load relationships
if err := loadImageRelationships(ctx, tt.want, got); err != nil {
t.Errorf("loadImageRelationships() error = %v", err)
return
}
assert.Equal(tt.want, *got)
s, err := qb.Find(ctx, tt.id)
@@ -455,6 +499,11 @@ func Test_imageQueryBuilder_UpdatePartial(t *testing.T) {
}
clearImageFileIDs(s)
// load relationships
if err := loadImageRelationships(ctx, tt.want, s); err != nil {
t.Errorf("loadImageRelationships() error = %v", err)
return
}
assert.Equal(tt.want, *s)
})
}
@@ -478,10 +527,10 @@ func Test_imageQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Image{
GalleryIDs: append(indexesToIDs(galleryIDs, imageGalleries[imageIdxWithGallery]),
GalleryIDs: models.NewRelatedIDs(append(indexesToIDs(galleryIDs, imageGalleries[imageIdxWithGallery]),
galleryIDs[galleryIdx1WithImage],
galleryIDs[galleryIdx1WithPerformer],
),
)),
},
false,
},
@@ -495,10 +544,10 @@ func Test_imageQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Image{
TagIDs: append(indexesToIDs(tagIDs, imageTags[imageIdxWithTwoTags]),
TagIDs: models.NewRelatedIDs(append(indexesToIDs(tagIDs, imageTags[imageIdxWithTwoTags]),
tagIDs[tagIdx1WithDupName],
tagIDs[tagIdx1WithGallery],
),
)),
},
false,
},
@@ -512,10 +561,10 @@ func Test_imageQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Image{
PerformerIDs: append(indexesToIDs(performerIDs, imagePerformers[imageIdxWithTwoPerformers]),
PerformerIDs: models.NewRelatedIDs(append(indexesToIDs(performerIDs, imagePerformers[imageIdxWithTwoPerformers]),
performerIDs[performerIdx1WithDupName],
performerIDs[performerIdx1WithGallery],
),
)),
},
false,
},
@@ -529,9 +578,9 @@ func Test_imageQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Image{
GalleryIDs: append(indexesToIDs(galleryIDs, imageGalleries[imageIdxWithGallery]),
GalleryIDs: models.NewRelatedIDs(append(indexesToIDs(galleryIDs, imageGalleries[imageIdxWithGallery]),
galleryIDs[galleryIdx1WithPerformer],
),
)),
},
false,
},
@@ -545,9 +594,9 @@ func Test_imageQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Image{
TagIDs: append(indexesToIDs(tagIDs, imageTags[imageIdxWithTwoTags]),
TagIDs: models.NewRelatedIDs(append(indexesToIDs(tagIDs, imageTags[imageIdxWithTwoTags]),
tagIDs[tagIdx1WithGallery],
),
)),
},
false,
},
@@ -561,9 +610,9 @@ func Test_imageQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Image{
PerformerIDs: append(indexesToIDs(performerIDs, imagePerformers[imageIdxWithTwoPerformers]),
PerformerIDs: models.NewRelatedIDs(append(indexesToIDs(performerIDs, imagePerformers[imageIdxWithTwoPerformers]),
performerIDs[performerIdx1WithGallery],
),
)),
},
false,
},
@@ -613,7 +662,7 @@ func Test_imageQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Image{
GalleryIDs: []int{},
GalleryIDs: models.NewRelatedIDs([]int{}),
},
false,
},
@@ -627,7 +676,7 @@ func Test_imageQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Image{
TagIDs: []int{tagIDs[tagIdx2WithImage]},
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx2WithImage]}),
},
false,
},
@@ -641,7 +690,7 @@ func Test_imageQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Image{
PerformerIDs: []int{performerIDs[performerIdx2WithImage]},
PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx2WithImage]}),
},
false,
},
@@ -655,7 +704,7 @@ func Test_imageQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Image{
GalleryIDs: []int{galleryIDs[galleryIdxWithImage]},
GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithImage]}),
},
false,
},
@@ -669,7 +718,7 @@ func Test_imageQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Image{
TagIDs: indexesToIDs(tagIDs, imageTags[imageIdxWithTwoTags]),
TagIDs: models.NewRelatedIDs(indexesToIDs(tagIDs, imageTags[imageIdxWithTwoTags])),
},
false,
},
@@ -683,7 +732,7 @@ func Test_imageQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Image{
PerformerIDs: indexesToIDs(performerIDs, imagePerformers[imageIdxWithTwoPerformers]),
PerformerIDs: models.NewRelatedIDs(indexesToIDs(performerIDs, imagePerformers[imageIdxWithTwoPerformers])),
},
false,
},
@@ -710,6 +759,16 @@ func Test_imageQueryBuilder_UpdatePartialRelationships(t *testing.T) {
t.Errorf("imageQueryBuilder.Find() error = %v", err)
}
// load relationships
if err := loadImageRelationships(ctx, tt.want, got); err != nil {
t.Errorf("loadImageRelationships() error = %v", err)
return
}
if err := loadImageRelationships(ctx, tt.want, s); err != nil {
t.Errorf("loadImageRelationships() error = %v", err)
return
}
// only compare fields that were in the partial
if tt.partial.PerformerIDs != nil {
assert.Equal(tt.want.PerformerIDs, got.PerformerIDs)
@@ -944,12 +1003,33 @@ func Test_imageQueryBuilder_Find(t *testing.T) {
if got != nil {
clearImageFileIDs(got)
// load relationships
if err := loadImageRelationships(ctx, *tt.want, got); err != nil {
t.Errorf("loadImageRelationships() error = %v", err)
return
}
}
assert.Equal(tt.want, got)
})
}
}
func postFindImages(ctx context.Context, want []*models.Image, got []*models.Image) error {
for i, s := range got {
clearImageFileIDs(s)
// load relationships
if i < len(want) {
if err := loadImageRelationships(ctx, *want[i], s); err != nil {
return err
}
}
}
return nil
}
func Test_imageQueryBuilder_FindMany(t *testing.T) {
tests := []struct {
name string
@@ -985,8 +1065,9 @@ func Test_imageQueryBuilder_FindMany(t *testing.T) {
return
}
for _, f := range got {
clearImageFileIDs(f)
if err := postFindImages(ctx, tt.want, got); err != nil {
t.Errorf("loadImageRelationships() error = %v", err)
return
}
if !reflect.DeepEqual(got, tt.want) {
@@ -1044,8 +1125,9 @@ func Test_imageQueryBuilder_FindByChecksum(t *testing.T) {
return
}
for _, f := range got {
clearImageFileIDs(f)
if err := postFindImages(ctx, tt.want, got); err != nil {
t.Errorf("loadImageRelationships() error = %v", err)
return
}
assert.Equal(tt.want, got)
@@ -1121,8 +1203,9 @@ func Test_imageQueryBuilder_FindByFingerprints(t *testing.T) {
return
}
for _, f := range got {
clearImageFileIDs(f)
if err := postFindImages(ctx, tt.want, got); err != nil {
t.Errorf("loadImageRelationships() error = %v", err)
return
}
assert.Equal(tt.want, got)
@@ -1162,8 +1245,9 @@ func Test_imageQueryBuilder_FindByGalleryID(t *testing.T) {
return
}
for _, f := range got {
clearImageFileIDs(f)
if err := postFindImages(ctx, tt.want, got); err != nil {
t.Errorf("loadImageRelationships() error = %v", err)
return
}
assert.Equal(tt.want, got)

View File

@@ -6,7 +6,10 @@ import (
"errors"
"fmt"
"github.com/doug-martin/goqu/v9"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
)
const movieTable = "movies"
@@ -66,21 +69,45 @@ func (qb *movieQueryBuilder) Find(ctx context.Context, id int) (*models.Movie, e
}
func (qb *movieQueryBuilder) FindMany(ctx context.Context, ids []int) ([]*models.Movie, error) {
var movies []*models.Movie
for _, id := range ids {
movie, err := qb.Find(ctx, id)
tableMgr := movieTableMgr
q := goqu.Select("*").From(tableMgr.table).Where(tableMgr.byIDInts(ids...))
unsorted, err := qb.getMany(ctx, q)
if err != nil {
return nil, err
}
if movie == nil {
return nil, fmt.Errorf("movie with id %d not found", id)
ret := make([]*models.Movie, len(ids))
for _, s := range unsorted {
i := intslice.IntIndex(ids, s.ID)
ret[i] = s
}
movies = append(movies, movie)
for i := range ret {
if ret[i] == nil {
return nil, fmt.Errorf("movie with id %d not found", ids[i])
}
}
return movies, nil
return ret, nil
}
func (qb *movieQueryBuilder) getMany(ctx context.Context, q *goqu.SelectDataset) ([]*models.Movie, error) {
const single = false
var ret []*models.Movie
if err := queryFunc(ctx, q, single, func(r *sqlx.Rows) error {
var f models.Movie
if err := r.StructScan(&f); err != nil {
return err
}
ret = append(ret, &f)
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (qb *movieQueryBuilder) FindByName(ctx context.Context, name string, nocase bool) (*models.Movie, error) {
@@ -156,16 +183,11 @@ func (qb *movieQueryBuilder) Query(ctx context.Context, movieFilter *models.Movi
return nil, 0, err
}
var movies []*models.Movie
for _, id := range idsResult {
movie, err := qb.Find(ctx, id)
movies, err := qb.FindMany(ctx, idsResult)
if err != nil {
return nil, 0, err
}
movies = append(movies, movie)
}
return movies, countResult, nil
}

View File

@@ -7,7 +7,10 @@ import (
"fmt"
"strings"
"github.com/doug-martin/goqu/v9"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
"github.com/stashapp/stash/pkg/utils"
)
@@ -92,21 +95,45 @@ func (qb *performerQueryBuilder) Find(ctx context.Context, id int) (*models.Perf
}
func (qb *performerQueryBuilder) FindMany(ctx context.Context, ids []int) ([]*models.Performer, error) {
var performers []*models.Performer
for _, id := range ids {
performer, err := qb.Find(ctx, id)
tableMgr := performerTableMgr
q := goqu.Select("*").From(tableMgr.table).Where(tableMgr.byIDInts(ids...))
unsorted, err := qb.getMany(ctx, q)
if err != nil {
return nil, err
}
if performer == nil {
return nil, fmt.Errorf("performer with id %d not found", id)
ret := make([]*models.Performer, len(ids))
for _, s := range unsorted {
i := intslice.IntIndex(ids, s.ID)
ret[i] = s
}
performers = append(performers, performer)
for i := range ret {
if ret[i] == nil {
return nil, fmt.Errorf("performer with id %d not found", ids[i])
}
}
return performers, nil
return ret, nil
}
func (qb *performerQueryBuilder) getMany(ctx context.Context, q *goqu.SelectDataset) ([]*models.Performer, error) {
const single = false
var ret []*models.Performer
if err := queryFunc(ctx, q, single, func(r *sqlx.Rows) error {
var f models.Performer
if err := r.StructScan(&f); err != nil {
return err
}
ret = append(ret, &f)
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (qb *performerQueryBuilder) FindBySceneID(ctx context.Context, sceneID int) ([]*models.Performer, error) {
@@ -324,14 +351,10 @@ func (qb *performerQueryBuilder) Query(ctx context.Context, performerFilter *mod
return nil, 0, err
}
var performers []*models.Performer
for _, id := range idsResult {
performer, err := qb.Find(ctx, id)
performers, err := qb.FindMany(ctx, idsResult)
if err != nil {
return nil, 0, err
}
performers = append(performers, performer)
}
return performers, countResult, nil
}
@@ -600,11 +623,11 @@ func (qb *performerQueryBuilder) stashIDRepository() *stashIDRepository {
}
}
func (qb *performerQueryBuilder) GetStashIDs(ctx context.Context, performerID int) ([]*models.StashID, error) {
func (qb *performerQueryBuilder) GetStashIDs(ctx context.Context, performerID int) ([]models.StashID, error) {
return qb.stashIDRepository().get(ctx, performerID)
}
func (qb *performerQueryBuilder) UpdateStashIDs(ctx context.Context, performerID int, stashIDs []*models.StashID) error {
func (qb *performerQueryBuilder) UpdateStashIDs(ctx context.Context, performerID int, stashIDs []models.StashID) error {
return qb.stashIDRepository().replace(ctx, performerID, stashIDs)
}

View File

@@ -441,24 +441,24 @@ type stashIDRepository struct {
repository
}
type stashIDs []*models.StashID
type stashIDs []models.StashID
func (s *stashIDs) Append(o interface{}) {
*s = append(*s, o.(*models.StashID))
*s = append(*s, *o.(*models.StashID))
}
func (s *stashIDs) New() interface{} {
return &models.StashID{}
}
func (r *stashIDRepository) get(ctx context.Context, id int) ([]*models.StashID, error) {
func (r *stashIDRepository) get(ctx context.Context, id int) ([]models.StashID, error) {
query := fmt.Sprintf("SELECT stash_id, endpoint from %s WHERE %s = ?", r.tableName, r.idColumn)
var ret stashIDs
err := r.query(ctx, query, []interface{}{id}, &ret)
return []*models.StashID(ret), err
return []models.StashID(ret), err
}
func (r *stashIDRepository) replace(ctx context.Context, id int, newIDs []*models.StashID) error {
func (r *stashIDRepository) replace(ctx context.Context, id int, newIDs []models.StashID) error {
if err := r.destroy(ctx, []int{id}); err != nil {
return err
}

View File

@@ -157,21 +157,34 @@ func (qb *SceneStore) Create(ctx context.Context, newObject *models.Scene, fileI
}
}
if err := scenesPerformersTableMgr.insertJoins(ctx, id, newObject.PerformerIDs); err != nil {
if newObject.PerformerIDs.Loaded() {
if err := scenesPerformersTableMgr.insertJoins(ctx, id, newObject.PerformerIDs.List()); err != nil {
return err
}
if err := scenesTagsTableMgr.insertJoins(ctx, id, newObject.TagIDs); err != nil {
}
if newObject.TagIDs.Loaded() {
if err := scenesTagsTableMgr.insertJoins(ctx, id, newObject.TagIDs.List()); err != nil {
return err
}
if err := scenesGalleriesTableMgr.insertJoins(ctx, id, newObject.GalleryIDs); err != nil {
}
if newObject.GalleryIDs.Loaded() {
if err := scenesGalleriesTableMgr.insertJoins(ctx, id, newObject.GalleryIDs.List()); err != nil {
return err
}
if err := scenesStashIDsTableMgr.insertJoins(ctx, id, newObject.StashIDs); err != nil {
}
if newObject.StashIDs.Loaded() {
if err := scenesStashIDsTableMgr.insertJoins(ctx, id, newObject.StashIDs.List()); err != nil {
return err
}
if err := scenesMoviesTableMgr.insertJoins(ctx, id, newObject.Movies); err != nil {
}
if newObject.Movies.Loaded() {
if err := scenesMoviesTableMgr.insertJoins(ctx, id, newObject.Movies.List()); err != nil {
return err
}
}
updated, err := qb.find(ctx, id)
if err != nil {
@@ -235,21 +248,35 @@ func (qb *SceneStore) Update(ctx context.Context, updatedObject *models.Scene) e
return err
}
if err := scenesPerformersTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.PerformerIDs); err != nil {
if updatedObject.PerformerIDs.Loaded() {
if err := scenesPerformersTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.PerformerIDs.List()); err != nil {
return err
}
if err := scenesTagsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.TagIDs); err != nil {
}
if updatedObject.TagIDs.Loaded() {
if err := scenesTagsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.TagIDs.List()); err != nil {
return err
}
if err := scenesGalleriesTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.GalleryIDs); err != nil {
}
if updatedObject.GalleryIDs.Loaded() {
if err := scenesGalleriesTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.GalleryIDs.List()); err != nil {
return err
}
if err := scenesStashIDsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.StashIDs); err != nil {
}
if updatedObject.StashIDs.Loaded() {
if err := scenesStashIDsTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.StashIDs.List()); err != nil {
return err
}
if err := scenesMoviesTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.Movies); err != nil {
}
if updatedObject.Movies.Loaded() {
if err := scenesMoviesTableMgr.replaceJoins(ctx, updatedObject.ID, updatedObject.Movies.List()); err != nil {
return err
}
}
fileIDs := make([]file.ID, len(updatedObject.Files))
for i, f := range updatedObject.Files {
@@ -333,16 +360,18 @@ func (qb *SceneStore) getMany(ctx context.Context, q *goqu.SelectDataset) ([]*mo
s := f.resolve()
if err := qb.resolveRelationships(ctx, s); err != nil {
return err
}
ret = append(ret, s)
return nil
}); err != nil {
return nil, err
}
for _, s := range ret {
if err := qb.resolveRelationships(ctx, s); err != nil {
return nil, err
}
}
return ret, nil
}
@@ -355,36 +384,6 @@ func (qb *SceneStore) resolveRelationships(ctx context.Context, s *models.Scene)
return fmt.Errorf("resolving scene files: %w", err)
}
// movies
s.Movies, err = qb.getMovies(ctx, s.ID)
if err != nil {
return fmt.Errorf("resolving scene movies: %w", err)
}
// performers
s.PerformerIDs, err = qb.performersRepository().getIDs(ctx, s.ID)
if err != nil {
return fmt.Errorf("resolving scene performers: %w", err)
}
// tags
s.TagIDs, err = qb.tagsRepository().getIDs(ctx, s.ID)
if err != nil {
return fmt.Errorf("resolving scene tags: %w", err)
}
// galleries
s.GalleryIDs, err = qb.galleriesRepository().getIDs(ctx, s.ID)
if err != nil {
return fmt.Errorf("resolving scene galleries: %w", err)
}
// stash ids
s.StashIDs, err = qb.getStashIDs(ctx, s.ID)
if err != nil {
return fmt.Errorf("resolving scene stash ids: %w", err)
}
return nil
}
@@ -412,37 +411,6 @@ func (qb *SceneStore) getFiles(ctx context.Context, id int) ([]*file.VideoFile,
return ret, nil
}
func (qb *SceneStore) getMovies(ctx context.Context, id int) (ret []models.MoviesScenes, err error) {
ret = []models.MoviesScenes{}
if err := qb.moviesRepository().getAll(ctx, id, func(rows *sqlx.Rows) error {
var ms moviesScenesRow
if err := rows.StructScan(&ms); err != nil {
return err
}
ret = append(ret, ms.resolve(id))
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (qb *SceneStore) getStashIDs(ctx context.Context, id int) ([]models.StashID, error) {
stashIDs, err := qb.stashIDRepository().get(ctx, id)
if err != nil {
return nil, err
}
ret := make([]models.StashID, len(stashIDs))
for i, sid := range stashIDs {
ret[i] = *sid
}
return ret, nil
}
func (qb *SceneStore) find(ctx context.Context, id int) (*models.Scene, error) {
q := qb.selectDataset().Where(qb.tableMgr.byID(id))
@@ -1399,6 +1367,24 @@ func (qb *SceneStore) moviesRepository() *repository {
}
}
func (qb *SceneStore) GetMovies(ctx context.Context, id int) (ret []models.MoviesScenes, err error) {
ret = []models.MoviesScenes{}
if err := qb.moviesRepository().getAll(ctx, id, func(rows *sqlx.Rows) error {
var ms moviesScenesRow
if err := rows.StructScan(&ms); err != nil {
return err
}
ret = append(ret, ms.resolve(id))
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (qb *SceneStore) filesRepository() *filesRepository {
return &filesRepository{
repository: repository{
@@ -1409,6 +1395,11 @@ func (qb *SceneStore) filesRepository() *filesRepository {
}
}
func (qb *SceneStore) AddFileID(ctx context.Context, id int, fileID file.ID) error {
const firstPrimary = false
return scenesFilesTableMgr.insertJoins(ctx, id, firstPrimary, []file.ID{fileID})
}
func (qb *SceneStore) performersRepository() *joinRepository {
return &joinRepository{
repository: repository{
@@ -1420,6 +1411,10 @@ func (qb *SceneStore) performersRepository() *joinRepository {
}
}
func (qb *SceneStore) GetPerformerIDs(ctx context.Context, id int) ([]int, error) {
return qb.performersRepository().getIDs(ctx, id)
}
func (qb *SceneStore) tagsRepository() *joinRepository {
return &joinRepository{
repository: repository{
@@ -1431,6 +1426,10 @@ func (qb *SceneStore) tagsRepository() *joinRepository {
}
}
func (qb *SceneStore) GetTagIDs(ctx context.Context, id int) ([]int, error) {
return qb.tagsRepository().getIDs(ctx, id)
}
func (qb *SceneStore) galleriesRepository() *joinRepository {
return &joinRepository{
repository: repository{
@@ -1442,6 +1441,14 @@ func (qb *SceneStore) galleriesRepository() *joinRepository {
}
}
func (qb *SceneStore) GetGalleryIDs(ctx context.Context, id int) ([]int, error) {
return qb.galleriesRepository().getIDs(ctx, id)
}
func (qb *SceneStore) AddGalleryIDs(ctx context.Context, sceneID int, galleryIDs []int) error {
return scenesGalleriesTableMgr.addJoins(ctx, sceneID, galleryIDs)
}
func (qb *SceneStore) stashIDRepository() *stashIDRepository {
return &stashIDRepository{
repository{
@@ -1452,6 +1459,10 @@ func (qb *SceneStore) stashIDRepository() *stashIDRepository {
}
}
func (qb *SceneStore) GetStashIDs(ctx context.Context, sceneID int) ([]models.StashID, error) {
return qb.stashIDRepository().get(ctx, sceneID)
}
func (qb *SceneStore) FindDuplicates(ctx context.Context, distance int) ([][]*models.Scene, error) {
var dupeIds [][]int
if distance == 0 {

View File

@@ -157,7 +157,13 @@ func TestMarkerQuerySceneTags(t *testing.T) {
t.Errorf("error getting marker tag ids: %v", err)
return
}
tagIDs := s.TagIDs
if err := s.LoadTagIDs(ctx, db.Scene); err != nil {
t.Errorf("error getting marker tag ids: %v", err)
return
}
tagIDs := s.TagIDs.List()
if markerFilter.SceneTags.Modifier == models.CriterionModifierIsNull && len(tagIDs) > 0 {
t.Errorf("expected marker %d to have no scene tags - found %d", m.ID, len(tagIDs))
}

View File

@@ -21,6 +21,36 @@ import (
"github.com/stretchr/testify/assert"
)
func loadSceneRelationships(ctx context.Context, expected models.Scene, actual *models.Scene) error {
if expected.GalleryIDs.Loaded() {
if err := actual.LoadGalleryIDs(ctx, db.Scene); err != nil {
return err
}
}
if expected.TagIDs.Loaded() {
if err := actual.LoadTagIDs(ctx, db.Scene); err != nil {
return err
}
}
if expected.PerformerIDs.Loaded() {
if err := actual.LoadPerformerIDs(ctx, db.Scene); err != nil {
return err
}
}
if expected.Movies.Loaded() {
if err := actual.LoadMovies(ctx, db.Scene); err != nil {
return err
}
}
if expected.StashIDs.Loaded() {
if err := actual.LoadStashIDs(ctx, db.Scene); err != nil {
return err
}
}
return nil
}
func Test_sceneQueryBuilder_Create(t *testing.T) {
var (
title = "title"
@@ -60,10 +90,10 @@ func Test_sceneQueryBuilder_Create(t *testing.T) {
StudioID: &studioIDs[studioIdxWithScene],
CreatedAt: createdAt,
UpdatedAt: updatedAt,
GalleryIDs: []int{galleryIDs[galleryIdxWithScene]},
TagIDs: []int{tagIDs[tagIdx1WithScene], tagIDs[tagIdx1WithDupName]},
PerformerIDs: []int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]},
Movies: []models.MoviesScenes{
GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithScene]}),
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithScene], tagIDs[tagIdx1WithDupName]}),
PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]}),
Movies: models.NewRelatedMovies([]models.MoviesScenes{
{
MovieID: movieIDs[movieIdxWithScene],
SceneIndex: &sceneIndex,
@@ -72,8 +102,8 @@ func Test_sceneQueryBuilder_Create(t *testing.T) {
MovieID: movieIDs[movieIdxWithStudio],
SceneIndex: &sceneIndex2,
},
},
StashIDs: []models.StashID{
}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{
{
StashID: stashID1,
Endpoint: endpoint1,
@@ -82,7 +112,7 @@ func Test_sceneQueryBuilder_Create(t *testing.T) {
StashID: stashID2,
Endpoint: endpoint2,
},
},
}),
Files: []*file.VideoFile{},
},
false,
@@ -103,10 +133,10 @@ func Test_sceneQueryBuilder_Create(t *testing.T) {
},
CreatedAt: createdAt,
UpdatedAt: updatedAt,
GalleryIDs: []int{galleryIDs[galleryIdxWithScene]},
TagIDs: []int{tagIDs[tagIdx1WithScene], tagIDs[tagIdx1WithDupName]},
PerformerIDs: []int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]},
Movies: []models.MoviesScenes{
GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithScene]}),
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithScene], tagIDs[tagIdx1WithDupName]}),
PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]}),
Movies: models.NewRelatedMovies([]models.MoviesScenes{
{
MovieID: movieIDs[movieIdxWithScene],
SceneIndex: &sceneIndex,
@@ -115,8 +145,8 @@ func Test_sceneQueryBuilder_Create(t *testing.T) {
MovieID: movieIDs[movieIdxWithStudio],
SceneIndex: &sceneIndex2,
},
},
StashIDs: []models.StashID{
}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{
{
StashID: stashID1,
Endpoint: endpoint1,
@@ -125,7 +155,7 @@ func Test_sceneQueryBuilder_Create(t *testing.T) {
StashID: stashID2,
Endpoint: endpoint2,
},
},
}),
},
false,
},
@@ -139,33 +169,33 @@ func Test_sceneQueryBuilder_Create(t *testing.T) {
{
"invalid gallery id",
models.Scene{
GalleryIDs: []int{invalidID},
GalleryIDs: models.NewRelatedIDs([]int{invalidID}),
},
true,
},
{
"invalid tag id",
models.Scene{
TagIDs: []int{invalidID},
TagIDs: models.NewRelatedIDs([]int{invalidID}),
},
true,
},
{
"invalid performer id",
models.Scene{
PerformerIDs: []int{invalidID},
PerformerIDs: models.NewRelatedIDs([]int{invalidID}),
},
true,
},
{
"invalid movie id",
models.Scene{
Movies: []models.MoviesScenes{
Movies: models.NewRelatedMovies([]models.MoviesScenes{
{
MovieID: invalidID,
SceneIndex: &sceneIndex,
},
},
}),
},
true,
},
@@ -197,6 +227,12 @@ func Test_sceneQueryBuilder_Create(t *testing.T) {
copy := tt.newObject
copy.ID = s.ID
// load relationships
if err := loadSceneRelationships(ctx, copy, &s); err != nil {
t.Errorf("loadSceneRelationships() error = %v", err)
return
}
assert.Equal(copy, s)
// ensure can find the scene
@@ -208,6 +244,12 @@ func Test_sceneQueryBuilder_Create(t *testing.T) {
if !assert.NotNil(found) {
return
}
// load relationships
if err := loadSceneRelationships(ctx, copy, found); err != nil {
t.Errorf("loadSceneRelationships() error = %v", err)
return
}
assert.Equal(copy, *found)
return
@@ -268,10 +310,10 @@ func Test_sceneQueryBuilder_Update(t *testing.T) {
StudioID: &studioIDs[studioIdxWithScene],
CreatedAt: createdAt,
UpdatedAt: updatedAt,
GalleryIDs: []int{galleryIDs[galleryIdxWithScene]},
TagIDs: []int{tagIDs[tagIdx1WithScene], tagIDs[tagIdx1WithDupName]},
PerformerIDs: []int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]},
Movies: []models.MoviesScenes{
GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithScene]}),
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithScene], tagIDs[tagIdx1WithDupName]}),
PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]}),
Movies: models.NewRelatedMovies([]models.MoviesScenes{
{
MovieID: movieIDs[movieIdxWithScene],
SceneIndex: &sceneIndex,
@@ -280,8 +322,8 @@ func Test_sceneQueryBuilder_Update(t *testing.T) {
MovieID: movieIDs[movieIdxWithStudio],
SceneIndex: &sceneIndex2,
},
},
StashIDs: []models.StashID{
}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{
{
StashID: stashID1,
Endpoint: endpoint1,
@@ -290,7 +332,7 @@ func Test_sceneQueryBuilder_Update(t *testing.T) {
StashID: stashID2,
Endpoint: endpoint2,
},
},
}),
},
false,
},
@@ -301,11 +343,11 @@ func Test_sceneQueryBuilder_Update(t *testing.T) {
Files: []*file.VideoFile{
makeSceneFileWithID(sceneIdxWithSpacedName),
},
GalleryIDs: []int{},
TagIDs: []int{},
PerformerIDs: []int{},
Movies: []models.MoviesScenes{},
StashIDs: []models.StashID{},
GalleryIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}),
PerformerIDs: models.NewRelatedIDs([]int{}),
Movies: models.NewRelatedMovies([]models.MoviesScenes{}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
},
false,
},
@@ -316,11 +358,7 @@ func Test_sceneQueryBuilder_Update(t *testing.T) {
Files: []*file.VideoFile{
makeSceneFileWithID(sceneIdxWithGallery),
},
GalleryIDs: []int{},
TagIDs: []int{},
PerformerIDs: []int{},
Movies: []models.MoviesScenes{},
StashIDs: []models.StashID{},
GalleryIDs: models.NewRelatedIDs([]int{}),
},
false,
},
@@ -331,11 +369,7 @@ func Test_sceneQueryBuilder_Update(t *testing.T) {
Files: []*file.VideoFile{
makeSceneFileWithID(sceneIdxWithTag),
},
TagIDs: []int{},
GalleryIDs: []int{},
PerformerIDs: []int{},
Movies: []models.MoviesScenes{},
StashIDs: []models.StashID{},
TagIDs: models.NewRelatedIDs([]int{}),
},
false,
},
@@ -346,11 +380,7 @@ func Test_sceneQueryBuilder_Update(t *testing.T) {
Files: []*file.VideoFile{
makeSceneFileWithID(sceneIdxWithPerformer),
},
PerformerIDs: []int{},
TagIDs: []int{},
GalleryIDs: []int{},
Movies: []models.MoviesScenes{},
StashIDs: []models.StashID{},
PerformerIDs: models.NewRelatedIDs([]int{}),
},
false,
},
@@ -361,11 +391,7 @@ func Test_sceneQueryBuilder_Update(t *testing.T) {
Files: []*file.VideoFile{
makeSceneFileWithID(sceneIdxWithMovie),
},
Movies: []models.MoviesScenes{},
GalleryIDs: []int{},
TagIDs: []int{},
PerformerIDs: []int{},
StashIDs: []models.StashID{},
Movies: models.NewRelatedMovies([]models.MoviesScenes{}),
},
false,
},
@@ -377,11 +403,6 @@ func Test_sceneQueryBuilder_Update(t *testing.T) {
makeSceneFileWithID(sceneIdxWithGallery),
},
StudioID: &invalidID,
GalleryIDs: []int{},
TagIDs: []int{},
PerformerIDs: []int{},
Movies: []models.MoviesScenes{},
StashIDs: []models.StashID{},
},
true,
},
@@ -392,7 +413,7 @@ func Test_sceneQueryBuilder_Update(t *testing.T) {
Files: []*file.VideoFile{
makeSceneFileWithID(sceneIdxWithGallery),
},
GalleryIDs: []int{invalidID},
GalleryIDs: models.NewRelatedIDs([]int{invalidID}),
},
true,
},
@@ -403,7 +424,7 @@ func Test_sceneQueryBuilder_Update(t *testing.T) {
Files: []*file.VideoFile{
makeSceneFileWithID(sceneIdxWithGallery),
},
TagIDs: []int{invalidID},
TagIDs: models.NewRelatedIDs([]int{invalidID}),
},
true,
},
@@ -414,7 +435,7 @@ func Test_sceneQueryBuilder_Update(t *testing.T) {
Files: []*file.VideoFile{
makeSceneFileWithID(sceneIdxWithGallery),
},
PerformerIDs: []int{invalidID},
PerformerIDs: models.NewRelatedIDs([]int{invalidID}),
},
true,
},
@@ -425,12 +446,12 @@ func Test_sceneQueryBuilder_Update(t *testing.T) {
Files: []*file.VideoFile{
makeSceneFileWithID(sceneIdxWithSpacedName),
},
Movies: []models.MoviesScenes{
Movies: models.NewRelatedMovies([]models.MoviesScenes{
{
MovieID: invalidID,
SceneIndex: &sceneIndex,
},
},
}),
},
true,
},
@@ -456,6 +477,12 @@ func Test_sceneQueryBuilder_Update(t *testing.T) {
t.Errorf("sceneQueryBuilder.Find() error = %v", err)
}
// load relationships
if err := loadSceneRelationships(ctx, copy, s); err != nil {
t.Errorf("loadSceneRelationships() error = %v", err)
return
}
assert.Equal(copy, *s)
})
}
@@ -571,10 +598,10 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
StudioID: &studioIDs[studioIdxWithScene],
CreatedAt: createdAt,
UpdatedAt: updatedAt,
GalleryIDs: []int{galleryIDs[galleryIdxWithScene]},
TagIDs: []int{tagIDs[tagIdx1WithScene], tagIDs[tagIdx1WithDupName]},
PerformerIDs: []int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]},
Movies: []models.MoviesScenes{
GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithScene]}),
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx1WithScene], tagIDs[tagIdx1WithDupName]}),
PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx1WithScene], performerIDs[performerIdx1WithDupName]}),
Movies: models.NewRelatedMovies([]models.MoviesScenes{
{
MovieID: movieIDs[movieIdxWithScene],
SceneIndex: &sceneIndex,
@@ -583,8 +610,8 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
MovieID: movieIDs[movieIdxWithStudio],
SceneIndex: &sceneIndex2,
},
},
StashIDs: []models.StashID{
}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{
{
StashID: stashID1,
Endpoint: endpoint1,
@@ -593,7 +620,7 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
StashID: stashID2,
Endpoint: endpoint2,
},
},
}),
},
false,
},
@@ -606,11 +633,11 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
Files: []*file.VideoFile{
makeSceneFile(sceneIdxWithSpacedName),
},
GalleryIDs: []int{},
TagIDs: []int{},
PerformerIDs: []int{},
Movies: []models.MoviesScenes{},
StashIDs: []models.StashID{},
GalleryIDs: models.NewRelatedIDs([]int{}),
TagIDs: models.NewRelatedIDs([]int{}),
PerformerIDs: models.NewRelatedIDs([]int{}),
Movies: models.NewRelatedMovies([]models.MoviesScenes{}),
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
},
false,
},
@@ -641,6 +668,12 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
// ignore file ids
clearSceneFileIDs(got)
// load relationships
if err := loadSceneRelationships(ctx, tt.want, got); err != nil {
t.Errorf("loadSceneRelationships() error = %v", err)
return
}
assert.Equal(tt.want, *got)
s, err := qb.Find(ctx, tt.id)
@@ -651,6 +684,12 @@ func Test_sceneQueryBuilder_UpdatePartial(t *testing.T) {
// ignore file ids
clearSceneFileIDs(s)
// load relationships
if err := loadSceneRelationships(ctx, tt.want, s); err != nil {
t.Errorf("loadSceneRelationships() error = %v", err)
return
}
assert.Equal(tt.want, *s)
})
}
@@ -705,10 +744,10 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Scene{
GalleryIDs: append(indexesToIDs(galleryIDs, sceneGalleries[sceneIdxWithGallery]),
GalleryIDs: models.NewRelatedIDs(append(indexesToIDs(galleryIDs, sceneGalleries[sceneIdxWithGallery]),
galleryIDs[galleryIdx1WithImage],
galleryIDs[galleryIdx1WithPerformer],
),
)),
},
false,
},
@@ -722,10 +761,10 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Scene{
TagIDs: append(indexesToIDs(tagIDs, sceneTags[sceneIdxWithTwoTags]),
TagIDs: models.NewRelatedIDs(append(indexesToIDs(tagIDs, sceneTags[sceneIdxWithTwoTags]),
tagIDs[tagIdx1WithDupName],
tagIDs[tagIdx1WithGallery],
),
)),
},
false,
},
@@ -739,10 +778,10 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Scene{
PerformerIDs: append(indexesToIDs(performerIDs, scenePerformers[sceneIdxWithTwoPerformers]),
PerformerIDs: models.NewRelatedIDs(append(indexesToIDs(performerIDs, scenePerformers[sceneIdxWithTwoPerformers]),
performerIDs[performerIdx1WithDupName],
performerIDs[performerIdx1WithGallery],
),
)),
},
false,
},
@@ -756,11 +795,11 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Scene{
Movies: append([]models.MoviesScenes{
Movies: models.NewRelatedMovies(append([]models.MoviesScenes{
{
MovieID: indexesToIDs(movieIDs, sceneMovies[sceneIdxWithMovie])[0],
},
}, movieScenes...),
}, movieScenes...)),
},
false,
},
@@ -774,7 +813,7 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Scene{
StashIDs: append([]models.StashID{sceneStashID(sceneIdxWithSpacedName)}, stashIDs...),
StashIDs: models.NewRelatedStashIDs(append([]models.StashID{sceneStashID(sceneIdxWithSpacedName)}, stashIDs...)),
},
false,
},
@@ -788,9 +827,9 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Scene{
GalleryIDs: append(indexesToIDs(galleryIDs, sceneGalleries[sceneIdxWithGallery]),
GalleryIDs: models.NewRelatedIDs(append(indexesToIDs(galleryIDs, sceneGalleries[sceneIdxWithGallery]),
galleryIDs[galleryIdx1WithPerformer],
),
)),
},
false,
},
@@ -804,9 +843,9 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Scene{
TagIDs: append(indexesToIDs(tagIDs, sceneTags[sceneIdxWithTwoTags]),
TagIDs: models.NewRelatedIDs(append(indexesToIDs(tagIDs, sceneTags[sceneIdxWithTwoTags]),
tagIDs[tagIdx1WithGallery],
),
)),
},
false,
},
@@ -820,9 +859,9 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Scene{
PerformerIDs: append(indexesToIDs(performerIDs, scenePerformers[sceneIdxWithTwoPerformers]),
PerformerIDs: models.NewRelatedIDs(append(indexesToIDs(performerIDs, scenePerformers[sceneIdxWithTwoPerformers]),
performerIDs[performerIdx1WithGallery],
),
)),
},
false,
},
@@ -843,11 +882,11 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Scene{
Movies: append([]models.MoviesScenes{
Movies: models.NewRelatedMovies(append([]models.MoviesScenes{
{
MovieID: indexesToIDs(movieIDs, sceneMovies[sceneIdxWithMovie])[0],
},
}, movieScenes...),
}, movieScenes...)),
},
false,
},
@@ -863,7 +902,7 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Scene{
StashIDs: []models.StashID{sceneStashID(sceneIdxWithSpacedName)},
StashIDs: models.NewRelatedStashIDs([]models.StashID{sceneStashID(sceneIdxWithSpacedName)}),
},
false,
},
@@ -929,7 +968,7 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Scene{
GalleryIDs: []int{},
GalleryIDs: models.NewRelatedIDs([]int{}),
},
false,
},
@@ -943,7 +982,7 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Scene{
TagIDs: []int{tagIDs[tagIdx2WithScene]},
TagIDs: models.NewRelatedIDs([]int{tagIDs[tagIdx2WithScene]}),
},
false,
},
@@ -957,7 +996,7 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Scene{
PerformerIDs: []int{performerIDs[performerIdx2WithScene]},
PerformerIDs: models.NewRelatedIDs([]int{performerIDs[performerIdx2WithScene]}),
},
false,
},
@@ -975,7 +1014,7 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Scene{
Movies: []models.MoviesScenes{},
Movies: models.NewRelatedMovies([]models.MoviesScenes{}),
},
false,
},
@@ -989,7 +1028,7 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Scene{
StashIDs: []models.StashID{},
StashIDs: models.NewRelatedStashIDs([]models.StashID{}),
},
false,
},
@@ -1003,7 +1042,7 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Scene{
GalleryIDs: []int{galleryIDs[galleryIdxWithScene]},
GalleryIDs: models.NewRelatedIDs([]int{galleryIDs[galleryIdxWithScene]}),
},
false,
},
@@ -1017,7 +1056,7 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Scene{
TagIDs: indexesToIDs(tagIDs, sceneTags[sceneIdxWithTwoTags]),
TagIDs: models.NewRelatedIDs(indexesToIDs(tagIDs, sceneTags[sceneIdxWithTwoTags])),
},
false,
},
@@ -1031,7 +1070,7 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Scene{
PerformerIDs: indexesToIDs(performerIDs, scenePerformers[sceneIdxWithTwoPerformers]),
PerformerIDs: models.NewRelatedIDs(indexesToIDs(performerIDs, scenePerformers[sceneIdxWithTwoPerformers])),
},
false,
},
@@ -1049,11 +1088,11 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Scene{
Movies: []models.MoviesScenes{
Movies: models.NewRelatedMovies([]models.MoviesScenes{
{
MovieID: indexesToIDs(movieIDs, sceneMovies[sceneIdxWithMovie])[0],
},
},
}),
},
false,
},
@@ -1067,7 +1106,7 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
},
},
models.Scene{
StashIDs: []models.StashID{sceneStashID(sceneIdxWithGallery)},
StashIDs: models.NewRelatedStashIDs([]models.StashID{sceneStashID(sceneIdxWithGallery)}),
},
false,
},
@@ -1094,6 +1133,16 @@ func Test_sceneQueryBuilder_UpdatePartialRelationships(t *testing.T) {
t.Errorf("sceneQueryBuilder.Find() error = %v", err)
}
// load relationships
if err := loadSceneRelationships(ctx, tt.want, got); err != nil {
t.Errorf("loadSceneRelationships() error = %v", err)
return
}
if err := loadSceneRelationships(ctx, tt.want, s); err != nil {
t.Errorf("loadSceneRelationships() error = %v", err)
return
}
// only compare fields that were in the partial
if tt.partial.PerformerIDs != nil {
assert.Equal(tt.want.PerformerIDs, got.PerformerIDs)
@@ -1353,6 +1402,12 @@ func Test_sceneQueryBuilder_Find(t *testing.T) {
if got != nil {
clearSceneFileIDs(got)
// load relationships
if err := loadSceneRelationships(ctx, *tt.want, got); err != nil {
t.Errorf("loadSceneRelationships() error = %v", err)
return nil
}
}
assert.Equal(tt.want, got)
@@ -1362,6 +1417,21 @@ func Test_sceneQueryBuilder_Find(t *testing.T) {
}
}
func postFindScenes(ctx context.Context, want []*models.Scene, got []*models.Scene) error {
for i, s := range got {
clearSceneFileIDs(s)
// load relationships
if i < len(want) {
if err := loadSceneRelationships(ctx, *want[i], s); err != nil {
return err
}
}
}
return nil
}
func Test_sceneQueryBuilder_FindMany(t *testing.T) {
tests := []struct {
name string
@@ -1404,8 +1474,9 @@ func Test_sceneQueryBuilder_FindMany(t *testing.T) {
return
}
for _, s := range got {
clearSceneFileIDs(s)
if err := postFindScenes(ctx, tt.want, got); err != nil {
t.Errorf("loadSceneRelationships() error = %v", err)
return
}
assert.Equal(tt.want, got)
@@ -1474,8 +1545,9 @@ func Test_sceneQueryBuilder_FindByChecksum(t *testing.T) {
return nil
}
for _, s := range got {
clearSceneFileIDs(s)
if err := postFindScenes(ctx, tt.want, got); err != nil {
t.Errorf("loadSceneRelationships() error = %v", err)
return nil
}
assert.Equal(tt.want, got)
@@ -1546,8 +1618,9 @@ func Test_sceneQueryBuilder_FindByOSHash(t *testing.T) {
return nil
}
for _, s := range got {
clearSceneFileIDs(s)
if err := postFindScenes(ctx, tt.want, got); err != nil {
t.Errorf("loadSceneRelationships() error = %v", err)
return nil
}
if !reflect.DeepEqual(got, tt.want) {
@@ -1620,8 +1693,9 @@ func Test_sceneQueryBuilder_FindByPath(t *testing.T) {
return nil
}
for _, s := range got {
clearSceneFileIDs(s)
if err := postFindScenes(ctx, tt.want, got); err != nil {
t.Errorf("loadSceneRelationships() error = %v", err)
return nil
}
assert.Equal(tt.want, got)
@@ -1664,8 +1738,9 @@ func Test_sceneQueryBuilder_FindByGalleryID(t *testing.T) {
return
}
for _, s := range got {
clearSceneFileIDs(s)
if err := postFindScenes(ctx, tt.want, got); err != nil {
t.Errorf("loadSceneRelationships() error = %v", err)
return
}
assert.Equal(tt.want, got)
@@ -3539,7 +3614,11 @@ func verifyScenesTagCount(t *testing.T, tagCountCriterion models.IntCriterionInp
assert.Greater(t, len(scenes), 0)
for _, scene := range scenes {
verifyInt(t, len(scene.TagIDs), tagCountCriterion)
if err := scene.LoadTagIDs(ctx, sqb); err != nil {
t.Errorf("scene.LoadTagIDs() error = %v", err)
return nil
}
verifyInt(t, len(scene.TagIDs.List()), tagCountCriterion)
}
return nil
@@ -3576,7 +3655,12 @@ func verifyScenesPerformerCount(t *testing.T, performerCountCriterion models.Int
assert.Greater(t, len(scenes), 0)
for _, scene := range scenes {
verifyInt(t, len(scene.PerformerIDs), performerCountCriterion)
if err := scene.LoadPerformerIDs(ctx, sqb); err != nil {
t.Errorf("scene.LoadPerformerIDs() error = %v", err)
return nil
}
verifyInt(t, len(scene.PerformerIDs.List()), performerCountCriterion)
}
return nil
@@ -3776,6 +3860,10 @@ func TestSceneStashIDs(t *testing.T) {
return fmt.Errorf("Error creating scene: %s", err.Error())
}
if err := scene.LoadStashIDs(ctx, qb); err != nil {
return err
}
testSceneStashIDs(ctx, t, scene)
return nil
}); err != nil {
@@ -3785,7 +3873,7 @@ func TestSceneStashIDs(t *testing.T) {
func testSceneStashIDs(ctx context.Context, t *testing.T, s *models.Scene) {
// ensure no stash IDs to begin with
assert.Len(t, s.StashIDs, 0)
assert.Len(t, s.StashIDs.List(), 0)
// add stash ids
const stashIDStr = "stashID"
@@ -3809,7 +3897,12 @@ func testSceneStashIDs(ctx context.Context, t *testing.T, s *models.Scene) {
t.Error(err.Error())
}
assert.Equal(t, []models.StashID{stashID}, s.StashIDs)
if err := s.LoadStashIDs(ctx, qb); err != nil {
t.Error(err.Error())
return
}
assert.Equal(t, []models.StashID{stashID}, s.StashIDs.List())
// remove stash ids and ensure was updated
s, err = qb.UpdatePartial(ctx, s.ID, models.ScenePartial{
@@ -3822,7 +3915,12 @@ func testSceneStashIDs(ctx context.Context, t *testing.T, s *models.Scene) {
t.Error(err.Error())
}
assert.Len(t, s.StashIDs, 0)
if err := s.LoadStashIDs(ctx, qb); err != nil {
t.Error(err.Error())
return
}
assert.Len(t, s.StashIDs.List(), 0)
}
func TestSceneQueryQTrim(t *testing.T) {

View File

@@ -975,13 +975,13 @@ func makeScene(i int) *models.Scene {
OCounter: getOCounter(i),
Date: getObjectDateObject(i),
StudioID: studioID,
GalleryIDs: gids,
PerformerIDs: pids,
TagIDs: tids,
Movies: movies,
StashIDs: []models.StashID{
GalleryIDs: models.NewRelatedIDs(gids),
PerformerIDs: models.NewRelatedIDs(pids),
TagIDs: models.NewRelatedIDs(tids),
Movies: models.NewRelatedMovies(movies),
StashIDs: models.NewRelatedStashIDs([]models.StashID{
sceneStashID(i),
},
}),
}
}
@@ -1051,9 +1051,9 @@ func makeImage(i int) *models.Image {
Rating: getIntPtr(getRating(i)),
OCounter: getOCounter(i),
StudioID: studioID,
GalleryIDs: gids,
PerformerIDs: pids,
TagIDs: tids,
GalleryIDs: models.NewRelatedIDs(gids),
PerformerIDs: models.NewRelatedIDs(pids),
TagIDs: models.NewRelatedIDs(tids),
}
}
@@ -1135,12 +1135,12 @@ func makeGallery(i int, includeScenes bool) *models.Gallery {
Rating: getIntPtr(getRating(i)),
Date: getObjectDateObject(i),
StudioID: studioID,
PerformerIDs: pids,
TagIDs: tids,
PerformerIDs: models.NewRelatedIDs(pids),
TagIDs: models.NewRelatedIDs(tids),
}
if includeScenes {
ret.SceneIDs = indexesToIDs(sceneIDs, sceneGalleries.reverseLookup(i))
ret.SceneIDs = models.NewRelatedIDs(indexesToIDs(sceneIDs, sceneGalleries.reverseLookup(i)))
}
return ret

View File

@@ -12,8 +12,8 @@ import (
)
type stashIDReaderWriter interface {
GetStashIDs(ctx context.Context, performerID int) ([]*models.StashID, error)
UpdateStashIDs(ctx context.Context, performerID int, stashIDs []*models.StashID) error
GetStashIDs(ctx context.Context, performerID int) ([]models.StashID, error)
UpdateStashIDs(ctx context.Context, performerID int, stashIDs []models.StashID) error
}
func testStashIDReaderWriter(ctx context.Context, t *testing.T, r stashIDReaderWriter, id int) {
@@ -26,25 +26,25 @@ func testStashIDReaderWriter(ctx context.Context, t *testing.T, r stashIDReaderW
// add stash ids
const stashIDStr = "stashID"
const endpoint = "endpoint"
stashID := &models.StashID{
stashID := models.StashID{
StashID: stashIDStr,
Endpoint: endpoint,
}
// update stash ids and ensure was updated
if err := r.UpdateStashIDs(ctx, id, []*models.StashID{stashID}); err != nil {
if err := r.UpdateStashIDs(ctx, id, []models.StashID{stashID}); err != nil {
t.Error(err.Error())
}
testStashIDs(ctx, t, r, id, []*models.StashID{stashID})
testStashIDs(ctx, t, r, id, []models.StashID{stashID})
// update non-existing id - should return error
if err := r.UpdateStashIDs(ctx, -1, []*models.StashID{stashID}); err == nil {
if err := r.UpdateStashIDs(ctx, -1, []models.StashID{stashID}); err == nil {
t.Error("expected error when updating non-existing id")
}
// remove stash ids and ensure was updated
if err := r.UpdateStashIDs(ctx, id, []*models.StashID{}); err != nil {
if err := r.UpdateStashIDs(ctx, id, []models.StashID{}); err != nil {
t.Error(err.Error())
}
@@ -62,7 +62,7 @@ func testNoStashIDs(ctx context.Context, t *testing.T, r stashIDReaderWriter, id
assert.Len(t, stashIDs, 0)
}
func testStashIDs(ctx context.Context, t *testing.T, r stashIDReaderWriter, id int, expected []*models.StashID) {
func testStashIDs(ctx context.Context, t *testing.T, r stashIDReaderWriter, id int, expected []models.StashID) {
t.Helper()
stashIDs, err := r.GetStashIDs(ctx, id)
if err != nil {

View File

@@ -7,7 +7,10 @@ import (
"fmt"
"strings"
"github.com/doug-martin/goqu/v9"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
)
const studioTable = "studios"
@@ -76,21 +79,45 @@ func (qb *studioQueryBuilder) Find(ctx context.Context, id int) (*models.Studio,
}
func (qb *studioQueryBuilder) FindMany(ctx context.Context, ids []int) ([]*models.Studio, error) {
var studios []*models.Studio
for _, id := range ids {
studio, err := qb.Find(ctx, id)
tableMgr := studioTableMgr
q := goqu.Select("*").From(tableMgr.table).Where(tableMgr.byIDInts(ids...))
unsorted, err := qb.getMany(ctx, q)
if err != nil {
return nil, err
}
if studio == nil {
return nil, fmt.Errorf("studio with id %d not found", id)
ret := make([]*models.Studio, len(ids))
for _, s := range unsorted {
i := intslice.IntIndex(ids, s.ID)
ret[i] = s
}
studios = append(studios, studio)
for i := range ret {
if ret[i] == nil {
return nil, fmt.Errorf("studio with id %d not found", ids[i])
}
}
return studios, nil
return ret, nil
}
func (qb *studioQueryBuilder) getMany(ctx context.Context, q *goqu.SelectDataset) ([]*models.Studio, error) {
const single = false
var ret []*models.Studio
if err := queryFunc(ctx, q, single, func(r *sqlx.Rows) error {
var f models.Studio
if err := r.StructScan(&f); err != nil {
return err
}
ret = append(ret, &f)
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (qb *studioQueryBuilder) FindChildren(ctx context.Context, id int) ([]*models.Studio, error) {
@@ -258,16 +285,11 @@ func (qb *studioQueryBuilder) Query(ctx context.Context, studioFilter *models.St
return nil, 0, err
}
var studios []*models.Studio
for _, id := range idsResult {
studio, err := qb.Find(ctx, id)
studios, err := qb.FindMany(ctx, idsResult)
if err != nil {
return nil, 0, err
}
studios = append(studios, studio)
}
return studios, countResult, nil
}
@@ -425,11 +447,11 @@ func (qb *studioQueryBuilder) stashIDRepository() *stashIDRepository {
}
}
func (qb *studioQueryBuilder) GetStashIDs(ctx context.Context, studioID int) ([]*models.StashID, error) {
func (qb *studioQueryBuilder) GetStashIDs(ctx context.Context, studioID int) ([]models.StashID, error) {
return qb.stashIDRepository().get(ctx, studioID)
}
func (qb *studioQueryBuilder) UpdateStashIDs(ctx context.Context, studioID int, stashIDs []*models.StashID) error {
func (qb *studioQueryBuilder) UpdateStashIDs(ctx context.Context, studioID int, stashIDs []models.StashID) error {
return qb.stashIDRepository().replace(ctx, studioID, stashIDs)
}

View File

@@ -69,6 +69,14 @@ func (t *table) byID(id interface{}) exp.Expression {
return t.idColumn.Eq(id)
}
func (t *table) byIDInts(ids ...int) exp.Expression {
ii := make([]interface{}, len(ids))
for i, id := range ids {
ii[i] = id
}
return t.idColumn.In(ii...)
}
func (t *table) idExists(ctx context.Context, id interface{}) (bool, error) {
q := dialect.Select(goqu.COUNT("*")).From(t.table).Where(t.byID(id))

View File

@@ -174,3 +174,31 @@ var (
idColumn: goqu.T(fingerprintTable).Col(idColumn),
}
)
var (
performerTableMgr = &table{
table: goqu.T(performerTable),
idColumn: goqu.T(performerTable).Col(idColumn),
}
)
var (
studioTableMgr = &table{
table: goqu.T(studioTable),
idColumn: goqu.T(studioTable).Col(idColumn),
}
)
var (
tagTableMgr = &table{
table: goqu.T(tagTable),
idColumn: goqu.T(tagTable).Col(idColumn),
}
)
var (
movieTableMgr = &table{
table: goqu.T(movieTable),
idColumn: goqu.T(movieTable).Col(idColumn),
}
)

View File

@@ -7,7 +7,10 @@ import (
"fmt"
"strings"
"github.com/doug-martin/goqu/v9"
"github.com/jmoiron/sqlx"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/sliceutil/intslice"
)
const tagTable = "tags"
@@ -94,21 +97,45 @@ func (qb *tagQueryBuilder) Find(ctx context.Context, id int) (*models.Tag, error
}
func (qb *tagQueryBuilder) FindMany(ctx context.Context, ids []int) ([]*models.Tag, error) {
var tags []*models.Tag
for _, id := range ids {
tag, err := qb.Find(ctx, id)
tableMgr := tagTableMgr
q := goqu.Select("*").From(tableMgr.table).Where(tableMgr.byIDInts(ids...))
unsorted, err := qb.getMany(ctx, q)
if err != nil {
return nil, err
}
if tag == nil {
return nil, fmt.Errorf("tag with id %d not found", id)
ret := make([]*models.Tag, len(ids))
for _, s := range unsorted {
i := intslice.IntIndex(ids, s.ID)
ret[i] = s
}
tags = append(tags, tag)
for i := range ret {
if ret[i] == nil {
return nil, fmt.Errorf("tag with id %d not found", ids[i])
}
}
return tags, nil
return ret, nil
}
func (qb *tagQueryBuilder) getMany(ctx context.Context, q *goqu.SelectDataset) ([]*models.Tag, error) {
const single = false
var ret []*models.Tag
if err := queryFunc(ctx, q, single, func(r *sqlx.Rows) error {
var f models.Tag
if err := r.StructScan(&f); err != nil {
return err
}
ret = append(ret, &f)
return nil
}); err != nil {
return nil, err
}
return ret, nil
}
func (qb *tagQueryBuilder) FindBySceneID(ctx context.Context, sceneID int) ([]*models.Tag, error) {
@@ -343,14 +370,10 @@ func (qb *tagQueryBuilder) Query(ctx context.Context, tagFilter *models.TagFilte
return nil, 0, err
}
var tags []*models.Tag
for _, id := range idsResult {
tag, err := qb.Find(ctx, id)
tags, err := qb.FindMany(ctx, idsResult)
if err != nil {
return nil, 0, err
}
tags = append(tags, tag)
}
return tags, countResult, nil
}

View File

@@ -961,7 +961,10 @@ func TestTagMerge(t *testing.T) {
if err != nil {
return err
}
sceneTagIDs := s.TagIDs
if err := s.LoadTagIDs(ctx, db.Scene); err != nil {
return err
}
sceneTagIDs := s.TagIDs.List()
assert.Contains(sceneTagIDs, destID)
@@ -993,8 +996,12 @@ func TestTagMerge(t *testing.T) {
return err
}
if err := g.LoadTagIDs(ctx, db.Gallery); err != nil {
return err
}
// ensure gallery points to new tag
assert.Contains(g.TagIDs, destID)
assert.Contains(g.TagIDs.List(), destID)
// ensure performer points to new tag
performerTagIDs, err := sqlite.PerformerReaderWriter.GetTagIDs(ctx, performerIDs[performerIdxWithTwoTags])

View File

@@ -14,7 +14,7 @@ type FinderImageStashIDGetter interface {
Finder
GetAliases(ctx context.Context, studioID int) ([]string, error)
GetImage(ctx context.Context, studioID int) ([]byte, error)
GetStashIDs(ctx context.Context, studioID int) ([]*models.StashID, error)
models.StashIDLoader
}
// ToJSON converts a Studio object into its JSON equivalent.
@@ -69,9 +69,9 @@ func ToJSON(ctx context.Context, reader FinderImageStashIDGetter, studio *models
}
stashIDs, _ := reader.GetStashIDs(ctx, studio.ID)
var ret []*models.StashID
var ret []models.StashID
for _, stashID := range stashIDs {
newJoin := &models.StashID{
newJoin := models.StashID{
StashID: stashID.StashID,
Endpoint: stashID.Endpoint,
}

View File

@@ -46,8 +46,8 @@ var stashID = models.StashID{
StashID: "StashID",
Endpoint: "Endpoint",
}
var stashIDs = []*models.StashID{
&stashID,
var stashIDs = []models.StashID{
stashID,
}
const image = "aW1hZ2VCeXRlcw=="
@@ -107,8 +107,8 @@ func createFullJSONStudio(parentStudio, image string, aliases []string) *jsonsch
Image: image,
Rating: rating,
Aliases: aliases,
StashIDs: []*models.StashID{
&stashID,
StashIDs: []models.StashID{
stashID,
},
IgnoreAutoTag: autoTagIgnored,
}

View File

@@ -18,7 +18,7 @@ type NameFinderCreatorUpdater interface {
UpdateFull(ctx context.Context, updatedStudio models.Studio) (*models.Studio, error)
UpdateImage(ctx context.Context, studioID int, image []byte) error
UpdateAliases(ctx context.Context, studioID int, aliases []string) error
UpdateStashIDs(ctx context.Context, studioID int, stashIDs []*models.StashID) error
UpdateStashIDs(ctx context.Context, studioID int, stashIDs []models.StashID) error
}
var ErrParentStudioNotExist = errors.New("parent studio does not exist")

View File

@@ -6,5 +6,6 @@ package main
import (
_ "github.com/99designs/gqlgen"
_ "github.com/Yamashou/gqlgenc"
_ "github.com/vektah/dataloaden"
_ "github.com/vektra/mockery/v2"
)

2
vendor/github.com/vektah/dataloaden/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,2 @@
/vendor
/.idea

97
vendor/github.com/vektah/dataloaden/README.md generated vendored Normal file
View File

@@ -0,0 +1,97 @@
### The DATALOADer gENerator [![CircleCI](https://circleci.com/gh/Vektah/dataloaden.svg?style=svg)](https://circleci.com/gh/vektah/dataloaden) [![Go Report Card](https://goreportcard.com/badge/github.com/vektah/dataloaden)](https://goreportcard.com/report/github.com/vektah/dataloaden) [![codecov](https://codecov.io/gh/vektah/dataloaden/branch/master/graph/badge.svg)](https://codecov.io/gh/vektah/dataloaden)
Requires golang 1.11+ for modules support.
This is a tool for generating type safe data loaders for go, inspired by https://github.com/facebook/dataloader.
The intended use is in graphql servers, to reduce the number of queries being sent to the database. These dataloader
objects should be request scoped and short lived. They should be cheap to create in every request even if they dont
get used.
#### Getting started
From inside the package you want to have the dataloader in:
```bash
go run github.com/vektah/dataloaden UserLoader string *github.com/dataloaden/example.User
```
This will generate a dataloader called `UserLoader` that looks up `*github.com/dataloaden/example.User`'s objects
based on a `string` key.
In another file in the same package, create the constructor method:
```go
func NewUserLoader() *UserLoader {
return &UserLoader{
wait: 2 * time.Millisecond,
maxBatch: 100,
fetch: func(keys []string) ([]*User, []error) {
users := make([]*User, len(keys))
errors := make([]error, len(keys))
for i, key := range keys {
users[i] = &User{ID: key, Name: "user " + key}
}
return users, errors
},
}
}
```
Then wherever you want to call the dataloader
```go
loader := NewUserLoader()
user, err := loader.Load("123")
```
This method will block for a short amount of time, waiting for any other similar requests to come in, call your fetch
function once. It also caches values and wont request duplicates in a batch.
#### Returning Slices
You may want to generate a dataloader that returns slices instead of single values. Both key and value types can be a
simple go type expression:
```bash
go run github.com/vektah/dataloaden UserSliceLoader string []*github.com/dataloaden/example.User
```
Now each key is expected to return a slice of values and the `fetch` function has the return type `[][]*User`.
#### Using with go modules
Create a tools.go that looks like this:
```go
// +build tools
package main
import _ "github.com/vektah/dataloaden"
```
This will allow go modules to see the dependency.
You can invoke it from anywhere within your module now using `go run github.com/vektah/dataloaden` and
always get the pinned version.
#### Wait, how do I use context with this?
I don't think context makes sense to be passed through a data loader. Consider a few scenarios:
1. a dataloader shared between requests: request A and B both get batched together, which context should be passed to the DB? context.Background is probably more suitable.
2. a dataloader per request for graphql: two different nodes in the graph get batched together, they have different context for tracing purposes, which should be passed to the db? neither, you should just use the root request context.
So be explicit about your context:
```go
func NewLoader(ctx context.Context) *UserLoader {
return &UserLoader{
wait: 2 * time.Millisecond,
maxBatch: 100,
fetch: func(keys []string) ([]*User, []error) {
// you now have a ctx to work with
},
}
}
```
If you feel like I'm wrong please raise an issue.

32
vendor/github.com/vektah/dataloaden/appveyor.yml generated vendored Normal file
View File

@@ -0,0 +1,32 @@
version: "{build}"
# Source Config
skip_branch_with_pr: true
clone_folder: c:\projects\dataloaden
# Build host
environment:
GOPATH: c:\gopath
GOVERSION: 1.11.5
PATH: '%PATH%;c:\gopath\bin'
init:
- git config --global core.autocrlf input
# Build
install:
# Install the specific Go version.
- rmdir c:\go /s /q
- appveyor DownloadFile https://storage.googleapis.com/golang/go%GOVERSION%.windows-amd64.msi
- msiexec /i go%GOVERSION%.windows-amd64.msi /q
- go version
build: false
deploy: false
test_script:
- go generate ./...
- go test -parallel 8 ./...

Some files were not shown because too many files have changed in this diff Show More