mirror of
https://github.com/stashapp/stash.git
synced 2026-06-11 18:35:26 -05:00
Compare commits
38 Commits
v0.27.2
...
docs-regex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5eb5621dbe | ||
|
|
0621d87133 | ||
|
|
cacfe5a268 | ||
|
|
8c8be22fe4 | ||
|
|
a0e09bbe5c | ||
|
|
4be793d4b3 | ||
|
|
60bb6bf50b | ||
|
|
7f8349469a | ||
|
|
6ad0951878 | ||
|
|
e097f2b3f4 | ||
|
|
3c81d3b154 | ||
|
|
ef2231f97b | ||
|
|
f81202660c | ||
|
|
6c5bf5f052 | ||
|
|
5f690d96bd | ||
|
|
64fed3553a | ||
|
|
a18c538c1f | ||
|
|
41d1b45fb9 | ||
|
|
602f95dd29 | ||
|
|
2a454e5a1e | ||
|
|
a100f8ffc8 | ||
|
|
527c282b92 | ||
|
|
e8125d08db | ||
|
|
0d40056f8c | ||
|
|
180a0fa8dd | ||
|
|
b1d5dc2a0e | ||
|
|
89f539ee24 | ||
|
|
f949fab231 | ||
|
|
edb66bd4e4 | ||
|
|
1b7e729750 | ||
|
|
7fb8f9172e | ||
|
|
069a4b1f80 | ||
|
|
c6bcdd89be | ||
|
|
093de3bce2 | ||
|
|
8c5ebf3797 | ||
|
|
33e46bad64 | ||
|
|
eca41dc7b4 | ||
|
|
33ca4f8887 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -12,7 +12,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COMPILER_IMAGE: stashapp/compiler:9
|
||||
COMPILER_IMAGE: stashapp/compiler:10
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
4
.github/workflows/golangci-lint.yml
vendored
4
.github/workflows/golangci-lint.yml
vendored
@@ -9,7 +9,7 @@ on:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
COMPILER_IMAGE: stashapp/compiler:9
|
||||
COMPILER_IMAGE: stashapp/compiler:10
|
||||
|
||||
jobs:
|
||||
golangci:
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
run: docker exec -t build /bin/bash -c "make generate-backend"
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
||||
version: latest
|
||||
|
||||
@@ -12,15 +12,9 @@ RUN if [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then BIN=stash-linux-arm32v6; \
|
||||
FROM --platform=$TARGETPLATFORM alpine:latest AS app
|
||||
COPY --from=binary /stash /usr/bin/
|
||||
|
||||
# vips version 8.15.0-r0 breaks thumbnail generation on arm32v6
|
||||
# need to use 8.14.3-r0 from alpine 3.18 instead
|
||||
|
||||
RUN apk add --no-cache --virtual .build-deps gcc python3-dev musl-dev \
|
||||
&& apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg ruby tzdata \
|
||||
&& apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.18/community vips=8.14.3-r0 vips-tools=8.14.3-r0 \
|
||||
&& pip install --user --break-system-packages mechanicalsoup cloudscraper bencoder.pyx stashapp-tools \
|
||||
&& gem install faraday \
|
||||
&& apk del .build-deps
|
||||
RUN apk add --no-cache ca-certificates python3 py3-requests py3-requests-toolbelt py3-lxml py3-pip ffmpeg ruby tzdata vips vips-tools \
|
||||
&& pip install --user --break-system-packages mechanicalsoup cloudscraper stashapp-tools \
|
||||
&& gem install faraday
|
||||
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
|
||||
|
||||
# Basic build-time metadata as defined at https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.22
|
||||
FROM golang:1.22.8
|
||||
|
||||
LABEL maintainer="https://discord.gg/2TsNFKt"
|
||||
|
||||
@@ -26,9 +26,9 @@ RUN apt-get update && \
|
||||
|
||||
# FreeBSD cross-compilation setup
|
||||
# https://github.com/smartmontools/docker-build/blob/6b8c92560d17d325310ba02d9f5a4b250cb0764a/Dockerfile#L66
|
||||
ENV FREEBSD_VERSION 12.4
|
||||
ENV FREEBSD_VERSION 13.4
|
||||
ENV FREEBSD_DOWNLOAD_URL http://ftp.plusline.de/FreeBSD/releases/amd64/${FREEBSD_VERSION}-RELEASE/base.txz
|
||||
ENV FREEBSD_SHA 581c7edacfd2fca2bdf5791f667402d22fccd8a5e184635e0cac075564d57aa8
|
||||
ENV FREEBSD_SHA 8e13b0a93daba349b8d28ad246d7beb327659b2ef4fe44d89f447392daec5a7c
|
||||
|
||||
RUN cd /tmp && \
|
||||
curl -o base.txz $FREEBSD_DOWNLOAD_URL && \
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
user=stashapp
|
||||
repo=compiler
|
||||
version=9
|
||||
version=10
|
||||
|
||||
latest:
|
||||
docker build -t ${user}/${repo}:latest .
|
||||
|
||||
37
go.mod
37
go.mod
@@ -1,11 +1,11 @@
|
||||
module github.com/stashapp/stash
|
||||
|
||||
go 1.22
|
||||
go 1.22.8
|
||||
|
||||
require (
|
||||
github.com/99designs/gqlgen v0.17.49
|
||||
github.com/99designs/gqlgen v0.17.55
|
||||
github.com/WithoutPants/sortorder v0.0.0-20230616003020-921c9ef69552
|
||||
github.com/Yamashou/gqlgenc v0.0.6
|
||||
github.com/Yamashou/gqlgenc v0.25.3
|
||||
github.com/anacrolix/dms v1.2.2
|
||||
github.com/antchfx/htmlquery v1.3.0
|
||||
github.com/asticode/go-astisub v0.25.1
|
||||
@@ -20,12 +20,13 @@ require (
|
||||
github.com/go-chi/httplog v0.3.1
|
||||
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4
|
||||
github.com/gofrs/uuid/v5 v5.1.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1
|
||||
github.com/golang-migrate/migrate/v4 v4.16.2
|
||||
github.com/gorilla/securecookie v1.1.1
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/hasura/go-graphql-client v0.13.1
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
@@ -39,7 +40,6 @@ require (
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
|
||||
github.com/remeh/sizedwaitgroup v1.0.0
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cast v1.6.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
@@ -47,27 +47,28 @@ require (
|
||||
github.com/tidwall/gjson v1.16.0
|
||||
github.com/vearutop/statigz v1.4.0
|
||||
github.com/vektah/dataloaden v0.3.0
|
||||
github.com/vektah/gqlparser/v2 v2.5.16
|
||||
github.com/vektah/gqlparser/v2 v2.5.18
|
||||
github.com/vektra/mockery/v2 v2.10.0
|
||||
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e
|
||||
github.com/zencoder/go-dash/v3 v3.0.2
|
||||
golang.org/x/crypto v0.24.0
|
||||
golang.org/x/crypto v0.28.0
|
||||
golang.org/x/image v0.18.0
|
||||
golang.org/x/net v0.26.0
|
||||
golang.org/x/sys v0.21.0
|
||||
golang.org/x/term v0.21.0
|
||||
golang.org/x/text v0.16.0
|
||||
golang.org/x/net v0.30.0
|
||||
golang.org/x/sys v0.26.0
|
||||
golang.org/x/term v0.25.0
|
||||
golang.org/x/text v0.19.0
|
||||
gopkg.in/guregu/null.v4 v4.0.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/agnivade/levenshtein v1.1.1 // indirect
|
||||
github.com/agnivade/levenshtein v1.2.0 // indirect
|
||||
github.com/antchfx/xpath v1.2.3 // indirect
|
||||
github.com/asticode/go-astikit v0.20.0 // indirect
|
||||
github.com/asticode/go-astits v1.8.0 // indirect
|
||||
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/coder/websocket v1.8.12 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
@@ -108,12 +109,12 @@ require (
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/urfave/cli/v2 v2.27.2 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
|
||||
github.com/urfave/cli/v2 v2.27.5 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
98
go.sum
98
go.sum
@@ -51,26 +51,23 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/99designs/gqlgen v0.17.2/go.mod h1:K5fzLKwtph+FFgh9j7nFbRUdBKvTcGnsta51fsMTn3o=
|
||||
github.com/99designs/gqlgen v0.17.49 h1:b3hNGexHd33fBSAd4NDT/c3NCcQzcAVkknhN9ym36YQ=
|
||||
github.com/99designs/gqlgen v0.17.49/go.mod h1:tC8YFVZMed81x7UJ7ORUwXF4Kn6SXuucFqQBhN8+BU0=
|
||||
github.com/99designs/gqlgen v0.17.55 h1:3vzrNWYyzSZjGDFo68e5j9sSauLxfKvLp+6ioRokVtM=
|
||||
github.com/99designs/gqlgen v0.17.55/go.mod h1:3Bq768f8hgVPGZxL8aY9MaYmbxa6llPM/qu1IGH1EJo=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
|
||||
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
|
||||
github.com/PuerkitoBio/goquery v1.9.3 h1:mpJr/ikUA9/GNJB/DBZcGeFDXUtosHRyRrwh7KGdTG0=
|
||||
github.com/PuerkitoBio/goquery v1.9.3/go.mod h1:1ndLHPdTz+DyQPICCWYlYQMPl0oXZj0G6D4LCYA6u4U=
|
||||
github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
|
||||
github.com/WithoutPants/sortorder v0.0.0-20230616003020-921c9ef69552 h1:eukVk+mGmbSZppLw8WJGpEUgMC570eb32y7FOsPW4Kc=
|
||||
github.com/WithoutPants/sortorder v0.0.0-20230616003020-921c9ef69552/go.mod h1:LKbO1i6L1lSlwWx4NHWVECxubHNKFz2YQoEMGXAFVy8=
|
||||
github.com/Yamashou/gqlgenc v0.0.6 h1:wfMTtuVSrX2N1z5/ssecxx+E7l1fa0FOq5mwFW47oY4=
|
||||
github.com/Yamashou/gqlgenc v0.0.6/go.mod h1:WOXjogecRGpD1WKgxnnyHJo0/Dxn44p/LNRoE6mtFQo=
|
||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||
github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
|
||||
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
|
||||
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
|
||||
github.com/Yamashou/gqlgenc v0.25.3 h1:mVV8/Ho8EDZUQKQZbQqqXGNq8jc8aQfPpHhZOnTkMNE=
|
||||
github.com/Yamashou/gqlgenc v0.25.3/go.mod h1:G0g1N81xpIklVdnyboW1zwOHcj/n4hNfhTwfN29Rjig=
|
||||
github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY=
|
||||
github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
@@ -156,23 +153,24 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
|
||||
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/corona10/goimagehash v1.1.0 h1:teNMX/1e+Wn/AYSbLHX8mj+mF9r60R1kBeqE9MkoYwI=
|
||||
github.com/corona10/goimagehash v1.1.0/go.mod h1:VkvE0mLn84L4aF8vCb6mafVajEb6QYMHl2ZJLn0mOGI=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
|
||||
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
|
||||
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
@@ -250,8 +248,8 @@ github.com/gofrs/uuid/v5 v5.1.0 h1:S5rqVKIigghZTCBKPCw0Y+bXkn26K3TB5mvQq2Ix8dk=
|
||||
github.com/gofrs/uuid/v5 v5.1.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA=
|
||||
github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
@@ -344,7 +342,6 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
@@ -399,6 +396,8 @@ github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoI
|
||||
github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M=
|
||||
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/hasura/go-graphql-client v0.13.1 h1:kKbjhxhpwz58usVl+Xvgah/TDha5K2akNTRQdsEHN6U=
|
||||
github.com/hasura/go-graphql-client v0.13.1/go.mod h1:k7FF7h53C+hSNFRG3++DdVZWIuHdCaTbI7siTJ//zGQ=
|
||||
github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs=
|
||||
github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E=
|
||||
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
|
||||
@@ -435,7 +434,6 @@ github.com/kermieisinthehouse/gosx-notifier v0.1.2 h1:KV0KBeKK2B24kIHY7iK0jgS64Q
|
||||
github.com/kermieisinthehouse/gosx-notifier v0.1.2/go.mod h1:xyWT07azFtUOcHl96qMVvKhvKzsMcS7rKTHQyv8WTho=
|
||||
github.com/kermieisinthehouse/systray v1.2.4 h1:pdH5vnl+KKjRrVCRU4g/2W1/0HVzuuJ6WXHlPPHYY6s=
|
||||
github.com/kermieisinthehouse/systray v1.2.4/go.mod h1:axh6C/jNuSyC0QGtidZJURc9h+h41HNoMySoLVrhVR4=
|
||||
github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs=
|
||||
@@ -459,7 +457,6 @@ github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1
|
||||
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
|
||||
@@ -468,7 +465,6 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/matryer/moq v0.2.3/go.mod h1:9RtPYjTnH1bSBIkpvtHkFN7nbWAnO7oRpdJkEIn6UtE=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
@@ -505,7 +501,6 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI
|
||||
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
@@ -587,7 +582,6 @@ github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+
|
||||
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
|
||||
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
|
||||
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
|
||||
@@ -599,12 +593,8 @@ github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5P
|
||||
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
|
||||
github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk=
|
||||
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
@@ -664,24 +654,21 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
|
||||
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
|
||||
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
||||
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
||||
github.com/vearutop/statigz v1.4.0 h1:RQL0KG3j/uyA/PFpHeZ/L6l2ta920/MxlOAIGEOuwmU=
|
||||
github.com/vearutop/statigz v1.4.0/go.mod h1:LYTolBLiz9oJISwiVKnOQoIwhO1LWX1A7OECawGS8XE=
|
||||
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/go.mod h1:flJWIR04IMQPGz+BXLrORkrARBxv/rtyIAFvd/MceW0=
|
||||
github.com/vektah/gqlparser/v2 v2.5.16 h1:1gcmLTvs3JLKXckwCwlUagVn/IlV2bwqle0vJ0vy5p8=
|
||||
github.com/vektah/gqlparser/v2 v2.5.16/go.mod h1:1lz1OeCqgQbQepsGxPVywrjdBHW2T08PUS3pJqepRww=
|
||||
github.com/vektah/gqlparser/v2 v2.5.18 h1:zSND3GtutylAQ1JpWnTHcqtaRZjl+y3NROeW8vuNo6Y=
|
||||
github.com/vektah/gqlparser/v2 v2.5.18/go.mod h1:6HLzf7JKv9Fi3APymudztFQNmLXR5qJeEo6BOFcXVfc=
|
||||
github.com/vektra/mockery/v2 v2.10.0 h1:MiiQWxwdq7/ET6dCXLaJzSGEN17k758H7JHS9kOdiks=
|
||||
github.com/vektra/mockery/v2 v2.10.0/go.mod h1:m/WO2UzWzqgVX3nvqpRQq70I4Z7jbSCRhdmkgtp+Ab4=
|
||||
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e h1:GruPsb+44XvYAzuAgJW1d1WHqmcI73L2XSjsbx/eJZw=
|
||||
github.com/xWTF/chardet v0.0.0-20230208095535-c780f2ac244e/go.mod h1:wA8kQ8WFipMciY9WcWzqQgZordm/P7l8IZdvx1crwmc=
|
||||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
|
||||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@@ -728,8 +715,8 @@ golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -770,10 +757,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/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
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=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -823,8 +809,8 @@ golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -854,8 +840,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
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/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
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=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -938,7 +924,6 @@ golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/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-20220310020820-b874c991c1a5/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -947,13 +932,13 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -966,8 +951,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -1013,7 +998,6 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200815165600-90abf76919f3/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
@@ -1030,11 +1014,9 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
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/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
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=
|
||||
|
||||
@@ -7,9 +7,6 @@ exec:
|
||||
filename: internal/api/generated_exec.go
|
||||
model:
|
||||
filename: internal/api/generated_models.go
|
||||
resolver:
|
||||
filename: internal/api/resolver.go
|
||||
type: Resolver
|
||||
|
||||
struct_tag: gqlgen
|
||||
|
||||
@@ -132,9 +129,6 @@ models:
|
||||
model: github.com/stashapp/stash/internal/identify.FieldStrategy
|
||||
ScraperSource:
|
||||
model: github.com/stashapp/stash/pkg/scraper.Source
|
||||
# rebind inputs to types
|
||||
StashIDInput:
|
||||
model: github.com/stashapp/stash/pkg/models.StashID
|
||||
IdentifySourceInput:
|
||||
model: github.com/stashapp/stash/internal/identify.Source
|
||||
IdentifyFieldOptionsInput:
|
||||
|
||||
@@ -300,6 +300,7 @@ type Mutation {
|
||||
sceneMarkerCreate(input: SceneMarkerCreateInput!): SceneMarker
|
||||
sceneMarkerUpdate(input: SceneMarkerUpdateInput!): SceneMarker
|
||||
sceneMarkerDestroy(id: ID!): Boolean!
|
||||
sceneMarkersDestroy(ids: [ID!]!): Boolean!
|
||||
|
||||
sceneAssignFile(input: AssignSceneFileInput!): Boolean!
|
||||
|
||||
|
||||
@@ -91,6 +91,12 @@ input StashIDCriterionInput {
|
||||
modifier: CriterionModifier!
|
||||
}
|
||||
|
||||
input CustomFieldCriterionInput {
|
||||
field: String!
|
||||
value: [Any!]
|
||||
modifier: CriterionModifier!
|
||||
}
|
||||
|
||||
input PerformerFilterType {
|
||||
AND: PerformerFilterType
|
||||
OR: PerformerFilterType
|
||||
@@ -182,6 +188,8 @@ input PerformerFilterType {
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
updated_at: TimestampCriterionInput
|
||||
|
||||
custom_fields: [CustomFieldCriterionInput!]
|
||||
}
|
||||
|
||||
input SceneMarkerFilterType {
|
||||
@@ -193,6 +201,8 @@ input SceneMarkerFilterType {
|
||||
performers: MultiCriterionInput
|
||||
"Filter to only include scene markers from these scenes"
|
||||
scenes: MultiCriterionInput
|
||||
"Filter by duration (in seconds)"
|
||||
duration: FloatCriterionInput
|
||||
"Filter by creation time"
|
||||
created_at: TimestampCriterionInput
|
||||
"Filter by last update time"
|
||||
|
||||
@@ -338,3 +338,10 @@ type SystemStatus {
|
||||
input MigrateInput {
|
||||
backupPath: String!
|
||||
}
|
||||
|
||||
input CustomFieldsInput {
|
||||
"If populated, the entire custom fields map will be replaced with this value"
|
||||
full: Map
|
||||
"If populated, only the keys in this map will be updated"
|
||||
partial: Map
|
||||
}
|
||||
|
||||
@@ -58,6 +58,8 @@ type Performer {
|
||||
updated_at: Time!
|
||||
groups: [Group!]!
|
||||
movies: [Movie!]! @deprecated(reason: "use groups instead")
|
||||
|
||||
custom_fields: Map!
|
||||
}
|
||||
|
||||
input PerformerCreateInput {
|
||||
@@ -93,6 +95,8 @@ input PerformerCreateInput {
|
||||
hair_color: String
|
||||
weight: Int
|
||||
ignore_auto_tag: Boolean
|
||||
|
||||
custom_fields: Map
|
||||
}
|
||||
|
||||
input PerformerUpdateInput {
|
||||
@@ -129,6 +133,8 @@ input PerformerUpdateInput {
|
||||
hair_color: String
|
||||
weight: Int
|
||||
ignore_auto_tag: Boolean
|
||||
|
||||
custom_fields: CustomFieldsInput
|
||||
}
|
||||
|
||||
input BulkUpdateStrings {
|
||||
@@ -167,6 +173,8 @@ input BulkPerformerUpdateInput {
|
||||
hair_color: String
|
||||
weight: Int
|
||||
ignore_auto_tag: Boolean
|
||||
|
||||
custom_fields: CustomFieldsInput
|
||||
}
|
||||
|
||||
input PerformerDestroyInput {
|
||||
|
||||
@@ -2,7 +2,10 @@ type SceneMarker {
|
||||
id: ID!
|
||||
scene: Scene!
|
||||
title: String!
|
||||
"The required start time of the marker (in seconds). Supports decimals."
|
||||
seconds: Float!
|
||||
"The optional end time of the marker (in seconds). Supports decimals."
|
||||
end_seconds: Float
|
||||
primary_tag: Tag!
|
||||
tags: [Tag!]!
|
||||
created_at: Time!
|
||||
@@ -18,7 +21,10 @@ type SceneMarker {
|
||||
|
||||
input SceneMarkerCreateInput {
|
||||
title: String!
|
||||
"The required start time of the marker (in seconds). Supports decimals."
|
||||
seconds: Float!
|
||||
"The optional end time of the marker (in seconds). Supports decimals."
|
||||
end_seconds: Float
|
||||
scene_id: ID!
|
||||
primary_tag_id: ID!
|
||||
tag_ids: [ID!]
|
||||
@@ -27,7 +33,10 @@ input SceneMarkerCreateInput {
|
||||
input SceneMarkerUpdateInput {
|
||||
id: ID!
|
||||
title: String
|
||||
"The start time of the marker (in seconds). Supports decimals."
|
||||
seconds: Float
|
||||
"The end time of the marker (in seconds). Supports decimals."
|
||||
end_seconds: Float
|
||||
scene_id: ID
|
||||
primary_tag_id: ID
|
||||
tag_ids: [ID!]
|
||||
|
||||
@@ -13,11 +13,13 @@ input StashBoxInput {
|
||||
type StashID {
|
||||
endpoint: String!
|
||||
stash_id: String!
|
||||
updated_at: Time!
|
||||
}
|
||||
|
||||
input StashIDInput {
|
||||
endpoint: String!
|
||||
stash_id: String!
|
||||
updated_at: Time
|
||||
}
|
||||
|
||||
input StashBoxFingerprintSubmissionInput {
|
||||
|
||||
@@ -30,11 +30,6 @@ fragment TagFragment on Tag {
|
||||
id
|
||||
}
|
||||
|
||||
fragment FuzzyDateFragment on FuzzyDate {
|
||||
date
|
||||
accuracy
|
||||
}
|
||||
|
||||
fragment MeasurementsFragment on Measurements {
|
||||
band_size
|
||||
cup_size
|
||||
@@ -60,9 +55,7 @@ fragment PerformerFragment on Performer {
|
||||
images {
|
||||
...ImageFragment
|
||||
}
|
||||
birthdate {
|
||||
...FuzzyDateFragment
|
||||
}
|
||||
birth_date
|
||||
ethnicity
|
||||
country
|
||||
eye_color
|
||||
|
||||
@@ -16,12 +16,12 @@ import (
|
||||
|
||||
const (
|
||||
tripwireActivatedErrMsg = "Stash is exposed to the public internet without authentication, and is not serving any more content to protect your privacy. " +
|
||||
"More information and fixes are available at https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet"
|
||||
"More information and fixes are available at https://docs.stashapp.cc/faq/setup/#protecting-against-accidental-exposure-to-the-internet"
|
||||
|
||||
externalAccessErrMsg = "You have attempted to access Stash over the internet, and authentication is not enabled. " +
|
||||
"This is extremely dangerous! The whole world can see your your stash page and browse your files! " +
|
||||
"Stash is not answering any other requests to protect your privacy. " +
|
||||
"Please read the log entry or visit https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet"
|
||||
"Please read the log entry or visit https://docs.stashapp.cc/faq/setup/#protecting-against-accidental-exposure-to-the-internet"
|
||||
)
|
||||
|
||||
func allowUnauthenticated(r *http.Request) bool {
|
||||
|
||||
@@ -335,13 +335,13 @@ func (t changesetTranslator) updateStringsBulk(value *BulkUpdateStrings, field s
|
||||
}
|
||||
}
|
||||
|
||||
func (t changesetTranslator) updateStashIDs(value []models.StashID, field string) *models.UpdateStashIDs {
|
||||
func (t changesetTranslator) updateStashIDs(value models.StashIDInputs, field string) *models.UpdateStashIDs {
|
||||
if !t.hasField(field) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &models.UpdateStashIDs{
|
||||
StashIDs: value,
|
||||
StashIDs: value.ToStashIDs(),
|
||||
Mode: models.RelationshipUpdateModeSet,
|
||||
}
|
||||
}
|
||||
|
||||
36
internal/api/json.go
Normal file
36
internal/api/json.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// JSONNumberToNumber converts a JSON number to either a float64 or int64.
|
||||
func jsonNumberToNumber(n json.Number) interface{} {
|
||||
if strings.Contains(string(n), ".") {
|
||||
f, _ := n.Float64()
|
||||
return f
|
||||
}
|
||||
ret, _ := n.Int64()
|
||||
return ret
|
||||
}
|
||||
|
||||
// ConvertMapJSONNumbers converts all JSON numbers in a map to either float64 or int64.
|
||||
func convertMapJSONNumbers(m map[string]interface{}) (ret map[string]interface{}) {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret = make(map[string]interface{})
|
||||
for k, v := range m {
|
||||
if n, ok := v.(json.Number); ok {
|
||||
ret[k] = jsonNumberToNumber(n)
|
||||
} else if mm, ok := v.(map[string]interface{}); ok {
|
||||
ret[k] = convertMapJSONNumbers(mm)
|
||||
} else {
|
||||
ret[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
60
internal/api/json_test.go
Normal file
60
internal/api/json_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConvertMapJSONNumbers(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input map[string]interface{}
|
||||
expected map[string]interface{}
|
||||
}{
|
||||
{
|
||||
name: "Convert JSON numbers to numbers",
|
||||
input: map[string]interface{}{
|
||||
"int": json.Number("12"),
|
||||
"float": json.Number("12.34"),
|
||||
"string": "foo",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"int": int64(12),
|
||||
"float": 12.34,
|
||||
"string": "foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Convert JSON numbers to numbers in nested maps",
|
||||
input: map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"int": json.Number("56"),
|
||||
"float": json.Number("56.78"),
|
||||
"nested-string": "bar",
|
||||
},
|
||||
"int": json.Number("12"),
|
||||
"float": json.Number("12.34"),
|
||||
"string": "foo",
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"int": int64(56),
|
||||
"float": 56.78,
|
||||
"nested-string": "bar",
|
||||
},
|
||||
"int": int64(12),
|
||||
"float": 12.34,
|
||||
"string": "foo",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := convertMapJSONNumbers(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
221
internal/api/loaders/customfieldsloader_gen.go
Normal file
221
internal/api/loaders/customfieldsloader_gen.go
Normal file
@@ -0,0 +1,221 @@
|
||||
// Code generated by github.com/vektah/dataloaden, DO NOT EDIT.
|
||||
|
||||
package loaders
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
// CustomFieldsLoaderConfig captures the config to create a new CustomFieldsLoader
|
||||
type CustomFieldsLoaderConfig struct {
|
||||
// Fetch is a method that provides the data for the loader
|
||||
Fetch func(keys []int) ([]models.CustomFieldMap, []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
|
||||
}
|
||||
|
||||
// NewCustomFieldsLoader creates a new CustomFieldsLoader given a fetch, wait, and maxBatch
|
||||
func NewCustomFieldsLoader(config CustomFieldsLoaderConfig) *CustomFieldsLoader {
|
||||
return &CustomFieldsLoader{
|
||||
fetch: config.Fetch,
|
||||
wait: config.Wait,
|
||||
maxBatch: config.MaxBatch,
|
||||
}
|
||||
}
|
||||
|
||||
// CustomFieldsLoader batches and caches requests
|
||||
type CustomFieldsLoader struct {
|
||||
// this method provides the data for the loader
|
||||
fetch func(keys []int) ([]models.CustomFieldMap, []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.CustomFieldMap
|
||||
|
||||
// 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 *customFieldsLoaderBatch
|
||||
|
||||
// mutex to prevent races
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type customFieldsLoaderBatch struct {
|
||||
keys []int
|
||||
data []models.CustomFieldMap
|
||||
error []error
|
||||
closing bool
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// Load a CustomFieldMap by key, batching and caching will be applied automatically
|
||||
func (l *CustomFieldsLoader) Load(key int) (models.CustomFieldMap, error) {
|
||||
return l.LoadThunk(key)()
|
||||
}
|
||||
|
||||
// LoadThunk returns a function that when called will block waiting for a CustomFieldMap.
|
||||
// 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 *CustomFieldsLoader) LoadThunk(key int) func() (models.CustomFieldMap, error) {
|
||||
l.mu.Lock()
|
||||
if it, ok := l.cache[key]; ok {
|
||||
l.mu.Unlock()
|
||||
return func() (models.CustomFieldMap, error) {
|
||||
return it, nil
|
||||
}
|
||||
}
|
||||
if l.batch == nil {
|
||||
l.batch = &customFieldsLoaderBatch{done: make(chan struct{})}
|
||||
}
|
||||
batch := l.batch
|
||||
pos := batch.keyIndex(l, key)
|
||||
l.mu.Unlock()
|
||||
|
||||
return func() (models.CustomFieldMap, error) {
|
||||
<-batch.done
|
||||
|
||||
var data models.CustomFieldMap
|
||||
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 *CustomFieldsLoader) LoadAll(keys []int) ([]models.CustomFieldMap, []error) {
|
||||
results := make([]func() (models.CustomFieldMap, error), len(keys))
|
||||
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
|
||||
customFieldMaps := make([]models.CustomFieldMap, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
customFieldMaps[i], errors[i] = thunk()
|
||||
}
|
||||
return customFieldMaps, errors
|
||||
}
|
||||
|
||||
// LoadAllThunk returns a function that when called will block waiting for a CustomFieldMaps.
|
||||
// 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 *CustomFieldsLoader) LoadAllThunk(keys []int) func() ([]models.CustomFieldMap, []error) {
|
||||
results := make([]func() (models.CustomFieldMap, error), len(keys))
|
||||
for i, key := range keys {
|
||||
results[i] = l.LoadThunk(key)
|
||||
}
|
||||
return func() ([]models.CustomFieldMap, []error) {
|
||||
customFieldMaps := make([]models.CustomFieldMap, len(keys))
|
||||
errors := make([]error, len(keys))
|
||||
for i, thunk := range results {
|
||||
customFieldMaps[i], errors[i] = thunk()
|
||||
}
|
||||
return customFieldMaps, 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 *CustomFieldsLoader) Prime(key int, value models.CustomFieldMap) bool {
|
||||
l.mu.Lock()
|
||||
var found bool
|
||||
if _, found = l.cache[key]; !found {
|
||||
l.unsafeSet(key, value)
|
||||
}
|
||||
l.mu.Unlock()
|
||||
return !found
|
||||
}
|
||||
|
||||
// Clear the value at key from the cache, if it exists
|
||||
func (l *CustomFieldsLoader) Clear(key int) {
|
||||
l.mu.Lock()
|
||||
delete(l.cache, key)
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
func (l *CustomFieldsLoader) unsafeSet(key int, value models.CustomFieldMap) {
|
||||
if l.cache == nil {
|
||||
l.cache = map[int]models.CustomFieldMap{}
|
||||
}
|
||||
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 *customFieldsLoaderBatch) keyIndex(l *CustomFieldsLoader, 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 *customFieldsLoaderBatch) startTimer(l *CustomFieldsLoader) {
|
||||
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 *customFieldsLoaderBatch) end(l *CustomFieldsLoader) {
|
||||
b.data, b.error = l.fetch(b.keys)
|
||||
close(b.done)
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
//go:generate go run github.com/vektah/dataloaden SceneFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
|
||||
//go:generate go run github.com/vektah/dataloaden ImageFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
|
||||
//go:generate go run github.com/vektah/dataloaden GalleryFileIDsLoader int []github.com/stashapp/stash/pkg/models.FileID
|
||||
//go:generate go run github.com/vektah/dataloaden CustomFieldsLoader int github.com/stashapp/stash/pkg/models.CustomFieldMap
|
||||
//go:generate go run github.com/vektah/dataloaden SceneOCountLoader int int
|
||||
//go:generate go run github.com/vektah/dataloaden ScenePlayCountLoader int int
|
||||
//go:generate go run github.com/vektah/dataloaden SceneOHistoryLoader int []time.Time
|
||||
@@ -51,13 +52,16 @@ type Loaders struct {
|
||||
ImageFiles *ImageFileIDsLoader
|
||||
GalleryFiles *GalleryFileIDsLoader
|
||||
|
||||
GalleryByID *GalleryLoader
|
||||
ImageByID *ImageLoader
|
||||
PerformerByID *PerformerLoader
|
||||
StudioByID *StudioLoader
|
||||
TagByID *TagLoader
|
||||
GroupByID *GroupLoader
|
||||
FileByID *FileLoader
|
||||
GalleryByID *GalleryLoader
|
||||
ImageByID *ImageLoader
|
||||
|
||||
PerformerByID *PerformerLoader
|
||||
PerformerCustomFields *CustomFieldsLoader
|
||||
|
||||
StudioByID *StudioLoader
|
||||
TagByID *TagLoader
|
||||
GroupByID *GroupLoader
|
||||
FileByID *FileLoader
|
||||
}
|
||||
|
||||
type Middleware struct {
|
||||
@@ -88,6 +92,11 @@ func (m Middleware) Middleware(next http.Handler) http.Handler {
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchPerformers(ctx),
|
||||
},
|
||||
PerformerCustomFields: &CustomFieldsLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchPerformerCustomFields(ctx),
|
||||
},
|
||||
StudioByID: &StudioLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
@@ -214,6 +223,18 @@ func (m Middleware) fetchPerformers(ctx context.Context) func(keys []int) ([]*mo
|
||||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchPerformerCustomFields(ctx context.Context) func(keys []int) ([]models.CustomFieldMap, []error) {
|
||||
return func(keys []int) (ret []models.CustomFieldMap, errs []error) {
|
||||
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
ret, err = m.Repository.Performer.GetCustomFieldsBulk(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.Repository.WithDB(ctx, func(ctx context.Context) error {
|
||||
|
||||
@@ -268,6 +268,19 @@ func (r *performerResolver) Groups(ctx context.Context, obj *models.Performer) (
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *performerResolver) CustomFields(ctx context.Context, obj *models.Performer) (map[string]interface{}, error) {
|
||||
m, err := loaders.From(ctx).PerformerCustomFields.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return make(map[string]interface{}), nil
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// deprecated
|
||||
func (r *performerResolver) Movies(ctx context.Context, obj *models.Performer) (ret []*models.Group, err error) {
|
||||
return r.Groups(ctx, obj)
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
@@ -643,10 +644,14 @@ func (r *mutationResolver) ConfigureUI(ctx context.Context, input map[string]int
|
||||
c := config.GetInstance()
|
||||
|
||||
if input != nil {
|
||||
// #5483 - convert JSON numbers to float64 or int64
|
||||
input = convertMapJSONNumbers(input)
|
||||
c.SetUIConfiguration(input)
|
||||
}
|
||||
|
||||
if partial != nil {
|
||||
// #5483 - convert JSON numbers to float64 or int64
|
||||
partial = convertMapJSONNumbers(partial)
|
||||
// merge partial into existing config
|
||||
existing := c.GetUIConfiguration()
|
||||
utils.MergeMaps(existing, partial)
|
||||
@@ -664,6 +669,14 @@ func (r *mutationResolver) ConfigureUISetting(ctx context.Context, key string, v
|
||||
c := config.GetInstance()
|
||||
|
||||
cfg := utils.NestedMap(c.GetUIConfiguration())
|
||||
|
||||
// #5483 - convert JSON numbers to float64 or int64
|
||||
if m, ok := value.(map[string]interface{}); ok {
|
||||
value = convertMapJSONNumbers(m)
|
||||
} else if n, ok := value.(json.Number); ok {
|
||||
value = jsonNumberToNumber(n)
|
||||
}
|
||||
|
||||
cfg.Set(key, value)
|
||||
|
||||
return r.ConfigureUI(ctx, cfg, nil)
|
||||
@@ -671,6 +684,9 @@ func (r *mutationResolver) ConfigureUISetting(ctx context.Context, key string, v
|
||||
|
||||
func (r *mutationResolver) ConfigurePlugin(ctx context.Context, pluginID string, input map[string]interface{}) (map[string]interface{}, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
// #5483 - convert JSON numbers to float64 or int64
|
||||
input = convertMapJSONNumbers(input)
|
||||
c.SetPluginConfiguration(pluginID, input)
|
||||
|
||||
if err := c.Write(); err != nil {
|
||||
|
||||
@@ -58,7 +58,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
newPerformer.Height = input.HeightCm
|
||||
newPerformer.Weight = input.Weight
|
||||
newPerformer.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
|
||||
newPerformer.StashIDs = models.NewRelatedStashIDs(input.StashIds)
|
||||
newPerformer.StashIDs = models.NewRelatedStashIDs(models.StashIDInputs(input.StashIds).ToStashIDs())
|
||||
|
||||
newPerformer.URLs = models.NewRelatedStrings([]string{})
|
||||
if input.URL != nil {
|
||||
@@ -108,7 +108,13 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
||||
return err
|
||||
}
|
||||
|
||||
err = qb.Create(ctx, &newPerformer)
|
||||
i := &models.CreatePerformerInput{
|
||||
Performer: &newPerformer,
|
||||
// convert json.Numbers to int/float
|
||||
CustomFields: convertMapJSONNumbers(input.CustomFields),
|
||||
}
|
||||
|
||||
err = qb.Create(ctx, i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -290,6 +296,11 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
}
|
||||
|
||||
updatedPerformer.CustomFields = input.CustomFields
|
||||
// convert json.Numbers to int/float
|
||||
updatedPerformer.CustomFields.Full = convertMapJSONNumbers(updatedPerformer.CustomFields.Full)
|
||||
updatedPerformer.CustomFields.Partial = convertMapJSONNumbers(updatedPerformer.CustomFields.Partial)
|
||||
|
||||
var imageData []byte
|
||||
imageIncluded := translator.hasField("image")
|
||||
if input.Image != nil {
|
||||
|
||||
@@ -50,7 +50,7 @@ func (r *mutationResolver) SceneCreate(ctx context.Context, input models.SceneCr
|
||||
newScene.Director = translator.string(input.Director)
|
||||
newScene.Rating = input.Rating100
|
||||
newScene.Organized = translator.bool(input.Organized)
|
||||
newScene.StashIDs = models.NewRelatedStashIDs(input.StashIds)
|
||||
newScene.StashIDs = models.NewRelatedStashIDs(models.StashIDInputs(input.StashIds).ToStashIDs())
|
||||
|
||||
newScene.Date, err = translator.datePtr(input.Date)
|
||||
if err != nil {
|
||||
@@ -655,6 +655,13 @@ func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input SceneMar
|
||||
newMarker.PrimaryTagID = primaryTagID
|
||||
newMarker.SceneID = sceneID
|
||||
|
||||
if input.EndSeconds != nil {
|
||||
if err := validateSceneMarkerEndSeconds(newMarker.Seconds, *input.EndSeconds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newMarker.EndSeconds = input.EndSeconds
|
||||
}
|
||||
|
||||
tagIDs, err := stringslice.StringSliceToIntSlice(input.TagIds)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting tag ids: %w", err)
|
||||
@@ -680,6 +687,13 @@ func (r *mutationResolver) SceneMarkerCreate(ctx context.Context, input SceneMar
|
||||
return r.getSceneMarker(ctx, newMarker.ID)
|
||||
}
|
||||
|
||||
func validateSceneMarkerEndSeconds(seconds, endSeconds float64) error {
|
||||
if endSeconds < seconds {
|
||||
return fmt.Errorf("end_seconds (%f) must be greater than or equal to seconds (%f)", endSeconds, seconds)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMarkerUpdateInput) (*models.SceneMarker, error) {
|
||||
markerID, err := strconv.Atoi(input.ID)
|
||||
if err != nil {
|
||||
@@ -695,6 +709,7 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
|
||||
|
||||
updatedMarker.Title = translator.optionalString(input.Title, "title")
|
||||
updatedMarker.Seconds = translator.optionalFloat64(input.Seconds, "seconds")
|
||||
updatedMarker.EndSeconds = translator.optionalFloat64(input.EndSeconds, "end_seconds")
|
||||
updatedMarker.SceneID, err = translator.optionalIntFromString(input.SceneID, "scene_id")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting scene id: %w", err)
|
||||
@@ -735,6 +750,26 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
|
||||
return fmt.Errorf("scene marker with id %d not found", markerID)
|
||||
}
|
||||
|
||||
// Validate end_seconds
|
||||
shouldValidateEndSeconds := (updatedMarker.Seconds.Set || updatedMarker.EndSeconds.Set) && !updatedMarker.EndSeconds.Null
|
||||
if shouldValidateEndSeconds {
|
||||
seconds := existingMarker.Seconds
|
||||
if updatedMarker.Seconds.Set {
|
||||
seconds = updatedMarker.Seconds.Value
|
||||
}
|
||||
|
||||
endSeconds := existingMarker.EndSeconds
|
||||
if updatedMarker.EndSeconds.Set {
|
||||
endSeconds = &updatedMarker.EndSeconds.Value
|
||||
}
|
||||
|
||||
if endSeconds != nil {
|
||||
if err := validateSceneMarkerEndSeconds(seconds, *endSeconds); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newMarker, err := qb.UpdatePartial(ctx, markerID, updatedMarker)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -749,7 +784,7 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
|
||||
}
|
||||
|
||||
// remove the marker preview if the scene changed or if the timestamp was changed
|
||||
if existingMarker.SceneID != newMarker.SceneID || existingMarker.Seconds != newMarker.Seconds {
|
||||
if existingMarker.SceneID != newMarker.SceneID || existingMarker.Seconds != newMarker.Seconds || existingMarker.EndSeconds != newMarker.EndSeconds {
|
||||
seconds := int(existingMarker.Seconds)
|
||||
if err := fileDeleter.MarkMarkerFiles(existingScene, seconds); err != nil {
|
||||
return err
|
||||
@@ -779,11 +814,16 @@ func (r *mutationResolver) SceneMarkerUpdate(ctx context.Context, input SceneMar
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (bool, error) {
|
||||
markerID, err := strconv.Atoi(id)
|
||||
return r.SceneMarkersDestroy(ctx, []string{id})
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SceneMarkersDestroy(ctx context.Context, markerIDs []string) (bool, error) {
|
||||
ids, err := stringslice.StringSliceToIntSlice(markerIDs)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("converting id: %w", err)
|
||||
return false, fmt.Errorf("converting ids: %w", err)
|
||||
}
|
||||
|
||||
var markers []*models.SceneMarker
|
||||
fileNamingAlgo := manager.GetInstance().Config.GetVideoFileNamingAlgorithm()
|
||||
|
||||
fileDeleter := &scene.FileDeleter{
|
||||
@@ -796,35 +836,45 @@ func (r *mutationResolver) SceneMarkerDestroy(ctx context.Context, id string) (b
|
||||
qb := r.repository.SceneMarker
|
||||
sqb := r.repository.Scene
|
||||
|
||||
marker, err := qb.Find(ctx, markerID)
|
||||
for _, markerID := range ids {
|
||||
marker, err := qb.Find(ctx, markerID)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if marker == nil {
|
||||
return fmt.Errorf("scene marker with id %d not found", markerID)
|
||||
}
|
||||
|
||||
s, err := sqb.Find(ctx, marker.SceneID)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s == nil {
|
||||
return fmt.Errorf("scene with id %d not found", marker.SceneID)
|
||||
}
|
||||
|
||||
markers = append(markers, marker)
|
||||
|
||||
if err := scene.DestroyMarker(ctx, s, marker, qb, fileDeleter); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if marker == nil {
|
||||
return fmt.Errorf("scene marker with id %d not found", markerID)
|
||||
}
|
||||
|
||||
s, err := sqb.Find(ctx, marker.SceneID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if s == nil {
|
||||
return fmt.Errorf("scene with id %d not found", marker.SceneID)
|
||||
}
|
||||
|
||||
return scene.DestroyMarker(ctx, s, marker, qb, fileDeleter)
|
||||
return nil
|
||||
}); err != nil {
|
||||
fileDeleter.Rollback()
|
||||
return false, err
|
||||
}
|
||||
|
||||
// perform the post-commit actions
|
||||
fileDeleter.Commit()
|
||||
|
||||
r.hookExecutor.ExecutePostHooks(ctx, markerID, hook.SceneMarkerDestroyPost, id, nil)
|
||||
for _, marker := range markers {
|
||||
r.hookExecutor.ExecutePostHooks(ctx, marker.ID, hook.SceneMarkerDestroyPost, markerIDs, nil)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
|
||||
newStudio.Details = translator.string(input.Details)
|
||||
newStudio.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
|
||||
newStudio.Aliases = models.NewRelatedStrings(input.Aliases)
|
||||
newStudio.StashIDs = models.NewRelatedStashIDs(input.StashIds)
|
||||
newStudio.StashIDs = models.NewRelatedStashIDs(models.StashIDInputs(input.StashIds).ToStashIDs())
|
||||
|
||||
var err error
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
@@ -95,11 +95,11 @@ func (r *queryResolver) FindImages(
|
||||
result, err = qb.Query(ctx, models.ImageQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: filter,
|
||||
Count: sliceutil.Contains(fields, "count"),
|
||||
Count: slices.Contains(fields, "count"),
|
||||
},
|
||||
ImageFilter: imageFilter,
|
||||
Megapixels: sliceutil.Contains(fields, "megapixels"),
|
||||
TotalSize: sliceutil.Contains(fields, "filesize"),
|
||||
Megapixels: slices.Contains(fields, "megapixels"),
|
||||
TotalSize: slices.Contains(fields, "filesize"),
|
||||
})
|
||||
if err == nil {
|
||||
images, err = result.Resolve(ctx)
|
||||
|
||||
@@ -2,13 +2,13 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
)
|
||||
|
||||
@@ -119,11 +119,11 @@ func (r *queryResolver) FindScenes(
|
||||
result, err = r.repository.Scene.Query(ctx, models.SceneQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: filter,
|
||||
Count: sliceutil.Contains(fields, "count"),
|
||||
Count: slices.Contains(fields, "count"),
|
||||
},
|
||||
SceneFilter: sceneFilter,
|
||||
TotalDuration: sliceutil.Contains(fields, "duration"),
|
||||
TotalSize: sliceutil.Contains(fields, "filesize"),
|
||||
TotalDuration: slices.Contains(fields, "duration"),
|
||||
TotalSize: slices.Contains(fields, "filesize"),
|
||||
})
|
||||
if err == nil {
|
||||
scenes, err = result.Resolve(ctx)
|
||||
@@ -174,11 +174,11 @@ func (r *queryResolver) FindScenesByPathRegex(ctx context.Context, filter *model
|
||||
result, err := r.repository.Scene.Query(ctx, models.SceneQueryOptions{
|
||||
QueryOptions: models.QueryOptions{
|
||||
FindFilter: queryFilter,
|
||||
Count: sliceutil.Contains(fields, "count"),
|
||||
Count: slices.Contains(fields, "count"),
|
||||
},
|
||||
SceneFilter: sceneFilter,
|
||||
TotalDuration: sliceutil.Contains(fields, "duration"),
|
||||
TotalSize: sliceutil.Contains(fields, "filesize"),
|
||||
TotalDuration: slices.Contains(fields, "duration"),
|
||||
TotalSize: slices.Contains(fields, "filesize"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -11,7 +12,6 @@ import (
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/pkg"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
)
|
||||
|
||||
var ErrInvalidPackageType = errors.New("invalid package type")
|
||||
@@ -166,7 +166,7 @@ func (r *queryResolver) InstalledPackages(ctx context.Context, typeArg PackageTy
|
||||
|
||||
var ret []*Package
|
||||
|
||||
if sliceutil.Contains(graphql.CollectAllFields(ctx), "source_package") {
|
||||
if slices.Contains(graphql.CollectAllFields(ctx), "source_package") {
|
||||
ret, err = r.getInstalledPackagesWithUpgrades(ctx, pm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/go-chi/httplog"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/vearutop/statigz"
|
||||
"github.com/vektah/gqlparser/v2/ast"
|
||||
|
||||
"github.com/stashapp/stash/internal/api/loaders"
|
||||
"github.com/stashapp/stash/internal/build"
|
||||
@@ -185,7 +186,7 @@ func Initialize() (*Server, error) {
|
||||
MaxUploadSize: cfg.GetMaxUploadSize(),
|
||||
})
|
||||
|
||||
gqlSrv.SetQueryCache(gqlLru.New(1000))
|
||||
gqlSrv.SetQueryCache(gqlLru.New[*ast.QueryDocument](1000))
|
||||
gqlSrv.Use(gqlExtension.Introspection{})
|
||||
|
||||
gqlSrv.SetErrorPresenter(gqlErrorHandler)
|
||||
|
||||
@@ -2,11 +2,11 @@ package autotag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/match"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
)
|
||||
|
||||
type GalleryFinderUpdater interface {
|
||||
@@ -53,7 +53,7 @@ func GalleryPerformers(ctx context.Context, s *models.Gallery, rw GalleryPerform
|
||||
}
|
||||
existing := s.PerformerIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, otherID) {
|
||||
if slices.Contains(existing, otherID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ func GalleryTags(ctx context.Context, s *models.Gallery, rw GalleryTagUpdater, t
|
||||
}
|
||||
existing := s.TagIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, otherID) {
|
||||
if slices.Contains(existing, otherID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ package autotag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/match"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
)
|
||||
|
||||
type ImageFinderUpdater interface {
|
||||
@@ -44,7 +44,7 @@ func ImagePerformers(ctx context.Context, s *models.Image, rw ImagePerformerUpda
|
||||
}
|
||||
existing := s.PerformerIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, otherID) {
|
||||
if slices.Contains(existing, otherID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ func ImageTags(ctx context.Context, s *models.Image, rw ImageTagUpdater, tagRead
|
||||
}
|
||||
existing := s.TagIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, otherID) {
|
||||
if slices.Contains(existing, otherID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ func createPerformer(ctx context.Context, pqb models.PerformerWriter) error {
|
||||
Name: testName,
|
||||
}
|
||||
|
||||
err := pqb.Create(ctx, &performer)
|
||||
err := pqb.Create(ctx, &models.CreatePerformerInput{Performer: &performer})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ package autotag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/match"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
)
|
||||
|
||||
@@ -63,7 +63,7 @@ func (tagger *Tagger) PerformerScenes(ctx context.Context, p *models.Performer,
|
||||
}
|
||||
existing := o.PerformerIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, p.ID) {
|
||||
if slices.Contains(existing, p.ID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ func (tagger *Tagger) PerformerImages(ctx context.Context, p *models.Performer,
|
||||
}
|
||||
existing := o.PerformerIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, p.ID) {
|
||||
if slices.Contains(existing, p.ID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ func (tagger *Tagger) PerformerGalleries(ctx context.Context, p *models.Performe
|
||||
}
|
||||
existing := o.PerformerIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, p.ID) {
|
||||
if slices.Contains(existing, p.ID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ package autotag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/stashapp/stash/pkg/match"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
)
|
||||
|
||||
type SceneFinderUpdater interface {
|
||||
@@ -44,7 +44,7 @@ func ScenePerformers(ctx context.Context, s *models.Scene, rw ScenePerformerUpda
|
||||
}
|
||||
existing := s.PerformerIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, otherID) {
|
||||
if slices.Contains(existing, otherID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ func SceneTags(ctx context.Context, s *models.Scene, rw SceneTagUpdater, tagRead
|
||||
}
|
||||
existing := s.TagIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, otherID) {
|
||||
if slices.Contains(existing, otherID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@ package autotag
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/stashapp/stash/pkg/gallery"
|
||||
"github.com/stashapp/stash/pkg/image"
|
||||
"github.com/stashapp/stash/pkg/match"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
)
|
||||
|
||||
@@ -61,7 +61,7 @@ func (tagger *Tagger) TagScenes(ctx context.Context, p *models.Tag, paths []stri
|
||||
}
|
||||
existing := o.TagIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, p.ID) {
|
||||
if slices.Contains(existing, p.ID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ func (tagger *Tagger) TagImages(ctx context.Context, p *models.Tag, paths []stri
|
||||
}
|
||||
existing := o.TagIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, p.ID) {
|
||||
if slices.Contains(existing, p.ID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ func (tagger *Tagger) TagGalleries(ctx context.Context, p *models.Tag, paths []s
|
||||
}
|
||||
existing := o.TagIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, p.ID) {
|
||||
if slices.Contains(existing, p.ID) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -40,7 +41,6 @@ import (
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
)
|
||||
|
||||
var pageSize = 100
|
||||
@@ -521,7 +521,7 @@ func (me *contentDirectoryService) getPageVideos(sceneFilter *models.SceneFilter
|
||||
}
|
||||
|
||||
func getPageFromID(paths []string) *int {
|
||||
i := sliceutil.Index(paths, "page")
|
||||
i := slices.Index(paths, "page")
|
||||
if i == -1 || i+1 >= len(paths) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package dlna
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
)
|
||||
|
||||
// only keep the 10 most recent IP addresses
|
||||
@@ -30,7 +29,7 @@ func (m *ipWhitelistManager) addRecent(addr string) bool {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
i := sliceutil.Index(m.recentIPAddresses, addr)
|
||||
i := slices.Index(m.recentIPAddresses, addr)
|
||||
if i != -1 {
|
||||
if i == 0 {
|
||||
// don't do anything if it's already at the start
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
@@ -244,7 +245,18 @@ func (t *SceneIdentifier) getSceneUpdater(ctx context.Context, s *models.Scene,
|
||||
}
|
||||
}
|
||||
|
||||
stashIDs, err := rel.stashIDs(ctx)
|
||||
// SetCoverImage defaults to true if unset
|
||||
if options.SetCoverImage == nil || *options.SetCoverImage {
|
||||
ret.CoverImage, err = rel.cover(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// if anything changed, also update the updated at time on the applicable stash id
|
||||
changed := !ret.IsEmpty()
|
||||
|
||||
stashIDs, err := rel.stashIDs(ctx, changed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -255,14 +267,6 @@ func (t *SceneIdentifier) getSceneUpdater(ctx context.Context, s *models.Scene,
|
||||
}
|
||||
}
|
||||
|
||||
// SetCoverImage defaults to true if unset
|
||||
if options.SetCoverImage == nil || *options.SetCoverImage {
|
||||
ret.CoverImage, err = rel.cover(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -333,7 +337,7 @@ func (t *SceneIdentifier) addTagToScene(ctx context.Context, s *models.Scene, ta
|
||||
}
|
||||
existing := s.TagIDs.List()
|
||||
|
||||
if sliceutil.Contains(existing, tagID) {
|
||||
if slices.Contains(existing, tagID) {
|
||||
// skip if the scene was already tagged
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,13 +4,13 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/models/mocks"
|
||||
"github.com/stashapp/stash/pkg/scraper"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
@@ -23,7 +23,7 @@ type mockSceneScraper struct {
|
||||
}
|
||||
|
||||
func (s mockSceneScraper) ScrapeScenes(ctx context.Context, sceneID int) ([]*scraper.ScrapedScene, error) {
|
||||
if sliceutil.Contains(s.errIDs, sceneID) {
|
||||
if slices.Contains(s.errIDs, sceneID) {
|
||||
return nil, errors.New("scrape scene error")
|
||||
}
|
||||
return s.results[sceneID], nil
|
||||
|
||||
@@ -41,7 +41,7 @@ func createMissingPerformer(ctx context.Context, endpoint string, w PerformerCre
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = w.Create(ctx, newPerformer)
|
||||
err = w.Create(ctx, &models.CreatePerformerInput{Performer: newPerformer})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating performer: %w", err)
|
||||
}
|
||||
|
||||
@@ -24,8 +24,8 @@ func Test_getPerformerID(t *testing.T) {
|
||||
|
||||
db := mocks.NewDatabase()
|
||||
|
||||
db.Performer.On("Create", testCtx, mock.Anything).Run(func(args mock.Arguments) {
|
||||
p := args.Get(1).(*models.Performer)
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.CreatePerformerInput")).Run(func(args mock.Arguments) {
|
||||
p := args.Get(1).(*models.CreatePerformerInput)
|
||||
p.ID = validStoredID
|
||||
}).Return(nil)
|
||||
|
||||
@@ -154,14 +154,14 @@ func Test_createMissingPerformer(t *testing.T) {
|
||||
|
||||
db := mocks.NewDatabase()
|
||||
|
||||
db.Performer.On("Create", testCtx, mock.MatchedBy(func(p *models.Performer) bool {
|
||||
db.Performer.On("Create", testCtx, mock.MatchedBy(func(p *models.CreatePerformerInput) bool {
|
||||
return p.Name == validName
|
||||
})).Run(func(args mock.Arguments) {
|
||||
p := args.Get(1).(*models.Performer)
|
||||
p := args.Get(1).(*models.CreatePerformerInput)
|
||||
p.ID = performerID
|
||||
}).Return(nil)
|
||||
|
||||
db.Performer.On("Create", testCtx, mock.MatchedBy(func(p *models.Performer) bool {
|
||||
db.Performer.On("Create", testCtx, mock.MatchedBy(func(p *models.CreatePerformerInput) bool {
|
||||
return p.Name == invalidName
|
||||
})).Return(errors.New("error creating performer"))
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -182,7 +183,13 @@ func (g sceneRelationships) tags(ctx context.Context) ([]int, error) {
|
||||
return tagIDs, nil
|
||||
}
|
||||
|
||||
func (g sceneRelationships) stashIDs(ctx context.Context) ([]models.StashID, error) {
|
||||
// stashIDs returns the updated stash IDs for the scene
|
||||
// returns nil if not applicable or no changes were made
|
||||
// if setUpdateTime is true, then the updated_at field will be set to the current time
|
||||
// for the applicable matching stash ID
|
||||
func (g sceneRelationships) stashIDs(ctx context.Context, setUpdateTime bool) ([]models.StashID, error) {
|
||||
updateTime := time.Now()
|
||||
|
||||
remoteSiteID := g.result.result.RemoteSiteID
|
||||
fieldStrategy := g.fieldOptions["stash_ids"]
|
||||
target := g.scene
|
||||
@@ -199,7 +206,7 @@ func (g sceneRelationships) stashIDs(ctx context.Context) ([]models.StashID, err
|
||||
strategy = fieldStrategy.Strategy
|
||||
}
|
||||
|
||||
var stashIDs []models.StashID
|
||||
var stashIDs models.StashIDs
|
||||
originalStashIDs := target.StashIDs.List()
|
||||
|
||||
if strategy == FieldStrategyMerge {
|
||||
@@ -208,15 +215,17 @@ func (g sceneRelationships) stashIDs(ctx context.Context) ([]models.StashID, err
|
||||
stashIDs = append(stashIDs, originalStashIDs...)
|
||||
}
|
||||
|
||||
// find and update the stash id if it exists
|
||||
for i, stashID := range stashIDs {
|
||||
if endpoint == stashID.Endpoint {
|
||||
// if stashID is the same, then don't set
|
||||
if stashID.StashID == *remoteSiteID {
|
||||
if !setUpdateTime && stashID.StashID == *remoteSiteID {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// replace the stash id and return
|
||||
stashID.StashID = *remoteSiteID
|
||||
stashID.UpdatedAt = updateTime
|
||||
stashIDs[i] = stashID
|
||||
return stashIDs, nil
|
||||
}
|
||||
@@ -224,11 +233,14 @@ func (g sceneRelationships) stashIDs(ctx context.Context) ([]models.StashID, err
|
||||
|
||||
// not found, create new entry
|
||||
stashIDs = append(stashIDs, models.StashID{
|
||||
StashID: *remoteSiteID,
|
||||
Endpoint: endpoint,
|
||||
StashID: *remoteSiteID,
|
||||
Endpoint: endpoint,
|
||||
UpdatedAt: updateTime,
|
||||
})
|
||||
|
||||
if sliceutil.SliceSame(originalStashIDs, stashIDs) {
|
||||
// don't return if nothing was changed
|
||||
// if we're setting update time, then we always return
|
||||
if !setUpdateTime && stashIDs.HasSameStashIDs(originalStashIDs) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/models/mocks"
|
||||
@@ -548,8 +549,9 @@ func Test_sceneRelationships_stashIDs(t *testing.T) {
|
||||
ID: sceneWithStashID,
|
||||
StashIDs: models.NewRelatedStashIDs([]models.StashID{
|
||||
{
|
||||
StashID: remoteSiteID,
|
||||
Endpoint: existingEndpoint,
|
||||
StashID: remoteSiteID,
|
||||
Endpoint: existingEndpoint,
|
||||
UpdatedAt: time.Time{},
|
||||
},
|
||||
}),
|
||||
}
|
||||
@@ -561,14 +563,17 @@ func Test_sceneRelationships_stashIDs(t *testing.T) {
|
||||
fieldOptions: make(map[string]*FieldOptions),
|
||||
}
|
||||
|
||||
setTime := time.Now()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
scene *models.Scene
|
||||
fieldOptions *FieldOptions
|
||||
endpoint string
|
||||
remoteSiteID *string
|
||||
want []models.StashID
|
||||
wantErr bool
|
||||
name string
|
||||
scene *models.Scene
|
||||
fieldOptions *FieldOptions
|
||||
endpoint string
|
||||
remoteSiteID *string
|
||||
setUpdateTime bool
|
||||
want []models.StashID
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"ignore",
|
||||
@@ -578,6 +583,7 @@ func Test_sceneRelationships_stashIDs(t *testing.T) {
|
||||
},
|
||||
newEndpoint,
|
||||
&remoteSiteID,
|
||||
false,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
@@ -587,6 +593,7 @@ func Test_sceneRelationships_stashIDs(t *testing.T) {
|
||||
defaultOptions,
|
||||
"",
|
||||
&remoteSiteID,
|
||||
false,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
@@ -596,6 +603,7 @@ func Test_sceneRelationships_stashIDs(t *testing.T) {
|
||||
defaultOptions,
|
||||
newEndpoint,
|
||||
nil,
|
||||
false,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
@@ -605,19 +613,38 @@ func Test_sceneRelationships_stashIDs(t *testing.T) {
|
||||
defaultOptions,
|
||||
existingEndpoint,
|
||||
&remoteSiteID,
|
||||
false,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"merge existing set update time",
|
||||
sceneWithStashIDs,
|
||||
defaultOptions,
|
||||
existingEndpoint,
|
||||
&remoteSiteID,
|
||||
true,
|
||||
[]models.StashID{
|
||||
{
|
||||
StashID: remoteSiteID,
|
||||
Endpoint: existingEndpoint,
|
||||
UpdatedAt: setTime,
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"merge existing new value",
|
||||
sceneWithStashIDs,
|
||||
defaultOptions,
|
||||
existingEndpoint,
|
||||
&newRemoteSiteID,
|
||||
false,
|
||||
[]models.StashID{
|
||||
{
|
||||
StashID: newRemoteSiteID,
|
||||
Endpoint: existingEndpoint,
|
||||
StashID: newRemoteSiteID,
|
||||
Endpoint: existingEndpoint,
|
||||
UpdatedAt: setTime,
|
||||
},
|
||||
},
|
||||
false,
|
||||
@@ -628,14 +655,17 @@ func Test_sceneRelationships_stashIDs(t *testing.T) {
|
||||
defaultOptions,
|
||||
newEndpoint,
|
||||
&newRemoteSiteID,
|
||||
false,
|
||||
[]models.StashID{
|
||||
{
|
||||
StashID: remoteSiteID,
|
||||
Endpoint: existingEndpoint,
|
||||
StashID: remoteSiteID,
|
||||
Endpoint: existingEndpoint,
|
||||
UpdatedAt: time.Time{},
|
||||
},
|
||||
{
|
||||
StashID: newRemoteSiteID,
|
||||
Endpoint: newEndpoint,
|
||||
StashID: newRemoteSiteID,
|
||||
Endpoint: newEndpoint,
|
||||
UpdatedAt: setTime,
|
||||
},
|
||||
},
|
||||
false,
|
||||
@@ -648,10 +678,12 @@ func Test_sceneRelationships_stashIDs(t *testing.T) {
|
||||
},
|
||||
newEndpoint,
|
||||
&newRemoteSiteID,
|
||||
false,
|
||||
[]models.StashID{
|
||||
{
|
||||
StashID: newRemoteSiteID,
|
||||
Endpoint: newEndpoint,
|
||||
StashID: newRemoteSiteID,
|
||||
Endpoint: newEndpoint,
|
||||
UpdatedAt: setTime,
|
||||
},
|
||||
},
|
||||
false,
|
||||
@@ -664,9 +696,28 @@ func Test_sceneRelationships_stashIDs(t *testing.T) {
|
||||
},
|
||||
existingEndpoint,
|
||||
&remoteSiteID,
|
||||
false,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"overwrite same set update time",
|
||||
sceneWithStashIDs,
|
||||
&FieldOptions{
|
||||
Strategy: FieldStrategyOverwrite,
|
||||
},
|
||||
existingEndpoint,
|
||||
&remoteSiteID,
|
||||
true,
|
||||
[]models.StashID{
|
||||
{
|
||||
StashID: remoteSiteID,
|
||||
Endpoint: existingEndpoint,
|
||||
UpdatedAt: setTime,
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -681,11 +732,20 @@ func Test_sceneRelationships_stashIDs(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
got, err := tr.stashIDs(testCtx)
|
||||
got, err := tr.stashIDs(testCtx, tt.setUpdateTime)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("sceneRelationships.stashIDs() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
// massage updatedAt times to be consistent for comparison
|
||||
for i := range got {
|
||||
if !got[i].UpdatedAt.IsZero() {
|
||||
got[i].UpdatedAt = setTime
|
||||
}
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("sceneRelationships.stashIDs() = %+v, want %+v", got, tt.want)
|
||||
}
|
||||
|
||||
@@ -1533,7 +1533,7 @@ func (i *Config) GetDefaultGenerateSettings() *models.GenerateMetadataOptions {
|
||||
}
|
||||
|
||||
// GetDangerousAllowPublicWithoutAuth determines if the security feature is enabled.
|
||||
// See https://docs.stashapp.cc/networking/authentication-required-when-accessing-stash-from-the-internet
|
||||
// See https://docs.stashapp.cc/faq/setup/#protecting-against-accidental-exposure-to-the-internet
|
||||
func (i *Config) GetDangerousAllowPublicWithoutAuth() bool {
|
||||
return i.getBool(dangerousAllowPublicWithoutAuth)
|
||||
}
|
||||
|
||||
@@ -42,3 +42,7 @@ func (jp *jsonUtils) saveGallery(fn string, gallery *jsonschema.Gallery) error {
|
||||
func (jp *jsonUtils) saveFile(fn string, file jsonschema.DirEntry) error {
|
||||
return jsonschema.SaveFileFile(filepath.Join(jp.json.Files, fn), file)
|
||||
}
|
||||
|
||||
func (jp *jsonUtils) saveSavedFilter(fn string, savedFilter *jsonschema.SavedFilter) error {
|
||||
return jsonschema.SaveSavedFilterFile(filepath.Join(jp.json.SavedFilters, fn), savedFilter)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models/jsonschema"
|
||||
"github.com/stashapp/stash/pkg/models/paths"
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
"github.com/stashapp/stash/pkg/savedfilter"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
@@ -176,6 +177,7 @@ func (t *ExportTask) Start(ctx context.Context, wg *sync.WaitGroup) {
|
||||
t.ExportPerformers(ctx, workerCount)
|
||||
t.ExportStudios(ctx, workerCount)
|
||||
t.ExportTags(ctx, workerCount)
|
||||
t.ExportSavedFilters(ctx, workerCount)
|
||||
|
||||
return nil
|
||||
})
|
||||
@@ -1186,3 +1188,62 @@ func (t *ExportTask) exportGroup(ctx context.Context, wg *sync.WaitGroup, jobCha
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ExportTask) ExportSavedFilters(ctx context.Context, workers int) {
|
||||
// don't export saved filters unless we're doing a full export
|
||||
if !t.full {
|
||||
return
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
reader := t.repository.SavedFilter
|
||||
var filters []*models.SavedFilter
|
||||
var err error
|
||||
filters, err = reader.All(ctx)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("[saved filters] failed to fetch saved filters: %v", err)
|
||||
}
|
||||
|
||||
logger.Info("[saved filters] exporting")
|
||||
startTime := time.Now()
|
||||
|
||||
jobCh := make(chan *models.SavedFilter, workers*2) // make a buffered channel to feed workers
|
||||
|
||||
for w := 0; w < workers; w++ { // create export Saved Filter workers
|
||||
wg.Add(1)
|
||||
go t.exportSavedFilter(ctx, &wg, jobCh)
|
||||
}
|
||||
|
||||
for i, savedFilter := range filters {
|
||||
index := i + 1
|
||||
logger.Progressf("[saved filters] %d of %d", index, len(filters))
|
||||
|
||||
jobCh <- savedFilter // feed workers
|
||||
}
|
||||
|
||||
close(jobCh)
|
||||
wg.Wait()
|
||||
|
||||
logger.Infof("[saved filters] export complete in %s. %d workers used.", time.Since(startTime), workers)
|
||||
}
|
||||
|
||||
func (t *ExportTask) exportSavedFilter(ctx context.Context, wg *sync.WaitGroup, jobChan <-chan *models.SavedFilter) {
|
||||
defer wg.Done()
|
||||
|
||||
for thisFilter := range jobChan {
|
||||
newJSON, err := savedfilter.ToJSON(ctx, thisFilter)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("[saved filter] <%s> error getting saved filter JSON: %v", thisFilter.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
fn := newJSON.Filename()
|
||||
|
||||
if err := t.json.saveSavedFilter(fn, newJSON); err != nil {
|
||||
logger.Errorf("[saved filter] <%s> failed to save json: %v", fn, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func (t *GenerateInteractiveHeatmapSpeedTask) Start(ctx context.Context) {
|
||||
err := generator.Generate(funscriptPath, heatmapPath, t.Scene.Files.Primary().Duration)
|
||||
|
||||
if err != nil {
|
||||
logger.Errorf("error generating heatmap: %s", err.Error())
|
||||
logger.Errorf("error generating heatmap for %s: %s", t.Scene.Path, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -46,8 +46,16 @@ func (t *GenerateInteractiveHeatmapSpeedTask) Start(ctx context.Context) {
|
||||
if err := r.WithTxn(ctx, func(ctx context.Context) error {
|
||||
primaryFile := t.Scene.Files.Primary()
|
||||
primaryFile.InteractiveSpeed = &median
|
||||
qb := r.File
|
||||
return qb.Update(ctx, primaryFile)
|
||||
if err := r.File.Update(ctx, primaryFile); err != nil {
|
||||
return fmt.Errorf("updating interactive speed for %s: %w", primaryFile.Path, err)
|
||||
}
|
||||
|
||||
// update the scene UpdatedAt field
|
||||
// NewScenePartial sets the UpdatedAt field to the current time
|
||||
if _, err := r.Scene.UpdatePartial(ctx, t.Scene.ID, models.NewScenePartial()); err != nil {
|
||||
return fmt.Errorf("updating UpdatedAt field for scene %d: %w", t.Scene.ID, err)
|
||||
}
|
||||
return nil
|
||||
}); err != nil && ctx.Err() == nil {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/stashapp/stash/pkg/models/jsonschema"
|
||||
"github.com/stashapp/stash/pkg/models/paths"
|
||||
"github.com/stashapp/stash/pkg/performer"
|
||||
"github.com/stashapp/stash/pkg/savedfilter"
|
||||
"github.com/stashapp/stash/pkg/scene"
|
||||
"github.com/stashapp/stash/pkg/studio"
|
||||
"github.com/stashapp/stash/pkg/tag"
|
||||
@@ -124,6 +125,7 @@ func (t *ImportTask) Start(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
t.ImportSavedFilters(ctx)
|
||||
t.ImportTags(ctx)
|
||||
t.ImportPerformers(ctx)
|
||||
t.ImportStudios(ctx)
|
||||
@@ -779,3 +781,53 @@ func (t *ImportTask) ImportImages(ctx context.Context) {
|
||||
|
||||
logger.Info("[images] import complete")
|
||||
}
|
||||
|
||||
func (t *ImportTask) ImportSavedFilters(ctx context.Context) {
|
||||
logger.Info("[saved filters] importing")
|
||||
|
||||
path := t.json.json.SavedFilters
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
logger.Errorf("[saved filters] failed to read saved filters directory: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
r := t.repository
|
||||
|
||||
for i, fi := range files {
|
||||
index := i + 1
|
||||
savedFilterJSON, err := jsonschema.LoadSavedFilterFile(filepath.Join(path, fi.Name()))
|
||||
if err != nil {
|
||||
logger.Errorf("[saved filters] failed to read json: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Progressf("[saved filters] %d of %d", index, len(files))
|
||||
|
||||
if err := r.WithTxn(ctx, func(ctx context.Context) error {
|
||||
return t.importSavedFilter(ctx, savedFilterJSON)
|
||||
}); err != nil {
|
||||
logger.Errorf("[saved filters] <%s> failed to import: %v", fi.Name(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("[saved filters] import complete")
|
||||
}
|
||||
|
||||
func (t *ImportTask) importSavedFilter(ctx context.Context, savedFilterJSON *jsonschema.SavedFilter) error {
|
||||
importer := &savedfilter.Importer{
|
||||
ReaderWriter: t.repository.SavedFilter,
|
||||
Input: *savedFilterJSON,
|
||||
MissingRefBehaviour: t.MissingRefBehaviour,
|
||||
}
|
||||
|
||||
if err := performImport(ctx, importer, t.DuplicateBehaviour); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ type handlerRequiredFilter struct {
|
||||
GalleryFinder galleryFinder
|
||||
CaptionUpdater video.CaptionUpdater
|
||||
|
||||
FolderCache *lru.LRU
|
||||
FolderCache *lru.LRU[bool]
|
||||
|
||||
videoFileNamingAlgorithm models.HashAlgorithm
|
||||
}
|
||||
@@ -138,7 +138,7 @@ func newHandlerRequiredFilter(c *config.Config, repo models.Repository) *handler
|
||||
ImageFinder: repo.Image,
|
||||
GalleryFinder: repo.Gallery,
|
||||
CaptionUpdater: repo.File,
|
||||
FolderCache: lru.New(processes * 2),
|
||||
FolderCache: lru.New[bool](processes * 2),
|
||||
videoFileNamingAlgorithm: c.GetVideoFileNamingAlgorithm(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ func (t *StashBoxBatchTagTask) processMatchedPerformer(ctx context.Context, p *m
|
||||
return err
|
||||
}
|
||||
|
||||
if err := qb.Create(ctx, newPerformer); err != nil {
|
||||
if err := qb.Create(ctx, &models.CreatePerformerInput{Performer: newPerformer}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -1100,7 +1100,8 @@ func (s *scanJob) removeOutdatedFingerprints(existing models.File, fp models.Fin
|
||||
|
||||
// oshash has changed, MD5 is missing - remove MD5 from the existing fingerprints
|
||||
logger.Infof("Removing outdated checksum from %s", existing.Base().Path)
|
||||
existing.Base().Fingerprints.Remove(models.FingerprintTypeMD5)
|
||||
b := existing.Base()
|
||||
b.Fingerprints = b.Fingerprints.Remove(models.FingerprintTypeMD5)
|
||||
}
|
||||
|
||||
// returns a file only if it was updated
|
||||
|
||||
@@ -3,6 +3,7 @@ package gallery
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -153,7 +154,7 @@ func (i *Importer) populatePerformers(ctx context.Context) error {
|
||||
}
|
||||
|
||||
missingPerformers := sliceutil.Filter(names, func(name string) bool {
|
||||
return !sliceutil.Contains(pluckedNames, name)
|
||||
return !slices.Contains(pluckedNames, name)
|
||||
})
|
||||
|
||||
if len(missingPerformers) > 0 {
|
||||
@@ -187,7 +188,9 @@ func (i *Importer) createPerformers(ctx context.Context, names []string) ([]*mod
|
||||
newPerformer := models.NewPerformer()
|
||||
newPerformer.Name = name
|
||||
|
||||
err := i.PerformerWriter.Create(ctx, &newPerformer)
|
||||
err := i.PerformerWriter.Create(ctx, &models.CreatePerformerInput{
|
||||
Performer: &newPerformer,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -212,7 +215,7 @@ func (i *Importer) populateTags(ctx context.Context) error {
|
||||
}
|
||||
|
||||
missingTags := sliceutil.Filter(names, func(name string) bool {
|
||||
return !sliceutil.Contains(pluckedNames, name)
|
||||
return !slices.Contains(pluckedNames, name)
|
||||
})
|
||||
|
||||
if len(missingTags) > 0 {
|
||||
|
||||
@@ -201,8 +201,8 @@ func TestImporterPreImportWithMissingPerformer(t *testing.T) {
|
||||
}
|
||||
|
||||
db.Performer.On("FindByNames", testCtx, []string{missingPerformerName}, false).Return(nil, nil).Times(3)
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.Performer")).Run(func(args mock.Arguments) {
|
||||
performer := args.Get(1).(*models.Performer)
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.CreatePerformerInput")).Run(func(args mock.Arguments) {
|
||||
performer := args.Get(1).(*models.CreatePerformerInput)
|
||||
performer.ID = existingPerformerID
|
||||
}).Return(nil)
|
||||
|
||||
@@ -235,7 +235,7 @@ func TestImporterPreImportWithMissingPerformerCreateErr(t *testing.T) {
|
||||
}
|
||||
|
||||
db.Performer.On("FindByNames", testCtx, []string{missingPerformerName}, false).Return(nil, nil).Once()
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.Performer")).Return(errors.New("Create error"))
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.CreatePerformerInput")).Return(errors.New("Create error"))
|
||||
|
||||
err := i.PreImport(testCtx)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
@@ -3,6 +3,7 @@ package group
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -96,7 +97,7 @@ func importTags(ctx context.Context, tagWriter models.TagFinderCreator, names []
|
||||
}
|
||||
|
||||
missingTags := sliceutil.Filter(names, func(name string) bool {
|
||||
return !sliceutil.Contains(pluckedNames, name)
|
||||
return !slices.Contains(pluckedNames, name)
|
||||
})
|
||||
|
||||
if len(missingTags) > 0 {
|
||||
|
||||
@@ -2,6 +2,7 @@ package group
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -105,7 +106,7 @@ func (s *Service) validateUpdateGroupHierarchy(ctx context.Context, existing *mo
|
||||
subIDs := idsFromGroupDescriptions(effectiveSubGroups)
|
||||
|
||||
// ensure we haven't set the group as a subgroup of itself
|
||||
if sliceutil.Contains(containingIDs, existing.ID) || sliceutil.Contains(subIDs, existing.ID) {
|
||||
if slices.Contains(containingIDs, existing.ID) || slices.Contains(subIDs, existing.ID) {
|
||||
return ErrHierarchyLoop
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package image
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
@@ -239,7 +240,7 @@ func (i *Importer) populatePerformers(ctx context.Context) error {
|
||||
}
|
||||
|
||||
missingPerformers := sliceutil.Filter(names, func(name string) bool {
|
||||
return !sliceutil.Contains(pluckedNames, name)
|
||||
return !slices.Contains(pluckedNames, name)
|
||||
})
|
||||
|
||||
if len(missingPerformers) > 0 {
|
||||
@@ -273,7 +274,9 @@ func (i *Importer) createPerformers(ctx context.Context, names []string) ([]*mod
|
||||
newPerformer := models.NewPerformer()
|
||||
newPerformer.Name = name
|
||||
|
||||
err := i.PerformerWriter.Create(ctx, &newPerformer)
|
||||
err := i.PerformerWriter.Create(ctx, &models.CreatePerformerInput{
|
||||
Performer: &newPerformer,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -375,7 +378,7 @@ func importTags(ctx context.Context, tagWriter models.TagFinderCreator, names []
|
||||
}
|
||||
|
||||
missingTags := sliceutil.Filter(names, func(name string) bool {
|
||||
return !sliceutil.Contains(pluckedNames, name)
|
||||
return !slices.Contains(pluckedNames, name)
|
||||
})
|
||||
|
||||
if len(missingTags) > 0 {
|
||||
|
||||
@@ -163,8 +163,8 @@ func TestImporterPreImportWithMissingPerformer(t *testing.T) {
|
||||
}
|
||||
|
||||
db.Performer.On("FindByNames", testCtx, []string{missingPerformerName}, false).Return(nil, nil).Times(3)
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.Performer")).Run(func(args mock.Arguments) {
|
||||
performer := args.Get(1).(*models.Performer)
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.CreatePerformerInput")).Run(func(args mock.Arguments) {
|
||||
performer := args.Get(1).(*models.CreatePerformerInput)
|
||||
performer.ID = existingPerformerID
|
||||
}).Return(nil)
|
||||
|
||||
@@ -197,7 +197,7 @@ func TestImporterPreImportWithMissingPerformerCreateErr(t *testing.T) {
|
||||
}
|
||||
|
||||
db.Performer.On("FindByNames", testCtx, []string{missingPerformerName}, false).Return(nil, nil).Once()
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.Performer")).Return(errors.New("Create error"))
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.CreatePerformerInput")).Return(errors.New("Create error"))
|
||||
|
||||
err := i.PreImport(testCtx)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
@@ -6,13 +6,13 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/models/paths"
|
||||
"github.com/stashapp/stash/pkg/plugin"
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
)
|
||||
|
||||
@@ -356,7 +356,7 @@ func (h *ScanHandler) getGalleryToAssociate(ctx context.Context, newImage *model
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if g != nil && !sliceutil.Contains(newImage.GalleryIDs.List(), g.ID) {
|
||||
if g != nil && !slices.Contains(newImage.GalleryIDs.List(), g.ID) {
|
||||
return g, nil
|
||||
}
|
||||
|
||||
|
||||
17
pkg/models/custom_fields.go
Normal file
17
pkg/models/custom_fields.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package models
|
||||
|
||||
import "context"
|
||||
|
||||
type CustomFieldMap map[string]interface{}
|
||||
|
||||
type CustomFieldsInput struct {
|
||||
// If populated, the entire custom fields map will be replaced with this value
|
||||
Full map[string]interface{} `json:"full"`
|
||||
// If populated, only the keys in this map will be updated
|
||||
Partial map[string]interface{} `json:"partial"`
|
||||
}
|
||||
|
||||
type CustomFieldsReader interface {
|
||||
GetCustomFields(ctx context.Context, id int) (map[string]interface{}, error)
|
||||
GetCustomFieldsBulk(ctx context.Context, ids []int) ([]CustomFieldMap, error)
|
||||
}
|
||||
@@ -194,3 +194,9 @@ type PhashDistanceCriterionInput struct {
|
||||
type OrientationCriterionInput struct {
|
||||
Value []OrientationEnum `json:"value"`
|
||||
}
|
||||
|
||||
type CustomFieldCriterionInput struct {
|
||||
Field string `json:"field"`
|
||||
Value []any `json:"value"`
|
||||
Modifier CriterionModifier `json:"modifier"`
|
||||
}
|
||||
|
||||
@@ -28,16 +28,31 @@ func (f *Fingerprint) Value() string {
|
||||
|
||||
type Fingerprints []Fingerprint
|
||||
|
||||
func (f *Fingerprints) Remove(type_ string) {
|
||||
func (f Fingerprints) Remove(type_ string) Fingerprints {
|
||||
var ret Fingerprints
|
||||
|
||||
for _, ff := range *f {
|
||||
for _, ff := range f {
|
||||
if ff.Type != type_ {
|
||||
ret = append(ret, ff)
|
||||
}
|
||||
}
|
||||
|
||||
*f = ret
|
||||
return ret
|
||||
}
|
||||
|
||||
func (f Fingerprints) Filter(types ...string) Fingerprints {
|
||||
var ret Fingerprints
|
||||
|
||||
for _, ff := range f {
|
||||
for _, t := range types {
|
||||
if ff.Type == t {
|
||||
ret = append(ret, ff)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// Equals returns true if the contents of this slice are equal to those in the other slice.
|
||||
|
||||
31
pkg/models/jsonschema/load.go
Normal file
31
pkg/models/jsonschema/load.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
func loadFile[T any](filePath string) (*T, error) {
|
||||
var ret T
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
var json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
jsonParser := json.NewDecoder(file)
|
||||
err = jsonParser.Decode(&ret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func saveFile[T any](filePath string, obj *T) error {
|
||||
if obj == nil {
|
||||
return fmt.Errorf("object must not be nil")
|
||||
}
|
||||
return marshalToFile(filePath, obj)
|
||||
}
|
||||
@@ -65,6 +65,8 @@ type Performer struct {
|
||||
StashIDs []models.StashID `json:"stash_ids,omitempty"`
|
||||
IgnoreAutoTag bool `json:"ignore_auto_tag,omitempty"`
|
||||
|
||||
CustomFields map[string]interface{} `json:"custom_fields,omitempty"`
|
||||
|
||||
// deprecated - for import only
|
||||
URL string `json:"url,omitempty"`
|
||||
Twitter string `json:"twitter,omitempty"`
|
||||
|
||||
27
pkg/models/jsonschema/saved_filter.go
Normal file
27
pkg/models/jsonschema/saved_filter.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package jsonschema
|
||||
|
||||
import (
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
type SavedFilter struct {
|
||||
Mode models.FilterMode `db:"mode" json:"mode"`
|
||||
Name string `db:"name" json:"name"`
|
||||
FindFilter *models.FindFilterType `json:"find_filter"`
|
||||
ObjectFilter map[string]interface{} `json:"object_filter"`
|
||||
UIOptions map[string]interface{} `json:"ui_options"`
|
||||
}
|
||||
|
||||
func (s SavedFilter) Filename() string {
|
||||
ret := fsutil.SanitiseBasename(s.Name + "_" + s.Mode.String())
|
||||
return ret + ".json"
|
||||
}
|
||||
|
||||
func LoadSavedFilterFile(filePath string) (*SavedFilter, error) {
|
||||
return loadFile[SavedFilter](filePath)
|
||||
}
|
||||
|
||||
func SaveSavedFilterFile(filePath string, image *SavedFilter) error {
|
||||
return saveFile[SavedFilter](filePath, image)
|
||||
}
|
||||
@@ -80,11 +80,11 @@ func (_m *PerformerReaderWriter) CountByTagID(ctx context.Context, tagID int) (i
|
||||
}
|
||||
|
||||
// Create provides a mock function with given fields: ctx, newPerformer
|
||||
func (_m *PerformerReaderWriter) Create(ctx context.Context, newPerformer *models.Performer) error {
|
||||
func (_m *PerformerReaderWriter) Create(ctx context.Context, newPerformer *models.CreatePerformerInput) error {
|
||||
ret := _m.Called(ctx, newPerformer)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.Performer) error); ok {
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.CreatePerformerInput) error); ok {
|
||||
r0 = rf(ctx, newPerformer)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
@@ -314,6 +314,52 @@ func (_m *PerformerReaderWriter) GetAliases(ctx context.Context, relatedID int)
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetCustomFields provides a mock function with given fields: ctx, id
|
||||
func (_m *PerformerReaderWriter) GetCustomFields(ctx context.Context, id int) (map[string]interface{}, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
var r0 map[string]interface{}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int) map[string]interface{}); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[string]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// GetCustomFieldsBulk provides a mock function with given fields: ctx, ids
|
||||
func (_m *PerformerReaderWriter) GetCustomFieldsBulk(ctx context.Context, ids []int) ([]models.CustomFieldMap, error) {
|
||||
ret := _m.Called(ctx, ids)
|
||||
|
||||
var r0 []models.CustomFieldMap
|
||||
if rf, ok := ret.Get(0).(func(context.Context, []int) []models.CustomFieldMap); ok {
|
||||
r0 = rf(ctx, ids)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]models.CustomFieldMap)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, []int) error); ok {
|
||||
r1 = rf(ctx, ids)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetImage provides a mock function with given fields: ctx, performerID
|
||||
func (_m *PerformerReaderWriter) GetImage(ctx context.Context, performerID int) ([]byte, error) {
|
||||
ret := _m.Called(ctx, performerID)
|
||||
@@ -502,11 +548,11 @@ func (_m *PerformerReaderWriter) QueryForAutoTag(ctx context.Context, words []st
|
||||
}
|
||||
|
||||
// Update provides a mock function with given fields: ctx, updatedPerformer
|
||||
func (_m *PerformerReaderWriter) Update(ctx context.Context, updatedPerformer *models.Performer) error {
|
||||
func (_m *PerformerReaderWriter) Update(ctx context.Context, updatedPerformer *models.UpdatePerformerInput) error {
|
||||
ret := _m.Called(ctx, updatedPerformer)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.Performer) error); ok {
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *models.UpdatePerformerInput) error); ok {
|
||||
r0 = rf(ctx, updatedPerformer)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
|
||||
@@ -39,6 +39,18 @@ type Performer struct {
|
||||
StashIDs RelatedStashIDs `json:"stash_ids"`
|
||||
}
|
||||
|
||||
type CreatePerformerInput struct {
|
||||
*Performer
|
||||
|
||||
CustomFields map[string]interface{} `json:"custom_fields"`
|
||||
}
|
||||
|
||||
type UpdatePerformerInput struct {
|
||||
*Performer
|
||||
|
||||
CustomFields CustomFieldsInput `json:"custom_fields"`
|
||||
}
|
||||
|
||||
func NewPerformer() Performer {
|
||||
currentTime := time.Now()
|
||||
return Performer{
|
||||
@@ -80,6 +92,8 @@ type PerformerPartial struct {
|
||||
Aliases *UpdateStrings
|
||||
TagIDs *UpdateIDs
|
||||
StashIDs *UpdateStashIDs
|
||||
|
||||
CustomFields CustomFieldsInput
|
||||
}
|
||||
|
||||
func NewPerformerPartial() PerformerPartial {
|
||||
|
||||
@@ -192,9 +192,9 @@ func (s ScenePartial) UpdateInput(id int) SceneUpdateInput {
|
||||
dateStr = &v
|
||||
}
|
||||
|
||||
var stashIDs []StashID
|
||||
var stashIDs StashIDs
|
||||
if s.StashIDs != nil {
|
||||
stashIDs = s.StashIDs.StashIDs
|
||||
stashIDs = StashIDs(s.StashIDs.StashIDs)
|
||||
}
|
||||
|
||||
ret := SceneUpdateInput{
|
||||
@@ -212,7 +212,7 @@ func (s ScenePartial) UpdateInput(id int) SceneUpdateInput {
|
||||
PerformerIds: s.PerformerIDs.IDStrings(),
|
||||
Movies: s.GroupIDs.SceneMovieInputs(),
|
||||
TagIds: s.TagIDs.IDStrings(),
|
||||
StashIds: stashIDs,
|
||||
StashIds: stashIDs.ToStashIDInputs(),
|
||||
}
|
||||
|
||||
return ret
|
||||
|
||||
@@ -8,6 +8,7 @@ type SceneMarker struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Seconds float64 `json:"seconds"`
|
||||
EndSeconds *float64 `json:"end_seconds"`
|
||||
PrimaryTagID int `json:"primary_tag_id"`
|
||||
SceneID int `json:"scene_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
@@ -27,6 +28,7 @@ func NewSceneMarker() SceneMarker {
|
||||
type SceneMarkerPartial struct {
|
||||
Title OptionalString
|
||||
Seconds OptionalFloat64
|
||||
EndSeconds OptionalFloat64
|
||||
PrimaryTagID OptionalInt
|
||||
SceneID OptionalInt
|
||||
CreatedAt OptionalTime
|
||||
|
||||
@@ -3,6 +3,7 @@ package models
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
@@ -29,8 +30,9 @@ func (s *ScrapedStudio) ToStudio(endpoint string, excluded map[string]bool) *Stu
|
||||
if s.RemoteSiteID != nil && endpoint != "" {
|
||||
ret.StashIDs = NewRelatedStashIDs([]StashID{
|
||||
{
|
||||
Endpoint: endpoint,
|
||||
StashID: *s.RemoteSiteID,
|
||||
Endpoint: endpoint,
|
||||
StashID: *s.RemoteSiteID,
|
||||
UpdatedAt: time.Now(),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -65,6 +67,7 @@ func (s *ScrapedStudio) GetImage(ctx context.Context, excluded map[string]bool)
|
||||
func (s *ScrapedStudio) ToPartial(id string, endpoint string, excluded map[string]bool, existingStashIDs []StashID) StudioPartial {
|
||||
ret := NewStudioPartial()
|
||||
ret.ID, _ = strconv.Atoi(id)
|
||||
currentTime := time.Now()
|
||||
|
||||
if s.Name != "" && !excluded["name"] {
|
||||
ret.Name = NewOptionalString(s.Name)
|
||||
@@ -90,8 +93,9 @@ func (s *ScrapedStudio) ToPartial(id string, endpoint string, excluded map[strin
|
||||
Mode: RelationshipUpdateModeSet,
|
||||
}
|
||||
ret.StashIDs.Set(StashID{
|
||||
Endpoint: endpoint,
|
||||
StashID: *s.RemoteSiteID,
|
||||
Endpoint: endpoint,
|
||||
StashID: *s.RemoteSiteID,
|
||||
UpdatedAt: currentTime,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -137,6 +141,7 @@ func (ScrapedPerformer) IsScrapedContent() {}
|
||||
|
||||
func (p *ScrapedPerformer) ToPerformer(endpoint string, excluded map[string]bool) *Performer {
|
||||
ret := NewPerformer()
|
||||
currentTime := time.Now()
|
||||
ret.Name = *p.Name
|
||||
|
||||
if p.Aliases != nil && !excluded["aliases"] {
|
||||
@@ -244,8 +249,9 @@ func (p *ScrapedPerformer) ToPerformer(endpoint string, excluded map[string]bool
|
||||
if p.RemoteSiteID != nil && endpoint != "" {
|
||||
ret.StashIDs = NewRelatedStashIDs([]StashID{
|
||||
{
|
||||
Endpoint: endpoint,
|
||||
StashID: *p.RemoteSiteID,
|
||||
Endpoint: endpoint,
|
||||
StashID: *p.RemoteSiteID,
|
||||
UpdatedAt: currentTime,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -375,8 +381,9 @@ func (p *ScrapedPerformer) ToPartial(endpoint string, excluded map[string]bool,
|
||||
Mode: RelationshipUpdateModeSet,
|
||||
}
|
||||
ret.StashIDs.Set(StashID{
|
||||
Endpoint: endpoint,
|
||||
StashID: *p.RemoteSiteID,
|
||||
Endpoint: endpoint,
|
||||
StashID: *p.RemoteSiteID,
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,11 @@ func Test_scrapedToStudioInput(t *testing.T) {
|
||||
|
||||
got.CreatedAt = time.Time{}
|
||||
got.UpdatedAt = time.Time{}
|
||||
if got.StashIDs.Loaded() && len(got.StashIDs.List()) > 0 {
|
||||
for stid := range got.StashIDs.List() {
|
||||
got.StashIDs.List()[stid].UpdatedAt = time.Time{}
|
||||
}
|
||||
}
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
@@ -243,6 +248,12 @@ func Test_scrapedToPerformerInput(t *testing.T) {
|
||||
|
||||
got.CreatedAt = time.Time{}
|
||||
got.UpdatedAt = time.Time{}
|
||||
|
||||
if got.StashIDs.Loaded() && len(got.StashIDs.List()) > 0 {
|
||||
for stid := range got.StashIDs.List() {
|
||||
got.StashIDs.List()[stid].UpdatedAt = time.Time{}
|
||||
}
|
||||
}
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
@@ -263,7 +274,7 @@ func TestScrapedStudio_ToPartial(t *testing.T) {
|
||||
images = []string{image}
|
||||
|
||||
existingEndpoint = "existingEndpoint"
|
||||
existingStashID = StashID{"existingStashID", existingEndpoint}
|
||||
existingStashID = StashID{"existingStashID", existingEndpoint, time.Time{}}
|
||||
existingStashIDs = []StashID{existingStashID}
|
||||
)
|
||||
|
||||
@@ -362,6 +373,11 @@ func TestScrapedStudio_ToPartial(t *testing.T) {
|
||||
|
||||
// unset updatedAt - we don't need to compare it
|
||||
got.UpdatedAt = OptionalTime{}
|
||||
if got.StashIDs != nil && len(got.StashIDs.StashIDs) > 0 {
|
||||
for stid := range got.StashIDs.StashIDs {
|
||||
got.StashIDs.StashIDs[stid].UpdatedAt = time.Time{}
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
|
||||
@@ -12,14 +12,15 @@ type JSONPaths struct {
|
||||
|
||||
ScrapedFile string
|
||||
|
||||
Performers string
|
||||
Scenes string
|
||||
Images string
|
||||
Galleries string
|
||||
Studios string
|
||||
Tags string
|
||||
Groups string
|
||||
Files string
|
||||
Performers string
|
||||
Scenes string
|
||||
Images string
|
||||
Galleries string
|
||||
Studios string
|
||||
Tags string
|
||||
Groups string
|
||||
Files string
|
||||
SavedFilters string
|
||||
}
|
||||
|
||||
func newJSONPaths(baseDir string) *JSONPaths {
|
||||
@@ -34,6 +35,7 @@ func newJSONPaths(baseDir string) *JSONPaths {
|
||||
jp.Groups = filepath.Join(baseDir, "movies")
|
||||
jp.Tags = filepath.Join(baseDir, "tags")
|
||||
jp.Files = filepath.Join(baseDir, "files")
|
||||
jp.SavedFilters = filepath.Join(baseDir, "saved_filters")
|
||||
return &jp
|
||||
}
|
||||
|
||||
@@ -52,6 +54,7 @@ func EmptyJSONDirs(baseDir string) {
|
||||
_ = fsutil.EmptyDir(jsonPaths.Groups)
|
||||
_ = fsutil.EmptyDir(jsonPaths.Tags)
|
||||
_ = fsutil.EmptyDir(jsonPaths.Files)
|
||||
_ = fsutil.EmptyDir(jsonPaths.SavedFilters)
|
||||
}
|
||||
|
||||
func EnsureJSONDirs(baseDir string) {
|
||||
@@ -83,4 +86,7 @@ func EnsureJSONDirs(baseDir string) {
|
||||
if err := fsutil.EnsureDir(jsonPaths.Files); err != nil {
|
||||
logger.Warnf("couldn't create directories for Files: %v", err)
|
||||
}
|
||||
if err := fsutil.EnsureDir(jsonPaths.SavedFilters); err != nil {
|
||||
logger.Warnf("couldn't create directories for Saved Filters: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +198,9 @@ type PerformerFilterType struct {
|
||||
CreatedAt *TimestampCriterionInput `json:"created_at"`
|
||||
// Filter by updated at
|
||||
UpdatedAt *TimestampCriterionInput `json:"updated_at"`
|
||||
|
||||
// Filter by custom fields
|
||||
CustomFields []CustomFieldCriterionInput `json:"custom_fields"`
|
||||
}
|
||||
|
||||
type PerformerCreateInput struct {
|
||||
@@ -226,14 +229,16 @@ type PerformerCreateInput struct {
|
||||
Favorite *bool `json:"favorite"`
|
||||
TagIds []string `json:"tag_ids"`
|
||||
// This should be a URL or a base64 encoded data URL
|
||||
Image *string `json:"image"`
|
||||
StashIds []StashID `json:"stash_ids"`
|
||||
Rating100 *int `json:"rating100"`
|
||||
Details *string `json:"details"`
|
||||
DeathDate *string `json:"death_date"`
|
||||
HairColor *string `json:"hair_color"`
|
||||
Weight *int `json:"weight"`
|
||||
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
|
||||
Image *string `json:"image"`
|
||||
StashIds []StashIDInput `json:"stash_ids"`
|
||||
Rating100 *int `json:"rating100"`
|
||||
Details *string `json:"details"`
|
||||
DeathDate *string `json:"death_date"`
|
||||
HairColor *string `json:"hair_color"`
|
||||
Weight *int `json:"weight"`
|
||||
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
|
||||
|
||||
CustomFields map[string]interface{} `json:"custom_fields"`
|
||||
}
|
||||
|
||||
type PerformerUpdateInput struct {
|
||||
@@ -263,12 +268,14 @@ type PerformerUpdateInput struct {
|
||||
Favorite *bool `json:"favorite"`
|
||||
TagIds []string `json:"tag_ids"`
|
||||
// This should be a URL or a base64 encoded data URL
|
||||
Image *string `json:"image"`
|
||||
StashIds []StashID `json:"stash_ids"`
|
||||
Rating100 *int `json:"rating100"`
|
||||
Details *string `json:"details"`
|
||||
DeathDate *string `json:"death_date"`
|
||||
HairColor *string `json:"hair_color"`
|
||||
Weight *int `json:"weight"`
|
||||
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
|
||||
Image *string `json:"image"`
|
||||
StashIds []StashIDInput `json:"stash_ids"`
|
||||
Rating100 *int `json:"rating100"`
|
||||
Details *string `json:"details"`
|
||||
DeathDate *string `json:"death_date"`
|
||||
HairColor *string `json:"hair_color"`
|
||||
Weight *int `json:"weight"`
|
||||
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
|
||||
|
||||
CustomFields CustomFieldsInput `json:"custom_fields"`
|
||||
}
|
||||
|
||||
@@ -43,12 +43,12 @@ type PerformerCounter interface {
|
||||
|
||||
// PerformerCreator provides methods to create performers.
|
||||
type PerformerCreator interface {
|
||||
Create(ctx context.Context, newPerformer *Performer) error
|
||||
Create(ctx context.Context, newPerformer *CreatePerformerInput) error
|
||||
}
|
||||
|
||||
// PerformerUpdater provides methods to update performers.
|
||||
type PerformerUpdater interface {
|
||||
Update(ctx context.Context, updatedPerformer *Performer) error
|
||||
Update(ctx context.Context, updatedPerformer *UpdatePerformerInput) error
|
||||
UpdatePartial(ctx context.Context, id int, updatedPerformer PerformerPartial) (*Performer, error)
|
||||
UpdateImage(ctx context.Context, performerID int, image []byte) error
|
||||
}
|
||||
@@ -80,6 +80,8 @@ type PerformerReader interface {
|
||||
TagIDLoader
|
||||
URLLoader
|
||||
|
||||
CustomFieldsReader
|
||||
|
||||
All(ctx context.Context) ([]*Performer, error)
|
||||
GetImage(ctx context.Context, performerID int) ([]byte, error)
|
||||
HasImage(ctx context.Context, performerID int) (bool, error)
|
||||
|
||||
@@ -163,8 +163,8 @@ type SceneCreateInput struct {
|
||||
Groups []SceneGroupInput `json:"groups"`
|
||||
TagIds []string `json:"tag_ids"`
|
||||
// This should be a URL or a base64 encoded data URL
|
||||
CoverImage *string `json:"cover_image"`
|
||||
StashIds []StashID `json:"stash_ids"`
|
||||
CoverImage *string `json:"cover_image"`
|
||||
StashIds []StashIDInput `json:"stash_ids"`
|
||||
// The first id will be assigned as primary.
|
||||
// Files will be reassigned from existing scenes if applicable.
|
||||
// Files must not already be primary for another scene.
|
||||
@@ -191,12 +191,12 @@ type SceneUpdateInput struct {
|
||||
Groups []SceneGroupInput `json:"groups"`
|
||||
TagIds []string `json:"tag_ids"`
|
||||
// This should be a URL or a base64 encoded data URL
|
||||
CoverImage *string `json:"cover_image"`
|
||||
StashIds []StashID `json:"stash_ids"`
|
||||
ResumeTime *float64 `json:"resume_time"`
|
||||
PlayDuration *float64 `json:"play_duration"`
|
||||
PlayCount *int `json:"play_count"`
|
||||
PrimaryFileID *string `json:"primary_file_id"`
|
||||
CoverImage *string `json:"cover_image"`
|
||||
StashIds []StashIDInput `json:"stash_ids"`
|
||||
ResumeTime *float64 `json:"resume_time"`
|
||||
PlayDuration *float64 `json:"play_duration"`
|
||||
PlayCount *int `json:"play_count"`
|
||||
PrimaryFileID *string `json:"primary_file_id"`
|
||||
}
|
||||
|
||||
type SceneDestroyInput struct {
|
||||
|
||||
@@ -11,6 +11,8 @@ type SceneMarkerFilterType struct {
|
||||
Performers *MultiCriterionInput `json:"performers"`
|
||||
// Filter to only include scene markers from these scenes
|
||||
Scenes *MultiCriterionInput `json:"scenes"`
|
||||
// Filter by duration (in seconds)
|
||||
Duration *FloatCriterionInput `json:"duration"`
|
||||
// Filter by created at
|
||||
CreatedAt *TimestampCriterionInput `json:"created_at"`
|
||||
// Filter by updated at
|
||||
|
||||
@@ -1,8 +1,89 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
type StashID struct {
|
||||
StashID string `db:"stash_id" json:"stash_id"`
|
||||
Endpoint string `db:"endpoint" json:"endpoint"`
|
||||
StashID string `db:"stash_id" json:"stash_id"`
|
||||
Endpoint string `db:"endpoint" json:"endpoint"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (s StashID) ToStashIDInput() StashIDInput {
|
||||
t := s.UpdatedAt
|
||||
return StashIDInput{
|
||||
StashID: s.StashID,
|
||||
Endpoint: s.Endpoint,
|
||||
UpdatedAt: &t,
|
||||
}
|
||||
}
|
||||
|
||||
type StashIDs []StashID
|
||||
|
||||
func (s StashIDs) ToStashIDInputs() StashIDInputs {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret := make(StashIDInputs, len(s))
|
||||
for i, v := range s {
|
||||
ret[i] = v.ToStashIDInput()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// HasSameStashIDs returns true if the two lists of StashIDs are the same, ignoring order and updated at time.
|
||||
func (s StashIDs) HasSameStashIDs(other StashIDs) bool {
|
||||
if len(s) != len(other) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, v := range s {
|
||||
if !slices.ContainsFunc(other, func(o StashID) bool {
|
||||
return o.StashID == v.StashID && o.Endpoint == v.Endpoint
|
||||
}) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type StashIDInput struct {
|
||||
StashID string `db:"stash_id" json:"stash_id"`
|
||||
Endpoint string `db:"endpoint" json:"endpoint"`
|
||||
UpdatedAt *time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
func (s StashIDInput) ToStashID() StashID {
|
||||
ret := StashID{
|
||||
StashID: s.StashID,
|
||||
Endpoint: s.Endpoint,
|
||||
}
|
||||
if s.UpdatedAt != nil {
|
||||
ret.UpdatedAt = *s.UpdatedAt
|
||||
} else {
|
||||
// default to now if not provided
|
||||
ret.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
type StashIDInputs []StashIDInput
|
||||
|
||||
func (s StashIDInputs) ToStashIDs() StashIDs {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret := make(StashIDs, len(s))
|
||||
for i, v := range s {
|
||||
ret[i] = v.ToStashID()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type UpdateStashIDs struct {
|
||||
|
||||
@@ -51,14 +51,14 @@ type StudioCreateInput struct {
|
||||
URL *string `json:"url"`
|
||||
ParentID *string `json:"parent_id"`
|
||||
// This should be a URL or a base64 encoded data URL
|
||||
Image *string `json:"image"`
|
||||
StashIds []StashID `json:"stash_ids"`
|
||||
Rating100 *int `json:"rating100"`
|
||||
Favorite *bool `json:"favorite"`
|
||||
Details *string `json:"details"`
|
||||
Aliases []string `json:"aliases"`
|
||||
TagIds []string `json:"tag_ids"`
|
||||
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
|
||||
Image *string `json:"image"`
|
||||
StashIds []StashIDInput `json:"stash_ids"`
|
||||
Rating100 *int `json:"rating100"`
|
||||
Favorite *bool `json:"favorite"`
|
||||
Details *string `json:"details"`
|
||||
Aliases []string `json:"aliases"`
|
||||
TagIds []string `json:"tag_ids"`
|
||||
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
|
||||
}
|
||||
|
||||
type StudioUpdateInput struct {
|
||||
@@ -67,12 +67,12 @@ type StudioUpdateInput struct {
|
||||
URL *string `json:"url"`
|
||||
ParentID *string `json:"parent_id"`
|
||||
// This should be a URL or a base64 encoded data URL
|
||||
Image *string `json:"image"`
|
||||
StashIds []StashID `json:"stash_ids"`
|
||||
Rating100 *int `json:"rating100"`
|
||||
Favorite *bool `json:"favorite"`
|
||||
Details *string `json:"details"`
|
||||
Aliases []string `json:"aliases"`
|
||||
TagIds []string `json:"tag_ids"`
|
||||
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
|
||||
Image *string `json:"image"`
|
||||
StashIds []StashIDInput `json:"stash_ids"`
|
||||
Rating100 *int `json:"rating100"`
|
||||
Favorite *bool `json:"favorite"`
|
||||
Details *string `json:"details"`
|
||||
Aliases []string `json:"aliases"`
|
||||
TagIds []string `json:"tag_ids"`
|
||||
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ type ImageAliasStashIDGetter interface {
|
||||
models.AliasLoader
|
||||
models.StashIDLoader
|
||||
models.URLLoader
|
||||
models.CustomFieldsReader
|
||||
}
|
||||
|
||||
// ToJSON converts a Performer object into its JSON equivalent.
|
||||
@@ -87,6 +88,12 @@ func ToJSON(ctx context.Context, reader ImageAliasStashIDGetter, performer *mode
|
||||
|
||||
newPerformerJSON.StashIDs = performer.StashIDs.List()
|
||||
|
||||
var err error
|
||||
newPerformerJSON.CustomFields, err = reader.GetCustomFields(ctx, performer.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting performer custom fields: %v", err)
|
||||
}
|
||||
|
||||
image, err := reader.GetImage(ctx, performer.ID)
|
||||
if err != nil {
|
||||
logger.Errorf("Error getting performer image: %v", err)
|
||||
|
||||
@@ -15,9 +15,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
performerID = 1
|
||||
noImageID = 2
|
||||
errImageID = 3
|
||||
performerID = 1
|
||||
noImageID = 2
|
||||
errImageID = 3
|
||||
customFieldsID = 4
|
||||
errCustomFieldsID = 5
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -50,6 +52,11 @@ var (
|
||||
penisLength = 1.23
|
||||
circumcisedEnum = models.CircumisedEnumCut
|
||||
circumcised = circumcisedEnum.String()
|
||||
|
||||
emptyCustomFields = make(map[string]interface{})
|
||||
customFields = map[string]interface{}{
|
||||
"customField1": "customValue1",
|
||||
}
|
||||
)
|
||||
|
||||
var imageBytes = []byte("imageBytes")
|
||||
@@ -118,8 +125,8 @@ func createEmptyPerformer(id int) models.Performer {
|
||||
}
|
||||
}
|
||||
|
||||
func createFullJSONPerformer(name string, image string) *jsonschema.Performer {
|
||||
return &jsonschema.Performer{
|
||||
func createFullJSONPerformer(name string, image string, withCustomFields bool) *jsonschema.Performer {
|
||||
ret := &jsonschema.Performer{
|
||||
Name: name,
|
||||
Disambiguation: disambiguation,
|
||||
URLs: []string{url, twitter, instagram},
|
||||
@@ -152,7 +159,13 @@ func createFullJSONPerformer(name string, image string) *jsonschema.Performer {
|
||||
Weight: weight,
|
||||
StashIDs: stashIDs,
|
||||
IgnoreAutoTag: autoTagIgnored,
|
||||
CustomFields: emptyCustomFields,
|
||||
}
|
||||
|
||||
if withCustomFields {
|
||||
ret.CustomFields = customFields
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func createEmptyJSONPerformer() *jsonschema.Performer {
|
||||
@@ -166,13 +179,15 @@ func createEmptyJSONPerformer() *jsonschema.Performer {
|
||||
UpdatedAt: json.JSONTime{
|
||||
Time: updateTime,
|
||||
},
|
||||
CustomFields: emptyCustomFields,
|
||||
}
|
||||
}
|
||||
|
||||
type testScenario struct {
|
||||
input models.Performer
|
||||
expected *jsonschema.Performer
|
||||
err bool
|
||||
input models.Performer
|
||||
customFields map[string]interface{}
|
||||
expected *jsonschema.Performer
|
||||
err bool
|
||||
}
|
||||
|
||||
var scenarios []testScenario
|
||||
@@ -181,20 +196,36 @@ func initTestTable() {
|
||||
scenarios = []testScenario{
|
||||
{
|
||||
*createFullPerformer(performerID, performerName),
|
||||
createFullJSONPerformer(performerName, image),
|
||||
emptyCustomFields,
|
||||
createFullJSONPerformer(performerName, image, false),
|
||||
false,
|
||||
},
|
||||
{
|
||||
*createFullPerformer(customFieldsID, performerName),
|
||||
customFields,
|
||||
createFullJSONPerformer(performerName, image, true),
|
||||
false,
|
||||
},
|
||||
{
|
||||
createEmptyPerformer(noImageID),
|
||||
emptyCustomFields,
|
||||
createEmptyJSONPerformer(),
|
||||
false,
|
||||
},
|
||||
{
|
||||
*createFullPerformer(errImageID, performerName),
|
||||
createFullJSONPerformer(performerName, ""),
|
||||
emptyCustomFields,
|
||||
createFullJSONPerformer(performerName, "", false),
|
||||
// failure to get image should not cause an error
|
||||
false,
|
||||
},
|
||||
{
|
||||
*createFullPerformer(errCustomFieldsID, performerName),
|
||||
customFields,
|
||||
nil,
|
||||
// failure to get custom fields should cause an error
|
||||
true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,11 +235,19 @@ func TestToJSON(t *testing.T) {
|
||||
db := mocks.NewDatabase()
|
||||
|
||||
imageErr := errors.New("error getting image")
|
||||
customFieldsErr := errors.New("error getting custom fields")
|
||||
|
||||
db.Performer.On("GetImage", testCtx, performerID).Return(imageBytes, nil).Once()
|
||||
db.Performer.On("GetImage", testCtx, customFieldsID).Return(imageBytes, nil).Once()
|
||||
db.Performer.On("GetImage", testCtx, noImageID).Return(nil, nil).Once()
|
||||
db.Performer.On("GetImage", testCtx, errImageID).Return(nil, imageErr).Once()
|
||||
|
||||
db.Performer.On("GetCustomFields", testCtx, performerID).Return(emptyCustomFields, nil).Once()
|
||||
db.Performer.On("GetCustomFields", testCtx, customFieldsID).Return(customFields, nil).Once()
|
||||
db.Performer.On("GetCustomFields", testCtx, noImageID).Return(emptyCustomFields, nil).Once()
|
||||
db.Performer.On("GetCustomFields", testCtx, errImageID).Return(emptyCustomFields, nil).Once()
|
||||
db.Performer.On("GetCustomFields", testCtx, errCustomFieldsID).Return(nil, customFieldsErr).Once()
|
||||
|
||||
for i, s := range scenarios {
|
||||
tag := s.input
|
||||
json, err := ToJSON(testCtx, db.Performer, &tag)
|
||||
|
||||
@@ -3,6 +3,7 @@ package performer
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -24,13 +25,15 @@ type Importer struct {
|
||||
Input jsonschema.Performer
|
||||
MissingRefBehaviour models.ImportMissingRefEnum
|
||||
|
||||
ID int
|
||||
performer models.Performer
|
||||
imageData []byte
|
||||
ID int
|
||||
performer models.Performer
|
||||
customFields models.CustomFieldMap
|
||||
imageData []byte
|
||||
}
|
||||
|
||||
func (i *Importer) PreImport(ctx context.Context) error {
|
||||
i.performer = performerJSONToPerformer(i.Input)
|
||||
i.customFields = i.Input.CustomFields
|
||||
|
||||
if err := i.populateTags(ctx); err != nil {
|
||||
return err
|
||||
@@ -75,7 +78,7 @@ func importTags(ctx context.Context, tagWriter models.TagFinderCreator, names []
|
||||
}
|
||||
|
||||
missingTags := sliceutil.Filter(names, func(name string) bool {
|
||||
return !sliceutil.Contains(pluckedNames, name)
|
||||
return !slices.Contains(pluckedNames, name)
|
||||
})
|
||||
|
||||
if len(missingTags) > 0 {
|
||||
@@ -164,7 +167,10 @@ func (i *Importer) FindExistingID(ctx context.Context) (*int, error) {
|
||||
}
|
||||
|
||||
func (i *Importer) Create(ctx context.Context) (*int, error) {
|
||||
err := i.ReaderWriter.Create(ctx, &i.performer)
|
||||
err := i.ReaderWriter.Create(ctx, &models.CreatePerformerInput{
|
||||
Performer: &i.performer,
|
||||
CustomFields: i.customFields,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating performer: %v", err)
|
||||
}
|
||||
@@ -174,9 +180,13 @@ func (i *Importer) Create(ctx context.Context) (*int, error) {
|
||||
}
|
||||
|
||||
func (i *Importer) Update(ctx context.Context, id int) error {
|
||||
performer := i.performer
|
||||
performer.ID = id
|
||||
err := i.ReaderWriter.Update(ctx, &performer)
|
||||
i.performer.ID = id
|
||||
err := i.ReaderWriter.Update(ctx, &models.UpdatePerformerInput{
|
||||
Performer: &i.performer,
|
||||
CustomFields: models.CustomFieldsInput{
|
||||
Full: i.customFields,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating existing performer: %v", err)
|
||||
}
|
||||
|
||||
@@ -53,13 +53,14 @@ func TestImporterPreImport(t *testing.T) {
|
||||
|
||||
assert.NotNil(t, err)
|
||||
|
||||
i.Input = *createFullJSONPerformer(performerName, image)
|
||||
i.Input = *createFullJSONPerformer(performerName, image, true)
|
||||
|
||||
err = i.PreImport(testCtx)
|
||||
|
||||
assert.Nil(t, err)
|
||||
expectedPerformer := *createFullPerformer(0, performerName)
|
||||
assert.Equal(t, expectedPerformer, i.performer)
|
||||
assert.Equal(t, models.CustomFieldMap(customFields), i.customFields)
|
||||
}
|
||||
|
||||
func TestImporterPreImportWithTag(t *testing.T) {
|
||||
@@ -234,10 +235,18 @@ func TestCreate(t *testing.T) {
|
||||
Name: performerName,
|
||||
}
|
||||
|
||||
performerInput := models.CreatePerformerInput{
|
||||
Performer: &performer,
|
||||
}
|
||||
|
||||
performerErr := models.Performer{
|
||||
Name: performerNameErr,
|
||||
}
|
||||
|
||||
performerErrInput := models.CreatePerformerInput{
|
||||
Performer: &performerErr,
|
||||
}
|
||||
|
||||
i := Importer{
|
||||
ReaderWriter: db.Performer,
|
||||
TagWriter: db.Tag,
|
||||
@@ -245,11 +254,11 @@ func TestCreate(t *testing.T) {
|
||||
}
|
||||
|
||||
errCreate := errors.New("Create error")
|
||||
db.Performer.On("Create", testCtx, &performer).Run(func(args mock.Arguments) {
|
||||
arg := args.Get(1).(*models.Performer)
|
||||
db.Performer.On("Create", testCtx, &performerInput).Run(func(args mock.Arguments) {
|
||||
arg := args.Get(1).(*models.CreatePerformerInput)
|
||||
arg.ID = performerID
|
||||
}).Return(nil).Once()
|
||||
db.Performer.On("Create", testCtx, &performerErr).Return(errCreate).Once()
|
||||
db.Performer.On("Create", testCtx, &performerErrInput).Return(errCreate).Once()
|
||||
|
||||
id, err := i.Create(testCtx)
|
||||
assert.Equal(t, performerID, *id)
|
||||
@@ -284,7 +293,10 @@ func TestUpdate(t *testing.T) {
|
||||
|
||||
// id needs to be set for the mock input
|
||||
performer.ID = performerID
|
||||
db.Performer.On("Update", testCtx, &performer).Return(nil).Once()
|
||||
performerInput := models.UpdatePerformerInput{
|
||||
Performer: &performer,
|
||||
}
|
||||
db.Performer.On("Update", testCtx, &performerInput).Return(nil).Once()
|
||||
|
||||
err := i.Update(testCtx, performerID)
|
||||
assert.Nil(t, err)
|
||||
@@ -293,7 +305,10 @@ func TestUpdate(t *testing.T) {
|
||||
|
||||
// need to set id separately
|
||||
performerErr.ID = errImageID
|
||||
db.Performer.On("Update", testCtx, &performerErr).Return(errUpdate).Once()
|
||||
performerErrInput := models.UpdatePerformerInput{
|
||||
Performer: &performerErr,
|
||||
}
|
||||
db.Performer.On("Update", testCtx, &performerErrInput).Return(errUpdate).Once()
|
||||
|
||||
err = i.Update(testCtx, errImageID)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/shurcooL/graphql"
|
||||
graphql "github.com/hasura/go-graphql-client"
|
||||
"github.com/stashapp/stash/pkg/plugin/common/log"
|
||||
)
|
||||
|
||||
|
||||
@@ -145,11 +145,16 @@ interface IPluginApi {
|
||||
});
|
||||
|
||||
const TestPage: React.FC = () => {
|
||||
const componentsLoading = PluginApi.hooks.useLoadComponents([PluginApi.loadableComponents.SceneCard]);
|
||||
const componentsToLoad = [
|
||||
PluginApi.loadableComponents.SceneCard,
|
||||
PluginApi.loadableComponents.PerformerSelect,
|
||||
];
|
||||
const componentsLoading = PluginApi.hooks.useLoadComponents(componentsToLoad);
|
||||
|
||||
const {
|
||||
SceneCard,
|
||||
LoadingIndicator,
|
||||
PerformerSelect,
|
||||
} = PluginApi.components;
|
||||
|
||||
// read a random scene and show a scene card for it
|
||||
@@ -172,6 +177,9 @@ interface IPluginApi {
|
||||
<div>
|
||||
<div>This is a test page.</div>
|
||||
{!!scene && <SceneCard scene={data.findScenes.scenes[0]} />}
|
||||
<div>
|
||||
<PerformerSelect isMulti onSelect={() => {}} values={[]} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -23,7 +24,6 @@ import (
|
||||
"github.com/stashapp/stash/pkg/plugin/common"
|
||||
"github.com/stashapp/stash/pkg/plugin/hook"
|
||||
"github.com/stashapp/stash/pkg/session"
|
||||
"github.com/stashapp/stash/pkg/sliceutil"
|
||||
"github.com/stashapp/stash/pkg/txn"
|
||||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
@@ -168,7 +168,7 @@ func (c Cache) enabledPlugins() []Config {
|
||||
|
||||
var ret []Config
|
||||
for _, p := range c.plugins {
|
||||
disabled := sliceutil.Contains(disabledPlugins, p.id)
|
||||
disabled := slices.Contains(disabledPlugins, p.id)
|
||||
|
||||
if !disabled {
|
||||
ret = append(ret, p)
|
||||
@@ -181,7 +181,7 @@ func (c Cache) enabledPlugins() []Config {
|
||||
func (c Cache) pluginDisabled(id string) bool {
|
||||
disabledPlugins := c.config.GetDisabledPlugins()
|
||||
|
||||
return sliceutil.Contains(disabledPlugins, id)
|
||||
return slices.Contains(disabledPlugins, id)
|
||||
}
|
||||
|
||||
// ListPlugins returns plugin details for all of the loaded plugins.
|
||||
@@ -192,7 +192,7 @@ func (c Cache) ListPlugins() []*Plugin {
|
||||
for _, s := range c.plugins {
|
||||
p := s.toPlugin()
|
||||
|
||||
disabled := sliceutil.Contains(disabledPlugins, p.ID)
|
||||
disabled := slices.Contains(disabledPlugins, p.ID)
|
||||
p.Enabled = !disabled
|
||||
|
||||
ret = append(ret, p)
|
||||
@@ -209,7 +209,7 @@ func (c Cache) GetPlugin(id string) *Plugin {
|
||||
if plugin != nil {
|
||||
p := plugin.toPlugin()
|
||||
|
||||
disabled := sliceutil.Contains(disabledPlugins, p.ID)
|
||||
disabled := slices.Contains(disabledPlugins, p.ID)
|
||||
p.Enabled = !disabled
|
||||
return p
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/shurcooL/graphql"
|
||||
graphql "github.com/hasura/go-graphql-client"
|
||||
|
||||
"github.com/stashapp/stash/pkg/plugin/common"
|
||||
)
|
||||
|
||||
19
pkg/savedfilter/export.go
Normal file
19
pkg/savedfilter/export.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package savedfilter
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/models/jsonschema"
|
||||
)
|
||||
|
||||
// ToJSON converts a SavedFilter object into its JSON equivalent.
|
||||
func ToJSON(ctx context.Context, filter *models.SavedFilter) (*jsonschema.SavedFilter, error) {
|
||||
return &jsonschema.SavedFilter{
|
||||
Name: filter.Name,
|
||||
Mode: filter.Mode,
|
||||
FindFilter: filter.FindFilter,
|
||||
ObjectFilter: filter.ObjectFilter,
|
||||
UIOptions: filter.UIOptions,
|
||||
}, nil
|
||||
}
|
||||
91
pkg/savedfilter/export_test.go
Normal file
91
pkg/savedfilter/export_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package savedfilter
|
||||
|
||||
import (
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/models/jsonschema"
|
||||
"github.com/stashapp/stash/pkg/models/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
savedFilterID = 1
|
||||
noImageID = 2
|
||||
errImageID = 3
|
||||
errAliasID = 4
|
||||
withParentsID = 5
|
||||
errParentsID = 6
|
||||
)
|
||||
|
||||
const (
|
||||
filterName = "testFilter"
|
||||
mode = models.FilterModeGalleries
|
||||
)
|
||||
|
||||
var (
|
||||
findFilter = models.FindFilterType{}
|
||||
objectFilter = make(map[string]interface{})
|
||||
uiOptions = make(map[string]interface{})
|
||||
)
|
||||
|
||||
func createSavedFilter(id int) models.SavedFilter {
|
||||
return models.SavedFilter{
|
||||
ID: id,
|
||||
Name: filterName,
|
||||
Mode: mode,
|
||||
FindFilter: &findFilter,
|
||||
ObjectFilter: objectFilter,
|
||||
UIOptions: uiOptions,
|
||||
}
|
||||
}
|
||||
|
||||
func createJSONSavedFilter() *jsonschema.SavedFilter {
|
||||
return &jsonschema.SavedFilter{
|
||||
Name: filterName,
|
||||
Mode: mode,
|
||||
FindFilter: &findFilter,
|
||||
ObjectFilter: objectFilter,
|
||||
UIOptions: uiOptions,
|
||||
}
|
||||
}
|
||||
|
||||
type testScenario struct {
|
||||
savedFilter models.SavedFilter
|
||||
expected *jsonschema.SavedFilter
|
||||
err bool
|
||||
}
|
||||
|
||||
var scenarios []testScenario
|
||||
|
||||
func initTestTable() {
|
||||
scenarios = []testScenario{
|
||||
{
|
||||
createSavedFilter(savedFilterID),
|
||||
createJSONSavedFilter(),
|
||||
false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestToJSON(t *testing.T) {
|
||||
initTestTable()
|
||||
|
||||
db := mocks.NewDatabase()
|
||||
|
||||
for i, s := range scenarios {
|
||||
savedFilter := s.savedFilter
|
||||
json, err := ToJSON(testCtx, &savedFilter)
|
||||
|
||||
switch {
|
||||
case !s.err && err != nil:
|
||||
t.Errorf("[%d] unexpected error: %s", i, err.Error())
|
||||
case s.err && err == nil:
|
||||
t.Errorf("[%d] expected error not returned", i)
|
||||
default:
|
||||
assert.Equal(t, s.expected, json, "[%d]", i)
|
||||
}
|
||||
}
|
||||
|
||||
db.AssertExpectations(t)
|
||||
}
|
||||
60
pkg/savedfilter/import.go
Normal file
60
pkg/savedfilter/import.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package savedfilter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/models/jsonschema"
|
||||
)
|
||||
|
||||
type ImporterReaderWriter interface {
|
||||
models.SavedFilterWriter
|
||||
}
|
||||
|
||||
type Importer struct {
|
||||
ReaderWriter ImporterReaderWriter
|
||||
Input jsonschema.SavedFilter
|
||||
MissingRefBehaviour models.ImportMissingRefEnum
|
||||
|
||||
savedFilter models.SavedFilter
|
||||
}
|
||||
|
||||
func (i *Importer) PreImport(ctx context.Context) error {
|
||||
i.savedFilter = models.SavedFilter{
|
||||
Name: i.Input.Name,
|
||||
Mode: i.Input.Mode,
|
||||
FindFilter: i.Input.FindFilter,
|
||||
ObjectFilter: i.Input.ObjectFilter,
|
||||
UIOptions: i.Input.UIOptions,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Importer) PostImport(ctx context.Context, id int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *Importer) Name() string {
|
||||
return i.Input.Name
|
||||
}
|
||||
|
||||
func (i *Importer) FindExistingID(ctx context.Context) (*int, error) {
|
||||
// for now, assume this is only imported in full, so we don't support updating existing filters
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (i *Importer) Create(ctx context.Context) (*int, error) {
|
||||
err := i.ReaderWriter.Create(ctx, &i.savedFilter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating saved filter: %v", err)
|
||||
}
|
||||
|
||||
id := i.savedFilter.ID
|
||||
return &id, nil
|
||||
}
|
||||
|
||||
func (i *Importer) Update(ctx context.Context, id int) error {
|
||||
return fmt.Errorf("updating existing saved filters is not supported")
|
||||
}
|
||||
124
pkg/savedfilter/import_test.go
Normal file
124
pkg/savedfilter/import_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package savedfilter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/models/jsonschema"
|
||||
"github.com/stashapp/stash/pkg/models/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
const (
|
||||
savedFilterNameErr = "savedFilterNameErr"
|
||||
existingSavedFilterName = "existingSavedFilterName"
|
||||
|
||||
existingFilterID = 100
|
||||
)
|
||||
|
||||
var testCtx = context.Background()
|
||||
|
||||
func TestImporterName(t *testing.T) {
|
||||
i := Importer{
|
||||
Input: jsonschema.SavedFilter{
|
||||
Name: filterName,
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, filterName, i.Name())
|
||||
}
|
||||
|
||||
func TestImporterPreImport(t *testing.T) {
|
||||
i := Importer{
|
||||
Input: jsonschema.SavedFilter{
|
||||
Name: filterName,
|
||||
},
|
||||
}
|
||||
|
||||
err := i.PreImport(testCtx)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestImporterPostImport(t *testing.T) {
|
||||
db := mocks.NewDatabase()
|
||||
|
||||
i := Importer{
|
||||
ReaderWriter: db.SavedFilter,
|
||||
Input: jsonschema.SavedFilter{},
|
||||
}
|
||||
|
||||
err := i.PostImport(testCtx, savedFilterID)
|
||||
assert.Nil(t, err)
|
||||
|
||||
db.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestImporterFindExistingID(t *testing.T) {
|
||||
db := mocks.NewDatabase()
|
||||
|
||||
i := Importer{
|
||||
ReaderWriter: db.SavedFilter,
|
||||
Input: jsonschema.SavedFilter{
|
||||
Name: filterName,
|
||||
},
|
||||
}
|
||||
|
||||
id, err := i.FindExistingID(testCtx)
|
||||
assert.Nil(t, id)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
db := mocks.NewDatabase()
|
||||
|
||||
savedFilter := models.SavedFilter{
|
||||
Name: filterName,
|
||||
}
|
||||
|
||||
savedFilterErr := models.SavedFilter{
|
||||
Name: savedFilterNameErr,
|
||||
}
|
||||
|
||||
i := Importer{
|
||||
ReaderWriter: db.SavedFilter,
|
||||
savedFilter: savedFilter,
|
||||
}
|
||||
|
||||
errCreate := errors.New("Create error")
|
||||
db.SavedFilter.On("Create", testCtx, &savedFilter).Run(func(args mock.Arguments) {
|
||||
t := args.Get(1).(*models.SavedFilter)
|
||||
t.ID = savedFilterID
|
||||
}).Return(nil).Once()
|
||||
db.SavedFilter.On("Create", testCtx, &savedFilterErr).Return(errCreate).Once()
|
||||
|
||||
id, err := i.Create(testCtx)
|
||||
assert.Equal(t, savedFilterID, *id)
|
||||
assert.Nil(t, err)
|
||||
|
||||
i.savedFilter = savedFilterErr
|
||||
id, err = i.Create(testCtx)
|
||||
assert.Nil(t, id)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
db.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
db := mocks.NewDatabase()
|
||||
|
||||
savedFilterErr := models.SavedFilter{
|
||||
Name: savedFilterNameErr,
|
||||
}
|
||||
|
||||
i := Importer{
|
||||
ReaderWriter: db.SavedFilter,
|
||||
savedFilter: savedFilterErr,
|
||||
}
|
||||
|
||||
// Update is not currently supported
|
||||
err := i.Update(testCtx, existingFilterID)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package scene
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -290,7 +291,7 @@ func (i *Importer) populatePerformers(ctx context.Context) error {
|
||||
}
|
||||
|
||||
missingPerformers := sliceutil.Filter(names, func(name string) bool {
|
||||
return !sliceutil.Contains(pluckedNames, name)
|
||||
return !slices.Contains(pluckedNames, name)
|
||||
})
|
||||
|
||||
if len(missingPerformers) > 0 {
|
||||
@@ -324,7 +325,9 @@ func (i *Importer) createPerformers(ctx context.Context, names []string) ([]*mod
|
||||
newPerformer := models.NewPerformer()
|
||||
newPerformer.Name = name
|
||||
|
||||
err := i.PerformerWriter.Create(ctx, &newPerformer)
|
||||
err := i.PerformerWriter.Create(ctx, &models.CreatePerformerInput{
|
||||
Performer: &newPerformer,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -517,7 +520,7 @@ func importTags(ctx context.Context, tagWriter models.TagFinderCreator, names []
|
||||
}
|
||||
|
||||
missingTags := sliceutil.Filter(names, func(name string) bool {
|
||||
return !sliceutil.Contains(pluckedNames, name)
|
||||
return !slices.Contains(pluckedNames, name)
|
||||
})
|
||||
|
||||
if len(missingTags) > 0 {
|
||||
|
||||
@@ -327,8 +327,8 @@ func TestImporterPreImportWithMissingPerformer(t *testing.T) {
|
||||
}
|
||||
|
||||
db.Performer.On("FindByNames", testCtx, []string{missingPerformerName}, false).Return(nil, nil).Times(3)
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.Performer")).Run(func(args mock.Arguments) {
|
||||
p := args.Get(1).(*models.Performer)
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.CreatePerformerInput")).Run(func(args mock.Arguments) {
|
||||
p := args.Get(1).(*models.CreatePerformerInput)
|
||||
p.ID = existingPerformerID
|
||||
}).Return(nil)
|
||||
|
||||
@@ -361,7 +361,7 @@ func TestImporterPreImportWithMissingPerformerCreateErr(t *testing.T) {
|
||||
}
|
||||
|
||||
db.Performer.On("FindByNames", testCtx, []string{missingPerformerName}, false).Return(nil, nil).Once()
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.Performer")).Return(errors.New("Create error"))
|
||||
db.Performer.On("Create", testCtx, mock.AnythingOfType("*models.CreatePerformerInput")).Return(errors.New("Create error"))
|
||||
|
||||
err := i.PreImport(testCtx)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
@@ -28,7 +29,7 @@ func (s *Service) Merge(ctx context.Context, sourceIDs []int, destinationID int,
|
||||
sourceIDs = sliceutil.AppendUniques(nil, sourceIDs)
|
||||
|
||||
// ensure destination is not in source list
|
||||
if sliceutil.Contains(sourceIDs, destinationID) {
|
||||
if slices.Contains(sourceIDs, destinationID) {
|
||||
return errors.New("destination scene cannot be in source list")
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ import (
|
||||
|
||||
var (
|
||||
ErrNotVideoFile = errors.New("not a video file")
|
||||
|
||||
// fingerprint types to match with
|
||||
// only try to match by data fingerprints, _not_ perceptual fingerprints
|
||||
matchableFingerprintTypes = []string{models.FingerprintTypeOshash, models.FingerprintTypeMD5}
|
||||
)
|
||||
|
||||
type ScanCreatorUpdater interface {
|
||||
@@ -87,7 +91,7 @@ func (h *ScanHandler) Handle(ctx context.Context, f models.File, oldFile models.
|
||||
|
||||
if len(existing) == 0 {
|
||||
// try also to match file by fingerprints
|
||||
existing, err = h.CreatorUpdater.FindByFingerprints(ctx, videoFile.Fingerprints)
|
||||
existing, err = h.CreatorUpdater.FindByFingerprints(ctx, videoFile.Fingerprints.Filter(matchableFingerprintTypes...))
|
||||
if err != nil {
|
||||
return fmt.Errorf("finding existing scene by fingerprints: %w", err)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/models/mocks"
|
||||
@@ -236,16 +237,19 @@ func TestUpdateSet_UpdateInput(t *testing.T) {
|
||||
tagIDStrs := intslice.IntSliceToStringSlice(tagIDs)
|
||||
stashID := "stashID"
|
||||
endpoint := "endpoint"
|
||||
updatedAt := time.Now()
|
||||
stashIDs := []models.StashID{
|
||||
{
|
||||
StashID: stashID,
|
||||
Endpoint: endpoint,
|
||||
StashID: stashID,
|
||||
Endpoint: endpoint,
|
||||
UpdatedAt: updatedAt,
|
||||
},
|
||||
}
|
||||
stashIDInputs := []models.StashID{
|
||||
stashIDInputs := []models.StashIDInput{
|
||||
{
|
||||
StashID: stashID,
|
||||
Endpoint: endpoint,
|
||||
StashID: stashID,
|
||||
Endpoint: endpoint,
|
||||
UpdatedAt: &updatedAt,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -114,7 +114,8 @@ func (c config) validate() error {
|
||||
}
|
||||
|
||||
type stashServer struct {
|
||||
URL string `yaml:"url"`
|
||||
URL string `yaml:"url"`
|
||||
ApiKey string `yaml:"apiKey"`
|
||||
}
|
||||
|
||||
type scraperTypeConfig struct {
|
||||
|
||||
55
pkg/scraper/graphql.go
Normal file
55
pkg/scraper/graphql.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package scraper
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/hasura/go-graphql-client"
|
||||
)
|
||||
|
||||
type graphqlErrors []error
|
||||
|
||||
func (e graphqlErrors) Error() string {
|
||||
b := strings.Builder{}
|
||||
for _, err := range e {
|
||||
_, _ = b.WriteString(err.Error())
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
type graphqlError struct {
|
||||
err graphql.Error
|
||||
}
|
||||
|
||||
func (e graphqlError) Error() string {
|
||||
unwrapped := e.err.Unwrap()
|
||||
if unwrapped != nil {
|
||||
var networkErr graphql.NetworkError
|
||||
if errors.As(unwrapped, &networkErr) {
|
||||
if networkErr.StatusCode() == 422 {
|
||||
return networkErr.Body()
|
||||
}
|
||||
}
|
||||
}
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
// convertGraphqlError converts a graphql.Error or graphql.Errors into an error with a useful message.
|
||||
// graphql.Error swallows important information, so we need to convert it to a more useful error type.
|
||||
func convertGraphqlError(err error) error {
|
||||
var gqlErrs graphql.Errors
|
||||
if errors.As(err, &gqlErrs) {
|
||||
ret := make(graphqlErrors, len(gqlErrs))
|
||||
for i, e := range gqlErrs {
|
||||
ret[i] = convertGraphqlError(e)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
var gqlErr graphql.Error
|
||||
if errors.As(err, &gqlErr) {
|
||||
return graphqlError{gqlErr}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -122,13 +122,19 @@ func setGroupBackImage(ctx context.Context, client *http.Client, m *models.Scrap
|
||||
return nil
|
||||
}
|
||||
|
||||
func getImage(ctx context.Context, url string, client *http.Client, globalConfig GlobalConfig) (*string, error) {
|
||||
type imageGetter struct {
|
||||
client *http.Client
|
||||
globalConfig GlobalConfig
|
||||
requestModifier func(req *http.Request)
|
||||
}
|
||||
|
||||
func (i *imageGetter) getImage(ctx context.Context, url string) (*string, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userAgent := globalConfig.GetScraperUserAgent()
|
||||
userAgent := i.globalConfig.GetScraperUserAgent()
|
||||
if userAgent != "" {
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
}
|
||||
@@ -140,7 +146,11 @@ func getImage(ctx context.Context, url string, client *http.Client, globalConfig
|
||||
req.Header.Set("Referer", req.URL.Scheme+"://"+req.Host+"/")
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if i.requestModifier != nil {
|
||||
i.requestModifier(req)
|
||||
}
|
||||
|
||||
resp, err := i.client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -167,10 +177,19 @@ func getImage(ctx context.Context, url string, client *http.Client, globalConfig
|
||||
return &img, nil
|
||||
}
|
||||
|
||||
func getStashPerformerImage(ctx context.Context, stashURL string, performerID string, client *http.Client, globalConfig GlobalConfig) (*string, error) {
|
||||
return getImage(ctx, stashURL+"/performer/"+performerID+"/image", client, globalConfig)
|
||||
func getImage(ctx context.Context, url string, client *http.Client, globalConfig GlobalConfig) (*string, error) {
|
||||
g := imageGetter{
|
||||
client: client,
|
||||
globalConfig: globalConfig,
|
||||
}
|
||||
|
||||
return g.getImage(ctx, url)
|
||||
}
|
||||
|
||||
func getStashSceneImage(ctx context.Context, stashURL string, sceneID string, client *http.Client, globalConfig GlobalConfig) (*string, error) {
|
||||
return getImage(ctx, stashURL+"/scene/"+sceneID+"/screenshot", client, globalConfig)
|
||||
func getStashPerformerImage(ctx context.Context, stashURL string, performerID string, imageGetter imageGetter) (*string, error) {
|
||||
return imageGetter.getImage(ctx, stashURL+"/performer/"+performerID+"/image")
|
||||
}
|
||||
|
||||
func getStashSceneImage(ctx context.Context, stashURL string, sceneID string, imageGetter imageGetter) (*string, error) {
|
||||
return imageGetter.getImage(ctx, stashURL+"/scene/"+sceneID+"/screenshot")
|
||||
}
|
||||
|
||||
@@ -4,9 +4,11 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
graphql "github.com/hasura/go-graphql-client"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/shurcooL/graphql"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
@@ -27,9 +29,21 @@ func newStashScraper(scraper scraperTypeConfig, client *http.Client, config conf
|
||||
}
|
||||
}
|
||||
|
||||
func setApiKeyHeader(apiKey string) func(req *http.Request) {
|
||||
return func(req *http.Request) {
|
||||
req.Header.Set("ApiKey", apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *stashScraper) getStashClient() *graphql.Client {
|
||||
url := s.config.StashServer.URL
|
||||
return graphql.NewClient(url+"/graphql", nil)
|
||||
url := s.config.StashServer.URL + "/graphql"
|
||||
ret := graphql.NewClient(url, s.client)
|
||||
|
||||
if s.config.StashServer.ApiKey != "" {
|
||||
ret = ret.WithRequestModifier(setApiKeyHeader(s.config.StashServer.ApiKey))
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
type stashFindPerformerNamePerformer struct {
|
||||
@@ -58,14 +72,12 @@ type scrapedTagStash struct {
|
||||
type scrapedPerformerStash struct {
|
||||
Name *string `graphql:"name" json:"name"`
|
||||
Gender *string `graphql:"gender" json:"gender"`
|
||||
URL *string `graphql:"url" json:"url"`
|
||||
Twitter *string `graphql:"twitter" json:"twitter"`
|
||||
Instagram *string `graphql:"instagram" json:"instagram"`
|
||||
URLs []string `graphql:"urls" json:"urls"`
|
||||
Birthdate *string `graphql:"birthdate" json:"birthdate"`
|
||||
Ethnicity *string `graphql:"ethnicity" json:"ethnicity"`
|
||||
Country *string `graphql:"country" json:"country"`
|
||||
EyeColor *string `graphql:"eye_color" json:"eye_color"`
|
||||
Height *string `graphql:"height" json:"height"`
|
||||
Height *int `graphql:"height_cm" json:"height_cm"`
|
||||
Measurements *string `graphql:"measurements" json:"measurements"`
|
||||
FakeTits *string `graphql:"fake_tits" json:"fake_tits"`
|
||||
PenisLength *string `graphql:"penis_length" json:"penis_length"`
|
||||
@@ -73,12 +85,25 @@ type scrapedPerformerStash struct {
|
||||
CareerLength *string `graphql:"career_length" json:"career_length"`
|
||||
Tattoos *string `graphql:"tattoos" json:"tattoos"`
|
||||
Piercings *string `graphql:"piercings" json:"piercings"`
|
||||
Aliases *string `graphql:"aliases" json:"aliases"`
|
||||
Aliases []string `graphql:"alias_list" json:"alias_list"`
|
||||
Tags []*scrapedTagStash `graphql:"tags" json:"tags"`
|
||||
Details *string `graphql:"details" json:"details"`
|
||||
DeathDate *string `graphql:"death_date" json:"death_date"`
|
||||
HairColor *string `graphql:"hair_color" json:"hair_color"`
|
||||
Weight *string `graphql:"weight" json:"weight"`
|
||||
Weight *int `graphql:"weight" json:"weight"`
|
||||
}
|
||||
|
||||
func (s *stashScraper) imageGetter() imageGetter {
|
||||
ret := imageGetter{
|
||||
client: s.client,
|
||||
globalConfig: s.globalConfig,
|
||||
}
|
||||
|
||||
if s.config.StashServer.ApiKey != "" {
|
||||
ret.requestModifier = setApiKeyHeader(s.config.StashServer.ApiKey)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *stashScraper) scrapeByFragment(ctx context.Context, input Input) (ScrapedContent, error) {
|
||||
@@ -102,12 +127,12 @@ func (s *stashScraper) scrapeByFragment(ctx context.Context, input Input) (Scrap
|
||||
|
||||
// get the id from the URL field
|
||||
vars := map[string]interface{}{
|
||||
"f": performerID,
|
||||
"f": graphql.ID(performerID),
|
||||
}
|
||||
|
||||
err := client.Query(ctx, &q, vars)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, convertGraphqlError(err)
|
||||
}
|
||||
|
||||
// need to copy back to a scraped performer
|
||||
@@ -117,11 +142,28 @@ func (s *stashScraper) scrapeByFragment(ctx context.Context, input Input) (Scrap
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// convert alias list to aliases
|
||||
aliasStr := strings.Join(q.FindPerformer.Aliases, ", ")
|
||||
ret.Aliases = &aliasStr
|
||||
|
||||
// convert numeric to string
|
||||
if q.FindPerformer.Height != nil {
|
||||
heightStr := strconv.Itoa(*q.FindPerformer.Height)
|
||||
ret.Height = &heightStr
|
||||
}
|
||||
if q.FindPerformer.Weight != nil {
|
||||
weightStr := strconv.Itoa(*q.FindPerformer.Weight)
|
||||
ret.Weight = &weightStr
|
||||
}
|
||||
|
||||
// get the performer image directly
|
||||
ret.Image, err = getStashPerformerImage(ctx, s.config.StashServer.URL, performerID, s.client, s.globalConfig)
|
||||
ig := s.imageGetter()
|
||||
img, err := getStashPerformerImage(ctx, s.config.StashServer.URL, performerID, ig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret.Images = []string{*img}
|
||||
ret.Image = img
|
||||
|
||||
return &ret, nil
|
||||
}
|
||||
@@ -143,8 +185,15 @@ func (s *stashScraper) scrapedStashSceneToScrapedScene(ctx context.Context, scen
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// get the performer image directly
|
||||
ret.Image, err = getStashSceneImage(ctx, s.config.StashServer.URL, scene.ID, s.client, s.globalConfig)
|
||||
// convert first in files to file
|
||||
if len(scene.Files) > 0 {
|
||||
f := scene.Files[0].SceneFileType()
|
||||
ret.File = &f
|
||||
}
|
||||
|
||||
// get the scene image directly
|
||||
ig := s.imageGetter()
|
||||
ret.Image, err = getStashSceneImage(ctx, s.config.StashServer.URL, scene.ID, ig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -175,7 +224,7 @@ func (s *stashScraper) scrapeByName(ctx context.Context, name string, ty ScrapeC
|
||||
|
||||
err := client.Query(ctx, &q, vars)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, convertGraphqlError(err)
|
||||
}
|
||||
|
||||
for _, scene := range q.FindScenes.Scenes {
|
||||
@@ -207,13 +256,41 @@ func (s *stashScraper) scrapeByName(ctx context.Context, name string, ty ScrapeC
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
type stashVideoFile struct {
|
||||
Size int64 `graphql:"size" json:"size"`
|
||||
Duration float64 `graphql:"duration" json:"duration"`
|
||||
VideoCodec string `graphql:"video_codec" json:"video_codec"`
|
||||
AudioCodec string `graphql:"audio_codec" json:"audio_codec"`
|
||||
Width int `graphql:"width" json:"width"`
|
||||
Height int `graphql:"height" json:"height"`
|
||||
Framerate float64 `graphql:"frame_rate" json:"frame_rate"`
|
||||
Bitrate int `graphql:"bit_rate" json:"bit_rate"`
|
||||
}
|
||||
|
||||
func (f stashVideoFile) SceneFileType() models.SceneFileType {
|
||||
ret := models.SceneFileType{
|
||||
Duration: &f.Duration,
|
||||
VideoCodec: &f.VideoCodec,
|
||||
AudioCodec: &f.AudioCodec,
|
||||
Width: &f.Width,
|
||||
Height: &f.Height,
|
||||
Framerate: &f.Framerate,
|
||||
Bitrate: &f.Bitrate,
|
||||
}
|
||||
|
||||
size := strconv.FormatInt(f.Size, 10)
|
||||
ret.Size = &size
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
type scrapedSceneStash struct {
|
||||
ID string `graphql:"id" json:"id"`
|
||||
Title *string `graphql:"title" json:"title"`
|
||||
Details *string `graphql:"details" json:"details"`
|
||||
URL *string `graphql:"url" json:"url"`
|
||||
URLs []string `graphql:"urls" json:"urls"`
|
||||
Date *string `graphql:"date" json:"date"`
|
||||
File *models.SceneFileType `graphql:"file" json:"file"`
|
||||
Files []stashVideoFile `graphql:"files" json:"files"`
|
||||
Studio *scrapedStudioStash `graphql:"studio" json:"studio"`
|
||||
Tags []*scrapedTagStash `graphql:"tags" json:"tags"`
|
||||
Performers []*scrapedPerformerStash `graphql:"performers" json:"performers"`
|
||||
@@ -239,12 +316,16 @@ func (s *stashScraper) scrapeSceneByScene(ctx context.Context, scene *models.Sce
|
||||
}
|
||||
|
||||
vars := map[string]interface{}{
|
||||
"c": &input,
|
||||
"c": input,
|
||||
}
|
||||
|
||||
client := s.getStashClient()
|
||||
if err := client.Query(ctx, &q, vars); err != nil {
|
||||
return nil, err
|
||||
return nil, convertGraphqlError(err)
|
||||
}
|
||||
|
||||
if q.FindScene == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// need to copy back to a scraped scene
|
||||
@@ -254,7 +335,8 @@ func (s *stashScraper) scrapeSceneByScene(ctx context.Context, scene *models.Sce
|
||||
}
|
||||
|
||||
// get the performer image directly
|
||||
ret.Image, err = getStashSceneImage(ctx, s.config.StashServer.URL, q.FindScene.ID, s.client, s.globalConfig)
|
||||
ig := s.imageGetter()
|
||||
ret.Image, err = getStashSceneImage(ctx, s.config.StashServer.URL, q.FindScene.ID, ig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user